refactor(bridge): migrate gravity bridge to pure websocket gateway architecture, deleting legacy local file scanners and dependencies
This commit is contained in:
96
extension/src/brain-watcher.ts
Normal file
96
extension/src/brain-watcher.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { WSBridgeClient } from './ws-client';
|
||||
|
||||
export interface BrainWatcherContext {
|
||||
logToFile: (msg: string) => void;
|
||||
wsBridge: WSBridgeClient;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export class BrainWatcher {
|
||||
private brainDir: string;
|
||||
private ctx: BrainWatcherContext;
|
||||
private currentSessionId: string = '';
|
||||
private watcher: fs.FSWatcher | null = null;
|
||||
private lastEventTimes: Map<string, number> = new Map();
|
||||
|
||||
constructor(ctx: BrainWatcherContext) {
|
||||
this.ctx = ctx;
|
||||
// The bridgePath is ~/.gemini/antigravity/bridge, so brain is sibling
|
||||
this.brainDir = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
|
||||
}
|
||||
|
||||
public updateSession(sessionId: string) {
|
||||
if (!sessionId || this.currentSessionId === sessionId) {
|
||||
return;
|
||||
}
|
||||
this.currentSessionId = sessionId;
|
||||
this.startWatching(sessionId);
|
||||
}
|
||||
|
||||
private startWatching(sessionId: string) {
|
||||
this.stop();
|
||||
|
||||
const sessionDir = path.join(this.brainDir, sessionId);
|
||||
if (!fs.existsSync(sessionDir)) {
|
||||
// It might not be created yet, poll gently
|
||||
setTimeout(() => this.startWatching(sessionId), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.watcher = fs.watch(sessionDir, { persistent: false }, (eventType, filename) => {
|
||||
if (!filename || !filename.endsWith('.md')) return;
|
||||
|
||||
// Dedup rapid events
|
||||
const now = Date.now();
|
||||
const last = this.lastEventTimes.get(filename) || 0;
|
||||
if (now - last < 500) return; // 500ms debounce
|
||||
this.lastEventTimes.set(filename, now);
|
||||
|
||||
this.handleFileChange(sessionDir, filename, eventType);
|
||||
});
|
||||
this.ctx.logToFile(`[BRAIN-WATCHER] Started watching session: ${sessionId.substring(0, 8)}`);
|
||||
} catch (e: any) {
|
||||
this.ctx.logToFile(`[BRAIN-WATCHER] Failed to watch ${sessionId}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private handleFileChange(dir: string, filename: string, rawEventType: string) {
|
||||
const filePath = path.join(dir, filename);
|
||||
let content = '';
|
||||
let eventType = 'file_changed';
|
||||
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
content = fs.readFileSync(filePath, 'utf-8');
|
||||
} else {
|
||||
eventType = 'file_deleted';
|
||||
}
|
||||
} catch (e) {
|
||||
// File might be locked or deleted during read
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ctx.wsBridge && this.ctx.wsBridge.isConnected()) {
|
||||
this.ctx.wsBridge.sendBrainEvent({
|
||||
event_type: eventType,
|
||||
conversation_id: this.currentSessionId,
|
||||
file_name: filename,
|
||||
content: content,
|
||||
timestamp: Date.now() / 1000,
|
||||
project_name: this.ctx.projectName,
|
||||
});
|
||||
this.ctx.logToFile(`[BRAIN-WATCHER] Sent ${eventType} for ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.watcher) {
|
||||
this.watcher.close();
|
||||
this.watcher = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,13 +86,7 @@ function detectProjectName(): string {
|
||||
|
||||
// ─── Bridge File I/O ───
|
||||
|
||||
function ensureBridgeDir() {
|
||||
const dirs = ['', 'response', 'commands', 'chat_snapshots'];
|
||||
for (const d of dirs) {
|
||||
const p = path.join(bridgePath, d);
|
||||
if (!fs.existsSync(p)) { fs.mkdirSync(p, { recursive: true }); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
||||
let activeSessionId = '';
|
||||
@@ -102,34 +96,15 @@ const recentDiscordSentTexts: Map<string, number> = new Map();
|
||||
|
||||
function writeChatSnapshot(text: string) {
|
||||
try {
|
||||
// WS route (preferred) — skip file write to prevent duplicate Discord delivery
|
||||
if (wsBridge && wsBridge.isConnected()) {
|
||||
wsBridge.sendChat({
|
||||
content: text,
|
||||
conversation_id: activeSessionId,
|
||||
conversation_id: getStepProbeSessionId(),
|
||||
project_name: projectName,
|
||||
});
|
||||
logToFile(`[SNAPSHOT-WS] sent (${text.length} chars)`);
|
||||
if (activeSessionId) { writeRegistration(activeSessionId); }
|
||||
return;
|
||||
if (getStepProbeSessionId()) { writeRegistration(getStepProbeSessionId()); }
|
||||
}
|
||||
// File route (fallback — only when WS is NOT connected)
|
||||
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
|
||||
if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); }
|
||||
const id = Date.now().toString();
|
||||
const data = {
|
||||
id,
|
||||
project_name: projectName,
|
||||
content: text,
|
||||
timestamp: Date.now() / 1000,
|
||||
};
|
||||
const filePath = path.join(snapshotDir, `${id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
|
||||
logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars)`);
|
||||
logToFile(`[SNAPSHOT] content: ${text.substring(0, 200)}`);
|
||||
// Lazily register session → project mapping (correct because projectName is per-window)
|
||||
if (activeSessionId) { writeRegistration(activeSessionId); }
|
||||
} catch (e: any) {
|
||||
console.log(`Gravity Bridge: snapshot write error: ${e.message}`);
|
||||
}
|
||||
@@ -137,33 +112,16 @@ function writeChatSnapshot(text: string) {
|
||||
|
||||
function writeChatSnapshotWithFiles(text: string, files: Array<{ name: string, content: string }>) {
|
||||
try {
|
||||
// WS route (preferred) — skip file write to prevent duplicate Discord delivery
|
||||
if (wsBridge && wsBridge.isConnected()) {
|
||||
wsBridge.sendChat({
|
||||
content: text,
|
||||
attached_files: files,
|
||||
conversation_id: activeSessionId,
|
||||
conversation_id: getStepProbeSessionId(),
|
||||
project_name: projectName,
|
||||
});
|
||||
logToFile(`[SNAPSHOT-WS] sent with ${files.length} files (${text.length} chars)`);
|
||||
if (activeSessionId) { writeRegistration(activeSessionId); }
|
||||
return;
|
||||
if (getStepProbeSessionId()) { writeRegistration(getStepProbeSessionId()); }
|
||||
}
|
||||
// File route (fallback — only when WS is NOT connected)
|
||||
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
|
||||
if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); }
|
||||
const id = Date.now().toString();
|
||||
const data = {
|
||||
id,
|
||||
project_name: projectName,
|
||||
content: text,
|
||||
attached_files: files,
|
||||
timestamp: Date.now() / 1000,
|
||||
};
|
||||
const filePath = path.join(snapshotDir, `${id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars, ${files.length} files)`);
|
||||
if (activeSessionId) { writeRegistration(activeSessionId); }
|
||||
} catch (e: any) {
|
||||
console.log(`Gravity Bridge: snapshot+files write error: ${e.message}`);
|
||||
}
|
||||
@@ -383,7 +341,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const config = vscode.workspace.getConfiguration('gravityBridge');
|
||||
const configPath = config.get<string>('bridgePath');
|
||||
bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge');
|
||||
ensureBridgeDir();
|
||||
|
||||
console.log(`Gravity Bridge: bridge path: ${bridgePath}`);
|
||||
|
||||
// ── WebSocket Hub Connection ──
|
||||
@@ -527,6 +485,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
get activeSessionId() { return getStepProbeContext().activeSessionId; },
|
||||
get sessionStalled() { return getStepProbeContext().sessionStalled; },
|
||||
get lastPendingStepIndex() { return getStepProbeContext().lastPendingStepIndex; },
|
||||
writeChatSnapshot,
|
||||
};
|
||||
const bridgePort = await startHttpBridge(httpBridgeCtx, sdk);
|
||||
let localPort = bridgePort;
|
||||
|
||||
@@ -13,6 +13,8 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { WSBridgeClient } from './ws-client';
|
||||
|
||||
let lastFilePermissionTime = 0;
|
||||
|
||||
// ─── Context interface (shared state from extension.ts) ───
|
||||
|
||||
export interface HttpBridgeContext {
|
||||
@@ -127,7 +129,7 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
fs.writeFileSync(path.join(ctx.bridgePath, 'dump_html.json'), dumpBody, 'utf-8');
|
||||
} catch(e) {}
|
||||
} catch (e) { }
|
||||
res.writeHead(200); res.end('ok');
|
||||
});
|
||||
return;
|
||||
@@ -140,9 +142,9 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
|
||||
try {
|
||||
const params = JSON.parse(rpcBody);
|
||||
const result = await sdk.ls.rawRPC(params.method, params.args || {});
|
||||
res.writeHead(200, {'Content-Type': 'application/json'});
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(typeof result === 'string' ? result : JSON.stringify(result));
|
||||
} catch(e: any) {
|
||||
} catch (e: any) {
|
||||
res.writeHead(500); res.end(e.message);
|
||||
}
|
||||
});
|
||||
@@ -246,9 +248,6 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
}
|
||||
|
||||
const rid = data.request_id || Date.now().toString();
|
||||
// Write pending file for Discord bot
|
||||
const pendingDir = path.join(ctx.bridgePath, 'pending');
|
||||
if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true });
|
||||
const pending: Record<string, any> = {
|
||||
...data,
|
||||
request_id: rid,
|
||||
@@ -265,22 +264,13 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
if (cmdLower.includes('allow') && !pending.buttons) {
|
||||
// Dedup: skip if another file_permission pending was created within 10s
|
||||
const nowMs = Date.now();
|
||||
try {
|
||||
const existingFiles = fs.readdirSync(pendingDir).filter((f: string) => f.endsWith('.json'));
|
||||
for (const ef of existingFiles) {
|
||||
const existing = JSON.parse(fs.readFileSync(path.join(pendingDir, ef), 'utf-8'));
|
||||
if (existing.step_type === 'file_permission' && existing.status === 'pending'
|
||||
&& existing.project_name === ctx.projectName) {
|
||||
const age = nowMs - (existing.timestamp * 1000);
|
||||
if (age < 10_000 && age >= 0) {
|
||||
ctx.logToFile(`[HTTP] filtered duplicate file_permission (${age}ms old): ${ef}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'dedup_file_permission' }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
if (nowMs - lastFilePermissionTime < 10000) {
|
||||
ctx.logToFile(`[HTTP] filtered duplicate file_permission (${nowMs - lastFilePermissionTime}ms old)`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'dedup_file_permission' }));
|
||||
return;
|
||||
}
|
||||
lastFilePermissionTime = nowMs;
|
||||
|
||||
pending.buttons = [
|
||||
{ text: 'Allow Once', index: 0 },
|
||||
@@ -292,8 +282,7 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
const rawDesc = (data.description || data.command || '').replace(/Deny|Allow Once|Allow This Conversation/gi, '').trim();
|
||||
pending.command = `파일 접근 권한${rawDesc ? ': ' + rawDesc : ''}`;
|
||||
}
|
||||
fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2));
|
||||
// WS dual-write
|
||||
// WS dispatch
|
||||
if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
|
||||
ctx.wsBridge.sendPending({
|
||||
request_id: rid,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function generateApprovalObserverScript(_port: number): string {
|
||||
return `
|
||||
// ── Gravity Bridge v4: React Tailwind UI Observer ──
|
||||
// ── Gravity Bridge v5: Context-First DOM Extraction ──
|
||||
(function(){
|
||||
'use strict';
|
||||
var BASE='',_obs=false,_sent={},_ready=false;
|
||||
@@ -9,7 +9,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
var CLEANUP_MS=300000;
|
||||
|
||||
function log(m){console.log('[GB Observer] '+m);}
|
||||
log('v4 Script loaded — deep Tailwind DOM traversal enabled');
|
||||
log('v5 Script loaded — Context-First Tailored Extraction');
|
||||
|
||||
// React-Compatible Synthetic Clicker
|
||||
function dispatchReactClick(el){
|
||||
@@ -21,19 +21,10 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
el.dispatchEvent(new MouseEvent('mouseup', {bubbles:true, cancelable:true, view:window, composed:true}));
|
||||
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window, composed:true}));
|
||||
} catch(e) {
|
||||
el.click(); // fallback
|
||||
el.click();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Find common container for the step ──
|
||||
function findButtonContainer(btn){
|
||||
return btn.closest('.p-1')
|
||||
|| btn.closest('.bg-agent-convo-background')
|
||||
|| btn.closest('[class*="border-gray-500/10"]')
|
||||
|| btn.closest('.monaco-list-row')
|
||||
|| btn.parentElement;
|
||||
}
|
||||
|
||||
function cleanButtonText(btn) {
|
||||
if (!btn) return '';
|
||||
var clone = btn.cloneNode(true);
|
||||
@@ -43,10 +34,9 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
// ── Stable button fingerprint ──
|
||||
function btnId(b,type){
|
||||
var txt = cleanButtonText(b);
|
||||
var parent = b.parentElement;
|
||||
@@ -58,104 +48,78 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
return type+'|'+txt+'|'+idx;
|
||||
}
|
||||
|
||||
// ── Context extraction — target BOTH chat history and command payload ──
|
||||
function extractCommandContext(b){
|
||||
var container = findButtonContainer(b);
|
||||
var container = b.closest('.p-1') || b.parentElement.parentElement;
|
||||
if (!container) return "";
|
||||
|
||||
var titleSpans = container.querySelectorAll('span[title^="command("]');
|
||||
if (titleSpans && titleSpans.length > 0) {
|
||||
var t = titleSpans[0].getAttribute('title');
|
||||
if (t && t.length > 5) return t.substring(0, 800);
|
||||
}
|
||||
|
||||
var preEls = container.querySelectorAll('pre');
|
||||
if (preEls && preEls.length > 0) {
|
||||
var t2 = (preEls[preEls.length-1].textContent || '').trim();
|
||||
if (t2.length > 2) return t2.substring(0, 800);
|
||||
}
|
||||
|
||||
var codeText = '';
|
||||
var codes = container.querySelectorAll('code, [class*="command"]');
|
||||
for(var i=0; i<codes.length; i++) {
|
||||
codeText += (codes[i].textContent || '').trim() + ' ';
|
||||
}
|
||||
if (codeText.length > 2) return codeText.trim().substring(0, 800);
|
||||
|
||||
var fallback = (container.textContent || '').replace(cleanButtonText(b), '').trim();
|
||||
return fallback.substring(0, 500);
|
||||
}
|
||||
|
||||
function extractChatContextFromNode(botTurn) {
|
||||
if (!botTurn) return '';
|
||||
|
||||
var res = '';
|
||||
// Use innerText if available on the markdown container (preserves spacing perfectly)
|
||||
var md = botTurn.querySelector('.markdown-body') || botTurn.querySelector('.prose');
|
||||
if (md && md.innerText && md.innerText.trim().length > 10) {
|
||||
res = md.innerText.trim();
|
||||
return res.substring(0, 3500);
|
||||
}
|
||||
|
||||
var toolContainer = botTurn.querySelector('.bg-agent-convo-background') || botTurn.querySelector('.bg-ide-background-color');
|
||||
var textParts = [];
|
||||
function walk(node) {
|
||||
if (toolContainer && node === toolContainer) return;
|
||||
if (node.id === 'antigravity.agentSidePanelInputBox') return;
|
||||
if (node.nodeType === 1) {
|
||||
var tag = node.tagName.toUpperCase();
|
||||
if (tag==='BUTTON' || tag==='SVG' || tag==='STYLE' || tag==='SCRIPT') return;
|
||||
// Skip tool action blocks aggressively if they masquerade as normal divs
|
||||
if (node !== botTurn && node.classList && (node.classList.contains('bg-ide-background-color') || node.classList.contains('bg-agent-convo-background'))) return;
|
||||
}
|
||||
if (node.nodeType === 3) {
|
||||
var val = node.nodeValue;
|
||||
if (val && val.trim()) textParts.push(val.trim());
|
||||
} else {
|
||||
if (node.childNodes && node.childNodes.length > 0) {
|
||||
for(var i=0; i<node.childNodes.length; i++) {
|
||||
walk(node.childNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.nodeType === 1) {
|
||||
var tg = node.tagName.toUpperCase();
|
||||
if (tg==='P' || tg==='DIV' || tg==='BR' || tg==='LI' || tg==='PRE') textParts.push('\\n');
|
||||
}
|
||||
}
|
||||
walk(botTurn);
|
||||
res = textParts.join(' ').replace(/ \\n /g, '\\n').replace(/\\n+/g, '\\n').trim();
|
||||
return res.substring(0, 3500);
|
||||
}
|
||||
|
||||
function extractChatContext(b) {
|
||||
try {
|
||||
var botTurn = b.closest('.bg-agent-convo-background') || b.closest('.text-ide-message-block-bot-color');
|
||||
var botTurn = b.closest('.text-ide-message-block-bot-color') || b.closest('.bg-agent-convo-background');
|
||||
if (!botTurn) {
|
||||
var container = findButtonContainer(b);
|
||||
var container = b.closest('.p-1') || b.parentElement;
|
||||
botTurn = container ? container.parentElement : null;
|
||||
}
|
||||
if (!botTurn) return '';
|
||||
|
||||
var toolContainer = findButtonContainer(b) || b;
|
||||
var textParts = [];
|
||||
|
||||
function walk(node) {
|
||||
if (node === toolContainer) return true; // Stop traversal at the tool box
|
||||
if (node.nodeType === 1) {
|
||||
var tag = node.tagName.toUpperCase();
|
||||
if (tag==='BUTTON' || tag==='SVG' || tag==='STYLE' || tag==='SCRIPT') return false;
|
||||
}
|
||||
if (node.nodeType === 3) {
|
||||
var val = node.nodeValue;
|
||||
if (val && val.trim()) textParts.push(val.trim());
|
||||
} else {
|
||||
for(var i=0; i<node.childNodes.length; i++) {
|
||||
if (walk(node.childNodes[i])) return true;
|
||||
}
|
||||
}
|
||||
if (node.nodeType === 1) {
|
||||
var tg = node.tagName.toUpperCase();
|
||||
if (tg==='P' || tg==='DIV' || tg==='BR' || tg==='LI' || tg==='PRE') textParts.push('\\n');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
walk(botTurn);
|
||||
var result = textParts.join(' ').replace(/ \\n /g, '\\n').replace(/\\n+/g, '\\n').trim();
|
||||
return result.substring(0, 1500);
|
||||
return extractChatContextFromNode(botTurn);
|
||||
} catch(e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractChatContextFromNode(botTurn) {
|
||||
if (!botTurn) return '';
|
||||
var toolContainer = botTurn.querySelector('.bg-ide-background-color'); // Stop at tool blocks
|
||||
var textParts = [];
|
||||
function walk(node) {
|
||||
if (toolContainer && node === toolContainer) return true;
|
||||
if (node.nodeType === 1) {
|
||||
var tag = node.tagName.toUpperCase();
|
||||
if (tag==='BUTTON' || tag==='SVG' || tag==='STYLE' || tag==='SCRIPT') return false;
|
||||
}
|
||||
if (node.nodeType === 3) {
|
||||
var val = node.nodeValue;
|
||||
if (val && val.trim()) textParts.push(val.trim());
|
||||
} else {
|
||||
for(var i=0; i<node.childNodes.length; i++) {
|
||||
if (walk(node.childNodes[i])) return true;
|
||||
}
|
||||
}
|
||||
if (node.nodeType === 1) {
|
||||
var tg = node.tagName.toUpperCase();
|
||||
if (tg==='P' || tg==='DIV' || tg==='BR' || tg==='LI' || tg==='PRE') textParts.push('\\n');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
walk(botTurn);
|
||||
var result = textParts.join(' ').replace(/ \\n /g, '\\n').replace(/\\n+/g, '\\n').trim();
|
||||
return result.substring(0, 3500);
|
||||
}
|
||||
|
||||
function extractContext(b) {
|
||||
var cmd = extractCommandContext(b);
|
||||
var chat = extractChatContext(b);
|
||||
@@ -166,15 +130,21 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
return combined.trim();
|
||||
}
|
||||
|
||||
// ── Action Buttons Patterns (EN / KO) ──
|
||||
var PATS = [
|
||||
{ type: 'command', re: /^(?:Always\\s*)?(?:Run\\b|결행사양\\s*항상|결행)/i },
|
||||
{ type: 'permission', re: /^(?:Always\\s*)?(?:Allow\\b|허용)/i },
|
||||
{ type: 'permission', re: /^(?:Always\\s*)?(?:Approve\\b|승인)/i },
|
||||
{ type: 'diff_review', re: /^(?:Always\\s*)?(?:Accept\\b|수락|반영)/i },
|
||||
];
|
||||
var ALL_ACTION_RE=[/^(?:Always\\s*)?(?:Run\\b|결행)/i,/^(?:Always\\s*)?(?:Accept\\b|수락|반영)/i,/^(?:Reject\\b|거절|거부)/i,/^(?:Always\\s*)?(?:Allow\\b|허용)/i,/^(?:Deny\\b|차단)/i,/^(?:Always\\s*)?(?:Approve\\b|승인)/i,/^(?:Cancel\\b|취소)/i,/^Retry\\b/i,/^(?:Dismiss\\b|무시)/i,/^(?:Stop\\b|정지)/i,/^Decline\\b/i];
|
||||
var REJECT_RE=[/^(?:Reject\\b|거절|거부)/i,/^(?:Cancel\\b|취소)/i,/^(?:Deny\\b|차단)/i,/^(?:Stop\\b|정지)/i,/^Decline\\b/i,/^(?:Dismiss\\b|무시)/i];
|
||||
var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', '결행', '수락', '반영', '허용', '승인'];
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isRejectBtn(txt) {
|
||||
for(var i=0; i<REJECT_WORDS.length; i++) {
|
||||
if(txt.indexOf(REJECT_WORDS[i]) !== -1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function collectSiblingButtons(container,triggerBtn){
|
||||
if(!container)return [];
|
||||
@@ -183,110 +153,86 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
for(var i=0;i<siblings.length;i++){
|
||||
var sb=siblings[i];
|
||||
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
|
||||
|
||||
var stxt = cleanButtonText(sb);
|
||||
if(stxt.length <= 1) continue; // Ignore icon buttons
|
||||
|
||||
var isAction=false;
|
||||
for(var a=0;a<ALL_ACTION_RE.length;a++){
|
||||
if(ALL_ACTION_RE[a].test(stxt)){isAction=true;break;}
|
||||
}
|
||||
if(!isAction)continue;
|
||||
if(stxt.length <= 1) continue;
|
||||
if(!isActionBtn(stxt) && !isRejectBtn(stxt)) continue;
|
||||
result.push({btn:sb,text:stxt,isPrimary:(sb===triggerBtn)});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var HARDCODED_PORT=${_port};
|
||||
|
||||
function tryPingAsync(port){
|
||||
return fetch('http://127.0.0.1:'+port+'/ping?t='+Date.now(),{signal:AbortSignal.timeout(2000)})
|
||||
.then(function(r){return r.text();})
|
||||
.then(function(t){return t==='pong';})
|
||||
.catch(function(){return false;});
|
||||
.then(function(r){return r.text();}).then(function(t){return t==='pong';}).catch(function(){return false;});
|
||||
}
|
||||
|
||||
function discoverPort(cb){
|
||||
log('Waiting for Gravity Bridge status...');
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
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]) {
|
||||
var domPort = parseInt(m[1], 10);
|
||||
clearInterval(timer);
|
||||
tryPingAsync(domPort).then(function(ok){
|
||||
if(ok) cb(domPort); else cb(HARDCODED_PORT);
|
||||
});
|
||||
tryPingAsync(parseInt(m[1], 10)).then(function(ok){ cb(ok ? parseInt(m[1],10) : HARDCODED_PORT); });
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we are in the webview, the status bar is invisible. Skip quickly.
|
||||
if(attempts>1){
|
||||
clearInterval(timer);
|
||||
tryPingAsync(HARDCODED_PORT).then(function(ok){ cb(HARDCODED_PORT); }); // Assume HARDCODED_PORT works!
|
||||
tryPingAsync(HARDCODED_PORT).then(function(){ cb(HARDCODED_PORT); });
|
||||
}
|
||||
},500); // Wait 500ms * 2 = 1 second total
|
||||
},500);
|
||||
}
|
||||
|
||||
discoverPort(function(port){
|
||||
BASE='http://127.0.0.1:'+port;
|
||||
fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){
|
||||
if(t==='pong'){_ready=true;startObserver();}
|
||||
}).catch(function(e){});
|
||||
_ready=true;
|
||||
startObserver();
|
||||
});
|
||||
|
||||
var _chatSnapshots = [];
|
||||
var _firstChatScan = true;
|
||||
var _lastText = "";
|
||||
var _lastTextTime = 0;
|
||||
var _lastTextSent = false;
|
||||
|
||||
function scanChatBodies() {
|
||||
if(!_ready)return;
|
||||
var botTurns = document.querySelectorAll('.text-ide-message-block-bot-color');
|
||||
for (var i = 0; i < botTurns.length; i++) {
|
||||
var turn = botTurns[i];
|
||||
if (turn.dataset.agChatScraped === "true" || turn.dataset.agChatScraped === "pending") continue;
|
||||
|
||||
if (_firstChatScan) {
|
||||
turn.dataset.agChatScraped = "true";
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentText = turn.textContent || '';
|
||||
var found = -1;
|
||||
for (var j = 0; j < _chatSnapshots.length; j++) {
|
||||
if (_chatSnapshots[j].node === turn) { found = j; break; }
|
||||
}
|
||||
|
||||
if (found === -1) {
|
||||
_chatSnapshots.push({ node: turn, text: currentText, lastChanged: Date.now() });
|
||||
} else {
|
||||
if (_chatSnapshots[found].text !== currentText) {
|
||||
_chatSnapshots[found].text = currentText;
|
||||
_chatSnapshots[found].lastChanged = Date.now();
|
||||
if (botTurns.length === 0) return;
|
||||
|
||||
var lastTurn = botTurns[botTurns.length - 1];
|
||||
if (lastTurn.dataset.agChatScraped === "true" || lastTurn.dataset.agChatScraped === "pending") return;
|
||||
|
||||
var currentText = lastTurn.textContent || '';
|
||||
if (currentText.length < 5) return;
|
||||
|
||||
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 = extractChatContextFromNode(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 {
|
||||
if (Date.now() - _chatSnapshots[found].lastChanged > 3500) {
|
||||
turn.dataset.agChatScraped = "pending"; // prevent re-entry
|
||||
var finalTxt = extractChatContextFromNode(turn);
|
||||
if (finalTxt && finalTxt.length > 5) {
|
||||
fetch(BASE+'/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: finalTxt })
|
||||
}).then(function(){
|
||||
turn.dataset.agChatScraped = "true";
|
||||
}).catch(function(){
|
||||
turn.dataset.agChatScraped = "false"; // retry
|
||||
});
|
||||
} else {
|
||||
turn.dataset.agChatScraped = "true";
|
||||
}
|
||||
}
|
||||
lastTurn.dataset.agChatScraped = "true";
|
||||
}
|
||||
}
|
||||
}
|
||||
_firstChatScan = false;
|
||||
}
|
||||
|
||||
function scan(){
|
||||
@@ -301,26 +247,17 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
if(b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed'))continue;
|
||||
|
||||
var txt=cleanButtonText(b);
|
||||
console.log("[JSDOM] Button scan:", txt);
|
||||
if(txt.length <= 1) continue; // Icon
|
||||
if(txt.length <= 1) continue;
|
||||
|
||||
var matchedType=null;
|
||||
for(var p=0;p<PATS.length;p++){
|
||||
if(PATS[p].re.test(txt)){
|
||||
if (b.closest('.codelens-decoration') && PATS[p].type !== 'diff_review' && PATS[p].type !== 'permission') {
|
||||
continue;
|
||||
}
|
||||
matchedType=PATS[p].type;
|
||||
break;
|
||||
}
|
||||
if(!isActionBtn(txt)) continue;
|
||||
// Skip inline code lens buttons unless they actually match the pattern properly
|
||||
if (b.closest('.codelens-decoration') && !txt.includes('Accept') && !txt.includes('수락') && !txt.includes('반영')) {
|
||||
continue;
|
||||
}
|
||||
if(!matchedType){
|
||||
console.log("[JSDOM] NOT MATCHED:", txt);
|
||||
continue;
|
||||
}
|
||||
var container=findButtonContainer(b);
|
||||
|
||||
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') ? 'command' : 'permission');
|
||||
var container=b.closest('.p-1') || b.parentElement.parentElement;
|
||||
var groupKey=matchedType+'|'+btnId(b,matchedType);
|
||||
console.log("[JSDOM] MATCHED:", matchedType, "KEY:", groupKey, "SENT:", !!_sent[groupKey]);
|
||||
if(_sent[groupKey])continue;
|
||||
|
||||
var siblings=collectSiblingButtons(container,b);
|
||||
@@ -338,7 +275,6 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
}
|
||||
|
||||
var desc=extractContext(b);
|
||||
|
||||
var is_dom_dummy = false;
|
||||
if (!desc || desc.trim().length <= 2) {
|
||||
desc = "MISSING_DESCRIPTION_FROM_DOM_FALLBACK_TO_STEP_PROBE";
|
||||
@@ -417,17 +353,15 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
}
|
||||
|
||||
function clickRejectButton(approveBtn){
|
||||
var container=findButtonContainer(approveBtn);
|
||||
var container=approveBtn.closest('.p-1') || approveBtn.parentElement.parentElement;
|
||||
if(!container)return;
|
||||
var siblings=container.querySelectorAll('button');
|
||||
for(var i=0;i<siblings.length;i++){
|
||||
var t=cleanButtonText(siblings[i]);
|
||||
for(var r=0;r<REJECT_RE.length;r++){
|
||||
if(REJECT_RE[r].test(t)){
|
||||
log('Clicking reject: '+t);
|
||||
dispatchReactClick(siblings[i]);
|
||||
return;
|
||||
}
|
||||
if(isRejectBtn(t)){
|
||||
log('Clicking reject: '+t);
|
||||
dispatchReactClick(siblings[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -476,22 +410,17 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
if(_ready&&BASE){
|
||||
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
||||
if(!d.action)return;
|
||||
var approveRe=[/^(?:Always\\\\s*)?(?:Run\\\\b|결행)/i,/^(?:Always\\\\s*)?(?:Accept\\\\b|수락)/i,/^(?:Always\\\\s*)?(?:Accept all\\\\b|모두 수락)/i,/^(?:Always\\\\s*)?(?:Allow\\\\b|허용)/i,/^(?:Always\\\\s*)?(?:Approve\\\\b|승인)/i];
|
||||
var rejectRe=[/^(?:Reject\\\\b|거절|거부)/i,/^(?:Cancel\\\\b|취소)/i,/^(?:Deny\\\\b|차단)/i,/^(?:Stop\\\\b|정지)/i,/^Decline\\\\b/i,/^(?:Dismiss\\\\b|무시)/i];
|
||||
var patterns=(d.action==='approve')?approveRe:rejectRe;
|
||||
|
||||
var isApprove = (d.action==='approve');
|
||||
var btns = document.querySelectorAll('button');
|
||||
for(var i=0;i<btns.length;i++){
|
||||
var bx = btns[i];
|
||||
if(bx.disabled||bx.hidden||(!bx.offsetParent&&bx.style.display!=='fixed'))continue;
|
||||
var t = cleanButtonText(bx);
|
||||
if(t.length <= 1) continue;
|
||||
for(var pi=0;pi<patterns.length;pi++){
|
||||
if(patterns[pi].test(t)){
|
||||
log('Fallback TRIGGER-CLICK on "' + t + '"');
|
||||
dispatchReactClick(bx);
|
||||
return;
|
||||
}
|
||||
if (isApprove ? isActionBtn(t) : isRejectBtn(t)) {
|
||||
log('Fallback TRIGGER-CLICK on "' + t + '"');
|
||||
dispatchReactClick(bx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}).catch(function(){});
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as path from 'path';
|
||||
import { WSBridgeClient } from './ws-client';
|
||||
import { extractPlannerText, extractToolCommand, extractToolDescription } from './step-utils';
|
||||
import { initApprovalHandler, setupResponseWatcher } from './approval-handler';
|
||||
import { BrainWatcher } from './brain-watcher';
|
||||
|
||||
// Re-export from approval-handler for backward compatibility with extension.ts imports
|
||||
export { handleDiffReviewResponse, tryApprovalStrategies } from './approval-handler';
|
||||
@@ -35,6 +36,7 @@ export interface BridgeContext {
|
||||
|
||||
let ctx: BridgeContext;
|
||||
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;
|
||||
@@ -276,15 +278,16 @@ function setupMonitor() {
|
||||
} catch (e: any) {
|
||||
if (pollCount <= 30) ctx.logToFile(`[POLL] Fallback 2 error for sid=${sid}: ${e.message}`);
|
||||
// If trajectory explicitly does not exist, it might be an Antigravity or non-Cascade session directory.
|
||||
if (e.message?.includes('trajectory not found')) {
|
||||
continue;
|
||||
}
|
||||
// FIXED: known-issues "AI Response Missing for New Sessions" -> Force register to prevent session loss on proto/UTF-8 parse errors
|
||||
// We MUST register it so activeSessionId tracks it properly.
|
||||
// To prevent old ghost sessions from hijacking, we only mark it RUNNING if it was recently modified.
|
||||
const ageMs = Date.now() - brainDirs[i].time;
|
||||
const isFresh = ageMs < 120_000; // updated within 2 mins
|
||||
|
||||
allTraj.trajectorySummaries[sid] = {
|
||||
status: 'CASCADE_RUN_STATUS_RUNNING',
|
||||
status: isFresh ? 'CASCADE_RUN_STATUS_RUNNING' : 'CASCADE_RUN_STATUS_IDLE',
|
||||
stepCount: 1, // Assume progressing to allow loop delta>0 trigger
|
||||
lastModifiedTime: new Date(brainDirs[i].time).toISOString(),
|
||||
summary: 'Discovered via brain/ scan (Fallback Error)',
|
||||
summary: 'Discovered via brain/ scan (Antigravity Native)',
|
||||
trajectoryMetadata: { workspaces: [{ workspaceFolderAbsoluteUri: ctx.workspaceUri.replace(/\\/g, '/') }] }
|
||||
};
|
||||
}
|
||||
@@ -381,6 +384,9 @@ function setupMonitor() {
|
||||
// Session changed?
|
||||
if (bestSessionId !== ctx.activeSessionId) {
|
||||
ctx.activeSessionId = bestSessionId;
|
||||
if (brainWatcher) {
|
||||
brainWatcher.updateSession(bestSessionId);
|
||||
}
|
||||
activeTrajectoryId = (bestSession as any).trajectoryId || '';
|
||||
activeSessionTitle = currentTitle;
|
||||
lastKnownStepCount = currentCount;
|
||||
@@ -1261,6 +1267,13 @@ export function writePendingApproval(data: { conversation_id: string; command: s
|
||||
*/
|
||||
export function initStepProbe(context: BridgeContext) {
|
||||
ctx = context;
|
||||
if (ctx.wsBridge) {
|
||||
brainWatcher = new BrainWatcher({
|
||||
logToFile: ctx.logToFile,
|
||||
wsBridge: ctx.wsBridge,
|
||||
projectName: ctx.projectName
|
||||
});
|
||||
}
|
||||
initApprovalHandler(context, () => activeTrajectoryId);
|
||||
setupMonitor();
|
||||
setupResponseWatcher();
|
||||
|
||||
Reference in New Issue
Block a user