fix(bridge): stall-based approval detection + known issues from deep debugging
- IDLE→stall detection: RUNNING+delta=0 for 6 polls (30s) - lastModifiedTime-based thinking filter (partial) - ResolveOutstandingSteps confirmed CANCELS steps (removed) - HandleCascadeUserInteraction always socket hang up (removed) - VS Code accept commands: silent success, no effect - Hybrid approval: focus+all commands sequential, no break - logToFile: console.log backup added - Known issues: 4 critical findings documented - better-antigravity reference added for future research
This commit is contained in:
235
better-antigravity-main/src/auto-run.ts
Normal file
235
better-antigravity-main/src/auto-run.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Auto-Run Fix — Patches the "Always Proceed" terminal policy to actually auto-execute.
|
||||
*
|
||||
* Uses structural regex matching to find the onChange handler in minified code
|
||||
* and injects a missing useEffect that auto-confirms commands when policy is EAGER.
|
||||
*
|
||||
* Works across AG versions because it matches code STRUCTURE, not variable NAMES.
|
||||
*
|
||||
* @module auto-run
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as fsp from 'fs/promises';
|
||||
|
||||
/** Marker comment to identify our patches */
|
||||
const PATCH_MARKER = '/*BA:autorun*/';
|
||||
|
||||
/**
|
||||
* Resolve the Antigravity workbench directory.
|
||||
*/
|
||||
export function getWorkbenchDir(): string | null {
|
||||
const appData = process.env.LOCALAPPDATA || '';
|
||||
const dir = path.join(
|
||||
appData,
|
||||
'Programs', 'Antigravity', 'resources', 'app', 'out',
|
||||
'vs', 'code', 'electron-browser', 'workbench',
|
||||
);
|
||||
return fs.existsSync(dir) ? dir : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target files that need the auto-run patch.
|
||||
*/
|
||||
export function getTargetFiles(workbenchDir: string): Array<{ path: string; label: string }> {
|
||||
return [
|
||||
{ path: path.join(workbenchDir, 'workbench.desktop.main.js'), label: 'workbench' },
|
||||
{ path: path.join(workbenchDir, 'jetskiAgent.js'), label: 'jetskiAgent' },
|
||||
].filter(f => fs.existsSync(f.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file already has the auto-run patch applied.
|
||||
*/
|
||||
export async function isPatched(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
// Read only first 50 bytes of the marker area via a small buffer scan
|
||||
// The marker is injected mid-file, so we must read the full file.
|
||||
// Use async to avoid blocking extension host.
|
||||
const content = await fsp.readFile(filePath, 'utf8');
|
||||
return content.includes(PATCH_MARKER);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a file to find the onChange handler and extract variable names.
|
||||
*
|
||||
* Returns null if pattern not found (file may already be fixed by AG update).
|
||||
*/
|
||||
function analyzeFile(content: string): AnalysisResult | null {
|
||||
// Find onChange handler for terminalAutoExecutionPolicy
|
||||
// Pattern: <callback>=<useCallback>((<arg>)=>{<setFn>(<arg>),<arg>===<ENUM>.EAGER&&<confirm>(true)},[...])
|
||||
const onChangeRegex = /(\w+)=(\w+)\((\(\w+\))=>\{(\w+)\(\w+\),\w+===(\w+)\.EAGER&&(\w+)\(!0\)\},\[/g;
|
||||
const match = onChangeRegex.exec(content);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const [fullMatch, , , , , enumName, confirmFn] = match;
|
||||
const insertPos = match.index + fullMatch.length;
|
||||
|
||||
// Extract context variables from surrounding code
|
||||
const contextStart = Math.max(0, match.index - 3000);
|
||||
const contextEnd = Math.min(content.length, match.index + 3000);
|
||||
const context = content.substring(contextStart, contextEnd);
|
||||
|
||||
// policyVar: <var>=<something>?.terminalAutoExecutionPolicy??<ENUM>.OFF
|
||||
const policyMatch = /(\w+)=\w+\?\.terminalAutoExecutionPolicy\?\?(\w+)\.OFF/.exec(context);
|
||||
// secureVar: <var>=<something>?.secureModeEnabled??!1
|
||||
const secureMatch = /(\w+)=\w+\?\.secureModeEnabled\?\?!1/.exec(context);
|
||||
|
||||
if (!policyMatch || !secureMatch) return null;
|
||||
|
||||
const policyVar = policyMatch[1];
|
||||
const secureVar = secureMatch[1];
|
||||
|
||||
// Find useEffect — most frequently used short-named function in the scope
|
||||
const useEffectFn = findUseEffect(context, [confirmFn]);
|
||||
|
||||
if (!useEffectFn) return null;
|
||||
|
||||
// Find insertion point: after the useCallback closing
|
||||
const afterOnChange = content.indexOf('])', insertPos);
|
||||
if (afterOnChange === -1) return null;
|
||||
|
||||
const insertAt = content.indexOf(';', afterOnChange);
|
||||
if (insertAt === -1) return null;
|
||||
|
||||
return {
|
||||
enumName,
|
||||
confirmFn,
|
||||
policyVar,
|
||||
secureVar,
|
||||
useEffectFn,
|
||||
insertAt: insertAt + 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the useEffect function name by frequency analysis.
|
||||
*/
|
||||
function findUseEffect(context: string, exclude: string[]): string | null {
|
||||
const candidates: Record<string, number> = {};
|
||||
const regex = /(\w{1,3})\(\(\)=>\{/g;
|
||||
let m;
|
||||
|
||||
while ((m = regex.exec(context)) !== null) {
|
||||
const fn = m[1];
|
||||
if (fn.length <= 3 && !exclude.includes(fn)) {
|
||||
candidates[fn] = (candidates[fn] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let best = '';
|
||||
let maxCount = 0;
|
||||
for (const [fn, count] of Object.entries(candidates)) {
|
||||
if (count > maxCount) {
|
||||
best = fn;
|
||||
maxCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return best || null;
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
enumName: string;
|
||||
confirmFn: string;
|
||||
policyVar: string;
|
||||
secureVar: string;
|
||||
useEffectFn: string;
|
||||
insertAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the auto-run patch to a single file.
|
||||
*
|
||||
* @returns Patch status message
|
||||
*/
|
||||
export async function patchFile(filePath: string, label: string): Promise<PatchResult> {
|
||||
try {
|
||||
let content = await fsp.readFile(filePath, 'utf8');
|
||||
|
||||
if (content.includes(PATCH_MARKER)) {
|
||||
return { success: true, label, status: 'already-patched' };
|
||||
}
|
||||
|
||||
const analysis = analyzeFile(content);
|
||||
if (!analysis) {
|
||||
return { success: false, label, status: 'pattern-not-found' };
|
||||
}
|
||||
|
||||
const { enumName, confirmFn, policyVar, secureVar, useEffectFn, insertAt } = analysis;
|
||||
|
||||
// Build the patch
|
||||
const patch = `${PATCH_MARKER}${useEffectFn}(()=>{${policyVar}===${enumName}.EAGER&&!${secureVar}&&${confirmFn}(!0)},[])`;
|
||||
|
||||
// Create backup (only if one doesn't exist)
|
||||
const backup = filePath + '.ba-backup';
|
||||
try { await fsp.access(backup); } catch {
|
||||
await fsp.copyFile(filePath, backup);
|
||||
}
|
||||
|
||||
// Insert
|
||||
content = content.substring(0, insertAt) + patch + content.substring(insertAt);
|
||||
await fsp.writeFile(filePath, content, 'utf8');
|
||||
|
||||
return { success: true, label, status: 'patched', bytesAdded: patch.length };
|
||||
} catch (err: any) {
|
||||
return { success: false, label, status: 'error', error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the auto-run patch on a single file.
|
||||
*/
|
||||
export function revertFile(filePath: string, label: string): PatchResult {
|
||||
const backup = filePath + '.ba-backup';
|
||||
if (!fs.existsSync(backup)) {
|
||||
return { success: false, label, status: 'no-backup' };
|
||||
}
|
||||
|
||||
try {
|
||||
fs.copyFileSync(backup, filePath);
|
||||
fs.unlinkSync(backup);
|
||||
return { success: true, label, status: 'reverted' };
|
||||
} catch (err: any) {
|
||||
return { success: false, label, status: 'error', error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export interface PatchResult {
|
||||
success: boolean;
|
||||
label: string;
|
||||
status: 'patched' | 'already-patched' | 'pattern-not-found' | 'reverted' | 'no-backup' | 'error';
|
||||
bytesAdded?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-apply the fix to all target files.
|
||||
*
|
||||
* @returns Array of results for each file
|
||||
*/
|
||||
export async function autoApply(): Promise<PatchResult[]> {
|
||||
const dir = getWorkbenchDir();
|
||||
if (!dir) return [];
|
||||
|
||||
const files = getTargetFiles(dir);
|
||||
return Promise.all(files.map(f => patchFile(f.path, f.label)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert all target files from backups.
|
||||
*
|
||||
* @returns Number of files reverted
|
||||
*/
|
||||
export function revertAll(): PatchResult[] {
|
||||
const dir = getWorkbenchDir();
|
||||
if (!dir) return [];
|
||||
|
||||
const files = getTargetFiles(dir);
|
||||
return files.map(f => revertFile(f.path, f.label));
|
||||
}
|
||||
Reference in New Issue
Block a user