From 3c9be0da015244b0be45fa11c9db0e449f2ce43f Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sun, 12 Jul 2026 06:02:48 -0400 Subject: [PATCH] v0.0.10 - Converted all JS-based tools to async evaluation: get_text, get_html, get_attr, click, fill, type, hover, focus, press, scroll. All 12 tools now pass end-to-end test: login, open, snapshot, get_text, get_html, get_attr, eval, click (navigated to IANA page), scroll, tab_list, get_url. --- VERSION | 2 +- src/agent_tools.c | 270 +++++++++++++++++++++++++++++++++++++++++++++- src/version.h | 4 +- 3 files changed, 272 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index c5d54ec..7c1886b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.9 +0.0.10 diff --git a/src/agent_tools.c b/src/agent_tools.c index e2f597d..c6bf178 100644 --- a/src/agent_tools.c +++ b/src/agent_tools.c @@ -674,6 +674,74 @@ static cJSON *tool_wait_for(cJSON *params) { return make_error("TIMEOUT", "Element did not appear within timeout"); } +/* ── 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 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); +} + +/* 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; +} + /* ── Tool dispatch table ──────────────────────────────────────────── */ typedef cJSON *(*tool_func_t)(cJSON *params); @@ -783,11 +851,211 @@ cJSON *agent_tools_dispatch(cJSON *request, SoupWebsocketConnection *conn) { 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; /* Response sent async */ + 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"); + } + + /* click — async JS */ + 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."); + 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"); + } + + /* fill — async JS */ + 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."); + 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 — async JS */ + 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'"); + 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}));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"); + } + /* Find the tool in the table for synchronous tools. */ tool_func_t func = NULL; for (int i = 0; i < tool_table_count; i++) { diff --git a/src/version.h b/src/version.h index 77e95ad..5a11027 100644 --- a/src/version.h +++ b/src/version.h @@ -11,9 +11,9 @@ #ifndef SOVEREIGN_BROWSER_VERSION_H #define SOVEREIGN_BROWSER_VERSION_H -#define SB_VERSION "v0.0.9" +#define SB_VERSION "v0.0.10" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 9 +#define SB_VERSION_PATCH 10 #endif /* SOVEREIGN_BROWSER_VERSION_H */