fix(extension): Retry auto-approve 흐름 복구 + Observer 형제 탐색 + thinking 필터링 (v0.5.79)
- WS response 파일에 _from_ws 마커 추가하여 processResponseFile 삭제 방지 - extractContextFromNearby에 sibling 탐색 추가 (AG Native DOM 구조 대응) - thinking 블록 (max-h-[200px]) 필터링으로 내부 사고 릴레이 차단 - DOM 탐색 depth 5→10 확대 + pre.font-mono 우선 탐색 - 사용자 메시지 셀렉터 (.select-text.rounded-lg) 추가
This commit is contained in:
@@ -397,6 +397,23 @@ async function activate(context) {
|
||||
return;
|
||||
}
|
||||
// Normal approval — tryApprovalStrategies
|
||||
// v22: ALSO write response file so Observer's pollResponseGroup can click
|
||||
// the correct button (with exact button_index). Without this, only the
|
||||
// imprecise pollTriggerClick fallback was used for WS-path responses.
|
||||
const responseDir = path.join(bridgePath, 'response');
|
||||
if (!fs.existsSync(responseDir)) {
|
||||
fs.mkdirSync(responseDir, { recursive: true });
|
||||
}
|
||||
const respPayload = {
|
||||
request_id: data.request_id,
|
||||
approved,
|
||||
button_index: data.button_index ?? 0,
|
||||
step_type: stepType,
|
||||
project_name: projectName,
|
||||
_from_ws: true,
|
||||
};
|
||||
fs.writeFileSync(path.join(responseDir, `${data.request_id}.json`), JSON.stringify(respPayload), 'utf-8');
|
||||
logToFile(`[WS-RESPONSE] Response file written for pollResponseGroup: ${data.request_id?.substring(0, 12)}`);
|
||||
const approvalCtx = (0, step_probe_1.getApprovalContext)();
|
||||
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
|
||||
(0, step_probe_1.tryApprovalStrategies)(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
|
||||
|
||||
File diff suppressed because one or more lines are too long
4
extension/package-lock.json
generated
4
extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gravity-bridge",
|
||||
"version": "0.5.63",
|
||||
"version": "0.5.79",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gravity-bridge",
|
||||
"version": "0.5.63",
|
||||
"version": "0.5.79",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.2.0",
|
||||
"ws": "^8.19.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gravity-bridge",
|
||||
"displayName": "Gravity Bridge",
|
||||
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
||||
"version": "0.5.63",
|
||||
"version": "0.5.79",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
|
||||
28
extension/scratch/analyze_dump.js
Normal file
28
extension/scratch/analyze_dump.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const dump = JSON.parse(fs.readFileSync(
|
||||
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
|
||||
));
|
||||
|
||||
const bodyStr = JSON.stringify(dump.body);
|
||||
|
||||
// Find all unique tag names
|
||||
const tagMatches = bodyStr.match(/"tag":"[a-z0-9]+"/g) || [];
|
||||
const uniqueTags = [...new Set(tagMatches)];
|
||||
console.log('=== Unique DOM tags ===');
|
||||
console.log(uniqueTags.sort().join('\n'));
|
||||
|
||||
// Check for pipe characters (markdown table syntax)
|
||||
console.log('\n=== Pipe | in text content ===');
|
||||
const pipeMatches = [...bodyStr.matchAll(/"text":"[^"]*\|[^"]*"/g)];
|
||||
console.log(`Found ${pipeMatches.length} text nodes with pipe |`);
|
||||
pipeMatches.slice(0, 5).forEach(m => console.log(' ', m[0].substring(0, 120)));
|
||||
|
||||
// Check for table-related class names
|
||||
console.log('\n=== Table-related classes ===');
|
||||
const classMatches = bodyStr.match(/"cls":"[^"]*"/g) || [];
|
||||
const tableClasses = classMatches.filter(c => /table|grid|cell|col|row/i.test(c));
|
||||
console.log(`Found ${tableClasses.length} table-related classes`);
|
||||
[...new Set(tableClasses)].slice(0, 10).forEach(c => console.log(' ', c));
|
||||
37
extension/scratch/discord_channels.js
Normal file
37
extension/scratch/discord_channels.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// List all channels in the guild
|
||||
const https = require('https');
|
||||
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
|
||||
const GUILD_ID = '1478722210460991662';
|
||||
|
||||
function apiGet(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {
|
||||
hostname: 'discord.com',
|
||||
path: `/api/v10${path}`,
|
||||
headers: { 'Authorization': `Bot ${TOKEN}` }
|
||||
};
|
||||
https.get(opts, res => {
|
||||
let data = '';
|
||||
res.on('data', c => data += c);
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const channels = await apiGet(`/guilds/${GUILD_ID}/channels`);
|
||||
if (!Array.isArray(channels)) {
|
||||
console.log('Error:', channels);
|
||||
return;
|
||||
}
|
||||
console.log(`Total channels: ${channels.length}\n`);
|
||||
channels.sort((a,b) => (a.position||0) - (b.position||0));
|
||||
channels.forEach(c => {
|
||||
const type = ['TEXT','DM','VOICE','GROUP_DM','CATEGORY','ANNOUNCE','','','','','','THREAD','THREAD','THREAD','','FORUM','MEDIA'][c.type] || c.type;
|
||||
console.log(`${c.id} | ${type.padEnd(10)} | #${c.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(e => console.error(e));
|
||||
55
extension/scratch/discord_read.js
Normal file
55
extension/scratch/discord_read.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// Read latest Discord messages from ag-gravity_control channel
|
||||
const https = require('https');
|
||||
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
|
||||
const CHANNEL_ID = '1483082084540223663';
|
||||
|
||||
function apiGet(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {
|
||||
hostname: 'discord.com',
|
||||
path: `/api/v10${path}`,
|
||||
headers: { 'Authorization': `Bot ${TOKEN}` }
|
||||
};
|
||||
https.get(opts, res => {
|
||||
let data = '';
|
||||
res.on('data', c => data += c);
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const limit = process.argv[2] || 15;
|
||||
const msgs = await apiGet(`/channels/${CHANNEL_ID}/messages?limit=${limit}`);
|
||||
if (!Array.isArray(msgs)) {
|
||||
console.log('Error:', JSON.stringify(msgs));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`=== #ag-gravity_control — Last ${msgs.length} messages ===\n`);
|
||||
|
||||
msgs.reverse().forEach(m => {
|
||||
const time = new Date(m.timestamp).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
const author = m.author?.username || '?';
|
||||
|
||||
if (m.embeds?.length > 0) {
|
||||
m.embeds.forEach(e => {
|
||||
const title = e.title || '(no title)';
|
||||
const desc = (e.description || '').substring(0, 300);
|
||||
const colorHex = e.color ? `#${e.color.toString(16).padStart(6, '0')}` : 'default';
|
||||
const footer = e.footer?.text || '';
|
||||
console.log(`[${time}] 📦 EMBED [${colorHex}] ${title}`);
|
||||
if (desc) console.log(` ${desc.replace(/\n/g, '\n ')}`);
|
||||
if (footer) console.log(` 📎 ${footer}`);
|
||||
});
|
||||
} else if (m.content) {
|
||||
const content = m.content.substring(0, 300);
|
||||
console.log(`[${time}] 💬 ${author}: ${content}`);
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(e => console.error(e));
|
||||
29
extension/scratch/find_user_msg.js
Normal file
29
extension/scratch/find_user_msg.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const d = JSON.parse(fs.readFileSync(
|
||||
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
|
||||
));
|
||||
const s = JSON.stringify(d.body);
|
||||
|
||||
console.log('title:', d.quickInfo.title);
|
||||
console.log('Has id=conversation:', s.includes('"id":"conversation"'));
|
||||
console.log('Has agent-side-panel:', s.includes('antigravity-agent-side-panel'));
|
||||
|
||||
// Find message-block patterns
|
||||
const mb = [...s.matchAll(/message-block/g)];
|
||||
console.log('message-block occurrences:', mb.length);
|
||||
|
||||
// Find user-related class patterns
|
||||
const userPatterns = ['user-color', 'user-background', 'user-message', 'user-query', 'user-input', 'human'];
|
||||
userPatterns.forEach(p => {
|
||||
const cnt = [...s.matchAll(new RegExp(p, 'gi'))].length;
|
||||
if (cnt > 0) console.log(` ${p}: ${cnt} occurrences`);
|
||||
});
|
||||
|
||||
// Show all unique classes that include 'message' or 'chat' or 'conversation'
|
||||
const clsMatches = [...s.matchAll(/"cls":"([^"]*(?:message|chat|conversation|query|user|human)[^"]*)"/gi)];
|
||||
console.log('\nClasses with message/chat/conversation/user/human:');
|
||||
const uniq = [...new Set(clsMatches.map(m => m[1]))];
|
||||
uniq.forEach(c => console.log(' ', c.substring(0, 120)));
|
||||
@@ -205,6 +205,17 @@ async function processResponseFile(filePath: string) {
|
||||
}
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const resp = JSON.parse(content);
|
||||
|
||||
// v22: Skip files written by the WS response handler (extension.ts onResponse).
|
||||
// Those files exist ONLY for Observer's pollResponseGroup to read via HTTP.
|
||||
// The WS handler already calls tryApprovalStrategies, so processing here is redundant.
|
||||
// Without this skip, the watcher deletes the file before Observer can poll it
|
||||
// (since no pending file exists for the isDomObserver check).
|
||||
if (resp._from_ws) {
|
||||
ctx.logToFile(`[RESPONSE] SKIP _from_ws file (for Observer pollResponseGroup): ${resp.request_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved} step_type=${resp.step_type || '(missing)'} keys=[${Object.keys(resp).join(',')}]`;
|
||||
console.log(`Gravity Bridge: ${msg}`);
|
||||
ctx.logToFile(msg);
|
||||
|
||||
@@ -388,6 +388,28 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
// Normal approval — tryApprovalStrategies
|
||||
// v22: ALSO write response file so Observer's pollResponseGroup can click
|
||||
// the correct button (with exact button_index). Without this, only the
|
||||
// imprecise pollTriggerClick fallback was used for WS-path responses.
|
||||
const responseDir = path.join(bridgePath, 'response');
|
||||
if (!fs.existsSync(responseDir)) {
|
||||
fs.mkdirSync(responseDir, { recursive: true });
|
||||
}
|
||||
const respPayload = {
|
||||
request_id: data.request_id,
|
||||
approved,
|
||||
button_index: data.button_index ?? 0,
|
||||
step_type: stepType,
|
||||
project_name: projectName,
|
||||
_from_ws: true,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(responseDir, `${data.request_id}.json`),
|
||||
JSON.stringify(respPayload),
|
||||
'utf-8'
|
||||
);
|
||||
logToFile(`[WS-RESPONSE] Response file written for pollResponseGroup: ${data.request_id?.substring(0, 12)}`);
|
||||
|
||||
const approvalCtx = getApprovalContext();
|
||||
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
|
||||
tryApprovalStrategies(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
|
||||
|
||||
@@ -298,7 +298,8 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
if (alwaysRunDetected) {
|
||||
// Try enrichment for better Discord display text
|
||||
let displayCmd = rawCmd;
|
||||
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
|
||||
ctx.logToFile(`[HTTP] AUTO-APPROVE raw: cmd="${rawCmd}" desc="${rawDesc.substring(0, 120)}" buttons=${JSON.stringify((data.buttons || []).map((b: any) => b.text)).substring(0, 200)}`);
|
||||
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 3 && rawDesc !== rawCmd) {
|
||||
const promptMatch = rawDesc.match(/[>»]\s*(.+)/);
|
||||
if (promptMatch && promptMatch[1].trim().length > 3) {
|
||||
displayCmd = promptMatch[1].trim().substring(0, 200);
|
||||
@@ -311,6 +312,28 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// v20: Final fallback — read latest Step Probe pending file for actual command
|
||||
if (displayCmd === rawCmd && GENERIC_BTN_RE.test(displayCmd)) {
|
||||
try {
|
||||
const pendingDir = path.join(ctx.bridgePath, 'pending');
|
||||
if (fs.existsSync(pendingDir)) {
|
||||
const pFiles = fs.readdirSync(pendingDir)
|
||||
.filter((f: string) => f.endsWith('.json'))
|
||||
.map((f: string) => ({ name: f, time: fs.statSync(path.join(pendingDir, f)).mtimeMs }))
|
||||
.sort((a: any, b: any) => b.time - a.time);
|
||||
if (pFiles.length > 0 && (Date.now() - pFiles[0].time) < 30_000) {
|
||||
const pData = JSON.parse(fs.readFileSync(path.join(pendingDir, pFiles[0].name), 'utf-8'));
|
||||
if (pData.command && pData.command.length > 3 && !GENERIC_BTN_RE.test(pData.command)) {
|
||||
displayCmd = pData.command.substring(0, 200);
|
||||
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending file: "${displayCmd.substring(0, 80)}"`);
|
||||
} else if (pData.description && pData.description.length > 5) {
|
||||
displayCmd = pData.description.substring(0, 200);
|
||||
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending desc: "${displayCmd.substring(0, 80)}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) { ctx.logToFile(`[HTTP] AUTO-APPROVE pending lookup error: ${e.message}`); }
|
||||
}
|
||||
const rid = data.request_id || Date.now().toString();
|
||||
ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunBtnIndex}): cmd="${displayCmd.substring(0, 80)}"`);
|
||||
// Write response file so observer's pollResponseGroup picks it up and clicks the button
|
||||
|
||||
@@ -12,7 +12,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
function log(m){
|
||||
console.log('[GB Observer] '+m);
|
||||
// v19: Relay important logs to extension via HTTP so they appear in extension.log
|
||||
if (BASE && (m.indexOf('CV-CLASSES')!==-1 || m.indexOf('Conversation view')!==-1 || m.indexOf('BEACON')!==-1 || m.indexOf('ERROR')!==-1 || m.indexOf('chat relay')!==-1 || m.indexOf('user-cls')!==-1)) {
|
||||
if (BASE && (m.indexOf('CV-CLASSES')!==-1 || m.indexOf('CV-CHILDREN')!==-1 || m.indexOf('child[')!==-1 || m.indexOf('CV found')!==-1 || m.indexOf('Conversation view')!==-1 || m.indexOf('BEACON')!==-1 || m.indexOf('ERROR')!==-1 || m.indexOf('chat relay')!==-1 || m.indexOf('user-cls')!==-1)) {
|
||||
try { fetch(BASE+'/log', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({msg:m.substring(0,2000)})}); } catch(e){}
|
||||
}
|
||||
}
|
||||
@@ -144,9 +144,11 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
var _bestCodeHeader = '';
|
||||
var _sawCodeEls = false;
|
||||
var _allSkipped = true;
|
||||
for (var depth = 0; depth < 5 && node; depth++) {
|
||||
// v22: Increased from 5 to 10 — AG Native command display (SRi) can be many levels up
|
||||
for (var depth = 0; depth < 10 && node; depth++) {
|
||||
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
|
||||
var codeEls = node.querySelectorAll('pre, code, [class*="terminal"]');
|
||||
// v22: Prioritize pre.font-mono (AG Native command line display from SRi component)
|
||||
var codeEls = node.querySelectorAll('pre.font-mono, pre, code, [class*="terminal"]');
|
||||
_debugTrail.push('d'+depth+':tag='+((node.tagName||'?').toLowerCase())+',cls='+(((typeof node.className==='string')?node.className:'').substring(0,60))+',codeEls='+codeEls.length);
|
||||
for (var ci = 0; ci < codeEls.length; ci++) {
|
||||
var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500));
|
||||
@@ -187,6 +189,33 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
_lastContextDebug = _debugTrail.join(' > ');
|
||||
return parts.join(' \u2014 ');
|
||||
}
|
||||
// v23: Also search sibling elements at each level
|
||||
// AG Native's command display (pre.font-mono) is a SIBLING of footer, not ancestor
|
||||
if (node && node.parentElement) {
|
||||
var siblings = node.parentElement.children;
|
||||
for (var si = 0; si < siblings.length; si++) {
|
||||
if (siblings[si] === node) continue;
|
||||
if (!siblings[si].querySelector) continue;
|
||||
var sibCodeEls = siblings[si].querySelectorAll('pre.font-mono, pre, code');
|
||||
for (var sci = 0; sci < sibCodeEls.length; sci++) {
|
||||
var sibCode = cleanLines((sibCodeEls[sci].textContent || '').trim().substring(0, 500));
|
||||
if (!sibCode || sibCode.length <= 5) continue;
|
||||
if (JUNK_CODE_RE.test(sibCode) || ICON_GLUE_RE.test(sibCode)) continue;
|
||||
if (PROMPT_ONLY_RE.test(sibCode.trim())) continue;
|
||||
_debugTrail.push('sibling_d'+depth+':tag='+siblings[si].tagName.toLowerCase()+',code='+sibCode.substring(0,40));
|
||||
_bestCodeText = sibCode;
|
||||
_allSkipped = false;
|
||||
// Found in sibling — return immediately
|
||||
var sibParts = [];
|
||||
var sibHdr = siblings[si].querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]');
|
||||
if (sibHdr) sibParts.push(cleanLines((sibHdr.textContent || '').trim().substring(0, 200)));
|
||||
sibParts.push(sibCode);
|
||||
log('CONTEXT-OK d='+depth+' src=sibling trail='+_debugTrail.join(' > '));
|
||||
_lastContextDebug = _debugTrail.join(' > ');
|
||||
return sibParts.join(' \u2014 ');
|
||||
}
|
||||
}
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
if (_sawCodeEls && _allSkipped) {
|
||||
@@ -345,7 +374,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
|
||||
var _dumpCount=0;
|
||||
var MAX_DUMPS=8;
|
||||
var _conversationDumped=false;
|
||||
var _conversationDumpCount=0;
|
||||
|
||||
function walkNode(el, depth, maxDepth, maxChildren) {
|
||||
if (depth > maxDepth) return {tag:'…',text:'depth limit'};
|
||||
@@ -725,12 +754,28 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
if (!cv) {
|
||||
cv = document.querySelector('.antigravity-agent-side-panel');
|
||||
}
|
||||
|
||||
// v19: Fallback — find conversation by tracing from known content elements
|
||||
if (!cv) {
|
||||
var probe = document.querySelector('.leading-relaxed.select-text') || document.querySelector('.text-ide-message-block-bot-color');
|
||||
if (probe) {
|
||||
// Walk up to find a reasonable container (has overflow-y or is big enough)
|
||||
var p = probe.parentElement;
|
||||
for (var pi2 = 0; pi2 < 10 && p && p !== document.body; pi2++) {
|
||||
var pCls = (typeof p.className === 'string') ? p.className : '';
|
||||
if (pCls.indexOf('overflow') !== -1 || p.children.length > 3) {
|
||||
cv = p;
|
||||
break;
|
||||
}
|
||||
p = p.parentElement;
|
||||
}
|
||||
if (!cv && probe.parentElement) cv = probe.parentElement.parentElement || probe.parentElement;
|
||||
}
|
||||
}
|
||||
if (cv) {
|
||||
// v19: Trigger DOM dump when conversation view is first found (prev dumps may have captured Settings tab)
|
||||
if (!_conversationDumped) {
|
||||
_conversationDumped = true;
|
||||
log('Conversation view found — triggering chat-context DOM dump');
|
||||
// v20: Dump CV structure for first 3 scans to ensure we capture it (even with stale HTML cache)
|
||||
if (_conversationDumpCount < 3) {
|
||||
_conversationDumpCount++;
|
||||
log('CV found via: ' + (cv.id || (typeof cv.className === 'string' ? cv.className : cv.tagName) || 'unknown').substring(0, 100));
|
||||
// Log all unique class names under #conversation for selector discovery
|
||||
var allCvEls = cv.querySelectorAll('*');
|
||||
var clsSet = {};
|
||||
@@ -745,26 +790,67 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
}
|
||||
var clsList = Object.keys(clsSet).sort().join(', ');
|
||||
log('CV-CLASSES (' + Object.keys(clsSet).length + '): ' + clsList.substring(0, 1500));
|
||||
// v19: Log direct children to discover message block structure
|
||||
var cvKids = cv.children;
|
||||
log('CV-CHILDREN (' + cvKids.length + '):');
|
||||
for (var ck = 0; ck < Math.min(cvKids.length, 15); ck++) {
|
||||
var kid = cvKids[ck];
|
||||
var kidCls = (typeof kid.className === 'string') ? kid.className : '';
|
||||
var kidText = (kid.textContent || '').trim().substring(0, 60);
|
||||
log(' child[' + ck + '] tag=' + kid.tagName + ' cls=' + kidCls.substring(0, 120) + ' text=' + kidText);
|
||||
}
|
||||
// v22: Deep-dive into gap-8 container to find individual message blocks
|
||||
var msgContainer = cv.querySelector('.gap-8') || cv.children[0];
|
||||
if (msgContainer) {
|
||||
var msgKids = msgContainer.children;
|
||||
log('MSG-BLOCKS (' + msgKids.length + '):');
|
||||
for (var mk = 0; mk < Math.min(msgKids.length, 30); mk++) {
|
||||
var mb = msgKids[mk];
|
||||
var mbCls = (typeof mb.className === 'string') ? mb.className : '';
|
||||
var mbText = (mb.textContent || '').trim().substring(0, 80);
|
||||
var hasLeadingRelaxed = mb.querySelector('.leading-relaxed.select-text') ? 'Y' : 'N';
|
||||
var firstChildCls = (mb.children[0] && typeof mb.children[0].className === 'string') ? mb.children[0].className : '';
|
||||
log(' msg[' + mk + '] cls=' + mbCls.substring(0, 120) + ' lr=' + hasLeadingRelaxed + ' fc=' + firstChildCls.substring(0, 80) + ' text=' + mbText.substring(0, 60));
|
||||
}
|
||||
}
|
||||
// Force a dump with conversation context
|
||||
dumpDOMStructure();
|
||||
}
|
||||
|
||||
// AG Native path: find AI and User response blocks by class pattern
|
||||
// v19: Also look for common AG Native message container patterns
|
||||
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, .text-ide-message-block-user-color, .text-ide-message-block-bot-color, .bg-ide-message-block-user-background, [data-message-role="user"], [data-role="user"]');
|
||||
// v22: AI response = .leading-relaxed.select-text, User message = .select-text.rounded-lg (Esn component, msn class)
|
||||
// Source: jetskiAgent/main.js — msn="bg-gray-500/10 border border-gray-500/20 p-2 rounded-lg w-full text-sm select-text"
|
||||
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, .select-text.rounded-lg');
|
||||
|
||||
if (responseBlocks.length > 0) {
|
||||
// Process the LAST (most recent) response block
|
||||
var lastBlock = responseBlocks[responseBlocks.length - 1];
|
||||
// v22: Filter out thinking/reasoning blocks — they have ancestor with max-h-[200px]
|
||||
// These are internal AI reasoning and should NOT be relayed to Discord
|
||||
var filteredBlocks = [];
|
||||
for (var fbi = 0; fbi < responseBlocks.length; fbi++) {
|
||||
var isThinking = false;
|
||||
var ancestor = responseBlocks[fbi].parentElement;
|
||||
for (var depth = 0; ancestor && depth < 5; depth++) {
|
||||
var aCls = (typeof ancestor.className === 'string') ? ancestor.className : '';
|
||||
if (aCls.indexOf('max-h-[200px]') !== -1 || aCls.indexOf('max-h-[150px]') !== -1) {
|
||||
isThinking = true;
|
||||
break;
|
||||
}
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
if (!isThinking) filteredBlocks.push(responseBlocks[fbi]);
|
||||
}
|
||||
if (filteredBlocks.length === 0) return;
|
||||
|
||||
// Process the LAST (most recent) non-thinking response block
|
||||
var lastBlock = filteredBlocks[filteredBlocks.length - 1];
|
||||
|
||||
// Skip if already scraped
|
||||
if (lastBlock.dataset.agChatScraped === 'true' || lastBlock.dataset.agChatScraped === 'pending') {
|
||||
// Check for NEW blocks since last scrape
|
||||
if (responseBlocks.length > _lastResponseBlockCount) {
|
||||
if (filteredBlocks.length > _lastResponseBlockCount) {
|
||||
// New block appeared — process it
|
||||
for (var rbi = responseBlocks.length - 1; rbi >= 0; rbi--) {
|
||||
if (responseBlocks[rbi].dataset.agChatScraped !== 'true' && responseBlocks[rbi].dataset.agChatScraped !== 'pending') {
|
||||
lastBlock = responseBlocks[rbi];
|
||||
for (var rbi = filteredBlocks.length - 1; rbi >= 0; rbi--) {
|
||||
if (filteredBlocks[rbi].dataset.agChatScraped !== 'true' && filteredBlocks[rbi].dataset.agChatScraped !== 'pending') {
|
||||
lastBlock = filteredBlocks[rbi];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -781,7 +867,8 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
var parentCls = lastBlock.parentElement ? ((typeof lastBlock.parentElement.className === 'string') ? lastBlock.parentElement.className : '') : '';
|
||||
var grandCls = (lastBlock.parentElement && lastBlock.parentElement.parentElement) ? ((typeof lastBlock.parentElement.parentElement.className === 'string') ? lastBlock.parentElement.parentElement.className : '') : '';
|
||||
log('user-cls-debug block=' + clsStr.substring(0, 150) + ' | parent=' + parentCls.substring(0, 150) + ' | grand=' + grandCls.substring(0, 150) + ' | text=' + (blockText||'').substring(0, 50));
|
||||
var isUser = clsStr.indexOf('user-color') !== -1 || clsStr.indexOf('user-background') !== -1 || clsStr.indexOf('user-message') !== -1;
|
||||
// v22: Detect user message: has select-text + rounded-lg but NOT leading-relaxed
|
||||
var isUser = (clsStr.indexOf('rounded-lg') !== -1 && clsStr.indexOf('leading-relaxed') === -1) || clsStr.indexOf('user-color') !== -1;
|
||||
var role = isUser ? 'user' : 'bot';
|
||||
|
||||
// Bot messages often start empty and stream in. User messages are usually immediate.
|
||||
@@ -809,12 +896,14 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
var waitTime = isUser ? 500 : 3000;
|
||||
if (Date.now() - _lastStepTextTime < waitTime) return; // Still waiting
|
||||
|
||||
// Content is stable — send it
|
||||
// v21: DOM-based chat relay RE-ENABLED — GetCascadeTrajectorySteps does NOT
|
||||
// return steps for in-progress cascades, making Step Probe RT-CAPTURE useless.
|
||||
// Observer DOM extraction is the ONLY real-time path for AI response relay.
|
||||
_lastStepTextSent = true;
|
||||
_lastResponseBlockCount = responseBlocks.length;
|
||||
_lastResponseBlockCount = filteredBlocks.length;
|
||||
lastBlock.dataset.agChatScraped = 'pending';
|
||||
|
||||
log('AG-Native chat relay [' + role + ']: blocks=' + responseBlocks.length + ' text=' + blockText.length + ' chars');
|
||||
log('AG-Native chat relay [' + role + ']: blocks=' + filteredBlocks.length + ' text=' + blockText.length + ' chars');
|
||||
(function(el, txt, count, r) {
|
||||
fetch(BASE + '/chat', {
|
||||
method: 'POST',
|
||||
@@ -822,7 +911,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
body: JSON.stringify({ text: txt, source: 'ag_native_block_' + count, block_index: count, role: r })
|
||||
}).then(function() { el.dataset.agChatScraped = 'true'; log('AG-Native chat sent OK'); })
|
||||
.catch(function(e) { el.dataset.agChatScraped = 'false'; log('AG-Native chat send error: ' + e.message); });
|
||||
})(lastBlock, blockText, responseBlocks.length, role);
|
||||
})(lastBlock, blockText, filteredBlocks.length, role);
|
||||
}
|
||||
return; // AG Native path handled — don't fall through to Cascade path
|
||||
}
|
||||
|
||||
@@ -453,28 +453,70 @@ function setupMonitor() {
|
||||
|
||||
// ── v15: Heartbeat probe — detect step changes when summary API is stale ──
|
||||
// GetAllCascadeTrajectories can return frozen stepCount/lastModifiedTime,
|
||||
// preventing delta>0 from ever firing. Every 10 polls (~50s), directly
|
||||
// probe GetCascadeTrajectorySteps to get the REAL latest step count.
|
||||
if (delta === 0 && ctx.sdk && ctx.activeSessionId && pollCount % 10 === 0) {
|
||||
// v20: Heartbeat every 3 polls (~15s) — AG API never reports delta for active sessions
|
||||
if (delta === 0 && ctx.sdk && ctx.activeSessionId && pollCount % 3 === 0) {
|
||||
try {
|
||||
const hbOffset = Math.max(0, currentCount - 1);
|
||||
const hbOffset = Math.max(0, currentCount - 5);
|
||||
const hbResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||
cascadeId: ctx.activeSessionId,
|
||||
stepOffset: hbOffset,
|
||||
verbosity: 0, // minimal — just need step count
|
||||
verbosity: 1, // need content for capture
|
||||
});
|
||||
if (hbResp?.steps?.length > 0) {
|
||||
const realStepCount = hbOffset + hbResp.steps.length;
|
||||
if (realStepCount > lastKnownStepCount) {
|
||||
ctx.logToFile(`[HEARTBEAT] summary stale! reported=${lastKnownStepCount} real=${realStepCount} — correcting`);
|
||||
ctx.logToFile(`[HEARTBEAT] stale! reported=${lastKnownStepCount} real=${realStepCount}`);
|
||||
// Process new steps for RT-CAPTURE
|
||||
for (let hi = 0; hi < hbResp.steps.length; hi++) {
|
||||
const hs = hbResp.steps[hi];
|
||||
const hIdx = hbOffset + hi;
|
||||
if (hIdx <= lastResponseCaptureStep) continue;
|
||||
const hType = hs?.type || '';
|
||||
// Capture AI responses
|
||||
if (hType.includes('PLANNER_RESPONSE') && hs?.status?.includes('DONE')) {
|
||||
let text = extractPlannerText(hs) || '';
|
||||
if (text.length > 10) {
|
||||
lastResponseCaptureStep = hIdx;
|
||||
ctx.logToFile(`[HB-CAPTURE] AI step=${hIdx} (${text.length} chars)`);
|
||||
const truncated = text.length > 3500
|
||||
? text.substring(0, 3500) + '\n\n_(이하 생략)_'
|
||||
: text;
|
||||
ctx.writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||
}
|
||||
}
|
||||
// Capture user messages
|
||||
if (hType.includes('USER_INPUT') && hIdx > lastUserInputStepIdx) {
|
||||
lastUserInputStepIdx = hIdx;
|
||||
const ui = hs?.userInput;
|
||||
const umText = (ui?.userResponse || ui?.text || '').trim();
|
||||
if (umText.length > 2) {
|
||||
const sentAt = ctx.recentDiscordSentTexts.get(umText);
|
||||
if (!sentAt || (Date.now() - sentAt) > 60_000) {
|
||||
const dedupKey = `user_msg:${umText}`;
|
||||
const lastRelayed = lastSnapshotText.get(dedupKey);
|
||||
if (!lastRelayed || (Date.now() - Number(lastRelayed)) > 30_000) {
|
||||
lastSnapshotText.set(dedupKey, String(Date.now()));
|
||||
const clientType = ui?.clientType || '';
|
||||
const source = clientType.includes('IDE') ? 'AG 직접 입력' : 'API';
|
||||
const truncated = umText.length > 800
|
||||
? umText.substring(0, 800) + '\n\n_(이하 생략)_'
|
||||
: umText;
|
||||
ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`);
|
||||
ctx.logToFile(`[HB-CAPTURE] User step=${hIdx} (${umText.length} chars)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lastKnownStepCount = realStepCount;
|
||||
// Trigger RT-CAPTURE by re-entering delta>0 path below
|
||||
// We set currentCount so delta recalculation works
|
||||
} else if (pollCount % 30 === 0) {
|
||||
ctx.logToFile(`[HEARTBEAT] ok offset=${hbOffset} got=${hbResp.steps.length} real=${realStepCount} known=${lastKnownStepCount}`);
|
||||
}
|
||||
} else if (pollCount % 30 === 0) {
|
||||
ctx.logToFile(`[HEARTBEAT] no steps returned for offset=${hbOffset}`);
|
||||
}
|
||||
} catch (hbErr: any) {
|
||||
// Non-critical — will retry next heartbeat
|
||||
if (pollCount <= 30) ctx.logToFile(`[HEARTBEAT] probe error: ${hbErr.message?.substring(0, 60)}`);
|
||||
if (pollCount % 10 === 0) ctx.logToFile(`[HEARTBEAT] probe error: ${hbErr.message?.substring(0, 100)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +570,38 @@ function setupMonitor() {
|
||||
}
|
||||
}
|
||||
|
||||
// v20: Capture USER_INPUT steps for user message relay
|
||||
if (sType.includes('USER_INPUT') && actualIdx > lastUserInputStepIdx) {
|
||||
lastUserInputStepIdx = actualIdx;
|
||||
const ui = s?.userInput;
|
||||
const umText = (ui?.userResponse || ui?.text || s?.plannerResponse?.textContent || '').trim();
|
||||
const clientType = ui?.clientType || '';
|
||||
const isFromIDE = clientType.includes('IDE');
|
||||
ctx.logToFile(`[RT-USER-MSG] step=${actualIdx} client=${clientType} text=${umText.substring(0, 100)}`);
|
||||
|
||||
if (umText.length > 2) {
|
||||
// Skip echo: if text was recently sent from Discord
|
||||
const sentAt = ctx.recentDiscordSentTexts.get(umText);
|
||||
if (sentAt && (Date.now() - sentAt) < 60_000) {
|
||||
ctx.recentDiscordSentTexts.delete(umText);
|
||||
ctx.logToFile(`[RT-USER-MSG] skipped echo relay (Discord origin)`);
|
||||
} else {
|
||||
// Content-based dedup
|
||||
const dedupKey = `user_msg:${umText}`;
|
||||
const lastRelayed = lastSnapshotText.get(dedupKey);
|
||||
if (!lastRelayed || (Date.now() - Number(lastRelayed)) > 30_000) {
|
||||
lastSnapshotText.set(dedupKey, String(Date.now()));
|
||||
const truncated = umText.length > 800
|
||||
? umText.substring(0, 800) + '\n\n_(이하 생략)_'
|
||||
: umText;
|
||||
const source = isFromIDE ? 'AG 직접 입력' : 'API';
|
||||
ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`);
|
||||
ctx.logToFile(`[RT-USER-MSG] relayed ${umText.length} chars`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s?.status === 'CORTEX_STEP_STATUS_WAITING') {
|
||||
const toolCall = s?.metadata?.toolCall;
|
||||
const toolName = toolCall?.name || (sType || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user