6067 lines
246 KiB
C
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;
|
|
}
|