feat: State-Info nach Klick/Screenshot, kein extra Warten

- 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.
This commit is contained in:
2026-08-10 09:05:43 +02:00
parent 35b3af97a2
commit ebe7dc72f1
2 changed files with 70 additions and 7 deletions
+37 -6
View File
@@ -420,6 +420,10 @@ async function wantsTrusted(tabId: number): Promise<boolean> {
async function clickElement(tabId: number, selector: string, forceTrusted: boolean): Promise<any> {
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<Record<string, unknown>> {
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<string, unknown>): 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':