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
+33 -1
View File
@@ -574,6 +574,19 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
s.imgH = Number.isFinite(shot.height) && shot.height > 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<string, unknown> | 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<string, unknown>;
} catch { /* chrome:// etc — egal */ }
return {
action,
image: shot.dataUrl,
@@ -582,6 +595,7 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
scale: s.scale,
fullPage,
cursor: { x: cx, y: cy },
...(page ? { page } : {}),
_method: 'cdp',
};
}
@@ -633,7 +647,25 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
s.x = p.x;
s.y = p.y;
await paintCursor(tabId, p.x, p.y, clicks > 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<string, unknown>;
if (post) result.afterState = post;
} catch { /* Seite navigiert — das ist selbst ein Signal */
result.afterState = { navigating: true };
}
return result;
}
case 'left_mouse_down':
+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':