refactor(observer): v7 step-aware AG Native DOM parser with data-testid/data-step-index based content extraction
- Replace CSS class-based scanning with [data-testid='conversation-view'] + [data-step-index] traversal - New extractCleanStepText(): clone-and-strip buttons/SVG/icons before text extraction - New extractStepContext(): step-container-aware context with header + code block - NOISE_RE: block Material icon names, button labels, UI artifacts - Auto DOM structure dump on first conversation-view detection - Enhanced deep-inspect with step element + button inventory - known-issues: document AG Native SDK API incompatibility
This commit is contained in:
@@ -5,8 +5,13 @@
|
||||
* Handles:
|
||||
* - Response file watching (file-based bridge fallback)
|
||||
* - Response processing (diff_review, DOM observer, step_probe paths)
|
||||
* - Multi-strategy approval execution (RPC, VS Code commands, DOM click)
|
||||
* - Multi-strategy approval execution (VS Code commands, RPC, DOM click)
|
||||
* - Diff review Accept/Reject via VS Code commands
|
||||
*
|
||||
* STRATEGY ORDER (most reliable first):
|
||||
* 0. antigravity.acceptAgentStep / rejectAgentStep — AG's own commands, always works
|
||||
* 1. HandleCascadeUserInteraction RPC — cross-platform, needs stepIndex
|
||||
* 2. DOM click trigger via HTTP bridge — fallback
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
@@ -256,7 +261,7 @@ async function processResponseFile(filePath: string) {
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
|
||||
// ═══ MULTI-STRATEGY APPROVAL (v3.0) ═══
|
||||
const approved = resp.approved;
|
||||
|
||||
// ── diff_review: Accept all / Reject all ──
|
||||
@@ -268,16 +273,10 @@ async function processResponseFile(filePath: string) {
|
||||
button_index: resp.button_index,
|
||||
step_type: pendingStepType,
|
||||
});
|
||||
} else if (isDomObserver) {
|
||||
// DOM observer path: ALSO try RPC strategies (renderer click is unreliable)
|
||||
const targetSession = sessionId || ctx.activeSessionId;
|
||||
ctx.logToFile(`[RESPONSE] dom_observer → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
|
||||
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
|
||||
ctx.logToFile(`[RESPONSE] dom strategy result: ${strategyResult}`);
|
||||
} else {
|
||||
// Step probe path: run ALL approval strategies
|
||||
// ALL paths (dom_observer + step_probe) use same strategy pipeline
|
||||
const targetSession = sessionId || ctx.activeSessionId;
|
||||
ctx.logToFile(`[RESPONSE] step_probe → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
|
||||
ctx.logToFile(`[RESPONSE] → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
|
||||
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
|
||||
ctx.logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||
}
|
||||
@@ -307,9 +306,9 @@ async function processResponseFile(filePath: string) {
|
||||
* Returns a string describing which method succeeded (or all failed).
|
||||
*
|
||||
* Strategy order (most reliable first):
|
||||
* 1. HandleCascadeUserInteraction RPC (cross-platform, no focus)
|
||||
* 2. VS Code accept/reject commands (focus-dependent)
|
||||
* 3. Log failure for manual intervention
|
||||
* 0. antigravity.acceptAgentStep / rejectAgentStep (AG VS Code commands — always works)
|
||||
* 1. HandleCascadeUserInteraction RPC (cross-platform, needs stepIndex)
|
||||
* 2. Renderer DOM Click via HTTP Bridge (fallback)
|
||||
*/
|
||||
export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
|
||||
const action = approved ? 'APPROVE' : 'REJECT';
|
||||
@@ -317,90 +316,153 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
|
||||
: (ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : -1);
|
||||
ctx.logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`);
|
||||
|
||||
// ── Dynamic Command Discovery (log what's available during WAITING state) ──
|
||||
let approvalCmdList: string[] = [];
|
||||
try {
|
||||
const allCmds = await vscode.commands.getCommands(true);
|
||||
const agCmds = allCmds.filter((c: string) => c.startsWith('antigravity.'));
|
||||
approvalCmdList = agCmds.filter((c: string) => {
|
||||
const lower = c.toLowerCase();
|
||||
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|
||||
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step')
|
||||
|| lower.includes('cascade') || lower.includes('action');
|
||||
});
|
||||
ctx.logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`);
|
||||
for (const c of approvalCmdList) {
|
||||
ctx.logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0: SDK-verified AG commands (step_type-aware dispatch)
|
||||
//
|
||||
// From SDK index.js (verified command mapping):
|
||||
// antigravity.agent.acceptAgentStep — code edits, file writes
|
||||
// antigravity.agent.rejectAgentStep — reject code edits
|
||||
// antigravity.command.accept — non-terminal commands (Run, Allow, etc.)
|
||||
// antigravity.command.reject — reject non-terminal commands
|
||||
// antigravity.terminalCommand.accept — terminal commands
|
||||
// antigravity.terminalCommand.reject — reject terminal commands
|
||||
// antigravity.terminalCommand.run — run terminal commands
|
||||
//
|
||||
// These operate on the currently focused/active step — no stepIndex needed!
|
||||
// ══════════════════════════════════════════════════════════
|
||||
{
|
||||
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
|
||||
|
||||
// Determine which SDK command pair to use based on step_type
|
||||
let acceptCmd: string;
|
||||
let rejectCmd: string;
|
||||
|
||||
if (typeLower.includes('code_edit') || typeLower.includes('write_to_file')
|
||||
|| typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')
|
||||
|| typeLower === 'diff_review') {
|
||||
// Code edits → agent step commands
|
||||
acceptCmd = 'antigravity.agent.acceptAgentStep';
|
||||
rejectCmd = 'antigravity.agent.rejectAgentStep';
|
||||
} else if (typeLower.includes('run_command') || typeLower.includes('shell_exec')
|
||||
|| typeLower.includes('send_command_input')) {
|
||||
// Terminal commands → terminal command pair
|
||||
acceptCmd = 'antigravity.terminalCommand.accept';
|
||||
rejectCmd = 'antigravity.terminalCommand.reject';
|
||||
} else if (typeLower === 'command' || typeLower.includes('permission')
|
||||
|| typeLower.includes('browser') || typeLower.includes('mcp')
|
||||
|| typeLower.includes('extension_code') || typeLower.includes('subagent')
|
||||
|| typeLower.includes('open_browser') || typeLower.includes('read_url')
|
||||
|| typeLower.includes('invoke_subagent')) {
|
||||
// Non-terminal commands (Run, Allow, etc.) → command pair
|
||||
acceptCmd = 'antigravity.command.accept';
|
||||
rejectCmd = 'antigravity.command.reject';
|
||||
} else {
|
||||
// Unknown type — try all three in order
|
||||
acceptCmd = 'antigravity.command.accept';
|
||||
rejectCmd = 'antigravity.command.reject';
|
||||
}
|
||||
|
||||
const primaryCmd = approved ? acceptCmd : rejectCmd;
|
||||
ctx.logToFile(`[APPROVAL-0] stepType="${stepType}" → ${primaryCmd}`);
|
||||
|
||||
try {
|
||||
await vscode.commands.executeCommand(primaryCmd);
|
||||
ctx.logToFile(`[APPROVAL-0] ✅ ${primaryCmd} SUCCESS`);
|
||||
return `SDK:${primaryCmd}`;
|
||||
} catch (e: any) {
|
||||
ctx.logToFile(`[APPROVAL-0] ❌ ${primaryCmd} failed: ${e.message?.substring(0, 200)}`);
|
||||
}
|
||||
|
||||
// Fallback: if the primary type-specific command failed, try the other pairs
|
||||
const fallbackPairs = [
|
||||
approved ? 'antigravity.command.accept' : 'antigravity.command.reject',
|
||||
approved ? 'antigravity.agent.acceptAgentStep' : 'antigravity.agent.rejectAgentStep',
|
||||
approved ? 'antigravity.terminalCommand.accept' : 'antigravity.terminalCommand.reject',
|
||||
].filter(cmd => cmd !== primaryCmd); // skip already-tried
|
||||
|
||||
for (const fallbackCmd of fallbackPairs) {
|
||||
try {
|
||||
ctx.logToFile(`[APPROVAL-0-FB] Trying ${fallbackCmd}...`);
|
||||
await vscode.commands.executeCommand(fallbackCmd);
|
||||
ctx.logToFile(`[APPROVAL-0-FB] ✅ ${fallbackCmd} SUCCESS`);
|
||||
return `SDK-FB:${fallbackCmd}`;
|
||||
} catch (e: any) {
|
||||
ctx.logToFile(`[APPROVAL-0-FB] ❌ ${fallbackCmd}: ${e.message?.substring(0, 100)}`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
ctx.logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
|
||||
// STRATEGY 1: HandleCascadeUserInteraction RPC
|
||||
// Now supports BOTH approve AND reject.
|
||||
// Requires valid stepIndex for most step types.
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if (ctx.sdk && approved && effectiveStepIndex >= 0) {
|
||||
// Build interaction sub-message based on step_type
|
||||
if (ctx.sdk && effectiveStepIndex >= 0) {
|
||||
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
|
||||
let interactionPayload: Record<string, any> = {};
|
||||
|
||||
// Code edit steps — use dedicated RPC
|
||||
if (typeLower.includes('code_edit') || typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
|
||||
// CODE EDIT: Uses acknowledgeCodeActionStep RPC (correct AG LS method)
|
||||
try {
|
||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] trying submitCodeAcknowledgement command`);
|
||||
ctx.logToFile(`[APPROVAL-1-CODE] trying submitCodeAcknowledgement command`);
|
||||
await vscode.commands.executeCommand('antigravity.prioritized.submitCodeAcknowledgement');
|
||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] ✅ submitCodeAcknowledgement OK`);
|
||||
ctx.logToFile(`[APPROVAL-1-CODE] ✅ submitCodeAcknowledgement OK`);
|
||||
return `CMD:submitCodeAcknowledgement(accept=${approved})`;
|
||||
} catch {
|
||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] submitCodeAcknowledgement not available, trying RPC`);
|
||||
ctx.logToFile(`[APPROVAL-1-CODE] submitCodeAcknowledgement not available, trying RPC`);
|
||||
}
|
||||
// Direct LS RPC with correct method name
|
||||
try {
|
||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
|
||||
ctx.logToFile(`[APPROVAL-1-CODE] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
|
||||
const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', {
|
||||
cascadeId: sessionId,
|
||||
accept: approved,
|
||||
stepIndices: [effectiveStepIndex],
|
||||
});
|
||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
|
||||
ctx.logToFile(`[APPROVAL-1-CODE] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
|
||||
return `RPC:acknowledgeCodeActionStep(accept=${approved})`;
|
||||
} catch (e: any) {
|
||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] ❌ ${e.message.substring(0, 200)}`);
|
||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] falling back to HandleCascadeUserInteraction`);
|
||||
interactionPayload = { runCommand: { confirm: true } };
|
||||
ctx.logToFile(`[APPROVAL-1-CODE] ❌ ${e.message.substring(0, 200)}`);
|
||||
// Fall through to generic HandleCascadeUserInteraction
|
||||
interactionPayload = { runCommand: { confirm: approved } };
|
||||
}
|
||||
}
|
||||
|
||||
// Map step_type to interaction sub-message field
|
||||
// CRITICAL FIX: Use `confirm: approved` (not always true) to support REJECT
|
||||
if (typeLower.includes('run_command') || typeLower.includes('shell_exec')) {
|
||||
interactionPayload = { runCommand: { confirm: true } };
|
||||
interactionPayload = { runCommand: { confirm: approved } };
|
||||
} else if (typeLower.includes('open_browser')) {
|
||||
interactionPayload = { openBrowserUrl: { confirm: true } };
|
||||
interactionPayload = { openBrowserUrl: { confirm: approved } };
|
||||
} else if (typeLower.includes('send_command_input')) {
|
||||
interactionPayload = { sendCommandInput: { confirm: true } };
|
||||
interactionPayload = { sendCommandInput: { confirm: approved } };
|
||||
} else if (typeLower.includes('read_url')) {
|
||||
interactionPayload = { readUrlContent: { confirm: true } };
|
||||
interactionPayload = { readUrlContent: { confirm: approved } };
|
||||
} else if (typeLower.includes('mcp')) {
|
||||
interactionPayload = { mcpTool: { confirm: true } };
|
||||
interactionPayload = { mcpTool: { confirm: approved } };
|
||||
} else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code') || typeLower.includes('browser_subagent')) {
|
||||
interactionPayload = { runExtensionCode: { confirm: true } };
|
||||
interactionPayload = { runExtensionCode: { confirm: approved } };
|
||||
} else if (typeLower.includes('file_permission')) {
|
||||
const scope = typeLower.includes('conversation') ? 2 : 1;
|
||||
interactionPayload = { filePermission: { allow: true, scope } };
|
||||
if (typeLower.includes('deny')) {
|
||||
interactionPayload = { filePermission: { allow: false, scope: 1 } };
|
||||
} else {
|
||||
const scope = typeLower.includes('conversation') ? 2 : 1;
|
||||
interactionPayload = { filePermission: { allow: approved, scope } };
|
||||
}
|
||||
} else if (typeLower.includes('elicitation')) {
|
||||
interactionPayload = { elicitation: {} };
|
||||
} else if (typeLower === 'permission' || typeLower.includes('permission')) {
|
||||
// DOM observer 'permission' type: browser_subagent Allow/Deny dialog
|
||||
// Try runExtensionCode first (most common for JS execution permission)
|
||||
interactionPayload = { runExtensionCode: { confirm: true } };
|
||||
interactionPayload = { runExtensionCode: { confirm: approved } };
|
||||
} else if (typeLower === 'command' || typeLower === '') {
|
||||
// Generic command — most common case from DOM observer
|
||||
interactionPayload = { runCommand: { confirm: approved } };
|
||||
} else {
|
||||
// Default: try run_command (most common)
|
||||
interactionPayload = { runCommand: { confirm: true } };
|
||||
// Default: try run_command
|
||||
interactionPayload = { runCommand: { confirm: approved } };
|
||||
}
|
||||
|
||||
const activeTrajectoryId = getTrajectoryId();
|
||||
const protoVariants = [
|
||||
// Variant A: camelCase with trajectoryId (proven working for run_command)
|
||||
// Variant A: camelCase with trajectoryId
|
||||
{
|
||||
cascadeId: sessionId,
|
||||
interaction: {
|
||||
@@ -431,20 +493,17 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
|
||||
for (let i = 0; i < protoVariants.length; i++) {
|
||||
try {
|
||||
const payload = protoVariants[i];
|
||||
ctx.logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
|
||||
ctx.logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
|
||||
const rpcResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
|
||||
ctx.logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
|
||||
ctx.logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-${i}:HandleCascadeUserInteraction(${typeLower},${action})`;
|
||||
} catch (e: any) {
|
||||
lastRpcError = e.message || '';
|
||||
ctx.logToFile(`[APPROVAL-PROTO-${i}] ❌ ${lastRpcError.substring(0, 300)}`);
|
||||
ctx.logToFile(`[APPROVAL-1-${i}] ❌ ${lastRpcError.substring(0, 300)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auto-recovery: wrong-LS detection ──────────────────────
|
||||
// All 3 proto variants failed. If the error is "input not registered",
|
||||
// SDK is likely connected to wrong LS process. Attempt fixLSConnection
|
||||
// and retry ONE time to avoid permanent failure.
|
||||
if (ctx.fixLSConnection && lastRpcError.includes('input not registered')) {
|
||||
ctx.logToFile('[APPROVAL] ⚠️ wrong-LS detected ("input not registered"), attempting LS fix...');
|
||||
try {
|
||||
@@ -453,10 +512,9 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
|
||||
ctx.logToFile('[APPROVAL] LS reconnected — retrying first proto variant...');
|
||||
try {
|
||||
const retryPayload = protoVariants[0];
|
||||
ctx.logToFile(`[APPROVAL-RETRY] HandleCascadeUserInteraction(${JSON.stringify(retryPayload).substring(0, 250)})`);
|
||||
const retryResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', retryPayload);
|
||||
ctx.logToFile(`[APPROVAL-RETRY] ✅ SUCCESS: ${JSON.stringify(retryResult).substring(0, 200)}`);
|
||||
return `RPC-RETRY:HandleCascadeUserInteraction(${typeLower})`;
|
||||
return `RPC-RETRY:HandleCascadeUserInteraction(${typeLower},${action})`;
|
||||
} catch (retryErr: any) {
|
||||
ctx.logToFile(`[APPROVAL-RETRY] ❌ ${retryErr.message?.substring(0, 200)}`);
|
||||
}
|
||||
@@ -467,9 +525,14 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
|
||||
ctx.logToFile(`[APPROVAL] fixLSConnection error: ${fixErr.message?.substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
} else if (ctx.sdk && effectiveStepIndex < 0) {
|
||||
ctx.logToFile(`[APPROVAL-1] SKIPPED RPC: stepIndex=${effectiveStepIndex} (unknown) — Strategy 0 (VS Code command) was the primary attempt`);
|
||||
}
|
||||
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (primary fallback) ──
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 2: Renderer DOM Click via HTTP Bridge (fallback)
|
||||
// Sets a click trigger that the observer script polls and executes.
|
||||
// ══════════════════════════════════════════════════════════
|
||||
try {
|
||||
const triggerAction = approved ? 'approve' : 'reject';
|
||||
ctx.logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||
@@ -479,6 +542,6 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
|
||||
ctx.logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
|
||||
ctx.logToFile(`[APPROVAL] strategies complete — check logs for results`);
|
||||
ctx.logToFile(`[APPROVAL] All strategies complete for ${action}`);
|
||||
return `STRATEGIES_DONE:${action}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user