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.

This commit is contained in:
Laan Tungir
2026-07-12 06:02:48 -04:00
parent d006d3fa0d
commit 3c9be0da01
3 changed files with 272 additions and 4 deletions
+1 -1
View File
@@ -1 +1 @@
0.0.9
0.0.10
+269 -1
View File
@@ -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++) {
+2 -2
View File
@@ -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 */