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:
2026-03-08 14:38:41 +09:00
parent 2574ce6f08
commit c97414cd37
23 changed files with 3516 additions and 280 deletions

View 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));
}

View 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.');
}
}

View 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;
}