feat: proto-based RPC approval for Run commands via Discord
Decoded HandleCascadeUserInteractionRequest protobuf schema from AG's extension.js (message #162, base64 FileDescriptor 78KB). Working payload (variant PROTO-0): cascadeId + interaction.{trajectoryId, stepIndex, runCommand.confirm} Changes: - extension.ts: Added Strategy 0-PROTO with decoded proto RPC call - extension.ts: Fixed processResponseFile to call tryApprovalStrategies() instead of direct clickTrigger (was bypassing all strategies) - extension.ts: Fixed false positive Run detection (sessionStalled reset when step_probe confirms no WAITING) - extension.ts: Moved lastPendingStepIndex to module scope - extension.ts: Added activeTrajectoryId tracking from session init - bot.py: Added MERGE detection + Discord message edit for command updates - bot.py: Added _sent_commands tracking for merge detection Proto RE methodology: 1. Found schema exports in AG extension.js 2. Located fileDesc() with base64 protobuf descriptor 3. Decoded 58KB raw proto, found message names 4. Extracted CascadeRunCommandInteraction.confirm field 5. Tested camelCase JSON via ConnectRPC = SUCCESS
This commit is contained in:
49
bot.py
49
bot.py
@@ -165,6 +165,7 @@ class GravityBot(commands.Bot):
|
||||
self.session_status_messages: dict[str, int] = {} # conv_id → msg_id
|
||||
self._sent_approval_ids: set[str] = set()
|
||||
self._deferred_ids: dict[str, int] = {} # request_id → defer count
|
||||
self._sent_commands: dict[str, str] = {} # request_id → command text (for MERGE edit detection)
|
||||
self._ready_event = asyncio.Event()
|
||||
self._channel_lock = asyncio.Lock()
|
||||
self.bridge = BridgeProtocol()
|
||||
@@ -542,6 +543,7 @@ class GravityBot(commands.Bot):
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
self._sent_approval_ids.add(req.request_id)
|
||||
self._sent_commands[req.request_id] = req.command
|
||||
await self._send_approval_request(channel, req)
|
||||
|
||||
# ── Check for auto_resolved pendings (approved directly in AG) ──
|
||||
@@ -567,6 +569,53 @@ class GravityBot(commands.Bot):
|
||||
pass
|
||||
f.unlink()
|
||||
self._deferred_ids.pop(data.get("request_id", ""), None)
|
||||
self._sent_commands.pop(data.get("request_id", ""), None)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
# ── Check for MERGE updates (step_probe updated command in already-sent pending) ──
|
||||
for f in self.bridge.pending_dir.glob("*.json"):
|
||||
try:
|
||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||
rid = data.get("request_id", "")
|
||||
if rid not in self._sent_approval_ids:
|
||||
continue
|
||||
if data.get("status") != "pending":
|
||||
continue
|
||||
msg_id = data.get("discord_message_id", 0)
|
||||
if not msg_id:
|
||||
continue
|
||||
# Check if command was updated via MERGE
|
||||
new_cmd = data.get("command", "")
|
||||
old_cmd = self._sent_commands.get(rid, "")
|
||||
if new_cmd and new_cmd != old_cmd and len(new_cmd) > len(old_cmd):
|
||||
# MERGE detected — edit Discord message
|
||||
self._sent_commands[rid] = new_cmd
|
||||
project = data.get("project_name", Config.PROJECT_NAME)
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
try:
|
||||
msg = await channel.fetch_message(msg_id)
|
||||
# Rebuild embed with full command
|
||||
buttons = data.get("buttons")
|
||||
desc_parts = [f"**명령어:**\n```\n{new_cmd[:1000]}\n```"]
|
||||
if buttons and len(buttons) > 1:
|
||||
btn_names = [b.get("text", "?") for b in buttons]
|
||||
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
|
||||
desc = data.get("description", "")
|
||||
if desc:
|
||||
desc_parts.append(desc[:500])
|
||||
embed = discord.Embed(
|
||||
title="⚠️ 승인 요청",
|
||||
description="\n".join(desc_parts),
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"ID: {rid}")
|
||||
await msg.edit(embed=embed)
|
||||
logger.info(f"MERGE edit: {rid[:12]} cmd='{new_cmd[:60]}'")
|
||||
except discord.NotFound:
|
||||
pass
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ function ensureBridgeDir() {
|
||||
}
|
||||
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
||||
let activeSessionId = '';
|
||||
let activeTrajectoryId = '';
|
||||
function writeChatSnapshot(text) {
|
||||
try {
|
||||
// Write to chat_snapshots/*.json for Bot's chat_snapshot_scanner to pick up
|
||||
@@ -426,6 +427,7 @@ const pendingResponses = new Map();
|
||||
// Click trigger: extension sets this, renderer polls and clicks button
|
||||
let clickTrigger = null;
|
||||
let sessionStalled = false; // true when session is stalled waiting for approval
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
|
||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||
let deepInspectRequested = false;
|
||||
let deepInspectResult = null;
|
||||
@@ -511,8 +513,15 @@ function startObserverHttpBridge() {
|
||||
if (fs.existsSync(respFile)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
|
||||
logToFile(`[HTTP] response served to renderer: ${rid} approved=${data.approved}`);
|
||||
// Delay deletion: processResponseFile (response watcher) may need to read it too.
|
||||
// The watcher fires with 300ms delay, so 2s is safe.
|
||||
setTimeout(() => {
|
||||
try {
|
||||
fs.unlinkSync(respFile);
|
||||
logToFile(`[HTTP] response sent: ${rid} approved=${data.approved}`);
|
||||
}
|
||||
catch { }
|
||||
}, 2000);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
@@ -1398,7 +1407,7 @@ function setupMonitor() {
|
||||
let lastKnownStepCount = 0;
|
||||
let lastNotifyStepIndex = -1;
|
||||
let lastTaskStepIndex = -1;
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step
|
||||
// lastPendingStepIndex is module-level (above sessionStalled)
|
||||
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
||||
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||
@@ -1452,6 +1461,7 @@ function setupMonitor() {
|
||||
// Session changed?
|
||||
if (bestSessionId !== activeSessionId) {
|
||||
activeSessionId = bestSessionId;
|
||||
activeTrajectoryId = bestSession.trajectoryId || '';
|
||||
activeSessionTitle = currentTitle;
|
||||
lastKnownStepCount = currentCount;
|
||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||
@@ -1603,6 +1613,10 @@ function setupMonitor() {
|
||||
if (!foundWaiting) {
|
||||
const lastStep = steps[steps.length - 1];
|
||||
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
||||
// CRITICAL: Reset sessionStalled when step_probe confirms no WAITING.
|
||||
// Without this, sessionStalled stays true during long AI generations
|
||||
// (PLANNER_RESPONSE with delta=0), causing false positive "Run" detections.
|
||||
sessionStalled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1746,6 +1760,11 @@ function setupResponseWatcher() {
|
||||
}
|
||||
async function processResponseFile(filePath) {
|
||||
try {
|
||||
// Gracefully handle files already consumed by HTTP handler
|
||||
if (!fs.existsSync(filePath)) {
|
||||
// HTTP GET /response/:rid already served and deleted this file — skip silently
|
||||
return;
|
||||
}
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const resp = JSON.parse(content);
|
||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
||||
@@ -1775,15 +1794,10 @@ async function processResponseFile(filePath) {
|
||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||
}
|
||||
else {
|
||||
// Step probe path: approve → trigger renderer click, reject → log only
|
||||
if (approved) {
|
||||
logToFile(`[RESPONSE] step_probe → approve via trigger-click`);
|
||||
clickTrigger = { action: 'approve', timestamp: Date.now() };
|
||||
}
|
||||
else {
|
||||
// SAFE: reject logs only — NO ResolveOutstandingSteps (it cancels AI work!)
|
||||
logToFile(`[RESPONSE] step_probe → reject (log only, no destructive action)`);
|
||||
}
|
||||
// Step probe path: run ALL approval strategies (5 vectors → 30+ methods)
|
||||
logToFile(`[RESPONSE] step_probe → running tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)})`);
|
||||
const strategyResult = await tryApprovalStrategies(approved, activeSessionId);
|
||||
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||
}
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||
// Cleanup response file
|
||||
@@ -1999,76 +2013,241 @@ function writePendingApproval(data) {
|
||||
async function tryApprovalStrategies(approved, sessionId) {
|
||||
const action = approved ? 'APPROVE' : 'REJECT';
|
||||
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
|
||||
// ── Dynamic Command Discovery (check if approval commands appear when step is WAITING) ──
|
||||
// ── Dynamic Command Discovery (log what's available during WAITING state) ──
|
||||
let approvalCmdList = [];
|
||||
try {
|
||||
const allCmds = await vscode.commands.getCommands(true);
|
||||
const agCmds = allCmds.filter((c) => c.startsWith('antigravity.'));
|
||||
const approvalCmds = agCmds.filter((c) => {
|
||||
approvalCmdList = agCmds.filter((c) => {
|
||||
const lower = c.toLowerCase();
|
||||
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|
||||
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step');
|
||||
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step')
|
||||
|| lower.includes('cascade') || lower.includes('action');
|
||||
});
|
||||
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmds.length} approval-related:`);
|
||||
for (const c of approvalCmds) {
|
||||
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`);
|
||||
for (const c of approvalCmdList) {
|
||||
logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
|
||||
}
|
||||
// Dump ALL antigravity.* commands for full comparison
|
||||
logToFile(`[APPROVAL-CMD-CHECK] FULL LIST (${agCmds.length}):`);
|
||||
for (const c of agCmds) {
|
||||
logToFile(`[APPROVAL-CMD-FULL] ${c}`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
|
||||
}
|
||||
// ── Strategy 1: HandleCascadeUserInteraction RPC ──
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
|
||||
// HandleCascadeUserInteractionRequest:
|
||||
// cascade_id: string
|
||||
// interaction: CascadeUserInteraction {
|
||||
// trajectory_id, step_index,
|
||||
// oneof: { run_command: CascadeRunCommandInteraction { confirm: bool } }
|
||||
// }
|
||||
// ConnectRPC uses camelCase for JSON encoding of snake_case proto fields
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if (sdk && approved) {
|
||||
const protoVariants = [
|
||||
// Variant A: camelCase JSON (ConnectRPC default)
|
||||
{
|
||||
cascadeId: sessionId,
|
||||
interaction: {
|
||||
trajectoryId: activeTrajectoryId || sessionId,
|
||||
stepIndex: lastPendingStepIndex,
|
||||
runCommand: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant B: snake_case JSON (proto native)
|
||||
{
|
||||
cascade_id: sessionId,
|
||||
interaction: {
|
||||
trajectory_id: activeTrajectoryId || sessionId,
|
||||
step_index: lastPendingStepIndex,
|
||||
run_command: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant C: camelCase, without trajectoryId (maybe optional)
|
||||
{
|
||||
cascadeId: sessionId,
|
||||
interaction: {
|
||||
stepIndex: lastPendingStepIndex,
|
||||
runCommand: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant D: camelCase, confirm only (minimal)
|
||||
{
|
||||
cascadeId: sessionId,
|
||||
interaction: {
|
||||
runCommand: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant E: snake_case minimal
|
||||
{
|
||||
cascade_id: sessionId,
|
||||
interaction: {
|
||||
run_command: { confirm: true },
|
||||
},
|
||||
},
|
||||
];
|
||||
for (let i = 0; i < protoVariants.length; i++) {
|
||||
try {
|
||||
const payload = protoVariants[i];
|
||||
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 200)})`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
|
||||
logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-PROTO-${i}:HandleCascadeUserInteraction`;
|
||||
}
|
||||
catch (e) {
|
||||
// Capture FULL error message (critical for diagnostics)
|
||||
logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0A: executeCascadeAction
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const cascadeActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject' },
|
||||
{ action: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ action: approved ? 'approve' : 'deny' },
|
||||
{ action: approved ? 'run' : 'reject' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
{ cascadeId: sessionId, type: approved ? 'accept' : 'reject' },
|
||||
approved ? 'accept' : 'reject', // plain string arg
|
||||
approved ? 1 : 0, // numeric arg
|
||||
];
|
||||
for (let i = 0; i < cascadeActionVariants.length; i++) {
|
||||
try {
|
||||
const arg = cascadeActionVariants[i];
|
||||
logToFile(`[APPROVAL-0A-${i}] executeCascadeAction(${JSON.stringify(arg)})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.executeCascadeAction', arg);
|
||||
logToFile(`[APPROVAL-0A-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0A:executeCascadeAction(variant=${i})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0A-${i}] ❌ ${e.message.substring(0, 100)}`);
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0B: Direct approval commands (brute-force try all 7 SDK commands)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const commandsToTry = approved
|
||||
? [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
'antigravity.command.accept',
|
||||
]
|
||||
: [
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
'antigravity.command.reject',
|
||||
];
|
||||
for (const cmd of commandsToTry) {
|
||||
// Try with no args, then with sessionId
|
||||
for (const args of [[], [sessionId], [{ cascadeId: sessionId }]]) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0B] ${cmd}(${JSON.stringify(args)})`);
|
||||
const result = await vscode.commands.executeCommand(cmd, ...args);
|
||||
logToFile(`[APPROVAL-0B] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0B:${cmd}`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0B] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0C: Electron webContents access (pierce webview isolation)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
try {
|
||||
logToFile(`[APPROVAL-0C] Attempting Electron webContents access...`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const electron = require('electron');
|
||||
const remote = electron.remote;
|
||||
if (remote) {
|
||||
logToFile(`[APPROVAL-0C] electron.remote available!`);
|
||||
const allWC = remote.webContents.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
for (const wc of allWC) {
|
||||
const wcUrl = wc.getURL() || '';
|
||||
logToFile(`[APPROVAL-0C] wc: ${wcUrl.substring(0, 100)}`);
|
||||
if (wcUrl.includes('vscode-webview://') || wcUrl.includes('webview')) {
|
||||
const clickScript = approved
|
||||
? `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Run|Accept|Allow/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`
|
||||
: `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Reject|Deny|Cancel/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`;
|
||||
const clickResult = await wc.executeJavaScript(clickScript);
|
||||
logToFile(`[APPROVAL-0C] executeJavaScript result: ${clickResult}`);
|
||||
if (clickResult && clickResult.startsWith('clicked:')) {
|
||||
return `ELECTRON-0C:webContents(${clickResult})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
logToFile(`[APPROVAL-0C] electron.remote NOT available`);
|
||||
// Try without remote (main process context)
|
||||
const wc = electron.webContents;
|
||||
if (wc && typeof wc.getAllWebContents === 'function') {
|
||||
logToFile(`[APPROVAL-0C] Direct electron.webContents available!`);
|
||||
const allWC = wc.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
}
|
||||
else {
|
||||
logToFile(`[APPROVAL-0C] No webContents access from Extension Host`);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0C] ❌ ${e.message.substring(0, 150)}`);
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0D: sendChatActionMessage (may route to agent)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const chatActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject', cascadeId: sessionId },
|
||||
{ message: approved ? 'accept' : 'reject', conversationId: sessionId },
|
||||
approved ? 'accept' : 'reject',
|
||||
];
|
||||
for (let i = 0; i < chatActionVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0D-${i}] sendChatActionMessage(${JSON.stringify(chatActionVariants[i])})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.sendChatActionMessage', chatActionVariants[i]);
|
||||
logToFile(`[APPROVAL-0D-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0D:sendChatActionMessage(variant=${i})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0D-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 1: HandleCascadeUserInteraction RPC (expanded variants)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if (sdk) {
|
||||
// Try variant A: { cascadeId, approved }
|
||||
const rpcVariants = [
|
||||
// Original variants
|
||||
{ cascadeId: sessionId, approved: approved },
|
||||
{ cascadeId: sessionId, stepAction: approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT' },
|
||||
{ cascadeId: sessionId, userAction: approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED' },
|
||||
// New variants — ConnectRPC protobuf field naming conventions
|
||||
{ cascade_id: sessionId, accepted: approved },
|
||||
{ cascadeId: sessionId, accepted: approved },
|
||||
{ cascade_id: sessionId, user_action: approved ? 1 : 2 }, // enum as int
|
||||
{ cascadeId: sessionId, interaction: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
// With step index from last known waiting step
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, accepted: approved },
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, approved: approved },
|
||||
];
|
||||
for (let i = 0; i < rpcVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-1A] HandleCascadeUserInteraction { cascadeId, approved: ${approved} }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
approved: approved,
|
||||
});
|
||||
logToFile(`[APPROVAL-1A] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1A:HandleCascadeUserInteraction(approved=${approved})`;
|
||||
logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(rpcVariants[i]).substring(0, 120)})`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', rpcVariants[i]);
|
||||
logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1-${i}:HandleCascadeUserInteraction`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-1A] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
// Try variant B: { cascadeId, stepAction }
|
||||
try {
|
||||
const stepAction = approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT';
|
||||
logToFile(`[APPROVAL-1B] HandleCascadeUserInteraction { cascadeId, stepAction: '${stepAction}' }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
stepAction: stepAction,
|
||||
});
|
||||
logToFile(`[APPROVAL-1B] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1B:HandleCascadeUserInteraction(stepAction=${stepAction})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-1B] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
// Try variant C: { cascadeId, userAction } (experimental)
|
||||
try {
|
||||
const userAction = approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED';
|
||||
logToFile(`[APPROVAL-1C] HandleCascadeUserInteraction { cascadeId, userAction: '${userAction}' }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
userAction: userAction,
|
||||
});
|
||||
logToFile(`[APPROVAL-1C] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1C:HandleCascadeUserInteraction(userAction=${userAction})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-1C] ❌ FAIL: ${e.message}`);
|
||||
logToFile(`[APPROVAL-1-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge ──
|
||||
// 2026-03-09: All SDK approval commands NOT REGISTERED (119 cmds tested).
|
||||
// Keyboard simulation sends Enter to chat input (sends empty message — WRONG).
|
||||
// New approach: set clickTrigger flag → renderer polls /trigger-click → clicks button.
|
||||
}
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (kept as fallback) ──
|
||||
try {
|
||||
const triggerAction = approved ? 'approve' : 'reject';
|
||||
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -81,6 +81,7 @@ function ensureBridgeDir() {
|
||||
|
||||
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
||||
let activeSessionId = '';
|
||||
let activeTrajectoryId = '';
|
||||
|
||||
function writeChatSnapshot(text: string) {
|
||||
try {
|
||||
@@ -403,6 +404,7 @@ const pendingResponses = new Map<string, { approved: boolean } | null>();
|
||||
// Click trigger: extension sets this, renderer polls and clicks button
|
||||
let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = null;
|
||||
let sessionStalled = false; // true when session is stalled waiting for approval
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
|
||||
|
||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||
let deepInspectRequested = false;
|
||||
@@ -489,8 +491,12 @@ function startObserverHttpBridge(): Promise<number> {
|
||||
if (fs.existsSync(respFile)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
|
||||
fs.unlinkSync(respFile);
|
||||
logToFile(`[HTTP] response sent: ${rid} approved=${data.approved}`);
|
||||
logToFile(`[HTTP] response served to renderer: ${rid} approved=${data.approved}`);
|
||||
// Delay deletion: processResponseFile (response watcher) may need to read it too.
|
||||
// The watcher fires with 300ms delay, so 2s is safe.
|
||||
setTimeout(() => {
|
||||
try { fs.unlinkSync(respFile); } catch { }
|
||||
}, 2000);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
} catch {
|
||||
@@ -1376,7 +1382,7 @@ function setupMonitor() {
|
||||
let lastKnownStepCount = 0;
|
||||
let lastNotifyStepIndex = -1;
|
||||
let lastTaskStepIndex = -1;
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step
|
||||
// lastPendingStepIndex is module-level (above sessionStalled)
|
||||
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
||||
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||
@@ -1432,6 +1438,7 @@ function setupMonitor() {
|
||||
// Session changed?
|
||||
if (bestSessionId !== activeSessionId) {
|
||||
activeSessionId = bestSessionId;
|
||||
activeTrajectoryId = (bestSession as any).trajectoryId || '';
|
||||
activeSessionTitle = currentTitle;
|
||||
lastKnownStepCount = currentCount;
|
||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||
@@ -1586,6 +1593,10 @@ function setupMonitor() {
|
||||
if (!foundWaiting) {
|
||||
const lastStep = steps[steps.length - 1];
|
||||
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
||||
// CRITICAL: Reset sessionStalled when step_probe confirms no WAITING.
|
||||
// Without this, sessionStalled stays true during long AI generations
|
||||
// (PLANNER_RESPONSE with delta=0), causing false positive "Run" detections.
|
||||
sessionStalled = false;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -1726,6 +1737,11 @@ function setupResponseWatcher() {
|
||||
|
||||
async function processResponseFile(filePath: string) {
|
||||
try {
|
||||
// Gracefully handle files already consumed by HTTP handler
|
||||
if (!fs.existsSync(filePath)) {
|
||||
// HTTP GET /response/:rid already served and deleted this file — skip silently
|
||||
return;
|
||||
}
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const resp = JSON.parse(content);
|
||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
||||
@@ -1757,14 +1773,10 @@ async function processResponseFile(filePath: string) {
|
||||
// DOM observer path: renderer polls /response/:rid and clicks directly
|
||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||
} else {
|
||||
// Step probe path: approve → trigger renderer click, reject → log only
|
||||
if (approved) {
|
||||
logToFile(`[RESPONSE] step_probe → approve via trigger-click`);
|
||||
clickTrigger = { action: 'approve' as const, timestamp: Date.now() };
|
||||
} else {
|
||||
// SAFE: reject logs only — NO ResolveOutstandingSteps (it cancels AI work!)
|
||||
logToFile(`[RESPONSE] step_probe → reject (log only, no destructive action)`);
|
||||
}
|
||||
// Step probe path: run ALL approval strategies (5 vectors → 30+ methods)
|
||||
logToFile(`[RESPONSE] step_probe → running tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)})`);
|
||||
const strategyResult = await tryApprovalStrategies(approved, activeSessionId);
|
||||
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||
}
|
||||
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||
@@ -1968,76 +1980,239 @@ async function tryApprovalStrategies(approved: boolean, sessionId: string): Prom
|
||||
const action = approved ? 'APPROVE' : 'REJECT';
|
||||
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
|
||||
|
||||
// ── Dynamic Command Discovery (check if approval commands appear when step is WAITING) ──
|
||||
// ── Dynamic Command Discovery (log what's available during WAITING state) ──
|
||||
let approvalCmdList: string[] = [];
|
||||
try {
|
||||
const allCmds = await vscode.commands.getCommands(true);
|
||||
const agCmds = allCmds.filter((c: string) => c.startsWith('antigravity.'));
|
||||
const approvalCmds = agCmds.filter((c: string) => {
|
||||
approvalCmdList = agCmds.filter((c: string) => {
|
||||
const lower = c.toLowerCase();
|
||||
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|
||||
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step');
|
||||
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step')
|
||||
|| lower.includes('cascade') || lower.includes('action');
|
||||
});
|
||||
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmds.length} approval-related:`);
|
||||
for (const c of approvalCmds) {
|
||||
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`);
|
||||
for (const c of approvalCmdList) {
|
||||
logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
|
||||
}
|
||||
// Dump ALL antigravity.* commands for full comparison
|
||||
logToFile(`[APPROVAL-CMD-CHECK] FULL LIST (${agCmds.length}):`);
|
||||
for (const c of agCmds) {
|
||||
logToFile(`[APPROVAL-CMD-FULL] ${c}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
|
||||
}
|
||||
|
||||
// ── Strategy 1: HandleCascadeUserInteraction RPC ──
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
|
||||
// HandleCascadeUserInteractionRequest:
|
||||
// cascade_id: string
|
||||
// interaction: CascadeUserInteraction {
|
||||
// trajectory_id, step_index,
|
||||
// oneof: { run_command: CascadeRunCommandInteraction { confirm: bool } }
|
||||
// }
|
||||
// ConnectRPC uses camelCase for JSON encoding of snake_case proto fields
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if (sdk && approved) {
|
||||
const protoVariants = [
|
||||
// Variant A: camelCase JSON (ConnectRPC default)
|
||||
{
|
||||
cascadeId: sessionId,
|
||||
interaction: {
|
||||
trajectoryId: activeTrajectoryId || sessionId,
|
||||
stepIndex: lastPendingStepIndex,
|
||||
runCommand: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant B: snake_case JSON (proto native)
|
||||
{
|
||||
cascade_id: sessionId,
|
||||
interaction: {
|
||||
trajectory_id: activeTrajectoryId || sessionId,
|
||||
step_index: lastPendingStepIndex,
|
||||
run_command: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant C: camelCase, without trajectoryId (maybe optional)
|
||||
{
|
||||
cascadeId: sessionId,
|
||||
interaction: {
|
||||
stepIndex: lastPendingStepIndex,
|
||||
runCommand: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant D: camelCase, confirm only (minimal)
|
||||
{
|
||||
cascadeId: sessionId,
|
||||
interaction: {
|
||||
runCommand: { confirm: true },
|
||||
},
|
||||
},
|
||||
// Variant E: snake_case minimal
|
||||
{
|
||||
cascade_id: sessionId,
|
||||
interaction: {
|
||||
run_command: { confirm: true },
|
||||
},
|
||||
},
|
||||
];
|
||||
for (let i = 0; i < protoVariants.length; i++) {
|
||||
try {
|
||||
const payload = protoVariants[i];
|
||||
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 200)})`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
|
||||
logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-PROTO-${i}:HandleCascadeUserInteraction`;
|
||||
} catch (e: any) {
|
||||
// Capture FULL error message (critical for diagnostics)
|
||||
logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0A: executeCascadeAction
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const cascadeActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject' },
|
||||
{ action: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ action: approved ? 'approve' : 'deny' },
|
||||
{ action: approved ? 'run' : 'reject' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
{ cascadeId: sessionId, type: approved ? 'accept' : 'reject' },
|
||||
approved ? 'accept' : 'reject', // plain string arg
|
||||
approved ? 1 : 0, // numeric arg
|
||||
];
|
||||
for (let i = 0; i < cascadeActionVariants.length; i++) {
|
||||
try {
|
||||
const arg = cascadeActionVariants[i];
|
||||
logToFile(`[APPROVAL-0A-${i}] executeCascadeAction(${JSON.stringify(arg)})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.executeCascadeAction', arg);
|
||||
logToFile(`[APPROVAL-0A-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0A:executeCascadeAction(variant=${i})`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0A-${i}] ❌ ${e.message.substring(0, 100)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0B: Direct approval commands (brute-force try all 7 SDK commands)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const commandsToTry = approved
|
||||
? [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
'antigravity.command.accept',
|
||||
]
|
||||
: [
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
'antigravity.command.reject',
|
||||
];
|
||||
for (const cmd of commandsToTry) {
|
||||
// Try with no args, then with sessionId
|
||||
for (const args of [[], [sessionId], [{ cascadeId: sessionId }]]) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0B] ${cmd}(${JSON.stringify(args)})`);
|
||||
const result = await vscode.commands.executeCommand(cmd, ...args);
|
||||
logToFile(`[APPROVAL-0B] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0B:${cmd}`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0B] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0C: Electron webContents access (pierce webview isolation)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
try {
|
||||
logToFile(`[APPROVAL-0C] Attempting Electron webContents access...`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const electron = require('electron');
|
||||
const remote = (electron as any).remote;
|
||||
if (remote) {
|
||||
logToFile(`[APPROVAL-0C] electron.remote available!`);
|
||||
const allWC = remote.webContents.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
for (const wc of allWC) {
|
||||
const wcUrl = wc.getURL() || '';
|
||||
logToFile(`[APPROVAL-0C] wc: ${wcUrl.substring(0, 100)}`);
|
||||
if (wcUrl.includes('vscode-webview://') || wcUrl.includes('webview')) {
|
||||
const clickScript = approved
|
||||
? `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Run|Accept|Allow/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`
|
||||
: `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Reject|Deny|Cancel/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`;
|
||||
const clickResult = await wc.executeJavaScript(clickScript);
|
||||
logToFile(`[APPROVAL-0C] executeJavaScript result: ${clickResult}`);
|
||||
if (clickResult && clickResult.startsWith('clicked:')) {
|
||||
return `ELECTRON-0C:webContents(${clickResult})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logToFile(`[APPROVAL-0C] electron.remote NOT available`);
|
||||
// Try without remote (main process context)
|
||||
const wc = (electron as any).webContents;
|
||||
if (wc && typeof wc.getAllWebContents === 'function') {
|
||||
logToFile(`[APPROVAL-0C] Direct electron.webContents available!`);
|
||||
const allWC = wc.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
} else {
|
||||
logToFile(`[APPROVAL-0C] No webContents access from Extension Host`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0C] ❌ ${e.message.substring(0, 150)}`);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0D: sendChatActionMessage (may route to agent)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const chatActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject', cascadeId: sessionId },
|
||||
{ message: approved ? 'accept' : 'reject', conversationId: sessionId },
|
||||
approved ? 'accept' : 'reject',
|
||||
];
|
||||
for (let i = 0; i < chatActionVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0D-${i}] sendChatActionMessage(${JSON.stringify(chatActionVariants[i])})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.sendChatActionMessage', chatActionVariants[i]);
|
||||
logToFile(`[APPROVAL-0D-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0D:sendChatActionMessage(variant=${i})`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0D-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 1: HandleCascadeUserInteraction RPC (expanded variants)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if (sdk) {
|
||||
// Try variant A: { cascadeId, approved }
|
||||
const rpcVariants = [
|
||||
// Original variants
|
||||
{ cascadeId: sessionId, approved: approved },
|
||||
{ cascadeId: sessionId, stepAction: approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT' },
|
||||
{ cascadeId: sessionId, userAction: approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED' },
|
||||
// New variants — ConnectRPC protobuf field naming conventions
|
||||
{ cascade_id: sessionId, accepted: approved },
|
||||
{ cascadeId: sessionId, accepted: approved },
|
||||
{ cascade_id: sessionId, user_action: approved ? 1 : 2 }, // enum as int
|
||||
{ cascadeId: sessionId, interaction: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
// With step index from last known waiting step
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, accepted: approved },
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, approved: approved },
|
||||
];
|
||||
for (let i = 0; i < rpcVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-1A] HandleCascadeUserInteraction { cascadeId, approved: ${approved} }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
approved: approved,
|
||||
});
|
||||
logToFile(`[APPROVAL-1A] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1A:HandleCascadeUserInteraction(approved=${approved})`;
|
||||
logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(rpcVariants[i]).substring(0, 120)})`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', rpcVariants[i]);
|
||||
logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1-${i}:HandleCascadeUserInteraction`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-1A] ❌ FAIL: ${e.message}`);
|
||||
logToFile(`[APPROVAL-1-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
|
||||
// Try variant B: { cascadeId, stepAction }
|
||||
try {
|
||||
const stepAction = approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT';
|
||||
logToFile(`[APPROVAL-1B] HandleCascadeUserInteraction { cascadeId, stepAction: '${stepAction}' }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
stepAction: stepAction,
|
||||
});
|
||||
logToFile(`[APPROVAL-1B] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1B:HandleCascadeUserInteraction(stepAction=${stepAction})`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-1B] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
|
||||
// Try variant C: { cascadeId, userAction } (experimental)
|
||||
try {
|
||||
const userAction = approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED';
|
||||
logToFile(`[APPROVAL-1C] HandleCascadeUserInteraction { cascadeId, userAction: '${userAction}' }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
userAction: userAction,
|
||||
});
|
||||
logToFile(`[APPROVAL-1C] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1C:HandleCascadeUserInteraction(userAction=${userAction})`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-1C] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge ──
|
||||
// 2026-03-09: All SDK approval commands NOT REGISTERED (119 cmds tested).
|
||||
// Keyboard simulation sends Enter to chat input (sends empty message — WRONG).
|
||||
// New approach: set clickTrigger flag → renderer polls /trigger-click → clicks button.
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (kept as fallback) ──
|
||||
try {
|
||||
const triggerAction = approved ? 'approve' : 'reject';
|
||||
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||
|
||||
Reference in New Issue
Block a user