Fix DOM observer regex/container bugs and add continuous AI Chat Body scraper via HTTP Bridge

This commit is contained in:
Variet Worker
2026-04-10 23:52:56 +09:00
parent b3825e1c8a
commit 5e697cd919
3 changed files with 211 additions and 103 deletions

View File

@@ -1,89 +1,89 @@
{
"name": "gravity-bridge",
"displayName": "Gravity Bridge",
"description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.28",
"publisher": "variet",
"engines": {
"vscode": "^1.100.0"
},
"categories": [
"Other",
"Chat"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./out/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./ \u0026\u0026 node -e \"const fs=require(\u0027fs\u0027),p=require(\u0027path\u0027);const s=p.join(\u0027src\u0027,\u0027sdk\u0027),d=p.join(\u0027out\u0027,\u0027sdk\u0027);if(fs.existsSync(s)){fs.mkdirSync(d,{recursive:true});fs.readdirSync(s).forEach(f=\u003efs.copyFileSync(p.join(s,f),p.join(d,f)));console.log(\u0027SDK copied to out/sdk\u0027)};\"",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"contributes": {
"chatParticipants": [
{
"id": "gravity-bridge.gravity",
"name": "gravity",
"fullName": "Gravity Bridge",
"description": "????ˆìФ? ë¦¬ë¥?Discordë¡??„송 + AI ?œì–´",
"isSticky": false
}
],
"commands": [
{
"command": "gravityBridge.start",
"title": "Gravity Bridge: Start"
},
{
"command": "gravityBridge.stop",
"title": "Gravity Bridge: Stop"
},
{
"command": "gravityBridge.approve",
"title": "Gravity Bridge: Approve Pending"
},
{
"command": "gravityBridge.reject",
"title": "Gravity Bridge: Reject Pending"
},
{
"command": "gravityBridge.connect",
"title": "Gravity Bridge: Connect Session"
}
],
"configuration": {
"title": "Gravity Bridge",
"properties": {
"gravityBridge.bridgePath": {
"type": "string",
"default": "",
"description": "Bridge ?”ë ‰? ë¦¬ 경로 (기본: ~/.gemini/antigravity/bridge)"
},
"gravityBridge.projectName": {
"type": "string",
"default": "",
"description": "?„로?<3F>트 ?´ë¦„ (기본: git remote ?ˆí<EFBFBD>¬ëª?"
},
"gravityBridge.hubUrl": {
"type": "string",
"default": "",
"description": "WebSocket Hub URL (?? wss://your-server.com/ws)"
},
"gravityBridge.registrationCode": {
"type": "string",
"default": "",
"description": "Hub ?±ë¡<EFBFBD> 코드 (?œë²„?<3F>서 발급)"
}
}
}
},
"dependencies": {
"ws": "^8.19.0"
}
"name": "gravity-bridge",
"displayName": "Gravity Bridge",
"description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.30",
"publisher": "variet",
"engines": {
"vscode": "^1.100.0"
},
"categories": [
"Other",
"Chat"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./out/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./ && node -e \"const fs=require('fs'),p=require('path');const s=p.join('src','sdk'),d=p.join('out','sdk');if(fs.existsSync(s)){fs.mkdirSync(d,{recursive:true});fs.readdirSync(s).forEach(f=>fs.copyFileSync(p.join(s,f),p.join(d,f)));console.log('SDK copied to out/sdk')};\"",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"contributes": {
"chatParticipants": [
{
"id": "gravity-bridge.gravity",
"name": "gravity",
"fullName": "Gravity Bridge",
"description": "?<EFBFBD>???<EFBFBD>스?<3F><EFBFBD>?Discord<72>??<3F> + AI ?<EFBFBD>",
"isSticky": false
}
],
"commands": [
{
"command": "gravityBridge.start",
"title": "Gravity Bridge: Start"
},
{
"command": "gravityBridge.stop",
"title": "Gravity Bridge: Stop"
},
{
"command": "gravityBridge.approve",
"title": "Gravity Bridge: Approve Pending"
},
{
"command": "gravityBridge.reject",
"title": "Gravity Bridge: Reject Pending"
},
{
"command": "gravityBridge.connect",
"title": "Gravity Bridge: Connect Session"
}
],
"configuration": {
"title": "Gravity Bridge",
"properties": {
"gravityBridge.bridgePath": {
"type": "string",
"default": "",
"description": "Bridge ?<EFBFBD>렉?<3F>리 경로 (기본: ~/.gemini/antigravity/bridge)"
},
"gravityBridge.projectName": {
"type": "string",
"default": "",
"description": "?<EFBFBD>로?<3F>트 ?<3F>름 (기본: git remote ?<EFBFBD><EFBFBD>?"
},
"gravityBridge.hubUrl": {
"type": "string",
"default": "",
"description": "WebSocket Hub URL (?? wss://your-server.com/ws)"
},
"gravityBridge.registrationCode": {
"type": "string",
"default": "",
"description": "Hub ?<EFBFBD>록 코드 (?<3F>버?<3F>서 발급)"
}
}
}
},
"dependencies": {
"ws": "^8.19.0"
}
}

View File

@@ -23,6 +23,7 @@ export interface HttpBridgeContext {
sessionStalled: boolean;
lastPendingStepIndex: number;
logToFile: (msg: string) => void;
writeChatSnapshot?: (text: string) => void;
}
// ─── Module-level state ───
@@ -106,6 +107,12 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
return;
}
// POST /chat — renderer posts chat snapshots directly
if (req.method === 'POST' && url.pathname === '/chat') {
_handleChatSnapshot(req, res, ctx);
return;
}
// POST /deep-inspect-result — renderer posts inspection results here
if (req.method === 'POST' && url.pathname === '/deep-inspect-result') {
_handleDeepInspectResult(req, res, ctx);
@@ -381,10 +388,8 @@ function _handleDeepInspectResult(req: any, res: any, ctx: HttpBridgeContext) {
const data = JSON.parse(body);
deepInspectResult = data;
ctx.logToFile(`[HTTP] deep-inspect result received (${body.length} bytes)`);
// Write to file for reference
const inspectFile = path.join(ctx.bridgePath, 'deep-inspect-result.json');
fs.writeFileSync(inspectFile, JSON.stringify(data, null, 2));
// Notify waiters
const waiters = [...deepInspectWaiters];
deepInspectWaiters = [];
waiters.forEach(w => w(data));
@@ -396,3 +401,22 @@ function _handleDeepInspectResult(req: any, res: any, ctx: HttpBridgeContext) {
}
});
}
function _handleChatSnapshot(req: any, res: any, ctx: HttpBridgeContext) {
let body = '';
req.on('data', (c: string) => body += c);
req.on('end', () => {
try {
const data = JSON.parse(body);
if (data.text && typeof ctx.writeChatSnapshot === 'function') {
ctx.writeChatSnapshot(`💬 **[DOM 추출] AI 응답**\n\n${data.text}`);
ctx.logToFile(`[HTTP] chat snapshot written (${data.text.length} chars)`);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
} catch (e: any) {
ctx.logToFile(`[HTTP] chat parse error: ${e.message}`);
res.writeHead(400); res.end(JSON.stringify({ error: e.message }));
}
});
}

View File

@@ -36,10 +36,14 @@ export function generateApprovalObserverScript(_port: number): string {
function cleanButtonText(btn) {
if (!btn) return '';
// if internal truncate span, use it
var tr = btn.querySelector('.truncate');
var txt = (tr ? tr.textContent : btn.textContent) || '';
return txt.trim().replace(/(Alt|Ctrl|Shift|Meta)\\\\+.*/i,'').trim();
var clone = btn.cloneNode(true);
var icons = clone.querySelectorAll('.google-symbols, .codicon');
for(var i=0; i<icons.length; i++) {
if(icons[i].parentNode) icons[i].parentNode.removeChild(icons[i]);
}
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();
}
// ── Stable button fingerprint ──
@@ -123,6 +127,35 @@ export function generateApprovalObserverScript(_port: number): string {
}
}
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);
@@ -133,15 +166,15 @@ export function generateApprovalObserverScript(_port: number): string {
return combined.trim();
}
// ── Action Buttons Patterns ──
// ── Action Buttons Patterns (EN / KO) ──
var PATS = [
{ type: 'command', re: /^(?:Always\\s*)?Run\\b/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 },
{ 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 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];
function collectSiblingButtons(container,triggerBtn){
if(!container)return [];
@@ -206,8 +239,59 @@ export function generateApprovalObserverScript(_port: number): string {
}).catch(function(e){});
});
var _chatSnapshots = [];
var _firstChatScan = true;
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();
} 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";
}
}
}
}
}
_firstChatScan = false;
}
function scan(){
if(!_ready)return;
scanChatBodies();
var now=Date.now();
var allBtns=document.querySelectorAll('button');
if(!allBtns.length)return;
@@ -392,8 +476,8 @@ 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 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 btns = document.querySelectorAll('button');