refactor(bridge): migrate gravity bridge to pure websocket gateway architecture, deleting legacy local file scanners and dependencies

This commit is contained in:
Variet Worker
2026-04-11 13:06:38 +09:00
parent 5e697cd919
commit 072f83bf25
20 changed files with 756 additions and 1537 deletions

Binary file not shown.

View File

@@ -120,15 +120,6 @@ function detectProjectName() {
return 'default';
}
// ─── 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 = '';
let activeTrajectoryId = '';
@@ -136,39 +127,16 @@ let activeTrajectoryId = '';
const recentDiscordSentTexts = new Map();
function writeChatSnapshot(text) {
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: (0, step_probe_1.getActiveSessionId)(),
project_name: projectName,
});
logToFile(`[SNAPSHOT-WS] sent (${text.length} chars)`);
if (activeSessionId) {
(0, step_probe_1.writeRegistration)(activeSessionId);
if ((0, step_probe_1.getActiveSessionId)()) {
(0, step_probe_1.writeRegistration)((0, step_probe_1.getActiveSessionId)());
}
return;
}
// 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) {
(0, step_probe_1.writeRegistration)(activeSessionId);
}
}
catch (e) {
@@ -177,38 +145,17 @@ function writeChatSnapshot(text) {
}
function writeChatSnapshotWithFiles(text, files) {
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: (0, step_probe_1.getActiveSessionId)(),
project_name: projectName,
});
logToFile(`[SNAPSHOT-WS] sent with ${files.length} files (${text.length} chars)`);
if (activeSessionId) {
(0, step_probe_1.writeRegistration)(activeSessionId);
if ((0, step_probe_1.getActiveSessionId)()) {
(0, step_probe_1.writeRegistration)((0, step_probe_1.getActiveSessionId)());
}
return;
}
// 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) {
(0, step_probe_1.writeRegistration)(activeSessionId);
}
}
catch (e) {
@@ -405,7 +352,6 @@ async function activate(context) {
const config = vscode.workspace.getConfiguration('gravityBridge');
const configPath = config.get('bridgePath');
bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge');
ensureBridgeDir();
console.log(`Gravity Bridge: bridge path: ${bridgePath}`);
// ── WebSocket Hub Connection ──
const hubUrl = process.env.GRAVITY_HUB_URL || config.get('hubUrl') || '';
@@ -541,6 +487,7 @@ async function activate(context) {
get activeSessionId() { return (0, step_probe_1.getStepProbeContext)().activeSessionId; },
get sessionStalled() { return (0, step_probe_1.getStepProbeContext)().sessionStalled; },
get lastPendingStepIndex() { return (0, step_probe_1.getStepProbeContext)().lastPendingStepIndex; },
writeChatSnapshot,
};
const bridgePort = await (0, http_bridge_1.startHttpBridge)(httpBridgeCtx, sdk);
let localPort = bridgePort;

File diff suppressed because one or more lines are too long

View File

@@ -1,82 +1,380 @@
{
"name": "gravity-bridge",
"version": "0.5.25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gravity-bridge",
"version": "0.5.25",
"dependencies": {
"ws": "^8.19.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"engines": {
"vscode": "^1.100.0"
}
},
"node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz",
"integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
"name": "gravity-bridge",
"version": "0.5.34",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gravity-bridge",
"version": "0.5.34",
"dependencies": {
"cheerio": "^1.2.0",
"ws": "^8.19.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"engines": {
"vscode": "^1.100.0"
}
},
"node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz",
"integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==",
"dev": true,
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/cheerio": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"encoding-sniffer": "^0.2.1",
"htmlparser2": "^10.1.0",
"parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^7.19.0",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=20.18.1"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/htmlparser2": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"entities": "^7.0.1"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -2,7 +2,7 @@
"name": "gravity-bridge",
"displayName": "Gravity Bridge",
"description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.30",
"version": "0.5.36",
"publisher": "variet",
"engines": {
"vscode": "^1.100.0"
@@ -84,6 +84,7 @@
}
},
"dependencies": {
"cheerio": "^1.2.0",
"ws": "^8.19.0"
}
}
}

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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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(){});

View File

@@ -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();