From ebe7dc72f1dc1e6aac59d2ef130725934223f38c Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Mon, 10 Aug 2026 09:05:43 +0200 Subject: [PATCH] feat: State-Info nach Klick/Screenshot, kein extra Warten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - browser_computer left_click: afterState (url, urlChanged, readyState, activeTag, activeLabel, dialogText) direkt im Ergebnis — kein sleep, kein Polling, kein Blockieren. - browser_computer screenshot: page (url, title, readyState, activeTag) im Ergebnis — ein einziger evaluateInMainWorld-Call. - browser_click (semantisch): snapshotAfterClick() nach Klick — gleiche Felder, gleiche Logik, kein extra Warten. - browser_navigate: gibt loading:true + hint zurück statt zu blockieren. Die KI entscheidet selbst ob sie wartet. - Keine adaptive Pace, kein waitForTabLoad, kein Polling — das war der Grund warum alles eingefroren ist. --- src/background/computer.ts | 34 +++++++++++++++++++++++++++++- src/background/index.ts | 43 ++++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/background/computer.ts b/src/background/computer.ts index c2e7c42..74f867c 100644 --- a/src/background/computer.ts +++ b/src/background/computer.ts @@ -574,6 +574,19 @@ async function execute(tabId: number, action: string, input: Record 0 ? shot.height : 0; const [cx, cy] = toModel(s, s.x, s.y); + + // Seitenstatus mitliefern — das Modell sieht sofort ob die Seite noch lädt + // und kann selbst entscheiden ob es warten will. Kein extra Warten hier. + let page: Record | undefined; + try { + page = await evaluateInMainWorld(tabId, `({ + url: location.href, + title: document.title, + readyState: document.readyState, + activeTag: (document.activeElement||{}).tagName?.toLowerCase() || null, + })`, { returnByValue: true }) as Record; + } catch { /* chrome:// etc — egal */ } + return { action, image: shot.dataUrl, @@ -582,6 +595,7 @@ async function execute(tabId: number, action: string, input: Record 1 ? 'double' : button === 'right' ? 'right' : 'left'); - return mouseResult(action, s, p, { button, clickCount: clicks, modifiers }); + + // State nach Klick abfragen — kein extra Warten, nur schnell schauen was + // sich geändert hat. Das Modell entscheidet selbst ob es einen Screenshot + // braucht oder ob die Info hier reicht. + const result = mouseResult(action, s, p, { button, clickCount: clicks, modifiers }); + try { + const post = await evaluateInMainWorld(tabId, `({ + url: location.href, + readyState: document.readyState, + activeTag: (document.activeElement||{}).tagName?.toLowerCase() || null, + activeLabel: document.activeElement?.getAttribute?.('aria-label') + || document.activeElement?.getAttribute?.('placeholder') || null, + hasDialog: !!(document.querySelector('dialog[open],[role="dialog"],[role="alertdialog"]')), + })`, { returnByValue: true }) as Record; + if (post) result.afterState = post; + } catch { /* Seite navigiert — das ist selbst ein Signal */ + result.afterState = { navigating: true }; + } + return result; } case 'left_mouse_down': diff --git a/src/background/index.ts b/src/background/index.ts index 1def4de..4cdff50 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -420,6 +420,10 @@ async function wantsTrusted(tabId: number): Promise { async function clickElement(tabId: number, selector: string, forceTrusted: boolean): Promise { const trusted = forceTrusted || await wantsTrusted(tabId); + // URL vor dem Klick merken — für den State-Vergleich danach + let preUrl = ''; + try { preUrl = await execInTab(tabId, () => location.href); } catch { /* ok */ } + if (trusted) { const box = await elementBox(tabId, selector); if (!box) throw new ToolError('ELEMENT_NOT_FOUND', `Element nicht gefunden oder unsichtbar: ${selector}`, true); @@ -427,9 +431,9 @@ async function clickElement(tabId: number, selector: string, forceTrusted: boole await cdp.attach(tabId); await cdp.mouseClick(tabId, box.x, box.y); notifyCursor(tabId, box.x, box.y, 'left'); - return { clicked: selector, tag: box.tag, text: box.text, _method: 'cdp' }; + const after = await snapshotAfterClick(tabId, preUrl); + return { clicked: selector, tag: box.tag, text: box.text, ...after, _method: 'cdp' }; } catch (e: any) { - // Kein harter Fehler: lieber synthetisch klicken als gar nicht handeln. console.warn('[Nexus] CDP-Klick fehlgeschlagen, weiche auf synthetisch aus:', e?.message); } } @@ -458,7 +462,6 @@ async function clickElement(tabId: number, selector: string, forceTrusted: boole if (!el) return { error: 'NOT_FOUND' }; el.scrollIntoView({ block: 'center', behavior: 'instant' as ScrollBehavior }); - // Vollständige Maus-Event-Sequenz für maximale Kompatibilität const rect = el.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; @@ -483,7 +486,35 @@ async function clickElement(tabId: number, selector: string, forceTrusted: boole if (!result || (result as any).error === 'NOT_FOUND') { throw new ToolError('ELEMENT_NOT_FOUND', `Element nicht gefunden: ${selector}`, true); } - return { ...result, _method: 'synthetic' }; + const after = await snapshotAfterClick(tabId, preUrl); + return { ...result, ...after, _method: 'synthetic' }; +} + +/** + * Liest nach einem Klick schnell den Seitenstatus — ohne extra Warten. + * Gibt dem Modell: hat sich die URL geändert? Lädt die Seite noch? + * Welches Element hat jetzt Fokus? Ist ein Dialog aufgegangen? + * Das Modell entscheidet selbst ob es einen Screenshot braucht. + */ +async function snapshotAfterClick(tabId: number, preUrl: string): Promise> { + try { + const s = await execInTab(tabId, (before: string) => { + const active = document.activeElement as HTMLElement | null; + const dialog = document.querySelector('dialog[open],[role="dialog"],[role="alertdialog"]') as HTMLElement | null; + return { + url: location.href, + urlChanged: location.href !== before, + readyState: document.readyState, + activeTag: active?.tagName?.toLowerCase() || null, + activeLabel: active?.getAttribute('aria-label') || active?.getAttribute('placeholder') || null, + dialogText: dialog ? dialog.innerText.slice(0, 120) : null, + }; + }, [preUrl]); + return { afterState: s }; + } catch { + // execInTab wirft wenn die Seite gerade navigiert — das ist selbst ein Signal + return { afterState: { navigating: true } }; + } } /** @@ -726,11 +757,11 @@ async function dispatchTool(name: string, input: Record): Promi const newTab = await chrome.tabs.create({ url, active: false }); targetTabId = newTab.id!; notifyTargetTab(); - return { tabId: newTab.id, url, _method: 'synthetic' }; + return { tabId: newTab.id, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' }; } await chrome.tabs.update(tabId!, { url }); notifyTargetTab(); - return { tabId, url, _method: 'synthetic' }; + return { tabId, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' }; } case 'browser_go_back':