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));
|
||||
}
|
||||
81
better-antigravity-main/src/commands.ts
Normal file
81
better-antigravity-main/src/commands.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Better Antigravity — VS Code command handlers.
|
||||
*
|
||||
* Each exported function is a command handler registered in extension.ts.
|
||||
*
|
||||
* @module commands
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fsp from 'fs/promises';
|
||||
import { AntigravitySDK } from 'antigravity-sdk';
|
||||
import { getWorkbenchDir, getTargetFiles, isPatched, revertAll } from './auto-run';
|
||||
|
||||
/**
|
||||
* Show extension status in the output channel.
|
||||
*/
|
||||
export async function status(sdk: AntigravitySDK | null, output: vscode.OutputChannel): Promise<void> {
|
||||
const lines = [
|
||||
'=== Better Antigravity ===',
|
||||
'',
|
||||
`SDK: ${sdk?.isInitialized ? `v${sdk.version}` : 'not initialized'}`,
|
||||
`LS: ${sdk?.ls?.isReady ? `port ${sdk.ls.port}` : 'not ready'}`,
|
||||
`UI: ${sdk?.integration.isInstalled() ? 'installed' : 'not installed'}`,
|
||||
`Titles: ${sdk?.integration.titles.count ?? 0} custom`,
|
||||
];
|
||||
|
||||
const dir = getWorkbenchDir();
|
||||
if (dir) {
|
||||
const files = getTargetFiles(dir);
|
||||
for (const f of files) {
|
||||
const patched = await isPatched(f.path);
|
||||
lines.push(`AutoRun: ${f.label} = ${patched ? 'fixed' : 'not fixed'}`);
|
||||
}
|
||||
} else {
|
||||
lines.push('AutoRun: workbench directory not found');
|
||||
}
|
||||
|
||||
output.appendLine(lines.join('\n'));
|
||||
output.show(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the auto-run fix and prompt for reload.
|
||||
*
|
||||
* Also clears V8 Code Cache to prevent stale cached patched code
|
||||
* from being loaded by Electron (which causes grey screen).
|
||||
*/
|
||||
export async function revertAutoRun(): Promise<void> {
|
||||
const dir = getWorkbenchDir();
|
||||
if (!dir) {
|
||||
vscode.window.showErrorMessage('Workbench directory not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const results = revertAll();
|
||||
const reverted = results.filter(r => r.status === 'reverted').length;
|
||||
|
||||
if (reverted > 0) {
|
||||
// Clear V8 Code Cache — stale cache after revert causes grey screen
|
||||
const appData = process.env.APPDATA || '';
|
||||
const cacheDirs = [
|
||||
path.join(appData, 'Antigravity', 'CachedData'),
|
||||
path.join(appData, 'Antigravity', 'GPUCache'),
|
||||
path.join(appData, 'Antigravity', 'Code Cache'),
|
||||
];
|
||||
for (const d of cacheDirs) {
|
||||
try { await fsp.rm(d, { recursive: true, force: true }); } catch { /* may not exist */ }
|
||||
}
|
||||
|
||||
const action = await vscode.window.showInformationMessage(
|
||||
`Auto-run fix reverted (${reverted} file(s)). Caches cleared. Reload to apply.`,
|
||||
'Reload Now',
|
||||
);
|
||||
if (action === 'Reload Now') {
|
||||
vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
} else {
|
||||
vscode.window.showInformationMessage('No backups found. Nothing to revert.');
|
||||
}
|
||||
}
|
||||
72
better-antigravity-main/src/extension.ts
Normal file
72
better-antigravity-main/src/extension.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Better Antigravity — Extension entry point.
|
||||
*
|
||||
* Thin orchestrator: wires up modules, no business logic here.
|
||||
*
|
||||
* @module extension
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { AntigravitySDK } from 'antigravity-sdk';
|
||||
import { autoApply } from './auto-run';
|
||||
import { status, revertAutoRun } from './commands';
|
||||
|
||||
let sdk: AntigravitySDK | null = null;
|
||||
let output: vscode.OutputChannel;
|
||||
|
||||
function log(msg: string): void {
|
||||
const ts = new Date().toISOString().substring(11, 19);
|
||||
output?.appendLine(`[${ts}] ${msg}`);
|
||||
}
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
output = vscode.window.createOutputChannel('Better Antigravity');
|
||||
context.subscriptions.push(output);
|
||||
log('Activating...');
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('better-antigravity.status', () => status(sdk, output)),
|
||||
vscode.commands.registerCommand('better-antigravity.revertAutoRun', revertAutoRun),
|
||||
);
|
||||
|
||||
// ── Auto-Run Fix (async, non-blocking, no prompt) ─────────────────
|
||||
autoApply().then(fixResults => {
|
||||
for (const r of fixResults) {
|
||||
log(`[auto-run] ${r.label}: ${r.status}${r.bytesAdded ? ` (+${r.bytesAdded}b)` : ''}${r.error ? ` -- ${r.error}` : ''}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── SDK Init ─────────────────────────────────────────────────────
|
||||
try {
|
||||
sdk = new AntigravitySDK(context);
|
||||
await sdk.initialize();
|
||||
log(`SDK v${sdk.version} initialized`);
|
||||
|
||||
// Title proxy for chat rename
|
||||
sdk.integration.enableTitleProxy();
|
||||
|
||||
// Seamless install (handles first-time prompt + auto-reload on update)
|
||||
await sdk.integration.installSeamless(
|
||||
(cmd) => vscode.commands.executeCommand(cmd),
|
||||
(msg, ...items) => vscode.window.showInformationMessage(msg, ...items),
|
||||
);
|
||||
|
||||
// Heartbeat (keeps renderer script alive)
|
||||
const hbTimer = setInterval(() => sdk?.integration.signalActive(), 30_000);
|
||||
context.subscriptions.push({ dispose: () => clearInterval(hbTimer) });
|
||||
|
||||
// Auto-repair (re-patch after AG updates)
|
||||
sdk.integration.enableAutoRepair();
|
||||
|
||||
log('Active');
|
||||
} catch (err: any) {
|
||||
log(`SDK init failed: ${err.message}`);
|
||||
log('Running in degraded mode (auto-run fix only)');
|
||||
}
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
sdk?.dispose();
|
||||
sdk = null;
|
||||
}
|
||||
Reference in New Issue
Block a user