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}`;
|
||||
}
|
||||
|
||||
@@ -151,6 +151,25 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /status — diagnostic endpoint
|
||||
if (req.method === 'GET' && url.pathname === '/status') {
|
||||
const { getStepProbeContext } = require('./step-probe');
|
||||
const probeCtx = getStepProbeContext();
|
||||
const status = {
|
||||
projectName: ctx.projectName,
|
||||
activeSessionId: probeCtx.activeSessionId || ctx.activeSessionId,
|
||||
lastPendingStepIndex: probeCtx.lastPendingStepIndex,
|
||||
sessionStalled: probeCtx.sessionStalled,
|
||||
wsConnected: ctx.wsBridge?.isConnected() ?? false,
|
||||
clickTrigger: clickTrigger ? { ...clickTrigger, ageMs: Date.now() - clickTrigger.timestamp } : null,
|
||||
uptime: Math.round(process.uptime()),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(status, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /ping — health check
|
||||
if (url.pathname === '/ping') {
|
||||
res.writeHead(200); res.end('pong');
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
export function generateApprovalObserverScript(_port: number): string {
|
||||
return `
|
||||
// ── Gravity Bridge v6: Clean Context Extraction ──
|
||||
// ── Gravity Bridge v7: Step-Aware AG Native DOM Parser ──
|
||||
// Uses data-testid="conversation-view" + data-step-index for reliable parsing
|
||||
(function(){
|
||||
'use strict';
|
||||
var BASE='',_obs=false,_sent={},_ready=false;
|
||||
var _scanScheduled=false,_lastScanTs=0;
|
||||
var THROTTLE_MS=500;
|
||||
var CLEANUP_MS=300000;
|
||||
|
||||
var _dumpSent=false; // one-time DOM dump
|
||||
|
||||
function log(m){console.log('[GB Observer] '+m);}
|
||||
log('v6 Script loaded — Clean Context Extraction');
|
||||
log('v7 Script loaded — Step-Aware AG Native DOM Parser');
|
||||
|
||||
// React-Compatible Synthetic Clicker
|
||||
function dispatchReactClick(el){
|
||||
@@ -26,57 +28,50 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
}
|
||||
|
||||
// ── Noise filter: lines that are UI artifacts, not real content ──
|
||||
var NOISE_PATTERNS = [
|
||||
/^chevron_right$/i,
|
||||
/^chevron_left$/i,
|
||||
/^arrow_/i,
|
||||
/^Thought for \\\\d+/i,
|
||||
/^expand_/i,
|
||||
/^close$/i,
|
||||
/^more_/i,
|
||||
/^content_copy$/i,
|
||||
/^check$/i,
|
||||
/^\\\\d+ lines?$/i,
|
||||
/^Show more$/i,
|
||||
/^Show less$/i,
|
||||
/^Copy$/i,
|
||||
/^Edit$/i,
|
||||
/^Copied!$/i,
|
||||
/^\\\\s*$/,
|
||||
/^declare\\\\s+(class|function|interface|type|enum|const|var|let)\\\\s/, // TypeScript declarations
|
||||
/^(import|export|from)\\\\s/, // JS imports
|
||||
/^\\\\s*[{}()\\\\[\\\\];]\\\\s*$/, // lone brackets
|
||||
/\\\\.ts:\\\\d+:/, // file:line references
|
||||
/extension.*src.*sdk/i, // SDK file paths
|
||||
];
|
||||
var NOISE_RE = new RegExp(
|
||||
'^(' +
|
||||
'chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|' +
|
||||
'arrow_forward|arrow_back|expand_more|expand_less|close|more_horiz|more_vert|' +
|
||||
'content_copy|content_paste|check|check_circle|error|warning|info|' +
|
||||
'keyboard_arrow_up|keyboard_arrow_down|keyboard_arrow_left|keyboard_arrow_right|' +
|
||||
'Thought for \\\\d+|Show more|Show less|Copy|Copied!|Edit|Cancel|' +
|
||||
'Always run|Always allow|Running command|Running \\\\d+ commands?|' +
|
||||
'Deny|Allow|Allow Once|Allow This Conversation|' +
|
||||
'Run|Send|Stop|Review Changes|Accept all|Reject all|Accept|Reject' +
|
||||
')$', 'i'
|
||||
);
|
||||
var NOISE_CODE_RE = /^(declare\\s+(class|function|interface|type|enum|const|var|let)\\s|(import|export|from)\\s|\\s*[{}()\\[\\];]\\s*$|\\.ts:\\d+:|extension.*src.*sdk)/i;
|
||||
|
||||
function isNoiseLine(line) {
|
||||
if (!line || line.trim().length < 2) return true;
|
||||
var trimmed = line.trim();
|
||||
for (var i = 0; i < NOISE_PATTERNS.length; i++) {
|
||||
if (NOISE_PATTERNS[i].test(trimmed)) return true;
|
||||
}
|
||||
if (NOISE_RE.test(trimmed)) return true;
|
||||
if (NOISE_CODE_RE.test(trimmed)) return true;
|
||||
// Single-word Material icon names (all lowercase, no spaces)
|
||||
if (/^[a-z_]+$/.test(trimmed) && trimmed.length < 30) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function cleanLines(text) {
|
||||
if (!text) return '';
|
||||
var lines = text.split('\\\\n');
|
||||
var lines = text.split('\\n');
|
||||
var clean = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (!isNoiseLine(lines[i])) clean.push(lines[i].trim());
|
||||
}
|
||||
return clean.join('\\\\n').trim();
|
||||
return clean.join('\\n').trim();
|
||||
}
|
||||
|
||||
function cleanButtonText(btn) {
|
||||
if (!btn) return '';
|
||||
var clone = btn.cloneNode(true);
|
||||
var icons = clone.querySelectorAll('.google-symbols, .codicon, [class*="icon"], svg');
|
||||
var icons = clone.querySelectorAll('.google-symbols, .codicon, [class*="icon"], svg, .material-symbols-outlined, .material-icons');
|
||||
for(var i=0; i<icons.length; i++) {
|
||||
if(icons[i].parentNode) icons[i].parentNode.removeChild(icons[i]);
|
||||
}
|
||||
var tr = clone.querySelector('.truncate');
|
||||
var txt = (tr ? tr.textContent : clone.textContent) || '';
|
||||
return txt.trim().replace(/^[\\\\s\\\\u200B-\\\\u200D\\\\uFEFF\\\\u00A0]+/, '').replace(/(Alt|Ctrl|Shift|Meta)\\\\+.*/i,'').trim();
|
||||
return txt.trim().replace(/^[\\s\\u200B-\\u200D\\uFEFF\\u00A0]+/, '').replace(/(Alt|Ctrl|Shift|Meta)\\+.*/i,'').trim();
|
||||
}
|
||||
|
||||
function btnId(b,type){
|
||||
@@ -90,81 +85,73 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
return type+'|'+txt+'|'+idx;
|
||||
}
|
||||
|
||||
// ── Context extraction: TIGHT scope — only button's immediate context ──
|
||||
// v6 FIX: Never climb more than 4 parents. Never grab editor/sidebar content.
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// v7: STEP-AWARE CONTEXT EXTRACTION
|
||||
// Find the closest [data-step-index] ancestor, extract step info
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
function extractCommandContext(b) {
|
||||
// Strategy 1: aria-label or title on button itself
|
||||
var ariaLabel = b.getAttribute('aria-label') || b.getAttribute('title') || '';
|
||||
if (ariaLabel && ariaLabel.length > 5 && ariaLabel.length < 500) {
|
||||
return ariaLabel;
|
||||
function getStepContainer(el) {
|
||||
var node = el;
|
||||
for (var depth = 0; depth < 10 && node; depth++) {
|
||||
if (node.hasAttribute && node.hasAttribute('data-step-index')) return node;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractStepContext(btn) {
|
||||
var stepEl = getStepContainer(btn);
|
||||
if (!stepEl) return cleanButtonText(btn);
|
||||
|
||||
var stepIdx = stepEl.getAttribute('data-step-index') || '?';
|
||||
|
||||
// Get step header text (first line, usually has the tool name/command)
|
||||
var header = stepEl.querySelector('[class*="cursor-pointer"]');
|
||||
var headerText = '';
|
||||
if (header) {
|
||||
// Clone and strip icons/buttons
|
||||
var hClone = header.cloneNode(true);
|
||||
var hRemove = hClone.querySelectorAll('button, svg, [class*="icon"], .google-symbols, .material-symbols-outlined');
|
||||
for (var i = 0; i < hRemove.length; i++) {
|
||||
if (hRemove[i].parentNode) hRemove[i].parentNode.removeChild(hRemove[i]);
|
||||
}
|
||||
headerText = (hClone.textContent || '').trim().substring(0, 300);
|
||||
// Clean noise
|
||||
headerText = cleanLines(headerText);
|
||||
}
|
||||
|
||||
// Strategy 2: Look for command text in button's DIRECT parent chain (max 3 levels)
|
||||
var el = b.parentElement;
|
||||
for (var depth = 0; depth < 3 && el; depth++) {
|
||||
// Check for code/pre elements (command text)
|
||||
var pres = el.querySelectorAll('pre, code');
|
||||
for (var pi = 0; pi < pres.length; pi++) {
|
||||
var preText = (pres[pi].textContent || '').trim();
|
||||
if (preText.length > 2 && preText.length < 500 && !isNoiseLine(preText)) {
|
||||
return preText.substring(0, 400);
|
||||
}
|
||||
}
|
||||
// Check for span with title attribute containing command info
|
||||
var titleSpans = el.querySelectorAll('span[title]');
|
||||
for (var ti = 0; ti < titleSpans.length; ti++) {
|
||||
var spanTitle = titleSpans[ti].getAttribute('title') || '';
|
||||
if (spanTitle.length > 5 && spanTitle.length < 500) {
|
||||
return spanTitle.substring(0, 400);
|
||||
}
|
||||
}
|
||||
el = el.parentElement;
|
||||
// Try to get code/pre content (command detail)
|
||||
var codeEl = stepEl.querySelector('pre, code');
|
||||
var codeText = '';
|
||||
if (codeEl) {
|
||||
codeText = (codeEl.textContent || '').trim().substring(0, 400);
|
||||
}
|
||||
|
||||
// Strategy 3: Immediate parent's text only (NOT full page)
|
||||
var immediateParent = b.parentElement;
|
||||
if (immediateParent) {
|
||||
var parentText = '';
|
||||
var children = immediateParent.childNodes;
|
||||
for (var ci = 0; ci < children.length; ci++) {
|
||||
var child = children[ci];
|
||||
if (child.nodeType === 3 && child.nodeValue && child.nodeValue.trim()) {
|
||||
parentText += child.nodeValue.trim() + ' ';
|
||||
} else if (child.nodeType === 1 && child.tagName !== 'BUTTON' && child.tagName !== 'SVG') {
|
||||
var childText = child.textContent || '';
|
||||
if (childText.trim().length > 2 && childText.trim().length < 200) {
|
||||
parentText += childText.trim() + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
parentText = parentText.trim();
|
||||
if (parentText.length > 3 && parentText.length < 300) {
|
||||
return cleanLines(parentText).substring(0, 300);
|
||||
}
|
||||
}
|
||||
// Try aria-label on button
|
||||
var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || '';
|
||||
|
||||
return '';
|
||||
var parts = [];
|
||||
if (headerText) parts.push(headerText);
|
||||
if (codeText && !headerText.includes(codeText.substring(0, 20))) parts.push(codeText);
|
||||
if (ariaLabel && ariaLabel.length > 5 && !headerText.includes(ariaLabel)) parts.push(ariaLabel);
|
||||
|
||||
var result = parts.join(' — ');
|
||||
if (!result) result = cleanButtonText(btn);
|
||||
return 'Step #' + stepIdx + ': ' + result;
|
||||
}
|
||||
|
||||
function extractContext(b) {
|
||||
var cmd = cleanButtonText(b);
|
||||
var detail = extractCommandContext(b);
|
||||
if (!detail) return cmd;
|
||||
// Deduplicate: if detail contains button text, just show detail
|
||||
if (detail.includes(cmd)) return cleanLines(detail);
|
||||
return cmd + ': ' + cleanLines(detail);
|
||||
return extractStepContext(b);
|
||||
}
|
||||
|
||||
var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', 'Always allow', '결행', '수락', '반영', '허용', '승인'];
|
||||
var REJECT_WORDS = ['Reject', 'Cancel', 'Deny', 'Stop', 'Decline', 'Dismiss', '거절', '거부', '취소', '차단', '정지', '무시'];
|
||||
var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', 'Always allow', 'Always run'];
|
||||
var REJECT_WORDS = ['Reject', 'Cancel', 'Deny', 'Stop', 'Decline', 'Dismiss'];
|
||||
|
||||
function isActionBtn(txt) {
|
||||
for(var i=0; i<ACTION_WORDS.length; i++) {
|
||||
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
|
||||
}
|
||||
// "Running N command(s)" pattern
|
||||
if (/Running\\\\d*\\\\s*command/i.test(txt)) return true;
|
||||
if (/Running\\s*\\d*\\s*command/i.test(txt)) return true;
|
||||
return false;
|
||||
}
|
||||
function isRejectBtn(txt) {
|
||||
@@ -202,7 +189,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
var items = document.querySelectorAll('[aria-label^="Gravity Bridge Control"], [title^="Gravity Bridge Control"]');
|
||||
if (items.length > 0) {
|
||||
var text = items[0].getAttribute('aria-label') || items[0].getAttribute('title') || '';
|
||||
var m = text.match(/port:(\\\\d+)/);
|
||||
var m = text.match(/port:(\\d+)/);
|
||||
if (m && m[1]) {
|
||||
clearInterval(timer);
|
||||
tryPingAsync(parseInt(m[1], 10)).then(function(ok){ cb(ok ? parseInt(m[1],10) : HARDCODED_PORT); });
|
||||
@@ -222,67 +209,218 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
startObserver();
|
||||
});
|
||||
|
||||
// ── Chat body scanning (for Discord relay of AI responses) ──
|
||||
var _lastText = "";
|
||||
var _lastTextTime = 0;
|
||||
var _lastTextSent = false;
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// v7: DOM STRUCTURE DUMP (one-time, on first conversation-view detection)
|
||||
// Posts the DOM tree structure to /dump-html for debugging
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
function extractCleanChatText(container) {
|
||||
if (!container) return '';
|
||||
// Try markdown body first
|
||||
var md = container.querySelector('.markdown-body') || container.querySelector('.prose');
|
||||
var rawText = '';
|
||||
if (md && md.innerText && md.innerText.trim().length > 10) {
|
||||
rawText = md.innerText.trim();
|
||||
} else {
|
||||
rawText = (container.innerText || container.textContent || '').trim();
|
||||
function dumpDOMStructure() {
|
||||
if (_dumpSent || !_ready) return;
|
||||
var cv = document.querySelector('[data-testid="conversation-view"]');
|
||||
if (!cv) return;
|
||||
_dumpSent = true;
|
||||
|
||||
// Walk the DOM tree and capture structure (classes, data-attrs, tag names)
|
||||
function walkNode(el, depth) {
|
||||
if (depth > 8) return null;
|
||||
var info = {
|
||||
tag: el.tagName ? el.tagName.toLowerCase() : '#text',
|
||||
cls: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : '',
|
||||
attrs: {},
|
||||
text: '',
|
||||
children: []
|
||||
};
|
||||
// Capture data-* and role attributes
|
||||
if (el.attributes) {
|
||||
for (var ai = 0; ai < el.attributes.length; ai++) {
|
||||
var attr = el.attributes[ai];
|
||||
if (attr.name.startsWith('data-') || attr.name === 'role' || attr.name === 'aria-label') {
|
||||
info.attrs[attr.name] = (attr.value || '').substring(0, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
// For leaf text nodes, capture short text
|
||||
if (!el.children || el.children.length === 0) {
|
||||
var t = (el.textContent || '').trim();
|
||||
if (t.length > 0 && t.length < 100) info.text = t;
|
||||
}
|
||||
// Recurse children (limit to first 10 per level)
|
||||
if (el.children) {
|
||||
for (var ci = 0; ci < Math.min(el.children.length, 10); ci++) {
|
||||
var childInfo = walkNode(el.children[ci], depth + 1);
|
||||
if (childInfo) info.children.push(childInfo);
|
||||
}
|
||||
if (el.children.length > 10) {
|
||||
info.children.push({tag: '...', text: '+' + (el.children.length - 10) + ' more'});
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
// Clean the text
|
||||
|
||||
var structure = walkNode(cv, 0);
|
||||
var payload = JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'v7_dom_dump',
|
||||
conversationView: structure
|
||||
});
|
||||
log('DOM dump: conversation-view found, sending ' + payload.length + ' bytes');
|
||||
fetch(BASE + '/dump-html', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: payload
|
||||
}).catch(function(e) { log('DOM dump failed: ' + e.message); });
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// v7: STEP-AWARE CHAT BODY SCANNING
|
||||
// Scans [data-step-index] elements inside [data-testid="conversation-view"]
|
||||
// Extracts AI response text while filtering UI noise
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
var _lastScrapedStepIndex = -1;
|
||||
var _lastStepText = '';
|
||||
var _lastStepTextTime = 0;
|
||||
var _lastStepTextSent = false;
|
||||
|
||||
function extractCleanStepText(stepEl) {
|
||||
if (!stepEl) return '';
|
||||
|
||||
// Clone the step element so we can strip UI elements without affecting the DOM
|
||||
var clone = stepEl.cloneNode(true);
|
||||
|
||||
// Remove all buttons (Run, Allow, Cancel, etc.)
|
||||
var buttons = clone.querySelectorAll('button');
|
||||
for (var bi = 0; bi < buttons.length; bi++) {
|
||||
if (buttons[bi].parentNode) buttons[bi].parentNode.removeChild(buttons[bi]);
|
||||
}
|
||||
|
||||
// Remove all SVGs and icon elements
|
||||
var icons = clone.querySelectorAll('svg, .google-symbols, .material-symbols-outlined, .material-icons, [class*="codicon"], [class*="icon"]');
|
||||
for (var ii = 0; ii < icons.length; ii++) {
|
||||
if (icons[ii].parentNode) icons[ii].parentNode.removeChild(icons[ii]);
|
||||
}
|
||||
|
||||
// Remove copy buttons and their containers
|
||||
var copyBtns = clone.querySelectorAll('[class*="copy"], [aria-label*="copy"], [title*="Copy"]');
|
||||
for (var ci = 0; ci < copyBtns.length; ci++) {
|
||||
if (copyBtns[ci].parentNode) copyBtns[ci].parentNode.removeChild(copyBtns[ci]);
|
||||
}
|
||||
|
||||
// Try to get text from markdown rendering area first
|
||||
// Look for known markdown container patterns
|
||||
var mdEl = clone.querySelector('.markdown-body, .prose, [class*="markdown"], [class*="rendered"]');
|
||||
var rawText = '';
|
||||
if (mdEl && mdEl.innerText && mdEl.innerText.trim().length > 10) {
|
||||
rawText = mdEl.innerText.trim();
|
||||
} else {
|
||||
// Fallback: get all text but filter aggressively
|
||||
rawText = (clone.innerText || clone.textContent || '').trim();
|
||||
}
|
||||
|
||||
// Apply line-by-line noise filter
|
||||
return cleanLines(rawText).substring(0, 3500);
|
||||
}
|
||||
|
||||
function scanChatBodies() {
|
||||
if(!_ready)return;
|
||||
// Find bot response containers — try multiple selectors for compatibility
|
||||
var botTurns = document.querySelectorAll(
|
||||
'.text-ide-message-block-bot-color, ' +
|
||||
'[data-testid*="bot"], [data-testid*="assistant"], ' +
|
||||
'[class*="agent-convo"], [class*="bot-message"]'
|
||||
);
|
||||
if (botTurns.length === 0) return;
|
||||
|
||||
var lastTurn = botTurns[botTurns.length - 1];
|
||||
if (lastTurn.dataset.agChatScraped === "true" || lastTurn.dataset.agChatScraped === "pending") return;
|
||||
if (!_ready) return;
|
||||
|
||||
var currentText = lastTurn.textContent || '';
|
||||
if (currentText.length < 5) return;
|
||||
// One-time DOM dump
|
||||
dumpDOMStructure();
|
||||
|
||||
if (_lastText !== currentText) {
|
||||
_lastText = currentText;
|
||||
_lastTextTime = Date.now();
|
||||
_lastTextSent = false;
|
||||
} else if (!_lastTextSent) {
|
||||
if (Date.now() - _lastTextTime > 3000) {
|
||||
_lastTextSent = true;
|
||||
lastTurn.dataset.agChatScraped = "pending";
|
||||
var finalTxt = extractCleanChatText(lastTurn);
|
||||
if (finalTxt && finalTxt.length > 5 && finalTxt !== "Review Changes") {
|
||||
fetch(BASE+'/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: finalTxt })
|
||||
}).then(function(){
|
||||
lastTurn.dataset.agChatScraped = "true";
|
||||
}).catch(function(){
|
||||
lastTurn.dataset.agChatScraped = "false";
|
||||
});
|
||||
} else {
|
||||
lastTurn.dataset.agChatScraped = "true";
|
||||
}
|
||||
// PRIMARY: Find conversation-view container
|
||||
var cv = document.querySelector('[data-testid="conversation-view"]');
|
||||
if (!cv) {
|
||||
// FALLBACK: Try older selectors
|
||||
cv = document.querySelector('[class*="conversation"], [class*="chat-container"]');
|
||||
if (!cv) return;
|
||||
}
|
||||
|
||||
// Find ALL step elements within the conversation
|
||||
var stepEls = cv.querySelectorAll('[data-step-index]');
|
||||
if (stepEls.length === 0) {
|
||||
// FALLBACK: Try text-ide-message-block-bot-color directly
|
||||
var botMsgs = cv.querySelectorAll('.text-ide-message-block-bot-color');
|
||||
if (botMsgs.length === 0) return;
|
||||
// Use the last bot message as a pseudo-step
|
||||
var lastBot = botMsgs[botMsgs.length - 1];
|
||||
if (lastBot.dataset.agChatScraped === 'true' || lastBot.dataset.agChatScraped === 'pending') return;
|
||||
var botText = extractCleanStepText(lastBot);
|
||||
if (botText && botText.length > 20) {
|
||||
if (_lastStepText !== botText) {
|
||||
_lastStepText = botText;
|
||||
_lastStepTextTime = Date.now();
|
||||
_lastStepTextSent = false;
|
||||
} else if (!_lastStepTextSent && (Date.now() - _lastStepTextTime > 3000)) {
|
||||
_lastStepTextSent = true;
|
||||
lastBot.dataset.agChatScraped = 'pending';
|
||||
fetch(BASE + '/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: botText, source: 'fallback_bot_msg' })
|
||||
}).then(function() { lastBot.dataset.agChatScraped = 'true'; })
|
||||
.catch(function() { lastBot.dataset.agChatScraped = 'false'; });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the LATEST step that has meaningful text content (AI response)
|
||||
// Iterate backwards to find the most recent unscraped step
|
||||
for (var si = stepEls.length - 1; si >= Math.max(0, stepEls.length - 5); si--) {
|
||||
var stepEl = stepEls[si];
|
||||
var stepIdx = parseInt(stepEl.getAttribute('data-step-index') || '-1', 10);
|
||||
|
||||
// Skip already scraped
|
||||
if (stepEl.dataset.agChatScraped === 'true' || stepEl.dataset.agChatScraped === 'pending') continue;
|
||||
|
||||
// Skip steps we've already processed
|
||||
if (stepIdx >= 0 && stepIdx <= _lastScrapedStepIndex) continue;
|
||||
|
||||
// Check if this step has substantial text content (not just a tool call header)
|
||||
var stepText = extractCleanStepText(stepEl);
|
||||
if (!stepText || stepText.length < 30) continue;
|
||||
|
||||
// QUALITY CHECK: Skip if the text is mostly short lines (UI artifacts)
|
||||
var lines = stepText.split('\\n').filter(function(l) { return l.trim().length > 0; });
|
||||
var longLines = lines.filter(function(l) { return l.trim().length > 20; });
|
||||
if (longLines.length === 0) {
|
||||
log('Step ' + stepIdx + ': skipped (no long lines, likely UI noise)');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for content to stabilize (3s no change)
|
||||
if (_lastStepText !== stepText) {
|
||||
_lastStepText = stepText;
|
||||
_lastStepTextTime = Date.now();
|
||||
_lastStepTextSent = false;
|
||||
break; // Wait for next scan cycle
|
||||
}
|
||||
|
||||
if (_lastStepTextSent) continue;
|
||||
if (Date.now() - _lastStepTextTime < 3000) break; // Still waiting
|
||||
|
||||
// Content is stable — send it
|
||||
_lastStepTextSent = true;
|
||||
_lastScrapedStepIndex = stepIdx;
|
||||
stepEl.dataset.agChatScraped = 'pending';
|
||||
|
||||
log('Chat relay: step=' + stepIdx + ' text=' + stepText.length + ' chars');
|
||||
(function(el, txt, idx) {
|
||||
fetch(BASE + '/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: txt, source: 'step_' + idx, step_index: idx })
|
||||
}).then(function() { el.dataset.agChatScraped = 'true'; })
|
||||
.catch(function() { el.dataset.agChatScraped = 'false'; });
|
||||
})(stepEl, stepText, stepIdx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// BUTTON SCANNING (approval detection)
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
function scan(){
|
||||
if(!_ready)return;
|
||||
scanChatBodies();
|
||||
@@ -299,15 +437,19 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
|
||||
if(!isActionBtn(txt)) continue;
|
||||
// Skip inline code lens buttons
|
||||
if (b.closest('.codelens-decoration') && !txt.includes('Accept') && !txt.includes('수락') && !txt.includes('반영')) {
|
||||
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') || /Running\\\\d/.test(txt) ? 'command' : 'permission');
|
||||
var container=b.parentElement;
|
||||
var groupKey=matchedType+'|'+btnId(b,matchedType);
|
||||
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') || /Running\\d/.test(txt) ? 'command' : 'permission');
|
||||
|
||||
// v7: Use step-index for more unique group key
|
||||
var stepContainer = getStepContainer(b);
|
||||
var stepIdx = stepContainer ? stepContainer.getAttribute('data-step-index') : 'none';
|
||||
var groupKey = matchedType + '|' + stepIdx + '|' + btnId(b, matchedType);
|
||||
if(_sent[groupKey])continue;
|
||||
|
||||
var container=b.parentElement;
|
||||
var siblings=collectSiblingButtons(container,b);
|
||||
if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}];
|
||||
|
||||
@@ -329,7 +471,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
_sent[groupKey]={rid:rid,ts:now};
|
||||
for(var mk=0;mk<bidList.length;mk++)_sent[bidList[mk]]={rid:rid,ts:now};
|
||||
|
||||
log('DETECTED '+matchedType+': '+txt+' ['+desc.substring(0,80)+']');
|
||||
log('DETECTED '+matchedType+': '+txt+' ['+desc.substring(0,80)+'] step='+stepIdx);
|
||||
|
||||
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
|
||||
var payload={
|
||||
@@ -379,13 +521,13 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
clearInterval(timer);
|
||||
var btnIdx=(typeof d.button_index==='number'&&d.button_index>=0)?d.button_index:-1;
|
||||
if(btnIdx>=0&&btnIdx<btnRefs.length){
|
||||
log((d.approved?'✅':'❌')+' CHOICE '+rid+' → clicking button['+btnIdx+']');
|
||||
log((d.approved?'OK':'NO')+' CHOICE '+rid+' -> clicking button['+btnIdx+']');
|
||||
dispatchReactClick(btnRefs[btnIdx]);
|
||||
} else if(d.approved){
|
||||
log('✅ APPROVED '+rid+' → clicking primary');
|
||||
log('OK APPROVED '+rid+' -> clicking primary');
|
||||
dispatchReactClick(btnRefs[0]);
|
||||
} else {
|
||||
log('❌ REJECTED '+rid+' → finding reject button');
|
||||
log('NO REJECTED '+rid+' -> finding reject button');
|
||||
clickRejectButton(btnRefs[0]);
|
||||
}
|
||||
delete _sent[groupKey];
|
||||
@@ -471,6 +613,57 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
setTimeout(pollTriggerClick, 2000);
|
||||
})();
|
||||
|
||||
// ── DEEP-INSPECT POLLING ──
|
||||
(function pollDeepInspect(){
|
||||
if(_ready&&BASE){
|
||||
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
||||
if(!d.inspect)return;
|
||||
log('Deep inspect triggered');
|
||||
var cv = document.querySelector('[data-testid="conversation-view"]');
|
||||
var result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
windowURL: window.location.href,
|
||||
conversationViewFound: !!cv,
|
||||
stepElements: [],
|
||||
buttons: [],
|
||||
totalElements: document.body ? document.querySelectorAll('*').length : 0,
|
||||
};
|
||||
if (cv) {
|
||||
var steps = cv.querySelectorAll('[data-step-index]');
|
||||
for (var si = 0; si < steps.length; si++) {
|
||||
var s = steps[si];
|
||||
var text = (s.textContent || '').trim().substring(0, 200);
|
||||
result.stepElements.push({
|
||||
stepIndex: s.getAttribute('data-step-index'),
|
||||
classes: (s.className || '').substring(0, 200),
|
||||
textPreview: text,
|
||||
childCount: s.children ? s.children.length : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
var allBtns = document.querySelectorAll('button');
|
||||
for (var bi = 0; bi < Math.min(allBtns.length, 30); bi++) {
|
||||
var btn = allBtns[bi];
|
||||
var btxt = cleanButtonText(btn);
|
||||
if (btxt.length > 0) {
|
||||
var stepC = getStepContainer(btn);
|
||||
result.buttons.push({
|
||||
text: btxt,
|
||||
stepIndex: stepC ? stepC.getAttribute('data-step-index') : null,
|
||||
visible: !!(btn.offsetParent || btn.style.display === 'fixed'),
|
||||
});
|
||||
}
|
||||
}
|
||||
fetch(BASE+'/deep-inspect-result', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(result)
|
||||
}).catch(function(){});
|
||||
}).catch(function(){});
|
||||
}
|
||||
setTimeout(pollDeepInspect, 3000);
|
||||
})();
|
||||
|
||||
_obs=true;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -39,7 +39,7 @@ let responseWatcher: fs.FSWatcher | null = null;
|
||||
let brainWatcher: BrainWatcher | null = null;
|
||||
let activeTrajectoryId = '';
|
||||
const recentPendingSteps = new Map<string, number>();
|
||||
const PENDING_MEMORY_TTL_MS = 60_000;
|
||||
const PENDING_MEMORY_TTL_MS = 30_000;
|
||||
|
||||
// generateApprovalObserverScript → extracted to ./observer-script.ts
|
||||
const lastSnapshotText = new Map<string, string>();
|
||||
@@ -589,6 +589,7 @@ function setupMonitor() {
|
||||
// lastModifiedTime is still changing = AI is thinking, NOT approval
|
||||
consecutiveIdleCount = 0; // Reset!
|
||||
ctx.stallProbed = false;
|
||||
ctx.sessionStalled = false; // FIX: also reset stalled flag on modTime change
|
||||
if (pollCount <= 10 || pollCount % 12 === 0) {
|
||||
ctx.logToFile(`[THINK] step=${currentCount} modTime changing → not stall`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user