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; s.imgH = Number.isFinite(shot.height) && shot.height > 0 ? shot.height : 0;
const [cx, cy] = toModel(s, s.x, s.y); 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 { return {
action, action,
image: shot.dataUrl, image: shot.dataUrl,
@@ -582,6 +595,7 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
scale: s.scale, scale: s.scale,
fullPage, fullPage,
cursor: { x: cx, y: cy }, cursor: { x: cx, y: cy },
...(page ? { page } : {}),
_method: 'cdp', _method: 'cdp',
}; };
} }
@@ -633,7 +647,25 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
s.x = p.x; s.x = p.x;
s.y = p.y; s.y = p.y;
await paintCursor(tabId, p.x, p.y, clicks > 1 ? 'double' : button === 'right' ? 'right' : 'left'); 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': 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> { async function clickElement(tabId: number, selector: string, forceTrusted: boolean): Promise<any> {
const trusted = forceTrusted || await wantsTrusted(tabId); 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) { if (trusted) {
const box = await elementBox(tabId, selector); const box = await elementBox(tabId, selector);
if (!box) throw new ToolError('ELEMENT_NOT_FOUND', `Element nicht gefunden oder unsichtbar: ${selector}`, true); 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.attach(tabId);
await cdp.mouseClick(tabId, box.x, box.y); await cdp.mouseClick(tabId, box.x, box.y);
notifyCursor(tabId, box.x, box.y, 'left'); 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) { } 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); 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' }; if (!el) return { error: 'NOT_FOUND' };
el.scrollIntoView({ block: 'center', behavior: 'instant' as ScrollBehavior }); el.scrollIntoView({ block: 'center', behavior: 'instant' as ScrollBehavior });
// Vollständige Maus-Event-Sequenz für maximale Kompatibilität
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2; const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 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') { if (!result || (result as any).error === 'NOT_FOUND') {
throw new ToolError('ELEMENT_NOT_FOUND', `Element nicht gefunden: ${selector}`, true); 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 }); const newTab = await chrome.tabs.create({ url, active: false });
targetTabId = newTab.id!; targetTabId = newTab.id!;
notifyTargetTab(); 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 }); await chrome.tabs.update(tabId!, { url });
notifyTargetTab(); 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': case 'browser_go_back':