Files
sovereign_browser/src/agent_tools.c
T

6067 lines
246 KiB
C

/*
* agent_tools.c — tool dispatch for agent browser automation
*
* Routes JSON tool requests to the appropriate implementation. Tools
* fall into two categories:
* - Login tools (available before login): login_status, login, logout, switch_identity
* - Browser tools (available after login): open, snapshot, click, fill, etc.
*/
#include "agent_tools.h"
#include "agent_login.h"
#include "agent_snapshot.h"
#include "agent_server.h"
#include "agent_fs_tools.h"
#include "tab_manager.h"
#include "settings.h"
#include "search.h"
#include "nostr_url.h"
#include "process_info.h"
#include <string.h>
#include <stdlib.h>
#include <cairo.h>
#include <gdk/gdk.h>
/* ── JSON helpers ─────────────────────────────────────────────────── */
static cJSON *make_success(cJSON *data) {
cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "success", TRUE);
if (data) {
cJSON_AddItemToObject(resp, "data", data);
}
return resp;
}
static cJSON *make_error(const char *code, const char *message) {
cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "success", FALSE);
cJSON *err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "code", code);
cJSON_AddStringToObject(err, "message", message);
cJSON_AddItemToObject(resp, "error", err);
return resp;
}
static const char *get_string_param(cJSON *params, const char *key) {
return cJSON_GetStringValue(cJSON_GetObjectItem(params, key));
}
static int get_int_param(cJSON *params, const char *key, int fallback) {
cJSON *item = cJSON_GetObjectItem(params, key);
if (item && cJSON_IsNumber(item)) return item->valueint;
return fallback;
}
static gboolean get_bool_param(cJSON *params, const char *key, gboolean fallback) {
cJSON *item = cJSON_GetObjectItem(params, key);
if (item && cJSON_IsBool(item)) return cJSON_IsTrue(item);
return fallback;
}
/* ── Normalize URL (same logic as tab_manager.c) ──────────────────── */
static char *normalize_fips_url(const char *input) {
const char *rest = input + 7;
const char *suffix = strpbrk(rest, "/?#");
size_t authority_len = suffix ? (size_t)(suffix - rest) : strlen(rest);
if (authority_len == 0) return NULL;
char *authority = g_strndup(rest, authority_len);
char *result;
if (g_str_has_suffix(authority, ".fips")) {
result = g_strdup_printf("http://%s%s", authority, suffix ? suffix : "");
} else {
char *colon = strrchr(authority, ':');
if (colon && strchr(authority, ':') == colon) {
*colon = '\0';
result = g_strdup_printf("http://%s.fips:%s%s", authority,
colon + 1, suffix ? suffix : "");
} else {
result = g_strdup_printf("http://%s.fips%s", authority,
suffix ? suffix : "");
}
}
g_free(authority);
return result;
}
static char *normalize_url(const char *input) {
if (input == NULL || input[0] == '\0') return NULL;
/* MCP open must apply the same fips:// shorthand as the interactive
* URL bar; otherwise WebKit sees an unregistered custom scheme. */
if (strncmp(input, "fips://", 7) == 0)
return normalize_fips_url(input);
/* Keep MCP/agent navigation consistent with the interactive URL bar. */
nostr_entity_type_t entity_type = nostr_url_detect(input);
if (entity_type != NOSTR_ENTITY_NONE) {
return nostr_url_normalize(input);
}
/* If it looks like a URL, normalize it. */
if (search_is_url(input)) {
if (strstr(input, "://") != NULL) return g_strdup(input);
if (strncmp(input, "about:", 6) == 0) return g_strdup(input);
if (strncmp(input, "sovereign://", 12) == 0) return g_strdup(input);
return g_strdup_printf("https://%s", input);
}
/* Not a URL — treat as a search query. */
return search_build_search_url(input);
}
/* ── Get active webview (or NULL if not logged in / no tabs) ────────
* When the agent sidebar is open, the "active" webview (the one with
* focus) might be the sidebar chat page. Agent tools must always
* operate on the MAIN webview (the web page), so we use
* tab_manager_get_main_webview() which never returns the sidebar. */
static WebKitWebView *get_active_webview(void) {
return tab_manager_get_main_webview();
}
/* ── Coordinate-based click via GDK event synthesis ────────────────── */
/* Synthesize a real GDK button press + release at (x, y) in the webview's
* GdkWindow coordinate space. This triggers WebKit's native hit-testing
* and full event propagation (pointerdown/click), which is required for
* SPA frameworks (React, Radix UI) that ignore JS synthetic .click() calls.
*
* Coordinates from getBoundingClientRect() are relative to the viewport
* and match the webview's GdkWindow coordinate space when the webview
* fills its parent window. `button` is 1 (left), 2 (middle), or 3 (right).
* Returns TRUE on success. */
static gboolean synthesize_click_at(WebKitWebView *wv, double x, double y,
int button) {
if (wv == NULL) return FALSE;
if (button < 1) button = 1;
GtkWidget *widget = GTK_WIDGET(wv);
GdkWindow *window = gtk_widget_get_window(widget);
if (window == NULL) return FALSE;
/* Process any pending events so the widget is in a consistent state. */
while (gtk_events_pending()) gtk_main_iteration();
/* --- Button press --- */
GdkEvent *press = gdk_event_new(GDK_BUTTON_PRESS);
press->button.window = g_object_ref(window);
press->button.send_event = TRUE;
press->button.time = GDK_CURRENT_TIME;
press->button.x = x;
press->button.y = y;
press->button.axes = NULL;
press->button.state = 0; /* no modifiers */
press->button.button = button;
press->button.device = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_window_get_display(window)));
gdk_event_put(press);
gdk_event_free(press);
/* Small delay between press and release so WebKit treats it as a click. */
g_usleep(10 * 1000); /* 10 ms — g_usleep is GLib's portable usleep */
/* Process the press before sending release. */
while (gtk_events_pending()) gtk_main_iteration();
/* --- Button release --- */
GdkEvent *release = gdk_event_new(GDK_BUTTON_RELEASE);
release->button.window = g_object_ref(window);
release->button.send_event = TRUE;
release->button.time = GDK_CURRENT_TIME;
release->button.x = x;
release->button.y = y;
release->button.axes = NULL;
release->button.state = 0;
release->button.button = button;
release->button.device = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_window_get_display(window)));
gdk_event_put(release);
gdk_event_free(release);
/* Let the release propagate before we return. */
while (gtk_events_pending()) gtk_main_iteration();
return TRUE;
}
/* Synthesize real GDK key press + release events for each character in
* `text`, dispatching them to the webview's GdkWindow. This triggers
* WebKit's native text input handling, which properly fires React's
* onChange handlers (and other SPA framework input listeners) that
* ignore JS synthetic `element.value = ...` + `input` event dispatch.
*
* Returns TRUE if at least one key event was dispatched. */
static gboolean synthesize_type_text(WebKitWebView *wv, const char *text) {
if (wv == NULL || text == NULL) return FALSE;
GtkWidget *widget = GTK_WIDGET(wv);
GdkWindow *window = gtk_widget_get_window(widget);
if (window == NULL) return FALSE;
gboolean dispatched_any = FALSE;
for (const char *p = text; *p != '\0'; p = g_utf8_next_char(p)) {
gunichar uc = g_utf8_get_char(p);
guint keyval;
/* Map special characters to their GDK keyvals. */
if (uc == '\n') {
keyval = GDK_KEY_Return;
} else if (uc == '\t') {
keyval = GDK_KEY_Tab;
} else if (uc == '\r') {
/* Skip carriage returns — handled by \n. */
continue;
} else {
keyval = gdk_unicode_to_keyval(uc);
}
if (keyval == 0) continue;
/* --- Key press --- */
GdkEvent *press = gdk_event_new(GDK_KEY_PRESS);
press->key.window = g_object_ref(window);
press->key.send_event = TRUE;
press->key.time = GDK_CURRENT_TIME;
press->key.state = 0; /* no modifiers */
press->key.keyval = keyval;
press->key.length = 1;
press->key.string = g_strdup_printf("%c", (gchar)(uc & 0xFF));
press->key.group = 0;
press->key.hardware_keycode = 0;
gdk_event_put(press);
gdk_event_free(press);
/* Small delay so WebKit treats press/release as a keystroke. */
g_usleep(5 * 1000); /* 5 ms */
while (gtk_events_pending()) gtk_main_iteration();
/* --- Key release --- */
GdkEvent *release = gdk_event_new(GDK_KEY_RELEASE);
release->key.window = g_object_ref(window);
release->key.send_event = TRUE;
release->key.time = GDK_CURRENT_TIME;
release->key.state = 0;
release->key.keyval = keyval;
release->key.length = 1;
release->key.string = g_strdup_printf("%c", (gchar)(uc & 0xFF));
release->key.group = 0;
release->key.hardware_keycode = 0;
gdk_event_put(release);
gdk_event_free(release);
while (gtk_events_pending()) gtk_main_iteration();
dispatched_any = TRUE;
}
return dispatched_any;
}
/* ── Resolve a ref or selector to a JS element query ──────────────── *
* Stale-ref fallback strategy (for @eN refs):
* 1. Try the stored CSS selector — if it still matches, return it.
* 2. If the selector is stale (page re-rendered), re-resolve by
* role + name: querySelectorAll('[role="ROLE"]') then filter by
* textContent.indexOf("NAME") >= 0. Update the stored selector.
* 3. If role+name fails, fall back to the stored bounding box:
* document.elementFromPoint(centerX, centerY). Update the selector.
* 4. Return NULL if all strategies fail.
*
* On successful re-resolution the new selector is written back into
* window.__agentRefs[refId].selector so subsequent calls are fast. */
static char *resolve_ref_to_selector(const char *ref) {
if (ref == NULL || ref[0] == '\0') return NULL;
/* If it starts with @, it's a ref from snapshot. */
if (ref[0] == '@') {
const char *ref_id = ref + 1;
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return NULL;
char *esc_ref = g_strescape(ref_id, NULL);
char *script = g_strdup_printf(
"(function(){\n"
" var refId = \"%s\";\n"
" var refs = window.__agentRefs;\n"
" if (!refs || !refs[refId]) return null;\n"
" var entry = refs[refId];\n"
" /* 1. Try the stored selector first. */\n"
" if (entry.selector) {\n"
" try { if (document.querySelector(entry.selector)) return entry.selector; } catch(e) {}\n"
" }\n"
" /* Helper: build a unique CSS selector for an element. */\n"
" function uniq(el) {\n"
" if (el.id) return '#' + CSS.escape(el.id);\n"
" var path = [];\n"
" while (el && el.nodeType === 1 && el !== document.documentElement) {\n"
" var index = 1;\n"
" var sib = el.previousElementSibling;\n"
" while (sib) { if (sib.tagName === el.tagName) index++; sib = sib.previousElementSibling; }\n"
" var part = el.tagName.toLowerCase();\n"
" if (el.className && typeof el.className === 'string') {\n"
" var cls = el.className.trim().split(/\\s+/).slice(0,2).join('.');\n"
" if (cls) part += '.' + cls;\n"
" }\n"
" if (index > 1) part += ':nth-of-type(' + index + ')';\n"
" path.unshift(part);\n"
" el = el.parentElement;\n"
" }\n"
" return path.length > 0 ? path.join(' > ') : 'html';\n"
" }\n"
" /* 2. Re-resolve by role + name. */\n"
" if (entry.role && entry.name) {\n"
" var role = entry.role;\n"
" var name = entry.name;\n"
" var candidates = [];\n"
" var byAttr = document.querySelectorAll('[role=\"' + role + '\"]');\n"
" for (var i = 0; i < byAttr.length; i++) candidates.push(byAttr[i]);\n"
" if (candidates.length === 0) {\n"
" var tagMap = { 'button':'button','link':'a','textbox':'input',\n"
" 'combobox':'select','image':'img','heading':'h1',\n"
" 'navigation':'nav','main':'main','list':'ul','listitem':'li' };\n"
" if (tagMap[role]) {\n"
" var byTag = document.querySelectorAll(tagMap[role]);\n"
" for (var j = 0; j < byTag.length; j++) candidates.push(byTag[j]);\n"
" }\n"
" }\n"
" for (var k = 0; k < candidates.length; k++) {\n"
" var c = candidates[k];\n"
" var al = c.getAttribute('aria-label') || '';\n"
" var tc = c.textContent.trim();\n"
" if (al.indexOf(name) >= 0 || tc.indexOf(name) >= 0) {\n"
" var newSel = uniq(c);\n"
" entry.selector = newSel;\n"
" return newSel;\n"
" }\n"
" }\n"
" }\n"
" /* 3. Re-resolve by bounding box via elementFromPoint. */\n"
" if (entry.bbox && entry.bbox.width > 0 && entry.bbox.height > 0) {\n"
" var cx = entry.bbox.x + entry.bbox.width / 2;\n"
" var cy = entry.bbox.y + entry.bbox.height / 2;\n"
" var el = document.elementFromPoint(cx, cy);\n"
" if (el) {\n"
" var newSel = uniq(el);\n"
" entry.selector = newSel;\n"
" return newSel;\n"
" }\n"
" }\n"
" return null;\n"
"})();",
esc_ref);
g_free(esc_ref);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (result == NULL || result[0] == '\0' || strcmp(result, "null") == 0) {
g_free(result);
return NULL;
}
return result;
}
/* Otherwise treat as a CSS selector. */
return g_strdup(ref);
}
/* ── Frame / dialog / debug state ─────────────────────────────────── */
/* Current iframe selector (NULL = main frame).
* Other JS-based tools should wrap their JS in the frame's contentDocument
* when this is set. For this batch we only store/clear the selector. */
static char *g_current_frame_selector = NULL;
/* JS to override window.alert/confirm/prompt so the agent can observe
* and resolve dialogs without a native modal. Injected lazily by
* dialog_status / dialog_accept / dialog_dismiss. */
static const char *DIALOG_OVERRIDE_JS =
"(function(){"
" if (window.__agentDialogInstalled) return 'installed';"
" window.__pendingDialog = null;"
" window.alert = function(msg){ window.__pendingDialog = {type:'alert', message:String(msg)}; };"
" window.confirm = function(msg){ window.__pendingDialog = {type:'confirm', message:String(msg), result:null}; return false; };"
" window.prompt = function(msg, def){ window.__pendingDialog = {type:'prompt', message:String(msg), default:def!=null?String(def):'', result:null}; return null; };"
" window.__agentDialogInstalled = true;"
" return 'installed';"
"})();";
/* JS to install a window error listener that collects page errors. */
static const char *ERROR_HANDLER_JS =
"(function(){"
" if (window.__agentErrorInstalled) return 'installed';"
" window.__pageErrors = [];"
" window.addEventListener('error', function(e){"
" window.__pageErrors.push({message:e.message||'', filename:e.filename||'', line:e.lineno||0});"
" });"
" window.__agentErrorInstalled = true;"
" return 'installed';"
"})();";
/* JS to install a console.* capture that mirrors messages into
* window.__pageConsole. WebKitGTK 4.1 has no C-level console signal,
* so we hook the JS console API directly. */
static const char *CONSOLE_HOOK_JS =
"(function(){"
" if (window.__agentConsoleInstalled) return 'installed';"
" window.__pageConsole = window.__pageConsole || [];"
" var orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };"
" function push(level, args){"
" var text = Array.prototype.map.call(args, function(a){"
" try { return typeof a === 'object' ? JSON.stringify(a) : String(a); } catch(e){ return String(a); }"
" }).join(' ');"
" window.__pageConsole.push({level: level, text: text});"
" }"
" console.log = function(){ push('log', arguments); orig.log.apply(console, arguments); };"
" console.info = function(){ push('info', arguments); orig.info.apply(console, arguments); };"
" console.warn = function(){ push('warning', arguments); orig.warn.apply(console, arguments); };"
" console.error = function(){ push('error', arguments); orig.error.apply(console, arguments); };"
" console.debug = function(){ push('debug', arguments); orig.debug.apply(console, arguments); };"
" window.__agentConsoleInstalled = true;"
" return 'installed';"
"})();";
/* ── Login tools ──────────────────────────────────────────────────── */
static cJSON *tool_login_status(cJSON *params) {
(void)params;
return agent_login_status();
}
static cJSON *tool_login(cJSON *params) {
cJSON *result = agent_login(params);
/* If login succeeded, notify the server so main.c can proceed. */
if (cJSON_IsTrue(cJSON_GetObjectItem(result, "success"))) {
agent_server_notify_login();
}
return result;
}
static cJSON *tool_logout(cJSON *params) {
(void)params;
return agent_logout();
}
static cJSON *tool_switch_identity(cJSON *params) {
cJSON *result = agent_switch_identity(params);
if (cJSON_IsTrue(cJSON_GetObjectItem(result, "success"))) {
agent_server_notify_login();
}
return result;
}
/* ── Navigation tools ─────────────────────────────────────────────── */
/* Idle callback data for main-thread webview navigation. WebKitGTK is
* NOT thread-safe — webkit_web_view_load_uri() must be called on the
* GTK main thread. The agent loop runs on a background thread, so we
* hop to the main thread via g_idle_add() to perform the navigation.
* (Issue 3: calling webkit_web_view_load_uri() directly from the
* agent-loop background thread caused an "Aborted" segfault.) */
typedef struct {
WebKitWebView *webview;
char *url;
} load_uri_idle_t;
static gboolean load_uri_idle(gpointer user_data) {
load_uri_idle_t *ctx = (load_uri_idle_t *)user_data;
if (ctx && ctx->webview && ctx->url) {
webkit_web_view_load_uri(ctx->webview, ctx->url);
}
if (ctx) {
g_free(ctx->url);
g_free(ctx);
}
return G_SOURCE_REMOVE;
}
static cJSON *tool_open(cJSON *params) {
const char *url = get_string_param(params, "url");
if (!url || !url[0]) return make_error("MISSING_PARAM", "Provide 'url'");
char *normalized = normalize_url(url);
if (!normalized) return make_error("INVALID_URL", "Invalid URL");
/* Always operate on the MAIN webview (the web page), never the
* sidebar chat webview. tab_manager_get_main_webview() returns
* the active tab's webview directly, never the sidebar. */
WebKitWebView *wv = tab_manager_get_main_webview();
if (!wv) {
g_free(normalized);
return make_error("NO_TAB", "No active tab");
}
/* Dispatch the load to the GTK main thread. The agent loop (and
* the libsoup MCP thread) are not the main thread, so calling
* webkit_web_view_load_uri() directly would crash. g_idle_add()
* schedules the call on the default main context, which is run by
* the GTK main loop on the main thread. */
load_uri_idle_t *ctx = g_new(load_uri_idle_t, 1);
ctx->webview = wv;
ctx->url = g_strdup(normalized);
g_idle_add(load_uri_idle, ctx);
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "url", normalized);
g_free(normalized);
return make_success(data);
}
/* Idle callback data for simple main-thread webview actions (back,
* forward, reload). Like tool_open, these must run on the GTK main
* thread because WebKitGTK is not thread-safe and the agent loop
* runs on a background thread. */
typedef enum {
WEBVIEW_ACTION_BACK,
WEBVIEW_ACTION_FORWARD,
WEBVIEW_ACTION_RELOAD,
WEBVIEW_ACTION_STOP
} webview_action_t;
typedef struct {
WebKitWebView *webview;
webview_action_t action;
} webview_action_idle_t;
static gboolean webview_action_idle(gpointer user_data) {
webview_action_idle_t *ctx = (webview_action_idle_t *)user_data;
if (ctx && ctx->webview) {
switch (ctx->action) {
case WEBVIEW_ACTION_BACK:
if (webkit_web_view_can_go_back(ctx->webview))
webkit_web_view_go_back(ctx->webview);
break;
case WEBVIEW_ACTION_FORWARD:
if (webkit_web_view_can_go_forward(ctx->webview))
webkit_web_view_go_forward(ctx->webview);
break;
case WEBVIEW_ACTION_RELOAD:
webkit_web_view_reload_bypass_cache(ctx->webview);
break;
case WEBVIEW_ACTION_STOP:
webkit_web_view_stop_loading(ctx->webview);
break;
}
}
g_free(ctx);
return G_SOURCE_REMOVE;
}
static cJSON *tool_back(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
webview_action_idle_t *ctx = g_new(webview_action_idle_t, 1);
ctx->webview = wv;
ctx->action = WEBVIEW_ACTION_BACK;
g_idle_add(webview_action_idle, ctx);
return make_success(NULL);
}
static cJSON *tool_forward(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
webview_action_idle_t *ctx = g_new(webview_action_idle_t, 1);
ctx->webview = wv;
ctx->action = WEBVIEW_ACTION_FORWARD;
g_idle_add(webview_action_idle, ctx);
return make_success(NULL);
}
static cJSON *tool_reload(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
webview_action_idle_t *ctx = g_new(webview_action_idle_t, 1);
ctx->webview = wv;
ctx->action = WEBVIEW_ACTION_RELOAD;
g_idle_add(webview_action_idle, ctx);
return make_success(NULL);
}
/* Stop loading the active tab. Mirrors the toolbar stop button and the
* hamburger menu's Stop action. Useful for testing the per-tab
* reload/stop button state machine. */
static cJSON *tool_stop(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
webview_action_idle_t *ctx = g_new(webview_action_idle_t, 1);
ctx->webview = wv;
ctx->action = WEBVIEW_ACTION_STOP;
g_idle_add(webview_action_idle, ctx);
return make_success(NULL);
}
static cJSON *tool_get_url(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
const gchar *uri = webkit_web_view_get_uri(wv);
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "url", uri ? uri : "");
return make_success(data);
}
static cJSON *tool_get_title(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
const gchar *title = webkit_web_view_get_title(wv);
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "title", title ? title : "");
return make_success(data);
}
/* ── Snapshot & inspection tools ──────────────────────────────────── */
static cJSON *tool_snapshot(cJSON *params) {
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
gboolean interactive = get_bool_param(params, "interactive", FALSE);
gboolean compact = get_bool_param(params, "compact", FALSE);
cJSON *result = agent_snapshot_take(wv, interactive, compact);
if (!result) return make_error("SNAPSHOT_FAILED", "Failed to take snapshot");
return make_success(result);
}
static cJSON *tool_get_text(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function() { var el = document.querySelector(%s); "
"return el ? el.textContent.trim() : null; })();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "text", result);
g_free(result);
return make_success(data);
}
static cJSON *tool_get_html(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function() { var el = document.querySelector(%s); "
"return el ? el.innerHTML : null; })();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "html", result);
g_free(result);
return make_success(data);
}
static cJSON *tool_get_attr(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
const char *attr = get_string_param(params, "attr");
if (!attr || !attr[0]) return make_error("MISSING_PARAM", "Provide 'attr'");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function() { var el = document.querySelector(%s); "
"return el ? (el.getAttribute('%s') || '') : null; })();",
sel, attr);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "attr", attr);
cJSON_AddStringToObject(data, "value", result);
g_free(result);
return make_success(data);
}
static cJSON *tool_get_value(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');return el?el.value:null;})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "value", result);
g_free(result);
return make_success(data);
}
static cJSON *tool_get_count(cJSON *params) {
const char *selector = get_string_param(params, "selector");
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"String(document.querySelectorAll('%s').length);",
selector);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed");
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "count", atoi(result));
g_free(result);
return make_success(data);
}
static cJSON *tool_get_box(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"var r=el.getBoundingClientRect();"
"return JSON.stringify({x:r.x,y:r.y,width:r.width,height:r.height,"
"top:r.top,right:r.right,bottom:r.bottom,left:r.left});})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse bounding box JSON");
return make_success(data);
}
static cJSON *tool_get_styles(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"var s=getComputedStyle(el);var result={};"
"for(var i=0;i<s.length;i++){var p=s[i];result[p]=s.getPropertyValue(p);}"
"return JSON.stringify(result);})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *styles = cJSON_Parse(result);
g_free(result);
if (!styles) return make_error("PARSE_ERROR", "Failed to parse computed styles JSON");
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "styles", styles);
return make_success(data);
}
static cJSON *tool_is_visible(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"var s=getComputedStyle(el);"
"return (s.display!=='none'&&s.visibility!=='hidden'&&"
"s.opacity!=='0'&&el.offsetParent!==null)?'true':'false';})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "visible", strcmp(result, "true") == 0);
g_free(result);
return make_success(data);
}
static cJSON *tool_is_enabled(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"return !el.disabled?'true':'false';})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "enabled", strcmp(result, "true") == 0);
g_free(result);
return make_success(data);
}
static cJSON *tool_is_checked(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"return el.checked?'true':'false';})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "checked", strcmp(result, "true") == 0);
g_free(result);
return make_success(data);
}
static cJSON *tool_eval(cJSON *params) {
const char *script_param = get_string_param(params, "script");
if (!script_param || !script_param[0]) return make_error("MISSING_PARAM", "Provide 'script'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *result = agent_js_eval_sync(wv, script_param, 10000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "result", result);
g_free(result);
return make_success(data);
}
/* ── Screenshot tool ──────────────────────────────────────────────── */
/* Context for the async webkit_web_view_snapshot() call. We use a
* async API into a synchronous one.
*
* We run a nested GMainLoop on the default main context (the standard
* GTK modal pattern) but do NOT call g_main_context_acquire()/
* g_main_context_release() around it — the default context is already
* owned by the outer dispatch (these tools are invoked from the MCP
* HTTP handler), so an explicit acquire fails silently and a matching
* release corrupts the owner_count, causing a segfault. The nested
* g_main_loop_run() handles ownership internally. See the block comment
* in agent_js_eval_sync() for the full rationale. */
typedef struct {
cairo_surface_t *surface;
gboolean done;
GMainLoop *loop;
} snapshot_ctx_t;
/* GAsyncReadyCallback for webkit_web_view_get_snapshot() */
static void snapshot_callback(GObject *source, GAsyncResult *res, gpointer user_data) {
snapshot_ctx_t *ctx = (snapshot_ctx_t *)user_data;
WebKitWebView *wv = WEBKIT_WEB_VIEW(source);
GError *error = NULL;
ctx->surface = webkit_web_view_get_snapshot_finish(wv, res, &error);
if (error != NULL) {
g_printerr("[agent] screenshot get_snapshot_finish error: %s\n", error->message);
g_error_free(error);
}
ctx->done = TRUE;
if (ctx->loop && g_main_loop_is_running(ctx->loop)) {
g_main_loop_quit(ctx->loop);
}
}
/* Timeout callback for the snapshot nested loop */
static gboolean snapshot_timeout_cb(gpointer user_data) {
snapshot_ctx_t *ctx = (snapshot_ctx_t *)user_data;
if (!ctx->done) {
g_printerr("[agent] screenshot timed out\n");
ctx->done = TRUE;
if (ctx->loop && g_main_loop_is_running(ctx->loop)) {
g_main_loop_quit(ctx->loop);
}
}
return G_SOURCE_REMOVE;
}
/* cairo_write_func_t that appends bytes to a GByteArray */
static cairo_status_t png_write_cb(void *closure, const unsigned char *data, unsigned int length) {
GByteArray *buf = (GByteArray *)closure;
g_byte_array_append(buf, data, (guint)length);
return CAIRO_STATUS_SUCCESS;
}
static cJSON *tool_screenshot(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Nested GMainLoop on the default context. No acquire/release —
* see the note on snapshot_ctx_t above. */
snapshot_ctx_t ctx = {0};
ctx.loop = g_main_loop_new(g_main_context_default(), FALSE);
guint timeout_id = g_timeout_add(10000, snapshot_timeout_cb, &ctx);
/* Kick off the async snapshot. We capture the visible region of
* the webview with no special options. */
webkit_web_view_get_snapshot(wv,
WEBKIT_SNAPSHOT_REGION_VISIBLE,
WEBKIT_SNAPSHOT_OPTIONS_NONE,
NULL, snapshot_callback, &ctx);
/* Run the nested loop until the snapshot completes or times out. */
g_main_loop_run(ctx.loop);
g_source_remove(timeout_id);
g_main_loop_unref(ctx.loop);
if (!ctx.done || ctx.surface == NULL) {
if (ctx.surface) cairo_surface_destroy(ctx.surface);
return make_error("SCREENSHOT_FAILED", "Failed to capture page snapshot");
}
/* Encode the cairo surface to PNG in memory. */
GByteArray *png_buf = g_byte_array_new();
cairo_status_t status = cairo_surface_write_to_png_stream(ctx.surface,
png_write_cb, png_buf);
/* Get dimensions before destroying the surface. */
int width = cairo_image_surface_get_width(ctx.surface);
int height = cairo_image_surface_get_height(ctx.surface);
cairo_surface_destroy(ctx.surface);
if (status != CAIRO_STATUS_SUCCESS || png_buf->len == 0) {
g_printerr("[agent] screenshot PNG encode failed: %s\n",
cairo_status_to_string(status));
g_byte_array_free(png_buf, TRUE);
return make_error("SCREENSHOT_FAILED", "Failed to encode PNG");
}
/* Base64-encode the PNG bytes. */
gchar *b64 = g_base64_encode(png_buf->data, png_buf->len);
g_byte_array_free(png_buf, TRUE);
if (!b64) return make_error("SCREENSHOT_FAILED", "Failed to base64-encode PNG");
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "screenshot", b64);
cJSON_AddStringToObject(data, "mimeType", "image/png");
cJSON_AddNumberToObject(data, "width", width);
cJSON_AddNumberToObject(data, "height", height);
g_free(b64);
return make_success(data);
}
/* ── Interaction tools ────────────────────────────────────────────── */
static cJSON *tool_click(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
/* --- Coordinate-based click via GDK event synthesis ---
* Resolve the element's bounding box via getBoundingClientRect(),
* then synthesize a real GDK button press/release at the center.
* This triggers WebKit's native hit-testing and full event
* propagation, which is required for SPA frameworks (React, Radix
* UI) that ignore JS synthetic .click() calls. Falls back to the
* JS .click() approach if the bounding box cannot be retrieved. */
/* Escape the selector for safe embedding in a JS string literal.
* g_strescape() escapes backslashes and quotes, which handles
* selectors containing brackets like min-h-[60vh]. */
char *esc_sel = g_strescape(sel, NULL);
char *box_js = g_strdup_printf(
"(function(){var el=document.querySelector(\"%s\");"
"if(!el)return null;"
"var r=el.getBoundingClientRect();"
"return JSON.stringify({x:r.x,y:r.y,width:r.width,height:r.height});"
"})();",
esc_sel);
g_free(esc_sel);
char *box_result = agent_js_eval_sync(wv, box_js, 5000);
g_free(box_js);
if (box_result && box_result[0] != '\0' && strcmp(box_result, "null") != 0) {
cJSON *box = cJSON_Parse(box_result);
g_free(box_result);
if (box) {
cJSON *jx = cJSON_GetObjectItem(box, "x");
cJSON *jy = cJSON_GetObjectItem(box, "y");
cJSON *jw = cJSON_GetObjectItem(box, "width");
cJSON *jh = cJSON_GetObjectItem(box, "height");
if (jx && jy && jw && jh && cJSON_IsNumber(jx) && cJSON_IsNumber(jy) &&
cJSON_IsNumber(jw) && cJSON_IsNumber(jh)) {
double cx = jx->valuedouble + jw->valuedouble / 2.0;
double cy = jy->valuedouble + jh->valuedouble / 2.0;
cJSON_Delete(box);
g_free(sel);
if (synthesize_click_at(wv, cx, cy, 1)) {
return make_success(NULL);
}
/* Fall through to JS fallback if GDK synthesis failed. */
/* Re-resolve sel since we freed it above. */
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("CLICK_FAILED", "GDK event synthesis failed and no selector for fallback");
}
if (!sel) return make_error("CLICK_FAILED", "GDK event synthesis failed");
wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
} else {
cJSON_Delete(box);
}
}
} else {
g_free(box_result);
}
/* --- Fallback: JS .click() --- */
char *script = g_strdup_printf(
"(function() { var el = document.querySelector('%s'); "
"if (el) { el.click(); return 'ok'; } return null; })();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
/* click_at — Click at explicit viewport coordinates via GDK event
* synthesis. Useful when the agent already knows the target point
* (e.g. from a screenshot or get_box result) and wants to bypass
* selector resolution entirely. Optional 'button': 1=left (default),
* 2=middle, 3=right (triggers the native context menu). */
static int click_button_from_params(cJSON *params) {
cJSON *jb = cJSON_GetObjectItem(params, "button");
if (jb && cJSON_IsNumber(jb)) {
int b = jb->valueint;
if (b >= 1 && b <= 3) return b;
}
return 1;
}
static cJSON *tool_click_at(cJSON *params) {
cJSON *jx = cJSON_GetObjectItem(params, "x");
cJSON *jy = cJSON_GetObjectItem(params, "y");
if (!jx || !jy || !cJSON_IsNumber(jx) || !cJSON_IsNumber(jy)) {
return make_error("MISSING_PARAM", "Provide numeric 'x' and 'y' coordinates");
}
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
double x = jx->valuedouble;
double y = jy->valuedouble;
int button = click_button_from_params(params);
if (!synthesize_click_at(wv, x, y, button)) {
return make_error("CLICK_FAILED", "Failed to synthesize GDK click event");
}
return make_success(NULL);
}
static cJSON *tool_fill(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
/* Focus the element and clear its value via JS, then type via GDK key
* events for SPA compatibility. */
char *esc_sel = g_strescape(sel, NULL);
char *focus_js = g_strdup_printf(
"(function(){var el=document.querySelector(\"%s\");if(!el)return 'nofocus';"
"el.focus();el.value=\"\";"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"return 'ok';})();",
esc_sel);
g_free(esc_sel);
char *focus_result = agent_js_eval_sync(wv, focus_js, 5000);
g_free(focus_js);
gboolean focused = (focus_result && strcmp(focus_result, "ok") == 0);
g_free(focus_result);
if (focused && synthesize_type_text(wv, value)) {
g_free(sel);
return make_success(NULL);
}
/* Fallback: legacy JS .value= approach (with proper selector quoting). */
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function() { var el = document.querySelector(\"%s\"); "
"if (el) { el.value = \"%s\"; el.dispatchEvent(new Event('input', {bubbles:true})); "
"el.dispatchEvent(new Event('change', {bubbles:true})); return 'ok'; } return null; })();",
sel, escaped);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(escaped);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_type(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
const char *value = get_string_param(params, "value");
gboolean do_clear = get_bool_param(params, "clear", FALSE);
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
/* Focus the element via JS, then type via GDK key events. */
char *esc_sel = g_strescape(sel, NULL);
char *focus_js = g_strdup_printf(
"(function(){var el=document.querySelector(\"%s\");if(!el)return 'nofocus';"
"el.focus();%s"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"return 'ok';})();",
esc_sel,
do_clear ? "el.value=\"\";" : "");
g_free(esc_sel);
char *focus_result = agent_js_eval_sync(wv, focus_js, 5000);
g_free(focus_js);
gboolean focused = (focus_result && strcmp(focus_result, "ok") == 0);
g_free(focus_result);
if (focused && synthesize_type_text(wv, value)) {
g_free(sel);
return make_success(NULL);
}
/* Fallback: legacy JS .value+= approach (with proper selector quoting). */
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function() { var el = document.querySelector(\"%s\"); "
"if (el) { el.value += \"%s\"; el.dispatchEvent(new Event('input', {bubbles:true})); "
"return 'ok'; } return null; })();",
sel, escaped);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(escaped);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_press(cJSON *params) {
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key' (e.g. 'Enter', 'Tab', 'Escape')");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Map common key names to key codes. */
char *script = g_strdup_printf(
"(function() { var key = '%s'; "
"var ev = new KeyboardEvent('keydown', {key: key, bubbles: true}); "
"document.dispatchEvent(ev); "
"ev = new KeyboardEvent('keyup', {key: key, bubbles: true}); "
"document.dispatchEvent(ev); return 'ok'; })();",
key);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("PRESS_FAILED", "Failed to press key");
g_free(result);
return make_success(NULL);
}
static cJSON *tool_scroll(cJSON *params) {
const char *direction = get_string_param(params, "direction");
int amount = get_int_param(params, "amount", 500);
if (!direction || !direction[0]) return make_error("MISSING_PARAM", "Provide 'direction' (up/down/left/right)");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
int dx = 0, dy = 0;
if (strcmp(direction, "down") == 0) dy = amount;
else if (strcmp(direction, "up") == 0) dy = -amount;
else if (strcmp(direction, "right") == 0) dx = amount;
else if (strcmp(direction, "left") == 0) dx = -amount;
else return make_error("INVALID_DIRECTION", "Use: up, down, left, right");
char *script = g_strdup_printf("window.scrollBy(%d, %d); 'ok';", dx, dy);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("SCROLL_FAILED", "Failed to scroll");
g_free(result);
return make_success(NULL);
}
static cJSON *tool_hover(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function() { var el = document.querySelector(%s); "
"if (el) { el.dispatchEvent(new MouseEvent('mouseover', {bubbles:true})); "
"el.dispatchEvent(new MouseEvent('mouseenter', {bubbles:true})); return 'ok'; } "
"return null; })();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_focus(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function() { var el = document.querySelector(%s); "
"if (el) { el.focus(); return 'ok'; } return null; })();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
/* ── Extended interaction tools ───────────────────────────────────── */
static cJSON *tool_dblclick(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){"
"el.dispatchEvent(new MouseEvent('dblclick',{bubbles:true}));return 'ok';}return null;})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_select(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){el.value=\"%s\";"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"el.dispatchEvent(new Event('change',{bubbles:true}));return 'ok';}return null;})();",
sel, escaped);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(escaped);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_check(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el&&el.type==='checkbox'){"
"el.checked=true;el.dispatchEvent(new Event('change',{bubbles:true}));return 'ok';}return null;})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_uncheck(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el&&el.type==='checkbox'){"
"el.checked=false;el.dispatchEvent(new Event('change',{bubbles:true}));return 'ok';}return null;})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_scroll_into_view(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){"
"el.scrollIntoView({behavior:'smooth',block:'center'});return 'ok';}return null;})();",
sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_keyboard_type(cJSON *params) {
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){var el=document.activeElement;if(!el)return null;var text=\"%s\";"
"for(var i=0;i<text.length;i++){var ch=text[i];"
"el.dispatchEvent(new KeyboardEvent('keydown',{key:ch,bubbles:true}));"
"el.dispatchEvent(new KeyboardEvent('keypress',{key:ch,bubbles:true}));"
"if(el.value!==undefined)el.value+=ch;"
"el.dispatchEvent(new KeyboardEvent('keyup',{key:ch,bubbles:true}));}"
"el.dispatchEvent(new Event('input',{bubbles:true}));return 'ok';})();",
escaped);
char *result = agent_js_eval_sync(wv, script, 10000);
g_free(script);
g_free(escaped);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No focused element to type into");
}
g_free(result);
return make_success(NULL);
}
static cJSON *tool_insert_text(cJSON *params) {
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){document.execCommand('insertText',false,\"%s\");return 'ok';})();",
escaped);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(escaped);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed");
g_free(result);
return make_success(NULL);
}
static cJSON *tool_keydown(cJSON *params) {
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"(function(){var ev=new KeyboardEvent('keydown',{key:'%s',bubbles:true});"
"document.dispatchEvent(ev);return 'ok';})();",
key);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "Failed to dispatch keydown");
g_free(result);
return make_success(NULL);
}
static cJSON *tool_keyup(cJSON *params) {
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"(function(){var ev=new KeyboardEvent('keyup',{key:'%s',bubbles:true});"
"document.dispatchEvent(ev);return 'ok';})();",
key);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "Failed to dispatch keyup");
g_free(result);
return make_success(NULL);
}
static cJSON *tool_drag(cJSON *params) {
const char *src_ref = get_string_param(params, "src_ref");
const char *src_selector = get_string_param(params, "src_selector");
const char *tgt_ref = get_string_param(params, "tgt_ref");
const char *tgt_selector = get_string_param(params, "tgt_selector");
char *src_sel = NULL;
if (src_ref && src_ref[0]) {
src_sel = resolve_ref_to_selector(src_ref);
if (!src_sel) return make_error("REF_NOT_FOUND", "No source element with that ref. Take a new snapshot.");
} else if (src_selector && src_selector[0]) {
src_sel = g_strdup(src_selector);
} else {
return make_error("MISSING_PARAM", "Provide 'src_ref' or 'src_selector'");
}
char *tgt_sel = NULL;
if (tgt_ref && tgt_ref[0]) {
tgt_sel = resolve_ref_to_selector(tgt_ref);
if (!tgt_sel) { g_free(src_sel); return make_error("REF_NOT_FOUND", "No target element with that ref. Take a new snapshot."); }
} else if (tgt_selector && tgt_selector[0]) {
tgt_sel = g_strdup(tgt_selector);
} else {
g_free(src_sel);
return make_error("MISSING_PARAM", "Provide 'tgt_ref' or 'tgt_selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(src_sel); g_free(tgt_sel); return make_error("NO_TAB", "No active tab"); }
char *script = g_strdup_printf(
"(function(){var src=document.querySelector('%s');var tgt=document.querySelector('%s');"
"if(!src||!tgt)return null;"
"src.dispatchEvent(new DragEvent('dragstart',{bubbles:true}));"
"tgt.dispatchEvent(new DragEvent('dragenter',{bubbles:true}));"
"tgt.dispatchEvent(new DragEvent('dragover',{bubbles:true}));"
"tgt.dispatchEvent(new DragEvent('drop',{bubbles:true}));"
"src.dispatchEvent(new DragEvent('dragend',{bubbles:true}));return 'ok';})();",
src_sel, tgt_sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(src_sel);
g_free(tgt_sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "Source or target element not found");
}
g_free(result);
return make_success(NULL);
}
/* ── Tab tools (main-thread dispatch) ────────────────────────────── *
* All tab management functions (new, switch, close, close_all) create
* or destroy GTK widgets, which MUST happen on the main thread. The
* agent loop runs on a background thread, so we dispatch via
* g_idle_add(). */
typedef struct {
char *url;
int index;
} tab_idle_t;
static gboolean tab_new_idle(gpointer user_data) {
tab_idle_t *ctx = (tab_idle_t *)user_data;
if (ctx) {
ctx->index = tab_manager_new_tab(ctx->url);
g_free(ctx->url);
g_free(ctx);
}
return G_SOURCE_REMOVE;
}
static gboolean tab_switch_idle(gpointer user_data) {
tab_idle_t *ctx = (tab_idle_t *)user_data;
if (ctx) {
tab_manager_switch_to(ctx->index);
g_free(ctx);
}
return G_SOURCE_REMOVE;
}
static gboolean tab_close_idle(gpointer user_data) {
tab_idle_t *ctx = (tab_idle_t *)user_data;
if (ctx) {
tab_manager_close_tab(ctx->index);
g_free(ctx);
}
return G_SOURCE_REMOVE;
}
static gboolean close_all_idle(gpointer user_data) {
(void)user_data;
tab_manager_close_all();
return G_SOURCE_REMOVE;
}
static gboolean close_active_idle(gpointer user_data) {
(void)user_data;
tab_manager_close_active();
return G_SOURCE_REMOVE;
}
static cJSON *tool_close_all(cJSON *params) {
(void)params;
g_idle_add(close_all_idle, NULL);
return make_success(NULL);
}
static cJSON *tool_tab_list(cJSON *params) {
(void)params;
int count = tab_manager_count();
cJSON *tabs = cJSON_CreateArray();
for (int i = 0; i < count; i++) {
tab_info_t *tab = tab_manager_get(i);
if (tab) {
cJSON *t = cJSON_CreateObject();
cJSON_AddNumberToObject(t, "index", i);
cJSON_AddStringToObject(t, "url", tab->current_url);
cJSON_AddStringToObject(t, "title", tab->title);
cJSON_AddItemToArray(tabs, t);
}
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "tabs", tabs);
return make_success(data);
}
static cJSON *tool_tab_new(cJSON *params) {
const char *url = get_string_param(params, "url");
/* Dispatch to main thread — tab_manager_new_tab() creates GTK
* widgets which must happen on the main thread. */
tab_idle_t *ctx = g_new(tab_idle_t, 1);
ctx->url = url ? g_strdup(url) : NULL;
ctx->index = -1;
g_idle_add(tab_new_idle, ctx);
/* We can't know the exact index since the creation is async, but
* the tab will be created. Return a placeholder. */
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "index", tab_manager_count());
return make_success(data);
}
static cJSON *tool_tab_switch(cJSON *params) {
int index = get_int_param(params, "index", -1);
if (index < 0) return make_error("MISSING_PARAM", "Provide 'index'");
if (index >= tab_manager_count()) return make_error("INVALID_INDEX", "Tab index out of range");
tab_idle_t *ctx = g_new(tab_idle_t, 1);
ctx->index = index;
g_idle_add(tab_switch_idle, ctx);
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "index", index);
return make_success(data);
}
static cJSON *tool_tab_close(cJSON *params) {
int index = get_int_param(params, "index", -1);
if (index < 0) {
/* Close active tab if no index specified. */
index = tab_manager_get_active_index();
if (index < 0) return make_error("NO_TAB", "No active tab");
}
if (index >= tab_manager_count()) return make_error("INVALID_INDEX", "Tab index out of range");
tab_idle_t *ctx = g_new(tab_idle_t, 1);
ctx->index = index;
g_idle_add(tab_close_idle, ctx);
return make_success(NULL);
}
static cJSON *tool_close(cJSON *params) {
(void)params;
g_idle_add(close_active_idle, NULL);
return make_success(NULL);
}
/* ── Wait tools ───────────────────────────────────────────────────── */
static cJSON *tool_wait(cJSON *params) {
int ms = get_int_param(params, "ms", 1000);
if (ms < 0) ms = 0;
if (ms > 30000) ms = 30000; /* cap at 30s */
g_usleep(ms * 1000);
return make_success(NULL);
}
static cJSON *tool_wait_for(cJSON *params) {
const char *selector = get_string_param(params, "selector");
int timeout_ms = get_int_param(params, "timeout", 10000);
if (timeout_ms > 30000) timeout_ms = 30000;
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Poll for the element to appear. */
int elapsed = 0;
int interval = 200;
while (elapsed < timeout_ms) {
char *script = g_strdup_printf(
"document.querySelector('%s') ? 'found' : 'not_found';",
selector);
char *result = agent_js_eval_sync(wv, script, 2000);
g_free(script);
if (result && strcmp(result, "found") == 0) {
g_free(result);
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "waited_ms", elapsed);
return make_success(data);
}
g_free(result);
g_usleep(interval * 1000);
elapsed += interval;
}
return make_error("TIMEOUT", "Element did not appear within timeout");
}
/* ── wait_for_text — poll until text appears in document.body.innerText ── */
static cJSON *tool_wait_for_text(cJSON *params) {
const char *text = get_string_param(params, "text");
int timeout_ms = get_int_param(params, "timeout", 10000);
if (timeout_ms > 30000) timeout_ms = 30000;
if (!text || !text[0]) return make_error("MISSING_PARAM", "Provide 'text'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Escape the text for embedding in a JS string literal. */
char *escaped = g_strescape(text, NULL);
int elapsed = 0;
int interval = 200;
while (elapsed < timeout_ms) {
char *script = g_strdup_printf(
"document.body.innerText.includes(\"%s\") ? 'found' : 'not_found';",
escaped);
char *result = agent_js_eval_sync(wv, script, 2000);
g_free(script);
if (result && strcmp(result, "found") == 0) {
g_free(result);
g_free(escaped);
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "waited_ms", elapsed);
return make_success(data);
}
g_free(result);
g_usleep(interval * 1000);
elapsed += interval;
}
g_free(escaped);
return make_error("TIMEOUT", "Text did not appear within timeout");
}
/* ── wait_for_url — poll until the page URL matches a pattern ─────────── */
static cJSON *tool_wait_for_url(cJSON *params) {
const char *url = get_string_param(params, "url");
int timeout_ms = get_int_param(params, "timeout", 10000);
if (timeout_ms > 30000) timeout_ms = 30000;
gboolean use_regex = get_bool_param(params, "regex", FALSE);
if (!url || !url[0]) return make_error("MISSING_PARAM", "Provide 'url'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
int elapsed = 0;
int interval = 200;
while (elapsed < timeout_ms) {
const char *current_uri = webkit_web_view_get_uri(wv);
gboolean matched = FALSE;
if (current_uri) {
if (use_regex) {
matched = g_regex_match_simple(url, current_uri, 0, 0);
} else {
matched = (strstr(current_uri, url) != NULL);
}
}
if (matched) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "waited_ms", elapsed);
cJSON_AddStringToObject(data, "url", current_uri);
return make_success(data);
}
g_usleep(interval * 1000);
elapsed += interval;
}
return make_error("TIMEOUT", "URL did not match within timeout");
}
/* ── wait_for_load — poll until the page finishes loading ─────────────── */
static cJSON *tool_wait_for_load(cJSON *params) {
int timeout_ms = get_int_param(params, "timeout", 10000);
if (timeout_ms > 30000) timeout_ms = 30000;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
int elapsed = 0;
int interval = 200;
while (elapsed < timeout_ms) {
if (!webkit_web_view_is_loading(wv)) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "waited_ms", elapsed);
return make_success(data);
}
g_usleep(interval * 1000);
elapsed += interval;
}
return make_error("TIMEOUT", "Page did not finish loading within timeout");
}
/* ── wait_for_fn — poll until a JS expression evaluates truthy ────────── */
static cJSON *tool_wait_for_fn(cJSON *params) {
const char *script_param = get_string_param(params, "script");
int timeout_ms = get_int_param(params, "timeout", 10000);
if (timeout_ms > 30000) timeout_ms = 30000;
if (!script_param || !script_param[0]) return make_error("MISSING_PARAM", "Provide 'script'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
int elapsed = 0;
int interval = 200;
while (elapsed < timeout_ms) {
char *script = g_strdup_printf(
"(function(){try{return !!(%s);}catch(e){return false;}})() ? 'true' : 'false';",
script_param);
char *result = agent_js_eval_sync(wv, script, 2000);
g_free(script);
if (result && strcmp(result, "true") == 0) {
g_free(result);
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "waited_ms", elapsed);
return make_success(data);
}
g_free(result);
g_usleep(interval * 1000);
elapsed += interval;
}
return make_error("TIMEOUT", "Script condition did not become true within timeout");
}
/* ── batch — execute multiple tool commands in sequence ───────────────── */
static cJSON *tool_batch(cJSON *params) {
cJSON *commands = cJSON_GetObjectItem(params, "commands");
if (!commands || !cJSON_IsArray(commands)) {
return make_error("MISSING_PARAM", "Provide 'commands' array");
}
gboolean continue_on_error = get_bool_param(params, "continueOnError", FALSE);
cJSON *results = cJSON_CreateArray();
cJSON *command;
cJSON_ArrayForEach(command, commands) {
/* Build a dispatch request from the command. */
cJSON *req = cJSON_CreateObject();
cJSON *tool = cJSON_GetObjectItem(command, "tool");
cJSON *cmd_params = cJSON_GetObjectItem(command, "params");
cJSON *id = cJSON_GetObjectItem(command, "id");
if (tool) cJSON_AddItemReferenceToObject(req, "tool", tool);
if (cmd_params) cJSON_AddItemReferenceToObject(req, "params", cmd_params);
else cJSON_AddItemToObject(req, "params", cJSON_CreateObject());
if (id) cJSON_AddItemReferenceToObject(req, "id", id);
cJSON *response = agent_tools_dispatch(req, NULL); /* sync dispatch */
cJSON_Delete(req);
if (response) {
cJSON_AddItemToArray(results, response);
if (!cJSON_IsTrue(cJSON_GetObjectItem(response, "success")) && !continue_on_error) {
/* Stop on first error — include partial results. */
cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "success", FALSE);
cJSON *err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "code", "BATCH_FAILED");
cJSON_AddStringToObject(err, "message", "A command in the batch failed");
cJSON_AddItemToObject(resp, "error", err);
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "results", results);
cJSON_AddNumberToObject(data, "executed", cJSON_GetArraySize(results));
cJSON_AddItemToObject(resp, "data", data);
return resp;
}
}
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "results", results);
cJSON_AddNumberToObject(data, "executed", cJSON_GetArraySize(results));
return make_success(data);
}
/* ── Find element tools ────────────────────────────────────────────── *
* These tools find an element by various criteria, assign a ref to it
* (storing it in window.__agentRefs), and return the ref/selector/role/name.
* They share a common JS helper for uniqueSelector() and ref assignment.
*/
/* Common JS helper: defines uniqueSelector() and assignRef() functions
* that are used by all find tools. The tool-specific finding code sets
* the variable `el` to the found element, then calls assignRef(el).
* Returns a JSON string with {ref, selector, role, name} or null. */
static const char *FIND_HELPER_JS =
" function uniqueSelector(el) {\n"
" if (el.id) return '#' + CSS.escape(el.id);\n"
" var path = [];\n"
" while (el && el.nodeType === 1 && el !== document.documentElement) {\n"
" var index = 1;\n"
" var sib = el.previousElementSibling;\n"
" while (sib) { if (sib.tagName === el.tagName) index++; sib = sib.previousElementSibling; }\n"
" var part = el.tagName.toLowerCase();\n"
" if (el.className && typeof el.className === 'string') {\n"
" var cls = el.className.trim().split(/\\s+/).slice(0, 2).join('.');\n"
" if (cls) part += '.' + cls;\n"
" }\n"
" if (index > 1) part += ':nth-of-type(' + index + ')';\n"
" path.unshift(part);\n"
" el = el.parentElement;\n"
" }\n"
" return path.length > 0 ? path.join(' > ') : 'html';\n"
" }\n"
" function assignRef(el) {\n"
" if (!el) return null;\n"
" if (!window.__agentRefs) window.__agentRefs = {};\n"
" if (!window.__agentRefCounter) window.__agentRefCounter = 0;\n"
" var refNum = ++window.__agentRefCounter;\n"
" var refId = 'e' + refNum;\n"
" var selector = uniqueSelector(el);\n"
" var role = el.getAttribute('role') || el.tagName.toLowerCase();\n"
" var name = el.getAttribute('aria-label') || el.textContent.trim().substring(0, 100) || '';\n"
" var rect = el.getBoundingClientRect();\n"
" window.__agentRefs[refId] = { selector: selector, role: role, name: name,\n"
" bbox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };\n"
" return JSON.stringify({ ref: '@' + refId, selector: selector, role: role, name: name,\n"
" bbox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });\n"
" }\n";
/* find_role — Find element by ARIA role (and optionally by name) */
static cJSON *tool_find_role(cJSON *params) {
const char *role = get_string_param(params, "role");
const char *name = get_string_param(params, "name");
if (!role || !role[0]) return make_error("MISSING_PARAM", "Provide 'role'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_role = g_strescape(role, NULL);
char *esc_name = name ? g_strescape(name, NULL) : g_strdup("");
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var role = \"%s\";\n"
" var nameFilter = \"%s\";\n"
" var roleMap = { 'button':'button','link':'a','navigation':'nav',\n"
" 'main':'main','article':'article','region':'section',\n"
" 'form':'form','list':'ul','listitem':'li','heading':'h1',\n"
" 'textbox':'input','combobox':'select','image':'img',\n"
" 'paragraph':'p','table':'table','contentinfo':'footer',\n"
" 'banner':'header','complementary':'aside','search':'search' };\n"
" var candidates = [];\n"
" var byAttr = document.querySelectorAll('[role=\"' + role + '\"]');\n"
" for (var i = 0; i < byAttr.length; i++) candidates.push(byAttr[i]);\n"
" if (candidates.length === 0 && roleMap[role]) {\n"
" var byTag = document.querySelectorAll(roleMap[role]);\n"
" for (var j = 0; j < byTag.length; j++) candidates.push(byTag[j]);\n"
" }\n"
" if (candidates.length === 0) return null;\n"
" var el = null;\n"
" if (nameFilter) {\n"
" for (var k = 0; k < candidates.length; k++) {\n"
" var c = candidates[k];\n"
" var al = c.getAttribute('aria-label') || '';\n"
" var tc = c.textContent.trim();\n"
" if (al.indexOf(nameFilter) >= 0 || tc.indexOf(nameFilter) >= 0) { el = c; break; }\n"
" }\n"
" } else { el = candidates[0]; }\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_role, esc_name);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_role);
g_free(esc_name);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_text — Find element by text content */
static cJSON *tool_find_text(cJSON *params) {
const char *text = get_string_param(params, "text");
gboolean exact = get_bool_param(params, "exact", FALSE);
if (!text || !text[0]) return make_error("MISSING_PARAM", "Provide 'text'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_text = g_strescape(text, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var text = \"%s\";\n"
" var exact = %s;\n"
" var all = document.querySelectorAll('*');\n"
" var best = null;\n"
" for (var i = 0; i < all.length; i++) {\n"
" var el = all[i];\n"
" var tc = el.textContent.trim();\n"
" var match = exact ? (tc === text) : (tc.indexOf(text) >= 0);\n"
" if (!match) continue;\n"
" /* Prefer leaf-level elements (no element children with the text). */\n"
" var hasChildMatch = false;\n"
" for (var j = 0; j < el.children.length; j++) {\n"
" var cc = el.children[j].textContent.trim();\n"
" if (exact ? (cc === text) : (cc.indexOf(text) >= 0)) { hasChildMatch = true; break; }\n"
" }\n"
" if (!hasChildMatch) { best = el; break; }\n"
" if (!best) best = el;\n"
" }\n"
" return assignRef(best);\n"
"})();",
FIND_HELPER_JS, esc_text, exact ? "true" : "false");
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_text);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_label — Find element by associated label */
static cJSON *tool_find_label(cJSON *params) {
const char *label = get_string_param(params, "label");
if (!label || !label[0]) return make_error("MISSING_PARAM", "Provide 'label'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_label = g_strescape(label, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var label = \"%s\";\n"
" /* 1. Try label[for] elements matching the text. */\n"
" var labels = document.querySelectorAll('label');\n"
" for (var i = 0; i < labels.length; i++) {\n"
" if (labels[i].textContent.trim().indexOf(label) < 0) continue;\n"
" var forId = labels[i].getAttribute('for');\n"
" if (forId) { var t = document.getElementById(forId); if (t) return assignRef(t); }\n"
" /* label wrapping the element */\n"
" var inner = labels[i].querySelector('input,select,textarea,button');\n"
" if (inner) return assignRef(inner);\n"
" }\n"
" /* 2. Try aria-label. */\n"
" var byAria = document.querySelectorAll('[aria-label]');\n"
" for (var j = 0; j < byAria.length; j++) {\n"
" if (byAria[j].getAttribute('aria-label').indexOf(label) >= 0) return assignRef(byAria[j]);\n"
" }\n"
" /* 3. Try aria-labelledby. */\n"
" var byLabeled = document.querySelectorAll('[aria-labelledby]');\n"
" for (var k = 0; k < byLabeled.length; k++) {\n"
" var ids = byLabeled[k].getAttribute('aria-labelledby').split(/\\s+/);\n"
" for (var m = 0; m < ids.length; m++) {\n"
" var ref = document.getElementById(ids[m]);\n"
" if (ref && ref.textContent.trim().indexOf(label) >= 0) return assignRef(byLabeled[k]);\n"
" }\n"
" }\n"
" return null;\n"
"})();",
FIND_HELPER_JS, esc_label);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_label);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_placeholder — Find element by placeholder (substring match) */
static cJSON *tool_find_placeholder(cJSON *params) {
const char *placeholder = get_string_param(params, "placeholder");
if (!placeholder || !placeholder[0]) return make_error("MISSING_PARAM", "Provide 'placeholder'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_ph = g_strescape(placeholder, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var el = document.querySelector('[placeholder*=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_ph);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_ph);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_alt — Find element by alt text (substring match) */
static cJSON *tool_find_alt(cJSON *params) {
const char *alt = get_string_param(params, "alt");
if (!alt || !alt[0]) return make_error("MISSING_PARAM", "Provide 'alt'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_alt = g_strescape(alt, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var el = document.querySelector('[alt*=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_alt);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_alt);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_title — Find element by title attribute (substring match) */
static cJSON *tool_find_title(cJSON *params) {
const char *title = get_string_param(params, "title");
if (!title || !title[0]) return make_error("MISSING_PARAM", "Provide 'title'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_title = g_strescape(title, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var el = document.querySelector('[title*=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_title);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_title);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_testid — Find element by data-testid (exact match) */
static cJSON *tool_find_testid(cJSON *params) {
const char *testid = get_string_param(params, "testid");
if (!testid || !testid[0]) return make_error("MISSING_PARAM", "Provide 'testid'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_id = g_strescape(testid, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var el = document.querySelector('[data-testid=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_id);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_id);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_first — Find first element matching a CSS selector */
static cJSON *tool_find_first(cJSON *params) {
const char *selector = get_string_param(params, "selector");
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_sel = g_strescape(selector, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var el = document.querySelector(\"%s\");\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_sel);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_last — Find last element matching a CSS selector */
static cJSON *tool_find_last(cJSON *params) {
const char *selector = get_string_param(params, "selector");
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_sel = g_strescape(selector, NULL);
char *script = g_strdup_printf(
"(function(){\n"
"%s"
" var list = document.querySelectorAll(\"%s\");\n"
" var el = list.length > 0 ? list[list.length - 1] : null;\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_sel);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* find_nth — Find nth element (0-based) matching a CSS selector */
static cJSON *tool_find_nth(cJSON *params) {
const char *selector = get_string_param(params, "selector");
int n = get_int_param(params, "n", -1);
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
if (n < 0) return make_error("MISSING_PARAM", "Provide 'n' (non-negative integer)");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_sel = g_strescape(selector, NULL);
char *script = g_strdup_printf(
"(function(){\n"
" var n = %d;\n"
"%s"
" var list = document.querySelectorAll(\"%s\");\n"
" var el = (n >= 0 && n < list.length) ? list[n] : null;\n"
" return assignRef(el);\n"
"})();",
n, FIND_HELPER_JS, esc_sel);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
g_free(esc_sel);
if (!result || strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(result);
g_free(result);
if (!data) return make_error("PARSE_ERROR", "Failed to parse find result");
return make_success(data);
}
/* ── Cookies & web storage tools ──────────────────────────────────── */
/* cookies_get — Get all cookies visible to JavaScript (non-httpOnly).
* Uses document.cookie which returns "name1=value1; name2=value2; ...". */
static cJSON *tool_cookies_get(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var cookies = document.cookie.split('; ');"
" var result = [];"
" for (var i = 0; i < cookies.length; i++) {"
" if (!cookies[i]) continue;"
" var idx = cookies[i].indexOf('=');"
" var name = idx > 0 ? cookies[i].substring(0, idx) : cookies[i];"
" var value = idx > 0 ? cookies[i].substring(idx + 1) : '';"
" result.push({name: name, value: value});"
" }"
" return JSON.stringify(result);"
"})();";
char *result = agent_js_eval_sync(wv, script, 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *arr = cJSON_Parse(result);
g_free(result);
if (!arr) return make_error("PARSE_ERROR", "Failed to parse cookies JSON");
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "cookies", arr);
return make_success(data);
}
/* cookies_set — Set a cookie via document.cookie.
* Note: http_only cannot be set via JavaScript and is ignored. */
static cJSON *tool_cookies_set(cJSON *params) {
const char *name = get_string_param(params, "name");
const char *value = get_string_param(params, "value");
const char *domain = get_string_param(params, "domain");
const char *path = get_string_param(params, "path");
gboolean secure = get_bool_param(params, "secure", FALSE);
int max_age = get_int_param(params, "max_age", -1);
if (!name || !name[0]) return make_error("MISSING_PARAM", "Provide 'name'");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
if (!path || !path[0]) path = "/";
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Build the cookie string. We escape name/value for safe JS string embedding. */
char *esc_name = g_strescape(name, NULL);
char *esc_value = g_strescape(value, NULL);
char *esc_path = g_strescape(path, NULL);
char *esc_domain = domain ? g_strescape(domain, NULL) : NULL;
GString *cookie_str = g_string_new(NULL);
g_string_printf(cookie_str, "%s=%s; path=%s", esc_name, esc_value, esc_path);
if (esc_domain && esc_domain[0]) {
g_string_append_printf(cookie_str, "; domain=%s", esc_domain);
}
if (secure) {
g_string_append(cookie_str, "; secure");
}
if (max_age >= 0) {
g_string_append_printf(cookie_str, "; max-age=%d", max_age);
}
/* Escape the cookie string for embedding in a JS string literal. */
char *esc_cookie = g_strescape(cookie_str->str, NULL);
char *script = g_strdup_printf(
"document.cookie = \"%s\"; 'ok';", esc_cookie);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(esc_cookie);
g_string_free(cookie_str, TRUE);
g_free(esc_name);
g_free(esc_value);
g_free(esc_path);
g_free(esc_domain);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* cookies_clear — Clear all cookies visible to JavaScript by expiring each. */
static cJSON *tool_cookies_clear(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var cookies = document.cookie.split('; ');"
" for (var i = 0; i < cookies.length; i++) {"
" var idx = cookies[i].indexOf('=');"
" var name = idx > 0 ? cookies[i].substring(0, idx) : cookies[i];"
" document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';"
" }"
" return 'ok';"
"})();";
char *result = agent_js_eval_sync(wv, script, 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* storage_local_get — Get all localStorage entries as a JSON object. */
static cJSON *tool_storage_local_get(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var result = {};"
" for (var i = 0; i < localStorage.length; i++) {"
" var k = localStorage.key(i);"
" result[k] = localStorage.getItem(k);"
" }"
" return JSON.stringify(result);"
"})();";
char *result = agent_js_eval_sync(wv, script, 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *obj = cJSON_Parse(result);
g_free(result);
if (!obj) return make_error("PARSE_ERROR", "Failed to parse localStorage JSON");
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "storage", obj);
return make_success(data);
}
/* storage_local_get_key — Get a specific localStorage key value. */
static cJSON *tool_storage_local_get_key(cJSON *params) {
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_key = g_strescape(key, NULL);
char *script = g_strdup_printf(
"(function(){var v=localStorage.getItem(\"%s\");"
"return v===null?'__null__':v;})();", esc_key);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(esc_key);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "key", key);
if (strcmp(result, "__null__") == 0) {
cJSON_AddNullToObject(data, "value");
} else {
cJSON_AddStringToObject(data, "value", result);
}
g_free(result);
return make_success(data);
}
/* storage_local_set — Set a localStorage key to a value. */
static cJSON *tool_storage_local_set(cJSON *params) {
const char *key = get_string_param(params, "key");
const char *value = get_string_param(params, "value");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_key = g_strescape(key, NULL);
char *esc_value = g_strescape(value, NULL);
char *script = g_strdup_printf(
"localStorage.setItem(\"%s\",\"%s\"); 'ok';", esc_key, esc_value);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(esc_key);
g_free(esc_value);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* storage_local_clear — Clear all localStorage entries. */
static cJSON *tool_storage_local_clear(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *result = agent_js_eval_sync(wv, "localStorage.clear(); 'ok';", 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* storage_session_get — Get all sessionStorage entries as a JSON object. */
static cJSON *tool_storage_session_get(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var result = {};"
" for (var i = 0; i < sessionStorage.length; i++) {"
" var k = sessionStorage.key(i);"
" result[k] = sessionStorage.getItem(k);"
" }"
" return JSON.stringify(result);"
"})();";
char *result = agent_js_eval_sync(wv, script, 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *obj = cJSON_Parse(result);
g_free(result);
if (!obj) return make_error("PARSE_ERROR", "Failed to parse sessionStorage JSON");
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "storage", obj);
return make_success(data);
}
/* storage_session_get_key — Get a specific sessionStorage key value. */
static cJSON *tool_storage_session_get_key(cJSON *params) {
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_key = g_strescape(key, NULL);
char *script = g_strdup_printf(
"(function(){var v=sessionStorage.getItem(\"%s\");"
"return v===null?'__null__':v;})();", esc_key);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(esc_key);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "key", key);
if (strcmp(result, "__null__") == 0) {
cJSON_AddNullToObject(data, "value");
} else {
cJSON_AddStringToObject(data, "value", result);
}
g_free(result);
return make_success(data);
}
/* storage_session_set — Set a sessionStorage key to a value. */
static cJSON *tool_storage_session_set(cJSON *params) {
const char *key = get_string_param(params, "key");
const char *value = get_string_param(params, "value");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *esc_key = g_strescape(key, NULL);
char *esc_value = g_strescape(value, NULL);
char *script = g_strdup_printf(
"sessionStorage.setItem(\"%s\",\"%s\"); 'ok';", esc_key, esc_value);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(esc_key);
g_free(esc_value);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* storage_session_clear — Clear all sessionStorage entries. */
static cJSON *tool_storage_session_clear(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *result = agent_js_eval_sync(wv, "sessionStorage.clear(); 'ok';", 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* ── Mouse tools ──────────────────────────────────────────────────── */
/* mouse_move — Move mouse to coordinates (clientX, clientY). */
static cJSON *tool_mouse_move(cJSON *params) {
int x = get_int_param(params, "x", 0);
int y = get_int_param(params, "y", 0);
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new MouseEvent('mousemove',"
"{clientX:%d,clientY:%d,bubbles:true}));return 'ok';})();",
x, y);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* Map button name to MouseEvent button number: left=0, middle=1, right=2. */
static int mouse_button_from_string(const char *button) {
if (button == NULL) return 0;
if (strcmp(button, "middle") == 0) return 1;
if (strcmp(button, "right") == 0) return 2;
return 0; /* "left" or default */
}
/* mouse_down — Press a mouse button at optional coordinates. */
static cJSON *tool_mouse_down(cJSON *params) {
const char *button_str = get_string_param(params, "button");
int button = mouse_button_from_string(button_str);
int x = get_int_param(params, "x", 0);
int y = get_int_param(params, "y", 0);
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new MouseEvent('mousedown',"
"{button:%d,clientX:%d,clientY:%d,bubbles:true}));return 'ok';})();",
button, x, y);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* mouse_up — Release a mouse button at optional coordinates. */
static cJSON *tool_mouse_up(cJSON *params) {
const char *button_str = get_string_param(params, "button");
int button = mouse_button_from_string(button_str);
int x = get_int_param(params, "x", 0);
int y = get_int_param(params, "y", 0);
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new MouseEvent('mouseup',"
"{button:%d,clientX:%d,clientY:%d,bubbles:true}));return 'ok';})();",
button, x, y);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* mouse_wheel — Scroll the mouse wheel by dy (vertical) and dx (horizontal). */
static cJSON *tool_mouse_wheel(cJSON *params) {
int dy = get_int_param(params, "dy", 0);
int dx = get_int_param(params, "dx", 0);
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new WheelEvent('wheel',"
"{deltaY:%d,deltaX:%d,bubbles:true}));return 'ok';})();",
dy, dx);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* ── Clipboard tools ──────────────────────────────────────────────── */
/* clipboard_read — Read text from the system clipboard (GTK3 API). */
static cJSON *tool_clipboard_read(cJSON *params) {
(void)params;
GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
if (!clip) return make_error("CLIPBOARD_ERROR", "Failed to get system clipboard");
gchar *text = gtk_clipboard_wait_for_text(clip);
if (text == NULL) {
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "text", "");
return make_success(data);
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "text", text);
g_free(text);
return make_success(data);
}
/* clipboard_write — Write text to the system clipboard (GTK3 API). */
static cJSON *tool_clipboard_write(cJSON *params) {
const char *text = get_string_param(params, "text");
if (!text) return make_error("MISSING_PARAM", "Provide 'text'");
GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
if (!clip) return make_error("CLIPBOARD_ERROR", "Failed to get system clipboard");
gtk_clipboard_set_text(clip, text, -1);
gtk_clipboard_store(clip); /* persist for after exit */
return make_success(NULL);
}
/* clipboard_copy — Copy current selection via document.execCommand('copy'). */
static cJSON *tool_clipboard_copy(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *result = agent_js_eval_sync(wv, "document.execCommand('copy'); 'ok';", 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* clipboard_paste — Paste from clipboard via document.execCommand('paste'). */
static cJSON *tool_clipboard_paste(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *result = agent_js_eval_sync(wv, "document.execCommand('paste'); 'ok';", 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* ── Settings tools ───────────────────────────────────────────────── */
/* set_viewport — Resize the browser window to width x height. */
static cJSON *tool_set_viewport(cJSON *params) {
int width = get_int_param(params, "width", 0);
int height = get_int_param(params, "height", 0);
if (width <= 0 || height <= 0) {
return make_error("MISSING_PARAM", "Provide positive 'width' and 'height'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(wv));
if (!toplevel || !GTK_IS_WINDOW(toplevel)) {
return make_error("NO_WINDOW", "No top-level window found for active webview");
}
gtk_window_resize(GTK_WINDOW(toplevel), width, height);
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "width", width);
cJSON_AddNumberToObject(data, "height", height);
return make_success(data);
}
/* set_offline — Toggle offline mode (not yet supported in WebKitGTK). */
static cJSON *tool_set_offline(cJSON *params) {
(void)params;
return make_error("NOT_SUPPORTED",
"Offline mode is not directly supported in WebKitGTK. "
"Use set_headers or network proxy settings instead.");
}
/* set_headers — Set extra HTTP headers (not yet supported in WebKitGTK). */
static cJSON *tool_set_headers(cJSON *params) {
(void)params;
return make_error("NOT_SUPPORTED",
"Custom HTTP headers not yet supported in WebKitGTK");
}
/* set_credentials — Set HTTP basic auth credentials (not supported; interactive). */
static cJSON *tool_set_credentials(cJSON *params) {
(void)params;
return make_error("NOT_SUPPORTED",
"Pre-setting credentials not supported. "
"Auth prompts are handled interactively by WebKitGTK.");
}
/* set_media — Emulate color scheme (dark or light) via CSS colorScheme. */
static cJSON *tool_set_media(cJSON *params) {
const char *scheme = get_string_param(params, "scheme");
if (!scheme || !scheme[0]) {
return make_error("MISSING_PARAM", "Provide 'scheme' ('dark' or 'light')");
}
if (strcmp(scheme, "dark") != 0 && strcmp(scheme, "light") != 0) {
return make_error("INVALID_PARAM", "scheme must be 'dark' or 'light'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script = g_strdup_printf(
"document.documentElement.style.colorScheme = '%s'; 'ok';", scheme);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
g_free(result);
return make_success(NULL);
}
/* ── Frame tools ──────────────────────────────────────────────────── */
/* frame_switch — Switch to an iframe by ref or CSS selector.
* Stores the selector in g_current_frame_selector. Other JS-based tools
* should wrap their JS in the frame's contentDocument when this is set. */
static cJSON *tool_frame_switch(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
g_free(g_current_frame_selector);
g_current_frame_selector = sel; /* take ownership */
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "frame", g_current_frame_selector);
return make_success(data);
}
/* frame_main — Switch back to the main frame (clear frame selector). */
static cJSON *tool_frame_main(cJSON *params) {
(void)params;
g_free(g_current_frame_selector);
g_current_frame_selector = NULL;
return make_success(NULL);
}
/* ── Dialog tools (JS override approach) ──────────────────────────── */
/* dialog_status — Check if a JS dialog (alert/confirm/prompt) is pending.
* Installs the dialog overrides on first call. */
static cJSON *tool_dialog_status(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Check if overrides are installed. */
char *check = agent_js_eval_sync(wv,
"(window.__agentDialogInstalled === undefined) ? 'not_installed' : "
"(window.__pendingDialog === null || window.__pendingDialog === undefined ? 'none' : "
"JSON.stringify(window.__pendingDialog))", 5000);
if (!check) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strcmp(check, "not_installed") == 0) {
g_free(check);
char *inst = agent_js_eval_sync(wv, DIALOG_OVERRIDE_JS, 5000);
g_free(inst);
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "pending", FALSE);
return make_success(data);
}
if (strcmp(check, "none") == 0) {
g_free(check);
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "pending", FALSE);
return make_success(data);
}
/* Parse the pending dialog JSON. */
cJSON *dlg = cJSON_Parse(check);
g_free(check);
if (!dlg) {
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "pending", FALSE);
return make_success(data);
}
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "pending", TRUE);
const char *type = cJSON_GetStringValue(cJSON_GetObjectItem(dlg, "type"));
const char *message = cJSON_GetStringValue(cJSON_GetObjectItem(dlg, "message"));
cJSON_AddStringToObject(data, "type", type ? type : "");
cJSON_AddStringToObject(data, "message", message ? message : "");
cJSON_Delete(dlg);
return make_success(data);
}
/* dialog_accept — Accept a pending JS dialog. */
static cJSON *tool_dialog_accept(cJSON *params) {
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Ensure overrides are installed. */
char *check = agent_js_eval_sync(wv,
"(window.__agentDialogInstalled === undefined) ? 'no' : 'yes'", 5000);
if (!check) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strcmp(check, "no") == 0) {
g_free(check);
char *inst = agent_js_eval_sync(wv, DIALOG_OVERRIDE_JS, 5000);
g_free(inst);
return make_error("NO_DIALOG", "No dialog is currently pending");
}
g_free(check);
const char *text = get_string_param(params, "text");
char *esc_text = text ? g_strescape(text, NULL) : NULL;
/* For confirm: set result=true. For prompt: set result=text. For alert: just clear. */
char *script = g_strdup_printf(
"(function(){"
" if (!window.__pendingDialog) return 'no_dialog';"
" var d = window.__pendingDialog;"
" if (d.type === 'confirm') { d.result = true; }"
" else if (d.type === 'prompt') { d.result = %s ? \"%s\" : ''; }"
" window.__pendingDialog = null;"
" return 'ok';"
"})();",
text ? "true" : "false",
esc_text ? esc_text : "");
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
g_free(esc_text);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strcmp(result, "no_dialog") == 0) {
g_free(result);
return make_error("NO_DIALOG", "No dialog is currently pending");
}
g_free(result);
return make_success(NULL);
}
/* dialog_dismiss — Dismiss a pending JS dialog (cancel). */
static cJSON *tool_dialog_dismiss(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
char *script =
"(function(){"
" if (!window.__pendingDialog) return 'no_dialog';"
" var d = window.__pendingDialog;"
" if (d.type === 'confirm') { d.result = false; }"
" else if (d.type === 'prompt') { d.result = null; }"
" window.__pendingDialog = null;"
" return 'ok';"
"})();";
char *result = agent_js_eval_sync(wv, script, 5000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strcmp(result, "no_dialog") == 0) {
g_free(result);
return make_error("NO_DIALOG", "No dialog is currently pending");
}
g_free(result);
return make_success(NULL);
}
/* ── Debug tools ──────────────────────────────────────────────────── */
/* console — Get collected console messages (JS console hook). */
static cJSON *tool_console(cJSON *params) {
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
gboolean clear = get_bool_param(params, "clear", FALSE);
/* Install the console hook if not yet installed. */
char *check = agent_js_eval_sync(wv,
"(window.__agentConsoleInstalled === undefined) ? 'no' : 'yes'", 5000);
if (!check) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strcmp(check, "no") == 0) {
g_free(check);
char *inst = agent_js_eval_sync(wv, CONSOLE_HOOK_JS, 5000);
g_free(inst);
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "messages", cJSON_CreateArray());
return make_success(data);
}
g_free(check);
char *script = g_strdup_printf(
"(function(){"
" var arr = window.__pageConsole || [];"
" var out = JSON.stringify(arr);"
" if (%s) { window.__pageConsole = []; }"
" return out;"
"})();",
clear ? "true" : "false");
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *arr = cJSON_Parse(result);
g_free(result);
if (!arr) {
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "messages", cJSON_CreateArray());
return make_success(data);
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "messages", arr);
return make_success(data);
}
/* errors — Get JS page errors (window error listener). */
static cJSON *tool_errors(cJSON *params) {
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
gboolean clear = get_bool_param(params, "clear", FALSE);
/* Install the error listener if not yet installed. */
char *check = agent_js_eval_sync(wv,
"(window.__agentErrorInstalled === undefined) ? 'no' : 'yes'", 5000);
if (!check) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strcmp(check, "no") == 0) {
g_free(check);
char *inst = agent_js_eval_sync(wv, ERROR_HANDLER_JS, 5000);
g_free(inst);
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "errors", cJSON_CreateArray());
return make_success(data);
}
g_free(check);
char *script = g_strdup_printf(
"(function(){"
" var arr = window.__pageErrors || [];"
" var out = JSON.stringify(arr);"
" if (%s) { window.__pageErrors = []; }"
" return out;"
"})();",
clear ? "true" : "false");
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
cJSON *arr = cJSON_Parse(result);
g_free(result);
if (!arr) {
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "errors", cJSON_CreateArray());
return make_success(data);
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "errors", arr);
return make_success(data);
}
/* highlight — Highlight an element with a temporary red outline. */
static cJSON *tool_highlight(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
int duration = get_int_param(params, "duration", 2000);
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else if (selector && selector[0]) {
sel = g_strdup(selector);
} else {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector'");
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(sel); return make_error("NO_TAB", "No active tab"); }
char *esc_sel = g_strescape(sel, NULL);
g_free(sel);
char *script = g_strdup_printf(
"(function(){"
" var el = document.querySelector(\"%s\");"
" if (!el) return null;"
" var old = el.style.outline;"
" el.style.outline = '3px solid red';"
" setTimeout(function(){ el.style.outline = old; }, %d);"
" return 'ok';"
"})();",
esc_sel, duration);
g_free(esc_sel);
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strcmp(result, "null") == 0) {
g_free(result);
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
g_free(result);
return make_success(NULL);
}
/* ── State save/load tools ────────────────────────────────────────── */
/* state_save — Save browser state (localStorage + cookies) to a file or
* return as a JSON string. */
static cJSON *tool_state_save(cJSON *params) {
const char *path = get_string_param(params, "path");
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var state = { localStorage: {}, cookies: [] };"
" for (var i = 0; i < localStorage.length; i++) {"
" var k = localStorage.key(i);"
" state.localStorage[k] = localStorage.getItem(k);"
" }"
" document.cookie.split('; ').forEach(function(c) {"
" if (!c) return;"
" var idx = c.indexOf('=');"
" state.cookies.push({name: idx>0?c.substring(0,idx):c, value: idx>0?c.substring(idx+1):''});"
" });"
" return JSON.stringify(state);"
"})();";
char *result = agent_js_eval_sync(wv, script, 8000);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (path && path[0]) {
GError *err = NULL;
if (!g_file_set_contents(path, result, -1, &err)) {
g_free(result);
char msg[512];
snprintf(msg, sizeof(msg), "Failed to write state file: %s",
err ? err->message : "unknown error");
if (err) g_error_free(err);
return make_error("FILE_ERROR", msg);
}
g_free(result);
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "saved", TRUE);
cJSON_AddStringToObject(data, "path", path);
return make_success(data);
}
/* No path — return the JSON state string. */
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "state", result);
g_free(result);
return make_success(data);
}
/* state_load — Load browser state from a file or JSON string. Restores
* localStorage and cookies. */
static cJSON *tool_state_load(cJSON *params) {
const char *path = get_string_param(params, "path");
const char *state_param = get_string_param(params, "state");
if ((!path || !path[0]) && (!state_param || !state_param[0])) {
return make_error("MISSING_PARAM", "Provide 'path' or 'state'");
}
char *state_json = NULL;
if (path && path[0]) {
GError *err = NULL;
if (!g_file_get_contents(path, &state_json, NULL, &err)) {
char msg[512];
snprintf(msg, sizeof(msg), "Failed to read state file: %s",
err ? err->message : "unknown error");
if (err) g_error_free(err);
return make_error("FILE_ERROR", msg);
}
} else {
state_json = g_strdup(state_param);
}
WebKitWebView *wv = get_active_webview();
if (!wv) { g_free(state_json); return make_error("NO_TAB", "No active tab"); }
char *esc_state = g_strescape(state_json, NULL);
g_free(state_json);
char *script = g_strdup_printf(
"(function(){"
" try {"
" var state = JSON.parse(\"%s\");"
" Object.keys(state.localStorage||{}).forEach(function(k) {"
" localStorage.setItem(k, state.localStorage[k]);"
" });"
" (state.cookies||[]).forEach(function(c) {"
" document.cookie = c.name + '=' + c.value + '; path=/';"
" });"
" return 'ok';"
" } catch(e) { return 'parse_error:' + e.message; }"
"})();",
esc_state);
g_free(esc_state);
char *result = agent_js_eval_sync(wv, script, 8000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
if (strncmp(result, "parse_error:", 12) == 0) {
char msg[512];
snprintf(msg, sizeof(msg), "Failed to parse state JSON: %s", result + 12);
g_free(result);
return make_error("PARSE_ERROR", msg);
}
g_free(result);
return make_success(NULL);
}
/* ── Async result handlers for JS-based tools ─────────────────────── */
/* Handler for tools that return a text value (get_text, get_html, get_attr).
* The JS result is the raw value, or "null" if element not found. */
static cJSON *text_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "text", js_result);
return make_success(data);
}
static cJSON *html_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "html", js_result);
return make_success(data);
}
static cJSON *attr_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "value", js_result);
return make_success(data);
}
/* Handler for get_value — JS returns the raw value or "null". */
static cJSON *value_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "value", js_result);
return make_success(data);
}
/* Handler for get_count — JS returns a numeric string. */
static cJSON *count_result_handler(const char *js_result) {
if (js_result == NULL) {
return make_error("EVAL_FAILED", "JavaScript evaluation failed");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "count", atoi(js_result));
return make_success(data);
}
/* Handler for get_box — JS returns a JSON string with bounding box fields. */
static cJSON *box_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_Parse(js_result);
if (data == NULL) {
return make_error("PARSE_ERROR", "Failed to parse bounding box JSON");
}
return make_success(data);
}
/* Handler for get_styles — JS returns a JSON string of computed styles. */
static cJSON *styles_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *styles = cJSON_Parse(js_result);
if (styles == NULL) {
return make_error("PARSE_ERROR", "Failed to parse computed styles JSON");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "styles", styles);
return make_success(data);
}
/* Handler for is_visible — JS returns "true"/"false" or "null". */
static cJSON *visible_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "visible", strcmp(js_result, "true") == 0);
return make_success(data);
}
/* Handler for is_enabled — JS returns "true"/"false" or "null". */
static cJSON *enabled_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "enabled", strcmp(js_result, "true") == 0);
return make_success(data);
}
/* Handler for is_checked — JS returns "true"/"false" or "null". */
static cJSON *checked_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "checked", strcmp(js_result, "true") == 0);
return make_success(data);
}
/* Handler for action tools (click, fill, type, hover, focus, press, scroll).
* The JS returns "ok" on success, "null" if element not found. */
static cJSON *action_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching selector");
}
return make_success(NULL);
}
/* Handler for find tools — JS returns a JSON string with
* {ref, selector, role, name} or "null" if not found. */
static cJSON *find_result_handler(const char *js_result) {
if (js_result == NULL || strcmp(js_result, "null") == 0) {
return make_error("ELEMENT_NOT_FOUND", "No element matching criteria");
}
cJSON *data = cJSON_Parse(js_result);
if (data == NULL) {
return make_error("PARSE_ERROR", "Failed to parse find result JSON");
}
return make_success(data);
}
/* Handler for cookies_get — JS returns a JSON array of {name, value}. */
static cJSON *cookies_get_result_handler(const char *js_result) {
if (js_result == NULL) {
return make_error("EVAL_FAILED", "JavaScript evaluation failed");
}
cJSON *arr = cJSON_Parse(js_result);
if (arr == NULL) {
return make_error("PARSE_ERROR", "Failed to parse cookies JSON");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "cookies", arr);
return make_success(data);
}
/* Handler for storage_*_get — JS returns a JSON object of key/value pairs. */
static cJSON *storage_get_result_handler(const char *js_result) {
if (js_result == NULL) {
return make_error("EVAL_FAILED", "JavaScript evaluation failed");
}
cJSON *obj = cJSON_Parse(js_result);
if (obj == NULL) {
return make_error("PARSE_ERROR", "Failed to parse storage JSON");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddItemToObject(data, "storage", obj);
return make_success(data);
}
/* Handler for storage_*_get_key — JS returns the value or "__null__". */
static cJSON *storage_get_key_result_handler(const char *js_result) {
if (js_result == NULL) {
return make_error("EVAL_FAILED", "JavaScript evaluation failed");
}
cJSON *data = cJSON_CreateObject();
if (strcmp(js_result, "__null__") == 0) {
cJSON_AddNullToObject(data, "value");
} else {
cJSON_AddStringToObject(data, "value", js_result);
}
return make_success(data);
}
/* Handler for dialog_status — JS returns "not_installed", "none", or a
* JSON string with the pending dialog. */
static cJSON *dialog_status_result_handler(const char *js_result) {
cJSON *data = cJSON_CreateObject();
if (js_result == NULL || strcmp(js_result, "not_installed") == 0 ||
strcmp(js_result, "none") == 0 || strcmp(js_result, "null") == 0) {
cJSON_AddBoolToObject(data, "pending", FALSE);
return make_success(data);
}
cJSON *dlg = cJSON_Parse(js_result);
if (!dlg) {
cJSON_AddBoolToObject(data, "pending", FALSE);
return make_success(data);
}
cJSON_AddBoolToObject(data, "pending", TRUE);
const char *type = cJSON_GetStringValue(cJSON_GetObjectItem(dlg, "type"));
const char *message = cJSON_GetStringValue(cJSON_GetObjectItem(dlg, "message"));
cJSON_AddStringToObject(data, "type", type ? type : "");
cJSON_AddStringToObject(data, "message", message ? message : "");
cJSON_Delete(dlg);
return make_success(data);
}
/* Handler for console — JS returns a JSON array of {level, text}. */
static cJSON *console_result_handler(const char *js_result) {
cJSON *data = cJSON_CreateObject();
if (js_result == NULL) {
cJSON_AddItemToObject(data, "messages", cJSON_CreateArray());
return make_success(data);
}
cJSON *arr = cJSON_Parse(js_result);
if (!arr) {
cJSON_AddItemToObject(data, "messages", cJSON_CreateArray());
return make_success(data);
}
cJSON_AddItemToObject(data, "messages", arr);
return make_success(data);
}
/* Handler for errors — JS returns a JSON array of {message, filename, line}. */
static cJSON *errors_result_handler(const char *js_result) {
cJSON *data = cJSON_CreateObject();
if (js_result == NULL) {
cJSON_AddItemToObject(data, "errors", cJSON_CreateArray());
return make_success(data);
}
cJSON *arr = cJSON_Parse(js_result);
if (!arr) {
cJSON_AddItemToObject(data, "errors", cJSON_CreateArray());
return make_success(data);
}
cJSON_AddItemToObject(data, "errors", arr);
return make_success(data);
}
/* Handler for state_save — JS returns the JSON state string. */
static cJSON *state_save_result_handler(const char *js_result) {
if (js_result == NULL) {
return make_error("EVAL_FAILED", "JavaScript evaluation failed");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "state", js_result);
return make_success(data);
}
/* Helper: resolve ref or selector to a JS selector string.
* Returns a newly allocated string (caller frees) or NULL on error. */
static char *resolve_ref_async(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
if (ref && ref[0]) {
if (ref[0] != '@') return g_strdup(ref);
const char *ref_id = ref + 1;
/* Build JS that looks up the selector from window.__agentRefs */
char *script = g_strdup_printf(
"(window.__agentRefs && window.__agentRefs['%s']) ? "
"window.__agentRefs['%s'].selector : null",
ref_id, ref_id);
WebKitWebView *wv = get_active_webview();
if (wv == NULL) { g_free(script); return NULL; }
char *result = agent_js_eval_sync(wv, script, 5000);
g_free(script);
if (result == NULL || strcmp(result, "null") == 0) {
g_free(result);
return NULL;
}
return result;
}
if (selector && selector[0]) return g_strdup(selector);
return NULL;
}
/* upload — Upload files to a file input element.
*
* Reads each file from the local filesystem, base64-encodes the
* contents, and injects JS that creates File objects via the
* DataTransfer API and assigns them to the input's files property. */
static cJSON *tool_upload(cJSON *params) {
const char *ref = get_string_param(params, "ref");
const char *selector = get_string_param(params, "selector");
cJSON *files_arr = cJSON_GetObjectItem(params, "files");
if (!files_arr || !cJSON_IsArray(files_arr) || cJSON_GetArraySize(files_arr) == 0) {
return make_error("MISSING_PARAM", "Provide 'files' (non-empty array of file paths)");
}
if ((!ref || !ref[0]) && (!selector || !selector[0])) {
return make_error("MISSING_PARAM", "Provide 'ref' or 'selector' for the file input element");
}
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Resolve ref to a CSS selector. */
char *sel = NULL;
if (ref && ref[0]) {
sel = resolve_ref_to_selector(ref);
if (!sel) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
} else {
sel = g_strdup(selector);
}
/* Build the JSON array of file data: [{"name","type","base64"}, ...] */
GString *file_data_json = g_string_new("[");
int n_files = cJSON_GetArraySize(files_arr);
gboolean any_error = FALSE;
char *error_msg = NULL;
for (int i = 0; i < n_files; i++) {
cJSON *fitem = cJSON_GetArrayItem(files_arr, i);
if (!fitem || !cJSON_IsString(fitem)) {
any_error = TRUE;
error_msg = g_strdup("'files' array must contain only strings (file paths)");
break;
}
const char *fpath = fitem->valuestring;
gchar *contents = NULL;
gsize length = 0;
GError *gerr = NULL;
if (!g_file_get_contents(fpath, &contents, &length, &gerr)) {
any_error = TRUE;
error_msg = g_strdup_printf("Cannot read file '%s': %s",
fpath, gerr ? gerr->message : "unknown error");
if (gerr) g_error_free(gerr);
break;
}
/* Base64-encode the file contents. */
gchar *b64 = g_base64_encode((const guchar *)contents, length);
g_free(contents);
if (!b64) {
any_error = TRUE;
error_msg = g_strdup_printf("Failed to base64-encode file '%s'", fpath);
break;
}
/* Guess MIME type from filename. */
gchar *content_type = g_content_type_guess(fpath, NULL, 0, NULL);
const char *mime = "application/octet-stream";
if (content_type) {
char *guessed = g_content_type_get_mime_type(content_type);
g_free(content_type);
if (guessed) {
mime = guessed;
}
}
/* Extract basename. */
const char *basename = strrchr(fpath, '/');
basename = basename ? basename + 1 : fpath;
/* Escape strings for JSON. */
char *esc_name = g_strescape(basename, NULL);
char *esc_mime = g_strescape(mime, NULL);
char *esc_b64 = g_strescape(b64, NULL);
if (i > 0) g_string_append_c(file_data_json, ',');
g_string_append_printf(file_data_json,
"{\"name\":\"%s\",\"type\":\"%s\",\"base64\":\"%s\"}",
esc_name, esc_mime, esc_b64);
g_free(esc_name);
g_free(esc_mime);
g_free(esc_b64);
g_free(b64);
}
g_string_append_c(file_data_json, ']');
if (any_error) {
g_string_free(file_data_json, TRUE);
g_free(sel);
cJSON *err = make_error("FILE_READ_FAILED", error_msg ? error_msg : "Failed to read files");
g_free(error_msg);
return err;
}
/* Escape the selector for JS. */
char *esc_sel = g_strescape(sel, NULL);
g_free(sel);
/* Build the JS that creates File objects and assigns them to the input. */
char *script = g_strdup_printf(
"(function(){"
" var el = document.querySelector(\"%s\");"
" if (!el) return 'element_not_found';"
" if (el.type !== 'file') return 'not_file_input';"
" var fileData = %s;"
" var files = [];"
" for (var i = 0; i < fileData.length; i++) {"
" var f = fileData[i];"
" var bytes = atob(f.base64);"
" var arr = new Uint8Array(bytes.length);"
" for (var j = 0; j < bytes.length; j++) arr[j] = bytes.charCodeAt(j);"
" var blob = new Blob([arr], {type: f.type});"
" var file = new File([blob], f.name, {type: f.type});"
" files.push(file);"
" }"
" var dt = new DataTransfer();"
" for (var k = 0; k < files.length; k++) dt.items.add(files[k]);"
" el.files = dt.files;"
" el.dispatchEvent(new Event('change', {bubbles: true}));"
" return 'ok:' + files.length;"
"})();",
esc_sel, file_data_json->str);
g_free(esc_sel);
g_string_free(file_data_json, TRUE);
char *result = agent_js_eval_sync(wv, script, 30000);
g_free(script);
if (!result) return make_error("EVAL_FAILED", "JavaScript evaluation failed or timed out");
/* Check the result. */
cJSON *resp;
if (strncmp(result, "ok:", 3) == 0) {
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "result", result);
cJSON_AddNumberToObject(data, "filesUploaded", n_files);
resp = make_success(data);
} else if (strcmp(result, "element_not_found") == 0) {
resp = make_error("ELEMENT_NOT_FOUND", "No element matches the selector");
} else if (strcmp(result, "not_file_input") == 0) {
resp = make_error("NOT_FILE_INPUT", "The element is not a file input");
} else {
resp = make_error("UPLOAD_FAILED", result);
}
g_free(result);
return resp;
}
/* pdf — Save the current page as a PDF file.
*
* Uses WebKitPrintOperation with GtkPrintSettings configured for
* PDF export to a file. Falls back to NOT_SUPPORTED if the print
* API cannot run headlessly. */
static cJSON *tool_pdf(cJSON *params) {
const char *path = get_string_param(params, "path");
if (!path || !path[0]) {
return make_error("MISSING_PARAM", "Provide 'path' for PDF output");
}
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Build the file URI for the output path. */
char *uri = NULL;
if (path[0] == '/') {
uri = g_strdup_printf("file://%s", path);
} else {
char *abs = g_canonicalize_filename(path, NULL);
if (!abs) {
return make_error("INVALID_PATH", "Cannot resolve the output path");
}
uri = g_strdup_printf("file://%s", abs);
g_free(abs);
}
/* Create the print operation and configure for PDF export. */
WebKitPrintOperation *print_op = webkit_print_operation_new(wv);
if (!print_op) {
g_free(uri);
return make_error("PDF_FAILED", "Failed to create print operation");
}
GtkPrintSettings *settings = gtk_print_settings_new();
gtk_print_settings_set(settings, GTK_PRINT_SETTINGS_OUTPUT_FILE_FORMAT, "pdf");
gtk_print_settings_set(settings, GTK_PRINT_SETTINGS_OUTPUT_URI, uri);
webkit_print_operation_set_print_settings(print_op, settings);
/* Run the print operation in export mode (no dialog).
* webkit_print_operation_print() runs synchronously in export mode
* when an output URI is set. */
webkit_print_operation_print(print_op);
/* Check if the file was created. */
gboolean file_exists = g_file_test(path, G_FILE_TEST_EXISTS);
g_object_unref(print_op);
g_object_unref(settings);
g_free(uri);
if (!file_exists) {
/* The print API may require a display. Fall back to NOT_SUPPORTED. */
return make_error("NOT_SUPPORTED",
"PDF export failed — the print API may require a display. "
"Use 'screenshot' for visual capture instead.");
}
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "path", path);
return make_success(data);
}
/* screenshot_annotated — Take a screenshot with element ref labels overlaid.
*
* 1. Take a snapshot (to populate window.__agentRefs and get the text tree)
* 2. Inject JS that overlays ref labels on each interactive element
* 3. Take the WebKit snapshot (cairo surface → PNG → base64)
* 4. Inject JS to remove the label overlays
* 5. Return both the screenshot (base64 PNG) and the snapshot text tree */
static cJSON *tool_screenshot_annotated(cJSON *params) {
gboolean interactive = get_bool_param(params, "interactive", TRUE);
gboolean compact = get_bool_param(params, "compact", TRUE);
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
/* Step 1: Take a snapshot to populate __agentRefs and get the text tree. */
cJSON *snap = agent_snapshot_take(wv, interactive, compact);
if (!snap) return make_error("SNAPSHOT_FAILED", "Failed to take snapshot");
/* Extract the snapshot text. The snapshot JSON has keys:
* {snapshot: "...", refs: {...}, refCount: N} */
const char *snap_text = cJSON_GetStringValue(cJSON_GetObjectItem(snap, "snapshot"));
char *snap_text_copy = snap_text ? g_strdup(snap_text) : g_strdup("");
/* Step 2: Inject JS annotation overlay using __agentRefs. */
const char *annotate_js =
"(function(){"
" var existing = document.querySelectorAll('[data-agent-annotation]');"
" existing.forEach(function(e) { e.remove(); });"
" if (!window.__agentRefs) return 'no_refs';"
" var count = 0;"
" Object.keys(window.__agentRefs).forEach(function(refId) {"
" var info = window.__agentRefs[refId];"
" if (!info || !info.selector) return;"
" var el = document.querySelector(info.selector);"
" if (!el) return;"
" var rect = el.getBoundingClientRect();"
" if (rect.width === 0 && rect.height === 0) return;"
" var label = document.createElement('div');"
" label.setAttribute('data-agent-annotation', 'true');"
" label.style.cssText = 'position:fixed;left:' + rect.left + 'px;top:' + rect.top + 'px;' +"
" 'background:red;color:white;font-size:10px;padding:1px 3px;z-index:999999;' +"
" 'pointer-events:none;font-family:monospace;border-radius:2px;line-height:1;';"
" label.textContent = refId;"
" document.body.appendChild(label);"
" count++;"
" });"
" return 'annotated:' + count;"
"})();";
char *ann_result = agent_js_eval_sync(wv, annotate_js, 5000);
/* We don't strictly need the result, but wait for it to complete. */
g_free(ann_result);
/* Step 3: Take the WebKit snapshot (same nested-loop pattern as tool_screenshot). */
snapshot_ctx_t ctx = {0};
ctx.loop = g_main_loop_new(g_main_context_default(), FALSE);
guint timeout_id = g_timeout_add(10000, snapshot_timeout_cb, &ctx);
webkit_web_view_get_snapshot(wv,
WEBKIT_SNAPSHOT_REGION_VISIBLE,
WEBKIT_SNAPSHOT_OPTIONS_NONE,
NULL, snapshot_callback, &ctx);
g_main_loop_run(ctx.loop);
g_source_remove(timeout_id);
g_main_loop_unref(ctx.loop);
if (!ctx.done || ctx.surface == NULL) {
if (ctx.surface) cairo_surface_destroy(ctx.surface);
/* Try to remove annotations even on failure. */
char *cleanup = agent_js_eval_sync(wv,
"document.querySelectorAll('[data-agent-annotation]').forEach(function(e){e.remove();}); 'ok';",
2000);
g_free(cleanup);
cJSON_Delete(snap);
g_free(snap_text_copy);
return make_error("SCREENSHOT_FAILED", "Failed to capture page snapshot");
}
/* Encode the cairo surface to PNG in memory. */
GByteArray *png_buf = g_byte_array_new();
cairo_status_t status = cairo_surface_write_to_png_stream(ctx.surface,
png_write_cb, png_buf);
int width = cairo_image_surface_get_width(ctx.surface);
int height = cairo_image_surface_get_height(ctx.surface);
cairo_surface_destroy(ctx.surface);
if (status != CAIRO_STATUS_SUCCESS || png_buf->len == 0) {
g_byte_array_free(png_buf, TRUE);
char *cleanup = agent_js_eval_sync(wv,
"document.querySelectorAll('[data-agent-annotation]').forEach(function(e){e.remove();}); 'ok';",
2000);
g_free(cleanup);
cJSON_Delete(snap);
g_free(snap_text_copy);
return make_error("SCREENSHOT_FAILED", "Failed to encode PNG");
}
/* Base64-encode the PNG bytes. */
gchar *b64 = g_base64_encode(png_buf->data, png_buf->len);
g_byte_array_free(png_buf, TRUE);
/* Step 4: Remove the annotation overlays. */
char *cleanup = agent_js_eval_sync(wv,
"document.querySelectorAll('[data-agent-annotation]').forEach(function(e){e.remove();}); 'ok';",
2000);
g_free(cleanup);
cJSON_Delete(snap);
if (!b64) {
g_free(snap_text_copy);
return make_error("SCREENSHOT_FAILED", "Failed to base64-encode PNG");
}
/* Step 5: Build the response with both screenshot and snapshot. */
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "screenshot", b64);
cJSON_AddStringToObject(data, "mimeType", "image/png");
cJSON_AddNumberToObject(data, "width", width);
cJSON_AddNumberToObject(data, "height", height);
cJSON_AddStringToObject(data, "snapshot", snap_text_copy);
g_free(b64);
g_free(snap_text_copy);
return make_success(data);
}
/* ── Tool dispatch table ──────────────────────────────────────────── */
typedef cJSON *(*tool_func_t)(cJSON *params);
typedef struct {
const char *name;
tool_func_t func;
gboolean requires_login; /* TRUE if only available after login */
} tool_entry_t;
static tool_entry_t tool_table[] = {
/* Login tools (available before login) */
{"login_status", tool_login_status, FALSE},
{"login", tool_login, FALSE},
{"logout", tool_logout, FALSE},
{"switch_identity", tool_switch_identity, FALSE},
/* Navigation tools */
{"open", tool_open, TRUE},
{"back", tool_back, TRUE},
{"forward", tool_forward, TRUE},
{"reload", tool_reload, TRUE},
{"stop", tool_stop, TRUE},
{"get_url", tool_get_url, TRUE},
{"get_title", tool_get_title, TRUE},
/* Snapshot & inspection tools */
{"snapshot", tool_snapshot, TRUE},
{"get_text", tool_get_text, TRUE},
{"get_html", tool_get_html, TRUE},
{"get_attr", tool_get_attr, TRUE},
{"get_value", tool_get_value, TRUE},
{"get_count", tool_get_count, TRUE},
{"get_box", tool_get_box, TRUE},
{"get_styles", tool_get_styles, TRUE},
{"is_visible", tool_is_visible, TRUE},
{"is_enabled", tool_is_enabled, TRUE},
{"is_checked", tool_is_checked, TRUE},
{"eval", tool_eval, TRUE},
{"screenshot", tool_screenshot, TRUE},
{"screenshot_annotated", tool_screenshot_annotated, TRUE},
{"upload", tool_upload, TRUE},
{"pdf", tool_pdf, TRUE},
/* Interaction tools */
{"click", tool_click, TRUE},
{"click_at", tool_click_at, TRUE},
{"fill", tool_fill, TRUE},
{"type", tool_type, TRUE},
{"press", tool_press, TRUE},
{"scroll", tool_scroll, TRUE},
{"hover", tool_hover, TRUE},
{"focus", tool_focus, TRUE},
{"close", tool_close, TRUE},
/* Extended interaction tools */
{"dblclick", tool_dblclick, TRUE},
{"select", tool_select, TRUE},
{"check", tool_check, TRUE},
{"uncheck", tool_uncheck, TRUE},
{"scroll_into_view", tool_scroll_into_view, TRUE},
{"keyboard_type", tool_keyboard_type, TRUE},
{"insert_text", tool_insert_text, TRUE},
{"keydown", tool_keydown, TRUE},
{"keyup", tool_keyup, TRUE},
{"drag", tool_drag, TRUE},
{"close_all", tool_close_all, TRUE},
/* Tab tools */
{"tab_list", tool_tab_list, TRUE},
{"tab_new", tool_tab_new, TRUE},
{"tab_switch", tool_tab_switch, TRUE},
{"tab_close", tool_tab_close, TRUE},
/* Wait tools */
{"wait", tool_wait, TRUE},
{"wait_for", tool_wait_for, TRUE},
{"wait_for_text", tool_wait_for_text, TRUE},
{"wait_for_url", tool_wait_for_url, TRUE},
{"wait_for_load", tool_wait_for_load, TRUE},
{"wait_for_fn", tool_wait_for_fn, TRUE},
/* Batch tool */
{"batch", tool_batch, TRUE},
/* Find element tools */
{"find_role", tool_find_role, TRUE},
{"find_text", tool_find_text, TRUE},
{"find_label", tool_find_label, TRUE},
{"find_placeholder", tool_find_placeholder, TRUE},
{"find_alt", tool_find_alt, TRUE},
{"find_title", tool_find_title, TRUE},
{"find_testid", tool_find_testid, TRUE},
{"find_first", tool_find_first, TRUE},
{"find_last", tool_find_last, TRUE},
{"find_nth", tool_find_nth, TRUE},
/* Cookies & web storage tools */
{"cookies_get", tool_cookies_get, TRUE},
{"cookies_set", tool_cookies_set, TRUE},
{"cookies_clear", tool_cookies_clear, TRUE},
{"storage_local_get", tool_storage_local_get, TRUE},
{"storage_local_get_key", tool_storage_local_get_key, TRUE},
{"storage_local_set", tool_storage_local_set, TRUE},
{"storage_local_clear", tool_storage_local_clear, TRUE},
{"storage_session_get", tool_storage_session_get, TRUE},
{"storage_session_get_key", tool_storage_session_get_key, TRUE},
{"storage_session_set", tool_storage_session_set, TRUE},
{"storage_session_clear", tool_storage_session_clear, TRUE},
/* Mouse tools */
{"mouse_move", tool_mouse_move, TRUE},
{"mouse_down", tool_mouse_down, TRUE},
{"mouse_up", tool_mouse_up, TRUE},
{"mouse_wheel", tool_mouse_wheel, TRUE},
/* Clipboard tools */
{"clipboard_read", tool_clipboard_read, TRUE},
{"clipboard_write", tool_clipboard_write, TRUE},
{"clipboard_copy", tool_clipboard_copy, TRUE},
{"clipboard_paste", tool_clipboard_paste, TRUE},
/* Settings tools */
{"set_viewport", tool_set_viewport, TRUE},
{"set_offline", tool_set_offline, TRUE},
{"set_headers", tool_set_headers, TRUE},
{"set_credentials", tool_set_credentials, TRUE},
{"set_media", tool_set_media, TRUE},
/* Frame tools */
{"frame_switch", tool_frame_switch, TRUE},
{"frame_main", tool_frame_main, TRUE},
/* Dialog tools */
{"dialog_accept", tool_dialog_accept, TRUE},
{"dialog_dismiss", tool_dialog_dismiss, TRUE},
{"dialog_status", tool_dialog_status, TRUE},
/* Debug tools */
{"console", tool_console, TRUE},
{"errors", tool_errors, TRUE},
{"highlight", tool_highlight, TRUE},
/* State tools */
{"state_save", tool_state_save, TRUE},
{"state_load", tool_state_load, TRUE},
};
static int tool_table_count = sizeof(tool_table) / sizeof(tool_table[0]);
/* ── Main dispatch function ───────────────────────────────────────── */
cJSON *agent_tools_dispatch(cJSON *request, SoupWebsocketConnection *conn) {
if (request == NULL) {
return make_error("INVALID_REQUEST", "Request is NULL");
}
/* Extract tool name and params. */
const char *tool_name = cJSON_GetStringValue(cJSON_GetObjectItem(request, "tool"));
cJSON *params = cJSON_GetObjectItem(request, "params");
cJSON *id = cJSON_GetObjectItem(request, "id");
int request_id = (id && cJSON_IsNumber(id)) ? (int)id->valuedouble : 0;
if (!tool_name || !tool_name[0]) {
return make_error("MISSING_TOOL", "No 'tool' field in request");
}
/* ── Filesystem & shell tools (work before login) ──────────── *
* These are system-level tools that give the agent direct access
* to the qube's filesystem and shell. They don't require a Nostr
* login, so we dispatch them before the login check below. */
if (agent_fs_is_tool(tool_name)) {
return agent_fs_tools_dispatch(tool_name, params);
}
/* ── Process / per-tab diagnostics (work before login) ────────── *
* These read /proc and the in-memory probe store; they don't touch
* Nostr or page content, so they're safe before login. */
if (strcmp(tool_name, "processes.list") == 0) {
cJSON *arr = process_info_get_processes_json();
return make_success(arr);
}
if (strcmp(tool_name, "processes.tabs") == 0) {
cJSON *arr = process_info_get_tabs_json();
return make_success(arr);
}
if (strcmp(tool_name, "processes.tab_probe") == 0) {
int idx = get_int_param(params, "tab_index", -1);
if (idx < 0) {
return make_error("MISSING_PARAM",
"Provide 'tab_index' (from processes.tabs)");
}
cJSON *obj = process_info_get_tab_probe_json(idx);
if (obj == NULL) {
return make_error("BAD_INDEX", "Tab index out of range");
}
return make_success(obj);
}
/* Check login requirement for known tools. */
gboolean requires_login = TRUE;
gboolean is_login_tool = (strcmp(tool_name, "login_status") == 0 ||
strcmp(tool_name, "login") == 0 ||
strcmp(tool_name, "logout") == 0 ||
strcmp(tool_name, "switch_identity") == 0);
if (is_login_tool) requires_login = FALSE;
/* batch dispatches sub-commands through agent_tools_dispatch(), which
* enforces login per sub-command. batch itself must not be gated so
* that callers can batch login_status / login / etc. without a prior
* login. */
if (strcmp(tool_name, "batch") == 0) requires_login = FALSE;
if (requires_login && !agent_is_logged_in()) {
return make_error("NOT_LOGGED_IN",
"This tool requires login. Use the 'login' tool first.");
}
/* Handle async tools (snapshot, eval) specially — they need async
* JS evaluation and send the response through the WebSocket
* connection directly. */
if (conn != NULL && strcmp(tool_name, "snapshot") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
gboolean interactive = get_bool_param(params, "interactive", FALSE);
gboolean compact = get_bool_param(params, "compact", FALSE);
if (agent_snapshot_take_async(wv, interactive, compact, conn, request_id)) {
return NULL; /* Response sent async */
}
return make_error("SNAPSHOT_FAILED", "Failed to start async snapshot");
}
if (conn != NULL && strcmp(tool_name, "eval") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *script_param = get_string_param(params, "script");
if (!script_param || !script_param[0]) return make_error("MISSING_PARAM", "Provide 'script'");
if (agent_js_eval_async(wv, script_param, conn, request_id, "eval", NULL)) {
return NULL;
}
return make_error("EVAL_FAILED", "Failed to start async eval");
}
/* get_text — async JS */
if (conn != NULL && strcmp(tool_name, "get_text") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');return el?el.textContent.trim():null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "get_text", text_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async get_text");
}
/* get_html — async JS */
if (conn != NULL && strcmp(tool_name, "get_html") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');return el?el.innerHTML:null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "get_html", html_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async get_html");
}
/* get_attr — async JS */
if (conn != NULL && strcmp(tool_name, "get_attr") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *attr = get_string_param(params, "attr");
if (!attr || !attr[0]) return make_error("MISSING_PARAM", "Provide 'attr'");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');return el?(el.getAttribute('%s')||''):null;})();",
sel, attr);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "get_attr", attr_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async get_attr");
}
/* get_value — async JS */
if (conn != NULL && strcmp(tool_name, "get_value") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');return el?el.value:null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "get_value", value_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async get_value");
}
/* get_count — async JS (uses selector directly, no ref) */
if (conn != NULL && strcmp(tool_name, "get_count") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *selector = get_string_param(params, "selector");
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
char *script = g_strdup_printf(
"String(document.querySelectorAll('%s').length);",
selector);
if (agent_js_eval_async(wv, script, conn, request_id, "get_count", count_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async get_count");
}
/* get_box — async JS */
if (conn != NULL && strcmp(tool_name, "get_box") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"var r=el.getBoundingClientRect();"
"return JSON.stringify({x:r.x,y:r.y,width:r.width,height:r.height,"
"top:r.top,right:r.right,bottom:r.bottom,left:r.left});})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "get_box", box_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async get_box");
}
/* get_styles — async JS */
if (conn != NULL && strcmp(tool_name, "get_styles") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"var s=getComputedStyle(el);var result={};"
"for(var i=0;i<s.length;i++){var p=s[i];result[p]=s.getPropertyValue(p);}"
"return JSON.stringify(result);})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "get_styles", styles_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async get_styles");
}
/* is_visible — async JS */
if (conn != NULL && strcmp(tool_name, "is_visible") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"var s=getComputedStyle(el);"
"return (s.display!=='none'&&s.visibility!=='hidden'&&"
"s.opacity!=='0'&&el.offsetParent!==null)?'true':'false';})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "is_visible", visible_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async is_visible");
}
/* is_enabled — async JS */
if (conn != NULL && strcmp(tool_name, "is_enabled") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"return !el.disabled?'true':'false';})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "is_enabled", enabled_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async is_enabled");
}
/* is_checked — async JS */
if (conn != NULL && strcmp(tool_name, "is_checked") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(!el)return null;"
"return el.checked?'true':'false';})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "is_checked", checked_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async is_checked");
}
/* click — coordinate-based via GDK event synthesis, with JS fallback.
* We first resolve the element's bounding box via getBoundingClientRect()
* (synchronous JS eval), then synthesize a real GDK button press/release
* at the center. This triggers WebKit's native hit-testing and full
* event propagation, which is required for SPA frameworks (React, Radix
* UI) that ignore JS synthetic .click() calls. If the bounding box
* cannot be retrieved, we fall back to the async JS .click() path. */
if (conn != NULL && strcmp(tool_name, "click") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
/* Escape the selector for safe embedding in a JS string literal. */
char *esc_sel = g_strescape(sel, NULL);
char *box_js = g_strdup_printf(
"(function(){var el=document.querySelector(\"%s\");"
"if(!el)return null;"
"var r=el.getBoundingClientRect();"
"return JSON.stringify({x:r.x,y:r.y,width:r.width,height:r.height});"
"})();",
esc_sel);
g_free(esc_sel);
char *box_result = agent_js_eval_sync(wv, box_js, 5000);
g_free(box_js);
gboolean coord_click_done = FALSE;
if (box_result && box_result[0] != '\0' && strcmp(box_result, "null") != 0) {
cJSON *box = cJSON_Parse(box_result);
if (box) {
cJSON *jx = cJSON_GetObjectItem(box, "x");
cJSON *jy = cJSON_GetObjectItem(box, "y");
cJSON *jw = cJSON_GetObjectItem(box, "width");
cJSON *jh = cJSON_GetObjectItem(box, "height");
if (jx && jy && jw && jh && cJSON_IsNumber(jx) && cJSON_IsNumber(jy) &&
cJSON_IsNumber(jw) && cJSON_IsNumber(jh)) {
double cx = jx->valuedouble + jw->valuedouble / 2.0;
double cy = jy->valuedouble + jh->valuedouble / 2.0;
cJSON_Delete(box);
g_free(box_result);
if (synthesize_click_at(wv, cx, cy, 1)) {
g_free(sel);
/* Send a success response directly through the
* WebSocket connection, matching the async path. */
cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "success", TRUE);
cJSON_AddNumberToObject(resp, "id", request_id);
char *str = cJSON_PrintUnformatted(resp);
if (str) {
soup_websocket_connection_send_text(conn, str);
free(str);
}
cJSON_Delete(resp);
return NULL;
}
coord_click_done = FALSE; /* synthesis failed, fall through */
} else {
cJSON_Delete(box);
}
}
}
if (!coord_click_done) g_free(box_result);
/* --- Fallback: async JS .click() --- */
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){el.click();return 'ok';}return null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "click", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async click");
}
/* click_at — coordinate-based click via GDK event synthesis (sync).
* Takes explicit x/y viewport coordinates and dispatches a real
* GDK button press/release. Optional 'button': 1=left (default),
* 2=middle, 3=right (triggers the native context menu). Bypasses
* selector resolution entirely. */
if (conn != NULL && strcmp(tool_name, "click_at") == 0) {
cJSON *jx = cJSON_GetObjectItem(params, "x");
cJSON *jy = cJSON_GetObjectItem(params, "y");
if (!jx || !jy || !cJSON_IsNumber(jx) || !cJSON_IsNumber(jy)) {
return make_error("MISSING_PARAM", "Provide numeric 'x' and 'y' coordinates");
}
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
double x = jx->valuedouble;
double y = jy->valuedouble;
int button = click_button_from_params(params);
if (!synthesize_click_at(wv, x, y, button)) {
return make_error("CLICK_FAILED", "Failed to synthesize GDK click event");
}
/* Send success response directly through the WebSocket. */
cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "success", TRUE);
cJSON_AddNumberToObject(resp, "id", request_id);
char *str = cJSON_PrintUnformatted(resp);
if (str) {
soup_websocket_connection_send_text(conn, str);
free(str);
}
cJSON_Delete(resp);
return NULL;
}
/* fill — focus + clear via JS, then type via GDK key events (SPA-safe).
* Falls back to the legacy JS .value= approach if GDK synthesis fails. */
if (conn != NULL && strcmp(tool_name, "fill") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
/* Focus the element and clear its value via JS so WebKit's input
* focus state is established before we synthesize key events. */
char *esc_sel = g_strescape(sel, NULL);
char *focus_js = g_strdup_printf(
"(function(){var el=document.querySelector(\"%s\");if(!el)return 'nofocus';"
"el.focus();el.value=\"\";"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"return 'ok';})();",
esc_sel);
g_free(esc_sel);
char *focus_result = agent_js_eval_sync(wv, focus_js, 5000);
g_free(focus_js);
gboolean focused = (focus_result && strcmp(focus_result, "ok") == 0);
g_free(focus_result);
if (focused && synthesize_type_text(wv, value)) {
g_free(sel);
/* Send success response directly through the WebSocket. */
cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "success", TRUE);
cJSON_AddNumberToObject(resp, "id", request_id);
char *str = cJSON_PrintUnformatted(resp);
if (str) {
soup_websocket_connection_send_text(conn, str);
free(str);
}
cJSON_Delete(resp);
return NULL;
}
/* --- Fallback: legacy async JS .value= approach --- */
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){el.value=\"%s\";"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"el.dispatchEvent(new Event('change',{bubbles:true}));return 'ok';}return null;})();",
sel, escaped);
g_free(escaped);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "fill", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async fill");
}
/* type — focus via JS, then type via GDK key events (SPA-safe).
* Optionally clears the field first if 'clear' is true.
* Falls back to the legacy JS .value+= approach if GDK synthesis fails. */
if (conn != NULL && strcmp(tool_name, "type") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
gboolean do_clear = get_bool_param(params, "clear", FALSE);
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
/* Focus the element; optionally clear its value via JS. */
char *esc_sel = g_strescape(sel, NULL);
char *focus_js = g_strdup_printf(
"(function(){var el=document.querySelector(\"%s\");if(!el)return 'nofocus';"
"el.focus();%s"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"return 'ok';})();",
esc_sel,
do_clear ? "el.value=\"\";" : "");
g_free(esc_sel);
char *focus_result = agent_js_eval_sync(wv, focus_js, 5000);
g_free(focus_js);
gboolean focused = (focus_result && strcmp(focus_result, "ok") == 0);
g_free(focus_result);
if (focused && synthesize_type_text(wv, value)) {
g_free(sel);
/* Send success response directly through the WebSocket. */
cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "success", TRUE);
cJSON_AddNumberToObject(resp, "id", request_id);
char *str = cJSON_PrintUnformatted(resp);
if (str) {
soup_websocket_connection_send_text(conn, str);
free(str);
}
cJSON_Delete(resp);
return NULL;
}
/* --- Fallback: legacy async JS .value+= approach --- */
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){el.value+=\"%s\";"
"el.dispatchEvent(new Event('input',{bubbles:true}));return 'ok';}return null;})();",
sel, escaped);
g_free(escaped);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "type", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async type");
}
/* hover — async JS */
if (conn != NULL && strcmp(tool_name, "hover") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){"
"el.dispatchEvent(new MouseEvent('mouseover',{bubbles:true}));"
"el.dispatchEvent(new MouseEvent('mouseenter',{bubbles:true}));return 'ok';}return null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "hover", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async hover");
}
/* focus — async JS */
if (conn != NULL && strcmp(tool_name, "focus") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){el.focus();return 'ok';}return null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "focus", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async focus");
}
/* press — async JS */
if (conn != NULL && strcmp(tool_name, "press") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
char *script = g_strdup_printf(
"(function(){var ev=new KeyboardEvent('keydown',{key:'%s',bubbles:true});"
"document.dispatchEvent(ev);ev=new KeyboardEvent('keyup',{key:'%s',bubbles:true});"
"document.dispatchEvent(ev);return 'ok';})();",
key, key);
if (agent_js_eval_async(wv, script, conn, request_id, "press", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async press");
}
/* scroll — async JS */
if (conn != NULL && strcmp(tool_name, "scroll") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *direction = get_string_param(params, "direction");
int amount = get_int_param(params, "amount", 500);
if (!direction || !direction[0]) return make_error("MISSING_PARAM", "Provide 'direction'");
int dx = 0, dy = 0;
if (strcmp(direction, "down") == 0) dy = amount;
else if (strcmp(direction, "up") == 0) dy = -amount;
else if (strcmp(direction, "right") == 0) dx = amount;
else if (strcmp(direction, "left") == 0) dx = -amount;
else return make_error("INVALID_DIRECTION", "Use: up, down, left, right");
char *script = g_strdup_printf("window.scrollBy(%d,%d);'ok';", dx, dy);
if (agent_js_eval_async(wv, script, conn, request_id, "scroll", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async scroll");
}
/* dblclick — async JS */
if (conn != NULL && strcmp(tool_name, "dblclick") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){"
"el.dispatchEvent(new MouseEvent('dblclick',{bubbles:true}));return 'ok';}return null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "dblclick", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async dblclick");
}
/* select — async JS */
if (conn != NULL && strcmp(tool_name, "select") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){el.value=\"%s\";"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"el.dispatchEvent(new Event('change',{bubbles:true}));return 'ok';}return null;})();",
sel, escaped);
g_free(escaped);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "select", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async select");
}
/* check — async JS */
if (conn != NULL && strcmp(tool_name, "check") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el&&el.type==='checkbox'){"
"el.checked=true;el.dispatchEvent(new Event('change',{bubbles:true}));return 'ok';}return null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "check", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async check");
}
/* uncheck — async JS */
if (conn != NULL && strcmp(tool_name, "uncheck") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el&&el.type==='checkbox'){"
"el.checked=false;el.dispatchEvent(new Event('change',{bubbles:true}));return 'ok';}return null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "uncheck", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async uncheck");
}
/* scroll_into_view — async JS */
if (conn != NULL && strcmp(tool_name, "scroll_into_view") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
char *script = g_strdup_printf(
"(function(){var el=document.querySelector('%s');if(el){"
"el.scrollIntoView({behavior:'smooth',block:'center'});return 'ok';}return null;})();",
sel);
g_free(sel);
if (agent_js_eval_async(wv, script, conn, request_id, "scroll_into_view", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async scroll_into_view");
}
/* keyboard_type — async JS */
if (conn != NULL && strcmp(tool_name, "keyboard_type") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){var el=document.activeElement;if(!el)return null;var text=\"%s\";"
"for(var i=0;i<text.length;i++){var ch=text[i];"
"el.dispatchEvent(new KeyboardEvent('keydown',{key:ch,bubbles:true}));"
"el.dispatchEvent(new KeyboardEvent('keypress',{key:ch,bubbles:true}));"
"if(el.value!==undefined)el.value+=ch;"
"el.dispatchEvent(new KeyboardEvent('keyup',{key:ch,bubbles:true}));}"
"el.dispatchEvent(new Event('input',{bubbles:true}));return 'ok';})();",
escaped);
g_free(escaped);
if (agent_js_eval_async(wv, script, conn, request_id, "keyboard_type", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async keyboard_type");
}
/* insert_text — async JS */
if (conn != NULL && strcmp(tool_name, "insert_text") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *value = get_string_param(params, "value");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *escaped = g_strescape(value, NULL);
char *script = g_strdup_printf(
"(function(){document.execCommand('insertText',false,\"%s\");return 'ok';})();",
escaped);
g_free(escaped);
if (agent_js_eval_async(wv, script, conn, request_id, "insert_text", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async insert_text");
}
/* keydown — async JS */
if (conn != NULL && strcmp(tool_name, "keydown") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
char *script = g_strdup_printf(
"(function(){var ev=new KeyboardEvent('keydown',{key:'%s',bubbles:true});"
"document.dispatchEvent(ev);return 'ok';})();",
key);
if (agent_js_eval_async(wv, script, conn, request_id, "keydown", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async keydown");
}
/* keyup — async JS */
if (conn != NULL && strcmp(tool_name, "keyup") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
char *script = g_strdup_printf(
"(function(){var ev=new KeyboardEvent('keyup',{key:'%s',bubbles:true});"
"document.dispatchEvent(ev);return 'ok';})();",
key);
if (agent_js_eval_async(wv, script, conn, request_id, "keyup", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async keyup");
}
/* drag — async JS */
if (conn != NULL && strcmp(tool_name, "drag") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *src_ref = get_string_param(params, "src_ref");
const char *src_selector = get_string_param(params, "src_selector");
const char *tgt_ref = get_string_param(params, "tgt_ref");
const char *tgt_selector = get_string_param(params, "tgt_selector");
char *src_sel = NULL;
if (src_ref && src_ref[0]) {
if (src_ref[0] != '@') src_sel = g_strdup(src_ref);
else {
const char *ref_id = src_ref + 1;
char *rscript = g_strdup_printf(
"(window.__agentRefs && window.__agentRefs['%s']) ? "
"window.__agentRefs['%s'].selector : null", ref_id, ref_id);
src_sel = agent_js_eval_sync(wv, rscript, 5000);
g_free(rscript);
if (src_sel == NULL || strcmp(src_sel, "null") == 0) {
g_free(src_sel);
return make_error("REF_NOT_FOUND", "No source element with that ref. Take a new snapshot.");
}
}
} else if (src_selector && src_selector[0]) {
src_sel = g_strdup(src_selector);
} else {
return make_error("MISSING_PARAM", "Provide 'src_ref' or 'src_selector'");
}
char *tgt_sel = NULL;
if (tgt_ref && tgt_ref[0]) {
if (tgt_ref[0] != '@') tgt_sel = g_strdup(tgt_ref);
else {
const char *ref_id = tgt_ref + 1;
char *rscript = g_strdup_printf(
"(window.__agentRefs && window.__agentRefs['%s']) ? "
"window.__agentRefs['%s'].selector : null", ref_id, ref_id);
tgt_sel = agent_js_eval_sync(wv, rscript, 5000);
g_free(rscript);
if (tgt_sel == NULL || strcmp(tgt_sel, "null") == 0) {
g_free(tgt_sel);
g_free(src_sel);
return make_error("REF_NOT_FOUND", "No target element with that ref. Take a new snapshot.");
}
}
} else if (tgt_selector && tgt_selector[0]) {
tgt_sel = g_strdup(tgt_selector);
} else {
g_free(src_sel);
return make_error("MISSING_PARAM", "Provide 'tgt_ref' or 'tgt_selector'");
}
char *script = g_strdup_printf(
"(function(){var src=document.querySelector('%s');var tgt=document.querySelector('%s');"
"if(!src||!tgt)return null;"
"src.dispatchEvent(new DragEvent('dragstart',{bubbles:true}));"
"tgt.dispatchEvent(new DragEvent('dragenter',{bubbles:true}));"
"tgt.dispatchEvent(new DragEvent('dragover',{bubbles:true}));"
"tgt.dispatchEvent(new DragEvent('drop',{bubbles:true}));"
"src.dispatchEvent(new DragEvent('dragend',{bubbles:true}));return 'ok';})();",
src_sel, tgt_sel);
g_free(src_sel);
g_free(tgt_sel);
if (agent_js_eval_async(wv, script, conn, request_id, "drag", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async drag");
}
/* ── Find element tools — async JS ─────────────────────────────── */
/* find_role — async JS */
if (conn != NULL && strcmp(tool_name, "find_role") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *role = get_string_param(params, "role");
const char *name = get_string_param(params, "name");
if (!role || !role[0]) return make_error("MISSING_PARAM", "Provide 'role'");
char *esc_role = g_strescape(role, NULL);
char *esc_name = name ? g_strescape(name, NULL) : g_strdup("");
char *script = g_strdup_printf(
"(function(){\n%s"
" var role = \"%s\";\n"
" var nameFilter = \"%s\";\n"
" var roleMap = { 'button':'button','link':'a','navigation':'nav',\n"
" 'main':'main','article':'article','region':'section',\n"
" 'form':'form','list':'ul','listitem':'li','heading':'h1',\n"
" 'textbox':'input','combobox':'select','image':'img',\n"
" 'paragraph':'p','table':'table','contentinfo':'footer',\n"
" 'banner':'header','complementary':'aside','search':'search' };\n"
" var candidates = [];\n"
" var byAttr = document.querySelectorAll('[role=\"' + role + '\"]');\n"
" for (var i = 0; i < byAttr.length; i++) candidates.push(byAttr[i]);\n"
" if (candidates.length === 0 && roleMap[role]) {\n"
" var byTag = document.querySelectorAll(roleMap[role]);\n"
" for (var j = 0; j < byTag.length; j++) candidates.push(byTag[j]);\n"
" }\n"
" if (candidates.length === 0) return null;\n"
" var el = null;\n"
" if (nameFilter) {\n"
" for (var k = 0; k < candidates.length; k++) {\n"
" var c = candidates[k];\n"
" var al = c.getAttribute('aria-label') || '';\n"
" var tc = c.textContent.trim();\n"
" if (al.indexOf(nameFilter) >= 0 || tc.indexOf(nameFilter) >= 0) { el = c; break; }\n"
" }\n"
" } else { el = candidates[0]; }\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_role, esc_name);
g_free(esc_role);
g_free(esc_name);
if (agent_js_eval_async(wv, script, conn, request_id, "find_role", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_role");
}
/* find_text — async JS */
if (conn != NULL && strcmp(tool_name, "find_text") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *text = get_string_param(params, "text");
gboolean exact = get_bool_param(params, "exact", FALSE);
if (!text || !text[0]) return make_error("MISSING_PARAM", "Provide 'text'");
char *esc_text = g_strescape(text, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var text = \"%s\";\n"
" var exact = %s;\n"
" var all = document.querySelectorAll('*');\n"
" var best = null;\n"
" for (var i = 0; i < all.length; i++) {\n"
" var el = all[i];\n"
" var tc = el.textContent.trim();\n"
" var match = exact ? (tc === text) : (tc.indexOf(text) >= 0);\n"
" if (!match) continue;\n"
" var hasChildMatch = false;\n"
" for (var j = 0; j < el.children.length; j++) {\n"
" var cc = el.children[j].textContent.trim();\n"
" if (exact ? (cc === text) : (cc.indexOf(text) >= 0)) { hasChildMatch = true; break; }\n"
" }\n"
" if (!hasChildMatch) { best = el; break; }\n"
" if (!best) best = el;\n"
" }\n"
" return assignRef(best);\n"
"})();",
FIND_HELPER_JS, esc_text, exact ? "true" : "false");
g_free(esc_text);
if (agent_js_eval_async(wv, script, conn, request_id, "find_text", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_text");
}
/* find_label — async JS */
if (conn != NULL && strcmp(tool_name, "find_label") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *label = get_string_param(params, "label");
if (!label || !label[0]) return make_error("MISSING_PARAM", "Provide 'label'");
char *esc_label = g_strescape(label, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var label = \"%s\";\n"
" var labels = document.querySelectorAll('label');\n"
" for (var i = 0; i < labels.length; i++) {\n"
" if (labels[i].textContent.trim().indexOf(label) < 0) continue;\n"
" var forId = labels[i].getAttribute('for');\n"
" if (forId) { var t = document.getElementById(forId); if (t) return assignRef(t); }\n"
" var inner = labels[i].querySelector('input,select,textarea,button');\n"
" if (inner) return assignRef(inner);\n"
" }\n"
" var byAria = document.querySelectorAll('[aria-label]');\n"
" for (var j = 0; j < byAria.length; j++) {\n"
" if (byAria[j].getAttribute('aria-label').indexOf(label) >= 0) return assignRef(byAria[j]);\n"
" }\n"
" var byLabeled = document.querySelectorAll('[aria-labelledby]');\n"
" for (var k = 0; k < byLabeled.length; k++) {\n"
" var ids = byLabeled[k].getAttribute('aria-labelledby').split(/\\s+/);\n"
" for (var m = 0; m < ids.length; m++) {\n"
" var ref = document.getElementById(ids[m]);\n"
" if (ref && ref.textContent.trim().indexOf(label) >= 0) return assignRef(byLabeled[k]);\n"
" }\n"
" }\n"
" return null;\n"
"})();",
FIND_HELPER_JS, esc_label);
g_free(esc_label);
if (agent_js_eval_async(wv, script, conn, request_id, "find_label", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_label");
}
/* find_placeholder — async JS */
if (conn != NULL && strcmp(tool_name, "find_placeholder") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *placeholder = get_string_param(params, "placeholder");
if (!placeholder || !placeholder[0]) return make_error("MISSING_PARAM", "Provide 'placeholder'");
char *esc_ph = g_strescape(placeholder, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var el = document.querySelector('[placeholder*=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_ph);
g_free(esc_ph);
if (agent_js_eval_async(wv, script, conn, request_id, "find_placeholder", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_placeholder");
}
/* find_alt — async JS */
if (conn != NULL && strcmp(tool_name, "find_alt") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *alt = get_string_param(params, "alt");
if (!alt || !alt[0]) return make_error("MISSING_PARAM", "Provide 'alt'");
char *esc_alt = g_strescape(alt, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var el = document.querySelector('[alt*=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_alt);
g_free(esc_alt);
if (agent_js_eval_async(wv, script, conn, request_id, "find_alt", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_alt");
}
/* find_title — async JS */
if (conn != NULL && strcmp(tool_name, "find_title") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *title = get_string_param(params, "title");
if (!title || !title[0]) return make_error("MISSING_PARAM", "Provide 'title'");
char *esc_title = g_strescape(title, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var el = document.querySelector('[title*=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_title);
g_free(esc_title);
if (agent_js_eval_async(wv, script, conn, request_id, "find_title", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_title");
}
/* find_testid — async JS */
if (conn != NULL && strcmp(tool_name, "find_testid") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *testid = get_string_param(params, "testid");
if (!testid || !testid[0]) return make_error("MISSING_PARAM", "Provide 'testid'");
char *esc_id = g_strescape(testid, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var el = document.querySelector('[data-testid=\"%s\"]');\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_id);
g_free(esc_id);
if (agent_js_eval_async(wv, script, conn, request_id, "find_testid", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_testid");
}
/* find_first — async JS */
if (conn != NULL && strcmp(tool_name, "find_first") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *selector = get_string_param(params, "selector");
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
char *esc_sel = g_strescape(selector, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var el = document.querySelector(\"%s\");\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_sel);
g_free(esc_sel);
if (agent_js_eval_async(wv, script, conn, request_id, "find_first", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_first");
}
/* find_last — async JS */
if (conn != NULL && strcmp(tool_name, "find_last") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *selector = get_string_param(params, "selector");
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
char *esc_sel = g_strescape(selector, NULL);
char *script = g_strdup_printf(
"(function(){\n%s"
" var list = document.querySelectorAll(\"%s\");\n"
" var el = list.length > 0 ? list[list.length - 1] : null;\n"
" return assignRef(el);\n"
"})();",
FIND_HELPER_JS, esc_sel);
g_free(esc_sel);
if (agent_js_eval_async(wv, script, conn, request_id, "find_last", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_last");
}
/* find_nth — async JS */
if (conn != NULL && strcmp(tool_name, "find_nth") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *selector = get_string_param(params, "selector");
int n = get_int_param(params, "n", -1);
if (!selector || !selector[0]) return make_error("MISSING_PARAM", "Provide 'selector'");
if (n < 0) return make_error("MISSING_PARAM", "Provide 'n' (non-negative integer)");
char *esc_sel = g_strescape(selector, NULL);
char *script = g_strdup_printf(
"(function(){\n"
" var n = %d;\n"
"%s"
" var list = document.querySelectorAll(\"%s\");\n"
" var el = (n >= 0 && n < list.length) ? list[n] : null;\n"
" return assignRef(el);\n"
"})();",
n, FIND_HELPER_JS, esc_sel);
g_free(esc_sel);
if (agent_js_eval_async(wv, script, conn, request_id, "find_nth", find_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async find_nth");
}
/* ── Cookies & web storage tools — async JS ────────────────────── */
/* cookies_get — async JS */
if (conn != NULL && strcmp(tool_name, "cookies_get") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var cookies = document.cookie.split('; ');"
" var result = [];"
" for (var i = 0; i < cookies.length; i++) {"
" if (!cookies[i]) continue;"
" var idx = cookies[i].indexOf('=');"
" var name = idx > 0 ? cookies[i].substring(0, idx) : cookies[i];"
" var value = idx > 0 ? cookies[i].substring(idx + 1) : '';"
" result.push({name: name, value: value});"
" }"
" return JSON.stringify(result);"
"})();";
char *js = g_strdup(script);
if (agent_js_eval_async(wv, js, conn, request_id, "cookies_get", cookies_get_result_handler)) {
g_free(js);
return NULL;
}
g_free(js);
return make_error("EVAL_FAILED", "Failed to start async cookies_get");
}
/* cookies_set — async JS */
if (conn != NULL && strcmp(tool_name, "cookies_set") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *name = get_string_param(params, "name");
const char *value = get_string_param(params, "value");
const char *domain = get_string_param(params, "domain");
const char *path = get_string_param(params, "path");
gboolean secure = get_bool_param(params, "secure", FALSE);
int max_age = get_int_param(params, "max_age", -1);
if (!name || !name[0]) return make_error("MISSING_PARAM", "Provide 'name'");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
if (!path || !path[0]) path = "/";
char *esc_name = g_strescape(name, NULL);
char *esc_value = g_strescape(value, NULL);
char *esc_path = g_strescape(path, NULL);
char *esc_domain = domain ? g_strescape(domain, NULL) : NULL;
GString *cookie_str = g_string_new(NULL);
g_string_printf(cookie_str, "%s=%s; path=%s", esc_name, esc_value, esc_path);
if (esc_domain && esc_domain[0]) {
g_string_append_printf(cookie_str, "; domain=%s", esc_domain);
}
if (secure) {
g_string_append(cookie_str, "; secure");
}
if (max_age >= 0) {
g_string_append_printf(cookie_str, "; max-age=%d", max_age);
}
char *esc_cookie = g_strescape(cookie_str->str, NULL);
char *script = g_strdup_printf("document.cookie = \"%s\"; 'ok';", esc_cookie);
g_free(esc_name);
g_free(esc_value);
g_free(esc_path);
g_free(esc_domain);
g_free(esc_cookie);
g_string_free(cookie_str, TRUE);
if (agent_js_eval_async(wv, script, conn, request_id, "cookies_set", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async cookies_set");
}
/* cookies_clear — async JS */
if (conn != NULL && strcmp(tool_name, "cookies_clear") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var cookies = document.cookie.split('; ');"
" for (var i = 0; i < cookies.length; i++) {"
" var idx = cookies[i].indexOf('=');"
" var name = idx > 0 ? cookies[i].substring(0, idx) : cookies[i];"
" document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';"
" }"
" return 'ok';"
"})();";
char *js = g_strdup(script);
if (agent_js_eval_async(wv, js, conn, request_id, "cookies_clear", action_result_handler)) {
g_free(js);
return NULL;
}
g_free(js);
return make_error("EVAL_FAILED", "Failed to start async cookies_clear");
}
/* storage_local_get — async JS */
if (conn != NULL && strcmp(tool_name, "storage_local_get") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var result = {};"
" for (var i = 0; i < localStorage.length; i++) {"
" var k = localStorage.key(i);"
" result[k] = localStorage.getItem(k);"
" }"
" return JSON.stringify(result);"
"})();";
char *js = g_strdup(script);
if (agent_js_eval_async(wv, js, conn, request_id, "storage_local_get", storage_get_result_handler)) {
g_free(js);
return NULL;
}
g_free(js);
return make_error("EVAL_FAILED", "Failed to start async storage_local_get");
}
/* storage_local_get_key — async JS */
if (conn != NULL && strcmp(tool_name, "storage_local_get_key") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
char *esc_key = g_strescape(key, NULL);
char *script = g_strdup_printf(
"(function(){var v=localStorage.getItem(\"%s\");"
"return v===null?'__null__':v;})();", esc_key);
g_free(esc_key);
if (agent_js_eval_async(wv, script, conn, request_id, "storage_local_get_key", storage_get_key_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async storage_local_get_key");
}
/* storage_local_set — async JS */
if (conn != NULL && strcmp(tool_name, "storage_local_set") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *key = get_string_param(params, "key");
const char *value = get_string_param(params, "value");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *esc_key = g_strescape(key, NULL);
char *esc_value = g_strescape(value, NULL);
char *script = g_strdup_printf(
"localStorage.setItem(\"%s\",\"%s\"); 'ok';", esc_key, esc_value);
g_free(esc_key);
g_free(esc_value);
if (agent_js_eval_async(wv, script, conn, request_id, "storage_local_set", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async storage_local_set");
}
/* storage_local_clear — async JS */
if (conn != NULL && strcmp(tool_name, "storage_local_clear") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *script = g_strdup("localStorage.clear(); 'ok';");
if (agent_js_eval_async(wv, script, conn, request_id, "storage_local_clear", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async storage_local_clear");
}
/* storage_session_get — async JS */
if (conn != NULL && strcmp(tool_name, "storage_session_get") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" var result = {};"
" for (var i = 0; i < sessionStorage.length; i++) {"
" var k = sessionStorage.key(i);"
" result[k] = sessionStorage.getItem(k);"
" }"
" return JSON.stringify(result);"
"})();";
char *js = g_strdup(script);
if (agent_js_eval_async(wv, js, conn, request_id, "storage_session_get", storage_get_result_handler)) {
g_free(js);
return NULL;
}
g_free(js);
return make_error("EVAL_FAILED", "Failed to start async storage_session_get");
}
/* storage_session_get_key — async JS */
if (conn != NULL && strcmp(tool_name, "storage_session_get_key") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *key = get_string_param(params, "key");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
char *esc_key = g_strescape(key, NULL);
char *script = g_strdup_printf(
"(function(){var v=sessionStorage.getItem(\"%s\");"
"return v===null?'__null__':v;})();", esc_key);
g_free(esc_key);
if (agent_js_eval_async(wv, script, conn, request_id, "storage_session_get_key", storage_get_key_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async storage_session_get_key");
}
/* storage_session_set — async JS */
if (conn != NULL && strcmp(tool_name, "storage_session_set") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *key = get_string_param(params, "key");
const char *value = get_string_param(params, "value");
if (!key || !key[0]) return make_error("MISSING_PARAM", "Provide 'key'");
if (!value) return make_error("MISSING_PARAM", "Provide 'value'");
char *esc_key = g_strescape(key, NULL);
char *esc_value = g_strescape(value, NULL);
char *script = g_strdup_printf(
"sessionStorage.setItem(\"%s\",\"%s\"); 'ok';", esc_key, esc_value);
g_free(esc_key);
g_free(esc_value);
if (agent_js_eval_async(wv, script, conn, request_id, "storage_session_set", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async storage_session_set");
}
/* storage_session_clear — async JS */
if (conn != NULL && strcmp(tool_name, "storage_session_clear") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *script = g_strdup("sessionStorage.clear(); 'ok';");
if (agent_js_eval_async(wv, script, conn, request_id, "storage_session_clear", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async storage_session_clear");
}
/* mouse_move — async JS */
if (conn != NULL && strcmp(tool_name, "mouse_move") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
int x = get_int_param(params, "x", 0);
int y = get_int_param(params, "y", 0);
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new MouseEvent('mousemove',"
"{clientX:%d,clientY:%d,bubbles:true}));return 'ok';})();",
x, y);
if (agent_js_eval_async(wv, script, conn, request_id, "mouse_move", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async mouse_move");
}
/* mouse_down — async JS */
if (conn != NULL && strcmp(tool_name, "mouse_down") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *button_str = get_string_param(params, "button");
int button = mouse_button_from_string(button_str);
int x = get_int_param(params, "x", 0);
int y = get_int_param(params, "y", 0);
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new MouseEvent('mousedown',"
"{button:%d,clientX:%d,clientY:%d,bubbles:true}));return 'ok';})();",
button, x, y);
if (agent_js_eval_async(wv, script, conn, request_id, "mouse_down", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async mouse_down");
}
/* mouse_up — async JS */
if (conn != NULL && strcmp(tool_name, "mouse_up") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *button_str = get_string_param(params, "button");
int button = mouse_button_from_string(button_str);
int x = get_int_param(params, "x", 0);
int y = get_int_param(params, "y", 0);
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new MouseEvent('mouseup',"
"{button:%d,clientX:%d,clientY:%d,bubbles:true}));return 'ok';})();",
button, x, y);
if (agent_js_eval_async(wv, script, conn, request_id, "mouse_up", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async mouse_up");
}
/* mouse_wheel — async JS */
if (conn != NULL && strcmp(tool_name, "mouse_wheel") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
int dy = get_int_param(params, "dy", 0);
int dx = get_int_param(params, "dx", 0);
char *script = g_strdup_printf(
"(function(){document.dispatchEvent(new WheelEvent('wheel',"
"{deltaY:%d,deltaX:%d,bubbles:true}));return 'ok';})();",
dy, dx);
if (agent_js_eval_async(wv, script, conn, request_id, "mouse_wheel", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async mouse_wheel");
}
/* clipboard_copy — async JS */
if (conn != NULL && strcmp(tool_name, "clipboard_copy") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *script = g_strdup("document.execCommand('copy'); 'ok';");
if (agent_js_eval_async(wv, script, conn, request_id, "clipboard_copy", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async clipboard_copy");
}
/* clipboard_paste — async JS */
if (conn != NULL && strcmp(tool_name, "clipboard_paste") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *script = g_strdup("document.execCommand('paste'); 'ok';");
if (agent_js_eval_async(wv, script, conn, request_id, "clipboard_paste", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async clipboard_paste");
}
/* set_media — async JS */
if (conn != NULL && strcmp(tool_name, "set_media") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *scheme = get_string_param(params, "scheme");
if (!scheme || !scheme[0]) return make_error("MISSING_PARAM", "Provide 'scheme' ('dark' or 'light')");
if (strcmp(scheme, "dark") != 0 && strcmp(scheme, "light") != 0) {
return make_error("INVALID_PARAM", "scheme must be 'dark' or 'light'");
}
char *script = g_strdup_printf(
"document.documentElement.style.colorScheme = '%s'; 'ok';", scheme);
if (agent_js_eval_async(wv, script, conn, request_id, "set_media", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async set_media");
}
/* frame_switch — store frame selector (no JS needed, sync-only).
* Falls through to sync dispatch. */
/* frame_main — clear frame selector (no JS needed, sync-only).
* Falls through to sync dispatch. */
/* dialog_accept — async JS */
if (conn != NULL && strcmp(tool_name, "dialog_accept") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *text = get_string_param(params, "text");
char *esc_text = text ? g_strescape(text, NULL) : NULL;
char *script = g_strdup_printf(
"(function(){"
" if (window.__agentDialogInstalled === undefined) return 'no_dialog';"
" if (!window.__pendingDialog) return 'no_dialog';"
" var d = window.__pendingDialog;"
" if (d.type === 'confirm') { d.result = true; }"
" else if (d.type === 'prompt') { d.result = %s ? \"%s\" : ''; }"
" window.__pendingDialog = null;"
" return 'ok';"
"})();",
text ? "true" : "false",
esc_text ? esc_text : "");
g_free(esc_text);
if (agent_js_eval_async(wv, script, conn, request_id, "dialog_accept", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async dialog_accept");
}
/* dialog_dismiss — async JS */
if (conn != NULL && strcmp(tool_name, "dialog_dismiss") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" if (window.__agentDialogInstalled === undefined) return 'no_dialog';"
" if (!window.__pendingDialog) return 'no_dialog';"
" var d = window.__pendingDialog;"
" if (d.type === 'confirm') { d.result = false; }"
" else if (d.type === 'prompt') { d.result = null; }"
" window.__pendingDialog = null;"
" return 'ok';"
"})();";
char *js = g_strdup(script);
if (agent_js_eval_async(wv, js, conn, request_id, "dialog_dismiss", action_result_handler)) {
g_free(js);
return NULL;
}
g_free(js);
return make_error("EVAL_FAILED", "Failed to start async dialog_dismiss");
}
/* dialog_status — async JS (install overrides + read status) */
if (conn != NULL && strcmp(tool_name, "dialog_status") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *script =
"(function(){"
" if (window.__agentDialogInstalled === undefined) {"
" window.__pendingDialog = null;"
" window.alert = function(msg){ window.__pendingDialog = {type:'alert', message:String(msg)}; };"
" window.confirm = function(msg){ window.__pendingDialog = {type:'confirm', message:String(msg), result:null}; return false; };"
" window.prompt = function(msg, def){ window.__pendingDialog = {type:'prompt', message:String(msg), default:def!=null?String(def):'', result:null}; return null; };"
" window.__agentDialogInstalled = true;"
" return 'none';"
" }"
" return (window.__pendingDialog === null || window.__pendingDialog === undefined) ? 'none' : JSON.stringify(window.__pendingDialog);"
"})();";
char *js = g_strdup(script);
if (agent_js_eval_async(wv, js, conn, request_id, "dialog_status", dialog_status_result_handler)) {
g_free(js);
return NULL;
}
g_free(js);
return make_error("EVAL_FAILED", "Failed to start async dialog_status");
}
/* console — async JS (install hook + read messages) */
if (conn != NULL && strcmp(tool_name, "console") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
gboolean clear = get_bool_param(params, "clear", FALSE);
char *script = g_strdup_printf(
"(function(){"
" if (window.__agentConsoleInstalled === undefined) {"
" window.__pageConsole = [];"
" var orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };"
" function push(level, args){"
" var text = Array.prototype.map.call(args, function(a){"
" try { return typeof a === 'object' ? JSON.stringify(a) : String(a); } catch(e){ return String(a); }"
" }).join(' ');"
" window.__pageConsole.push({level: level, text: text});"
" }"
" console.log = function(){ push('log', arguments); orig.log.apply(console, arguments); };"
" console.info = function(){ push('info', arguments); orig.info.apply(console, arguments); };"
" console.warn = function(){ push('warning', arguments); orig.warn.apply(console, arguments); };"
" console.error = function(){ push('error', arguments); orig.error.apply(console, arguments); };"
" console.debug = function(){ push('debug', arguments); orig.debug.apply(console, arguments); };"
" window.__agentConsoleInstalled = true;"
" return '[]';"
" }"
" var arr = window.__pageConsole || [];"
" var out = JSON.stringify(arr);"
" if (%s) { window.__pageConsole = []; }"
" return out;"
"})();",
clear ? "true" : "false");
if (agent_js_eval_async(wv, script, conn, request_id, "console", console_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async console");
}
/* errors — async JS (install listener + read errors) */
if (conn != NULL && strcmp(tool_name, "errors") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
gboolean clear = get_bool_param(params, "clear", FALSE);
char *script = g_strdup_printf(
"(function(){"
" if (window.__agentErrorInstalled === undefined) {"
" window.__pageErrors = [];"
" window.addEventListener('error', function(e){"
" window.__pageErrors.push({message:e.message||'', filename:e.filename||'', line:e.lineno||0});"
" });"
" window.__agentErrorInstalled = true;"
" return '[]';"
" }"
" var arr = window.__pageErrors || [];"
" var out = JSON.stringify(arr);"
" if (%s) { window.__pageErrors = []; }"
" return out;"
"})();",
clear ? "true" : "false");
if (agent_js_eval_async(wv, script, conn, request_id, "errors", errors_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async errors");
}
/* highlight — async JS */
if (conn != NULL && strcmp(tool_name, "highlight") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
char *sel = resolve_ref_async(params);
if (sel == NULL) return make_error("REF_NOT_FOUND", "No element with that ref. Take a new snapshot.");
int duration = get_int_param(params, "duration", 2000);
char *esc_sel = g_strescape(sel, NULL);
g_free(sel);
char *script = g_strdup_printf(
"(function(){"
" var el = document.querySelector(\"%s\");"
" if (!el) return null;"
" var old = el.style.outline;"
" el.style.outline = '3px solid red';"
" setTimeout(function(){ el.style.outline = old; }, %d);"
" return 'ok';"
"})();",
esc_sel, duration);
g_free(esc_sel);
if (agent_js_eval_async(wv, script, conn, request_id, "highlight", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async highlight");
}
/* state_save — async JS (return state as JSON string) */
if (conn != NULL && strcmp(tool_name, "state_save") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *path = get_string_param(params, "path");
const char *script =
"(function(){"
" var state = { localStorage: {}, cookies: [] };"
" for (var i = 0; i < localStorage.length; i++) {"
" var k = localStorage.key(i);"
" state.localStorage[k] = localStorage.getItem(k);"
" }"
" document.cookie.split('; ').forEach(function(c) {"
" if (!c) return;"
" var idx = c.indexOf('=');"
" state.cookies.push({name: idx>0?c.substring(0,idx):c, value: idx>0?c.substring(idx+1):''});"
" });"
" return JSON.stringify(state);"
"})();";
char *js = g_strdup(script);
if (agent_js_eval_async(wv, js, conn, request_id, "state_save", state_save_result_handler)) {
g_free(js);
/* If a path was provided, we still need to write the file.
* The async handler returns the state in data.state. The
* caller (MCP/WS) will get the JSON back and can write it.
* For file writing in async mode, fall back to sync path. */
if (path && path[0]) {
/* Async path can't easily write the file after JS completes.
* Fall through to sync dispatch which handles file I/O. */
return NULL;
}
return NULL;
}
g_free(js);
return make_error("EVAL_FAILED", "Failed to start async state_save");
}
/* state_load — async JS */
if (conn != NULL && strcmp(tool_name, "state_load") == 0) {
WebKitWebView *wv = get_active_webview();
if (wv == NULL) return make_error("NO_TAB", "No active tab");
const char *path = get_string_param(params, "path");
const char *state_param = get_string_param(params, "state");
if ((!path || !path[0]) && (!state_param || !state_param[0])) {
return make_error("MISSING_PARAM", "Provide 'path' or 'state'");
}
char *state_json = NULL;
if (path && path[0]) {
GError *err = NULL;
if (!g_file_get_contents(path, &state_json, NULL, &err)) {
char msg[512];
snprintf(msg, sizeof(msg), "Failed to read state file: %s",
err ? err->message : "unknown error");
if (err) g_error_free(err);
return make_error("FILE_ERROR", msg);
}
} else {
state_json = g_strdup(state_param);
}
char *esc_state = g_strescape(state_json, NULL);
g_free(state_json);
char *script = g_strdup_printf(
"(function(){"
" try {"
" var state = JSON.parse(\"%s\");"
" Object.keys(state.localStorage||{}).forEach(function(k) {"
" localStorage.setItem(k, state.localStorage[k]);"
" });"
" (state.cookies||[]).forEach(function(c) {"
" document.cookie = c.name + '=' + c.value + '; path=/';"
" });"
" return 'ok';"
" } catch(e) { return 'parse_error:' + e.message; }"
"})();",
esc_state);
g_free(esc_state);
if (agent_js_eval_async(wv, script, conn, request_id, "state_load", action_result_handler)) {
g_free(script);
return NULL;
}
g_free(script);
return make_error("EVAL_FAILED", "Failed to start async state_load");
}
/* Find the tool in the table for synchronous tools. */
tool_func_t func = NULL;
for (int i = 0; i < tool_table_count; i++) {
if (strcmp(tool_table[i].name, tool_name) == 0) {
func = tool_table[i].func;
break;
}
}
if (func == NULL) {
return make_error("UNKNOWN_TOOL", "Unknown tool name");
}
/* Execute the tool synchronously. */
cJSON *response = func(params ? params : cJSON_CreateObject());
/* Copy the request id into the response. */
if (id && response) {
if (cJSON_IsNumber(id)) {
cJSON_AddNumberToObject(response, "id", id->valuedouble);
} else if (cJSON_IsString(id)) {
cJSON_AddStringToObject(response, "id", id->valuestring);
}
}
return response;
}