feat(bridge): step-type-specific approval commands + SDK research
- tryApprovalStrategies: terminalCommand.run > terminalCommand.accept > command.accept > acceptAgentStep - Step probe: immediate on first stall (5s), 775-limit detection with dynamic fallback - NOTIFY filter: skip <50 chars, TASK dedup by taskName+taskStatus - BTN-DUMP diagnostic removed from renderer - Focus: agentPanel.focus + agentSidePanel.focus (verified SDK commands) - known-issues: add step-type command mismatch finding
This commit is contained in:
@@ -164,3 +164,8 @@
|
|||||||
- **해결**: 항상 `extension.ts`의 `generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
|
- **해결**: 항상 `extension.ts`의 `generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
|
||||||
- **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트
|
- **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트
|
||||||
|
|
||||||
|
### [2026-03-09] VS Code Accept — Run 버튼에 잘못된 명령 사용
|
||||||
|
- **증상**: Discord 승인 → `acceptAgentStep` 실행 → "Silent Success" (실제 승인 안 됨)
|
||||||
|
- **원인**: `acceptAgentStep`은 **코드 변경** 승인 전용. Run 버튼 = **터미널 명령** 승인으로 `terminalCommand.run` 또는 `terminalCommand.accept`가 올바른 명령
|
||||||
|
- **해결**: SDK 7개 승인 명령을 step type별로 분기 시도 (`terminalCommand.run` → `terminalCommand.accept` → `command.accept` → `acceptAgentStep`)
|
||||||
|
- **주의**: `terminalCommand.run`의 개별 동작 결과는 아직 미검증. devlog-004에서 순차 시도만 언급됨. AG 재시작 후 E2E 테스트 필요
|
||||||
|
|||||||
5
docs/devlog/2026-03-09.md
Normal file
5
docs/devlog/2026-03-09.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 2026-03-09 Devlog
|
||||||
|
|
||||||
|
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||||
|
|---|------|----------|------|------|
|
||||||
|
| 001 | 08:00~09:17 | 승인 실행 메커니즘 연구 + step-type별 VS Code 명령 분기 구현 | `pending` | 🔧 |
|
||||||
34
docs/devlog/entries/20260309-001.md
Normal file
34
docs/devlog/entries/20260309-001.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 승인 실행 메커니즘 연구 + step-type별 명령 분기
|
||||||
|
|
||||||
|
- **시간**: 2026-03-09 08:00~09:17
|
||||||
|
- **Commit**: `pending`
|
||||||
|
- **Vikunja**: #263 진행중
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
|
||||||
|
### acceptAgentStep은 Run 버튼에 무효
|
||||||
|
- SDK에 7개 별도 승인 명령 존재 (우리는 1개만 사용 중이었음)
|
||||||
|
- Run 버튼 = **터미널 명령** → `terminalCommand.run` / `terminalCommand.accept`가 올바른 명령
|
||||||
|
- `acceptAgentStep`은 **코드 변경** 승인 전용 → Run 버튼과 무관
|
||||||
|
|
||||||
|
### 빌트인 Turbo Mode 발견
|
||||||
|
- `TerminalExecutionPolicy.EAGER` = 항상 자동 실행 (AG Settings에서 on/off)
|
||||||
|
- 사용자 판단: 통제권 유지가 중요 → Discord 승인 방식 유지, Turbo는 최후 수단
|
||||||
|
|
||||||
|
### pywinauto 폐기
|
||||||
|
- 크로스플랫폼 불가 (Linux 사용 가능성)
|
||||||
|
- 창 겹침 시 동작 불가
|
||||||
|
- 사용자 결정으로 폐기
|
||||||
|
|
||||||
|
## 코드 변경
|
||||||
|
- `extension.ts`:
|
||||||
|
- `tryApprovalStrategies()` — HandleCascadeUserInteraction RPC 3가지 variant + VS Code 7개 명령 순차 시도
|
||||||
|
- Step probe: stall 1 poll (5초) 후 즉시 probe, 775-limit 감지 + 동적 fallback (20s/40s)
|
||||||
|
- NOTIFY 필터: 50자 미만 무시, TASK 중복 skip
|
||||||
|
- BTN-DUMP 진단 로그 제거
|
||||||
|
- `.agents/references/known-issues.md`: 1건 추가
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
1. **AG 재시작 후 E2E 테스트** — `terminalCommand.run` 개별 동작 확인 (핵심)
|
||||||
|
2. **`extension.log` 분석** — 어떤 전략이 실제 승인을 트리거하는지 로그로 확인
|
||||||
|
3. **실패 시 대안**: `executeCascadeAction` 파라미터 탐색 또는 Turbo Mode 활성화
|
||||||
@@ -689,23 +689,6 @@ function generateApprovalObserverScript(_port) {
|
|||||||
if(document.body)searchRoots.push(document.body);
|
if(document.body)searchRoots.push(document.body);
|
||||||
if(!searchRoots.length)return;
|
if(!searchRoots.length)return;
|
||||||
|
|
||||||
// ── DIAGNOSTIC: dump ALL button-like elements EVERY scan cycle ──
|
|
||||||
{
|
|
||||||
var dumpBtns=[];
|
|
||||||
var totalChecked=0;
|
|
||||||
for(var dr=0;dr<searchRoots.length;dr++){
|
|
||||||
var dbs=searchRoots[dr].querySelectorAll('button,[role="button"]');
|
|
||||||
totalChecked+=dbs.length;
|
|
||||||
for(var di=0;di<dbs.length;di++){
|
|
||||||
var db=dbs[di];
|
|
||||||
var dt=(db.textContent||'').trim();
|
|
||||||
var dtShort=dt.replace(/\\s+/g,' ').substring(0,50);
|
|
||||||
dumpBtns.push(db.tagName+'['+dt.length+']:'+dtShort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log('[BTN-DUMP] roots='+searchRoots.length+' total='+totalChecked+' btns='+dumpBtns.length+': '+JSON.stringify(dumpBtns.slice(0,15)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var seen={}; // dedupe buttons across search roots
|
var seen={}; // dedupe buttons across search roots
|
||||||
for(var r=0;r<searchRoots.length;r++){
|
for(var r=0;r<searchRoots.length;r++){
|
||||||
var allBtns=searchRoots[r].querySelectorAll('button');
|
var allBtns=searchRoots[r].querySelectorAll('button');
|
||||||
@@ -936,6 +919,7 @@ function setupMonitor() {
|
|||||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||||
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
||||||
let stallProbed = false; // prevent repeated step probes during same stall
|
let stallProbed = false; // prevent repeated step probes during same stall
|
||||||
|
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
if (pollCount <= 3 || pollCount % 12 === 0) {
|
if (pollCount <= 3 || pollCount % 12 === 0) {
|
||||||
@@ -1012,9 +996,8 @@ function setupMonitor() {
|
|||||||
logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`);
|
logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`);
|
||||||
}
|
}
|
||||||
// ── PRIMARY: Step-probe-based approval detection ──
|
// ── PRIMARY: Step-probe-based approval detection ──
|
||||||
// latestToolCallStep does NOT exist in GetAllCascadeTrajectories response.
|
// On stall (idle=1, ~5s), probe GetCascadeTrajectorySteps to check WAITING.
|
||||||
// Instead, on early stall (idle=2, ~10s), probe GetCascadeTrajectorySteps
|
// 775-step limit: probe fails for long sessions → faster stall fallback.
|
||||||
// to fetch the latest step and check if it's a tool call awaiting approval.
|
|
||||||
// ── STALL-BASED approval detection with step probe ──
|
// ── STALL-BASED approval detection with step probe ──
|
||||||
const currentModTime = bestSession.lastModifiedTime || bestSession.lastModifiedTimestamp || bestSession.modifiedTime || '';
|
const currentModTime = bestSession.lastModifiedTime || bestSession.lastModifiedTimestamp || bestSession.modifiedTime || '';
|
||||||
const modTimeChanged = currentModTime !== lastModTime;
|
const modTimeChanged = currentModTime !== lastModTime;
|
||||||
@@ -1046,7 +1029,7 @@ function setupMonitor() {
|
|||||||
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
|
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
|
||||||
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
|
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
|
||||||
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet
|
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet
|
||||||
if (consecutiveIdleCount >= 1 && consecutiveIdleCount % 2 === 1 && !stallProbed) {
|
if (consecutiveIdleCount >= 1 && !stallProbed) {
|
||||||
try {
|
try {
|
||||||
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
cascadeId: bestSessionId,
|
cascadeId: bestSessionId,
|
||||||
@@ -1055,6 +1038,9 @@ function setupMonitor() {
|
|||||||
const steps = stepsResp.steps;
|
const steps = stepsResp.steps;
|
||||||
// Diagnostic: compare returned steps vs trajectory stepCount
|
// Diagnostic: compare returned steps vs trajectory stepCount
|
||||||
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`);
|
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`);
|
||||||
|
if (steps.length < currentCount) {
|
||||||
|
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
|
||||||
|
}
|
||||||
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
|
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
|
||||||
let foundWaiting = false;
|
let foundWaiting = false;
|
||||||
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 5); si--) {
|
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 5); si--) {
|
||||||
@@ -1116,8 +1102,9 @@ function setupMonitor() {
|
|||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
const cooldownOk = (now - lastPendingTime) > 60_000;
|
||||||
if (consecutiveIdleCount >= 8 && sawRunningAfterPending && cooldownOk) {
|
const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
|
||||||
// 8 polls × 5s = 40 seconds — fallback (reduced from 100s)
|
if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
|
||||||
|
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
|
||||||
lastPendingStepIndex = currentCount;
|
lastPendingStepIndex = currentCount;
|
||||||
lastPendingTime = now;
|
lastPendingTime = now;
|
||||||
sawRunningAfterPending = false;
|
sawRunningAfterPending = false;
|
||||||
@@ -1126,7 +1113,7 @@ function setupMonitor() {
|
|||||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
||||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
||||||
}
|
}
|
||||||
else if (consecutiveIdleCount === 8) {
|
else if (consecutiveIdleCount === fallbackThreshold) {
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
if (!sawRunningAfterPending)
|
if (!sawRunningAfterPending)
|
||||||
reasons.push('needDelta>0');
|
reasons.push('needDelta>0');
|
||||||
@@ -1145,10 +1132,14 @@ function setupMonitor() {
|
|||||||
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
lastNotifyStepIndex = notifyStep.stepIndex;
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||||||
if (content.length > 10) {
|
// Filter: only relay meaningful notifications (skip trivial ones)
|
||||||
|
if (content.length > 50) {
|
||||||
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
||||||
}
|
}
|
||||||
|
else if (content.length > 0) {
|
||||||
|
logToFile(`[POLL] NOTIFY skipped (too short: ${content.length} chars): ${content.substring(0, 80)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ── Process latestTaskBoundaryStep ──
|
// ── Process latestTaskBoundaryStep ──
|
||||||
const taskStep = bestSession.latestTaskBoundaryStep;
|
const taskStep = bestSession.latestTaskBoundaryStep;
|
||||||
@@ -1157,8 +1148,16 @@ function setupMonitor() {
|
|||||||
const tb = taskStep.step?.taskBoundary;
|
const tb = taskStep.step?.taskBoundary;
|
||||||
if (tb?.taskName) {
|
if (tb?.taskName) {
|
||||||
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||||||
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
// Filter: skip status-only updates with same task name (noise)
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
|
||||||
|
if (taskText !== lastRelayedTaskText) {
|
||||||
|
lastRelayedTaskText = taskText;
|
||||||
|
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile(`[POLL] TASK skipped (duplicate): "${tb.taskName}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1225,52 +1224,23 @@ async function processResponseFile(filePath) {
|
|||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
// ═══ APPROVAL STRATEGY ═══
|
// ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
|
||||||
// DOM observer approvals: renderer handles clicking via pollResponse — skip VS Code commands
|
// Tries multiple methods sequentially with detailed logging.
|
||||||
// Stall-detection approvals: use VS Code commands as fallback (focus-dependent)
|
// DOM observer: renderer handles clicking via pollResponse
|
||||||
|
// Step probe/stall: try RPC → VS Code commands → log results
|
||||||
const approved = resp.approved;
|
const approved = resp.approved;
|
||||||
if (isDomObserver) {
|
if (isDomObserver) {
|
||||||
// DOM observer path: renderer polls /response/:rid and clicks directly
|
// DOM observer path: renderer polls /response/:rid and clicks directly
|
||||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Step probe / stall path: relay approval to DOM observer pending files
|
// Step probe / stall path: try all approval strategies
|
||||||
// The renderer polls /response/:rid and can click the actual button
|
logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
|
||||||
logToFile(`[RESPONSE] step_probe/stall → relaying to DOM observer pending files`);
|
const approvalResult = await tryApprovalStrategies(approved, sessionId);
|
||||||
const pendingDir = path.join(bridgePath, 'pending');
|
logToFile(`[RESPONSE] approval result: ${approvalResult}`);
|
||||||
const responseDir = path.join(bridgePath, 'response');
|
|
||||||
if (!fs.existsSync(responseDir))
|
|
||||||
fs.mkdirSync(responseDir, { recursive: true });
|
|
||||||
let relayCount = 0;
|
|
||||||
try {
|
|
||||||
const files = fs.readdirSync(pendingDir).filter((f) => f.endsWith('.json'));
|
|
||||||
for (const f of files) {
|
|
||||||
try {
|
|
||||||
const pd = JSON.parse(fs.readFileSync(path.join(pendingDir, f), 'utf-8'));
|
|
||||||
if (pd.source === 'dom_observer' && pd.status === 'pending') {
|
|
||||||
// Write response file for this DOM observer pending
|
|
||||||
const responsePayload = {
|
|
||||||
request_id: pd.request_id,
|
|
||||||
approved,
|
|
||||||
timestamp: Date.now() / 1000,
|
|
||||||
};
|
|
||||||
fs.writeFileSync(path.join(responseDir, `${pd.request_id}.json`), JSON.stringify(responsePayload, null, 2), 'utf-8');
|
|
||||||
relayCount++;
|
|
||||||
logToFile(`[RESPONSE] relayed to DOM pending: ${pd.request_id} approved=${approved}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
logToFile(`[RESPONSE] relay scan error: ${e.message}`);
|
|
||||||
}
|
|
||||||
logToFile(`[RESPONSE] relayed to ${relayCount} DOM observer pending files`);
|
|
||||||
}
|
}
|
||||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'stall'})`);
|
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||||
// Cleanup response file — BUT NOT for DOM observer!
|
// Cleanup response file — BUT NOT for DOM observer!
|
||||||
// DOM observer: renderer's pollResponse needs to find it via HTTP GET /response/:rid
|
|
||||||
// Non-DOM (stall/step_probe relay): watcher already handled it, safe to delete
|
|
||||||
if (!isDomObserver) {
|
if (!isDomObserver) {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
@@ -1437,6 +1407,129 @@ function writePendingApproval(data) {
|
|||||||
console.log(`Gravity Bridge: pending write error: ${e.message}`);
|
console.log(`Gravity Bridge: pending write error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ─── Multi-Strategy Approval Execution ───
|
||||||
|
/**
|
||||||
|
* Try multiple approval methods sequentially.
|
||||||
|
* Returns a string describing which method succeeded (or all failed).
|
||||||
|
*
|
||||||
|
* Strategy order (most reliable first):
|
||||||
|
* 1. HandleCascadeUserInteraction RPC (cross-platform, no focus)
|
||||||
|
* 2. VS Code accept/reject commands (focus-dependent)
|
||||||
|
* 3. Log failure for manual intervention
|
||||||
|
*/
|
||||||
|
async function tryApprovalStrategies(approved, sessionId) {
|
||||||
|
const action = approved ? 'APPROVE' : 'REJECT';
|
||||||
|
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
|
||||||
|
// ── Strategy 1: HandleCascadeUserInteraction RPC ──
|
||||||
|
if (sdk) {
|
||||||
|
// Try variant A: { cascadeId, approved }
|
||||||
|
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})`;
|
||||||
|
}
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── Strategy 2: VS Code Commands — step-type-specific (focus-dependent) ──
|
||||||
|
// Per SDK research (2026-03-09):
|
||||||
|
// Run button = terminal command → terminalCommand.run / terminalCommand.accept
|
||||||
|
// Code changes = agent step → agent.acceptAgentStep
|
||||||
|
// General commands = command.accept
|
||||||
|
// Previously only tried acceptAgentStep (Silent Success) — now tries ALL 7
|
||||||
|
// Try to focus the panel first (required for command.accept / acceptAgentStep)
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-2] focusing panel...`);
|
||||||
|
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
await vscode.commands.executeCommand('antigravity.agentSidePanel.focus');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
logToFile(`[APPROVAL-2] panel focus attempted (agentPanel + agentSidePanel)`);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-2] panel focus failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
// All 7 approval commands in priority order (terminal first for Run button)
|
||||||
|
const commands = approved
|
||||||
|
? [
|
||||||
|
// Terminal commands (Run button) — UNTESTED INDIVIDUALLY (devlog-004)
|
||||||
|
'antigravity.terminalCommand.run', // SDK: TERMINAL_RUN
|
||||||
|
'antigravity.terminalCommand.accept', // SDK: TERMINAL_ACCEPT
|
||||||
|
// General command approval
|
||||||
|
'antigravity.command.accept', // SDK: COMMAND_ACCEPT
|
||||||
|
// Agent step approval (known: Silent Success with focus)
|
||||||
|
'antigravity.agent.acceptAgentStep', // SDK: ACCEPT_AGENT_STEP
|
||||||
|
// Cascade action (experimental)
|
||||||
|
// 'antigravity.executeCascadeAction', // SDK: needs action param — skip
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'antigravity.terminalCommand.reject', // SDK: TERMINAL_REJECT
|
||||||
|
'antigravity.command.reject', // SDK: COMMAND_REJECT
|
||||||
|
'antigravity.agent.rejectAgentStep', // SDK: REJECT_AGENT_STEP
|
||||||
|
];
|
||||||
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
const cmd = commands[i];
|
||||||
|
try {
|
||||||
|
const t0 = Date.now();
|
||||||
|
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] executing: ${cmd}`);
|
||||||
|
const result = await vscode.commands.executeCommand(cmd);
|
||||||
|
const dt = Date.now() - t0;
|
||||||
|
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] returned: ${JSON.stringify(result)} (${dt}ms)`);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] ❌ FAIL: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── Strategy 3: ResolveOutstandingSteps (REJECT ONLY — this CANCELS!) ──
|
||||||
|
if (!approved && sdk) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-3] ResolveOutstandingSteps (REJECT/CANCEL only!)`);
|
||||||
|
const rpcResult = await sdk.ls.rawRPC('ResolveOutstandingSteps', {
|
||||||
|
cascadeId: sessionId,
|
||||||
|
});
|
||||||
|
logToFile(`[APPROVAL-3] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
|
return `RPC-3:ResolveOutstandingSteps(cancel)`;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-3] ❌ FAIL: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
|
||||||
|
return `ALL_ATTEMPTED:${action}`;
|
||||||
|
}
|
||||||
// ─── Activation ───
|
// ─── Activation ───
|
||||||
async function activate(context) {
|
async function activate(context) {
|
||||||
console.log('Gravity Bridge: activating...');
|
console.log('Gravity Bridge: activating...');
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -660,23 +660,6 @@ function generateApprovalObserverScript(_port: number): string {
|
|||||||
if(document.body)searchRoots.push(document.body);
|
if(document.body)searchRoots.push(document.body);
|
||||||
if(!searchRoots.length)return;
|
if(!searchRoots.length)return;
|
||||||
|
|
||||||
// ── DIAGNOSTIC: dump ALL button-like elements EVERY scan cycle ──
|
|
||||||
{
|
|
||||||
var dumpBtns=[];
|
|
||||||
var totalChecked=0;
|
|
||||||
for(var dr=0;dr<searchRoots.length;dr++){
|
|
||||||
var dbs=searchRoots[dr].querySelectorAll('button,[role="button"]');
|
|
||||||
totalChecked+=dbs.length;
|
|
||||||
for(var di=0;di<dbs.length;di++){
|
|
||||||
var db=dbs[di];
|
|
||||||
var dt=(db.textContent||'').trim();
|
|
||||||
var dtShort=dt.replace(/\\s+/g,' ').substring(0,50);
|
|
||||||
dumpBtns.push(db.tagName+'['+dt.length+']:'+dtShort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log('[BTN-DUMP] roots='+searchRoots.length+' total='+totalChecked+' btns='+dumpBtns.length+': '+JSON.stringify(dumpBtns.slice(0,15)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var seen={}; // dedupe buttons across search roots
|
var seen={}; // dedupe buttons across search roots
|
||||||
for(var r=0;r<searchRoots.length;r++){
|
for(var r=0;r<searchRoots.length;r++){
|
||||||
var allBtns=searchRoots[r].querySelectorAll('button');
|
var allBtns=searchRoots[r].querySelectorAll('button');
|
||||||
@@ -906,6 +889,7 @@ function setupMonitor() {
|
|||||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||||
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
||||||
let stallProbed = false; // prevent repeated step probes during same stall
|
let stallProbed = false; // prevent repeated step probes during same stall
|
||||||
|
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
@@ -988,9 +972,8 @@ function setupMonitor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── PRIMARY: Step-probe-based approval detection ──
|
// ── PRIMARY: Step-probe-based approval detection ──
|
||||||
// latestToolCallStep does NOT exist in GetAllCascadeTrajectories response.
|
// On stall (idle=1, ~5s), probe GetCascadeTrajectorySteps to check WAITING.
|
||||||
// Instead, on early stall (idle=2, ~10s), probe GetCascadeTrajectorySteps
|
// 775-step limit: probe fails for long sessions → faster stall fallback.
|
||||||
// to fetch the latest step and check if it's a tool call awaiting approval.
|
|
||||||
|
|
||||||
// ── STALL-BASED approval detection with step probe ──
|
// ── STALL-BASED approval detection with step probe ──
|
||||||
|
|
||||||
@@ -1025,7 +1008,7 @@ function setupMonitor() {
|
|||||||
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
|
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
|
||||||
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
|
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
|
||||||
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet
|
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet
|
||||||
if (consecutiveIdleCount >= 1 && consecutiveIdleCount % 2 === 1 && !stallProbed) {
|
if (consecutiveIdleCount >= 1 && !stallProbed) {
|
||||||
try {
|
try {
|
||||||
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
cascadeId: bestSessionId,
|
cascadeId: bestSessionId,
|
||||||
@@ -1034,6 +1017,9 @@ function setupMonitor() {
|
|||||||
const steps = stepsResp.steps;
|
const steps = stepsResp.steps;
|
||||||
// Diagnostic: compare returned steps vs trajectory stepCount
|
// Diagnostic: compare returned steps vs trajectory stepCount
|
||||||
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`);
|
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`);
|
||||||
|
if (steps.length < currentCount) {
|
||||||
|
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
|
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
|
||||||
let foundWaiting = false;
|
let foundWaiting = false;
|
||||||
@@ -1096,8 +1082,9 @@ function setupMonitor() {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
const cooldownOk = (now - lastPendingTime) > 60_000;
|
||||||
|
|
||||||
if (consecutiveIdleCount >= 8 && sawRunningAfterPending && cooldownOk) {
|
const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
|
||||||
// 8 polls × 5s = 40 seconds — fallback (reduced from 100s)
|
if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
|
||||||
|
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
|
||||||
lastPendingStepIndex = currentCount;
|
lastPendingStepIndex = currentCount;
|
||||||
lastPendingTime = now;
|
lastPendingTime = now;
|
||||||
sawRunningAfterPending = false;
|
sawRunningAfterPending = false;
|
||||||
@@ -1107,7 +1094,7 @@ function setupMonitor() {
|
|||||||
|
|
||||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
||||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
||||||
} else if (consecutiveIdleCount === 8) {
|
} else if (consecutiveIdleCount === fallbackThreshold) {
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
if (!sawRunningAfterPending) reasons.push('needDelta>0');
|
if (!sawRunningAfterPending) reasons.push('needDelta>0');
|
||||||
if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
||||||
@@ -1123,9 +1110,12 @@ function setupMonitor() {
|
|||||||
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
lastNotifyStepIndex = notifyStep.stepIndex;
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||||||
if (content.length > 10) {
|
// Filter: only relay meaningful notifications (skip trivial ones)
|
||||||
|
if (content.length > 50) {
|
||||||
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
||||||
|
} else if (content.length > 0) {
|
||||||
|
logToFile(`[POLL] NOTIFY skipped (too short: ${content.length} chars): ${content.substring(0, 80)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,8 +1126,15 @@ function setupMonitor() {
|
|||||||
const tb = taskStep.step?.taskBoundary;
|
const tb = taskStep.step?.taskBoundary;
|
||||||
if (tb?.taskName) {
|
if (tb?.taskName) {
|
||||||
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||||||
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
// Filter: skip status-only updates with same task name (noise)
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
|
||||||
|
if (taskText !== lastRelayedTaskText) {
|
||||||
|
lastRelayedTaskText = taskText;
|
||||||
|
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
||||||
|
} else {
|
||||||
|
logToFile(`[POLL] TASK skipped (duplicate): "${tb.taskName}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -1207,9 +1204,10 @@ async function processResponseFile(filePath: string) {
|
|||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══ APPROVAL STRATEGY ═══
|
// ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
|
||||||
// DOM observer approvals: renderer handles clicking via pollResponse — skip VS Code commands
|
// Tries multiple methods sequentially with detailed logging.
|
||||||
// Stall-detection approvals: use VS Code commands as fallback (focus-dependent)
|
// DOM observer: renderer handles clicking via pollResponse
|
||||||
|
// Step probe/stall: try RPC → VS Code commands → log results
|
||||||
|
|
||||||
const approved = resp.approved;
|
const approved = resp.approved;
|
||||||
|
|
||||||
@@ -1217,47 +1215,15 @@ async function processResponseFile(filePath: string) {
|
|||||||
// DOM observer path: renderer polls /response/:rid and clicks directly
|
// DOM observer path: renderer polls /response/:rid and clicks directly
|
||||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||||
} else {
|
} else {
|
||||||
// Step probe / stall path: relay approval to DOM observer pending files
|
// Step probe / stall path: try all approval strategies
|
||||||
// The renderer polls /response/:rid and can click the actual button
|
logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
|
||||||
logToFile(`[RESPONSE] step_probe/stall → relaying to DOM observer pending files`);
|
const approvalResult = await tryApprovalStrategies(approved, sessionId);
|
||||||
|
logToFile(`[RESPONSE] approval result: ${approvalResult}`);
|
||||||
const pendingDir = path.join(bridgePath, 'pending');
|
|
||||||
const responseDir = path.join(bridgePath, 'response');
|
|
||||||
if (!fs.existsSync(responseDir)) fs.mkdirSync(responseDir, { recursive: true });
|
|
||||||
|
|
||||||
let relayCount = 0;
|
|
||||||
try {
|
|
||||||
const files = fs.readdirSync(pendingDir).filter((f: string) => f.endsWith('.json'));
|
|
||||||
for (const f of files) {
|
|
||||||
try {
|
|
||||||
const pd = JSON.parse(fs.readFileSync(path.join(pendingDir, f), 'utf-8'));
|
|
||||||
if (pd.source === 'dom_observer' && pd.status === 'pending') {
|
|
||||||
// Write response file for this DOM observer pending
|
|
||||||
const responsePayload = {
|
|
||||||
request_id: pd.request_id,
|
|
||||||
approved,
|
|
||||||
timestamp: Date.now() / 1000,
|
|
||||||
};
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(responseDir, `${pd.request_id}.json`),
|
|
||||||
JSON.stringify(responsePayload, null, 2), 'utf-8'
|
|
||||||
);
|
|
||||||
relayCount++;
|
|
||||||
logToFile(`[RESPONSE] relayed to DOM pending: ${pd.request_id} approved=${approved}`);
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
logToFile(`[RESPONSE] relay scan error: ${e.message}`);
|
|
||||||
}
|
|
||||||
logToFile(`[RESPONSE] relayed to ${relayCount} DOM observer pending files`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'stall'})`);
|
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||||
|
|
||||||
// Cleanup response file — BUT NOT for DOM observer!
|
// Cleanup response file — BUT NOT for DOM observer!
|
||||||
// DOM observer: renderer's pollResponse needs to find it via HTTP GET /response/:rid
|
|
||||||
// Non-DOM (stall/step_probe relay): watcher already handled it, safe to delete
|
|
||||||
if (!isDomObserver) {
|
if (!isDomObserver) {
|
||||||
try { fs.unlinkSync(filePath); } catch { }
|
try { fs.unlinkSync(filePath); } catch { }
|
||||||
}
|
}
|
||||||
@@ -1409,6 +1375,134 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Multi-Strategy Approval Execution ───
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try multiple approval methods sequentially.
|
||||||
|
* Returns a string describing which method succeeded (or all failed).
|
||||||
|
*
|
||||||
|
* Strategy order (most reliable first):
|
||||||
|
* 1. HandleCascadeUserInteraction RPC (cross-platform, no focus)
|
||||||
|
* 2. VS Code accept/reject commands (focus-dependent)
|
||||||
|
* 3. Log failure for manual intervention
|
||||||
|
*/
|
||||||
|
async function tryApprovalStrategies(approved: boolean, sessionId: string): Promise<string> {
|
||||||
|
const action = approved ? 'APPROVE' : 'REJECT';
|
||||||
|
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
|
||||||
|
|
||||||
|
// ── Strategy 1: HandleCascadeUserInteraction RPC ──
|
||||||
|
if (sdk) {
|
||||||
|
// Try variant A: { cascadeId, approved }
|
||||||
|
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})`;
|
||||||
|
} catch (e: any) {
|
||||||
|
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: 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: VS Code Commands — step-type-specific (focus-dependent) ──
|
||||||
|
// Per SDK research (2026-03-09):
|
||||||
|
// Run button = terminal command → terminalCommand.run / terminalCommand.accept
|
||||||
|
// Code changes = agent step → agent.acceptAgentStep
|
||||||
|
// General commands = command.accept
|
||||||
|
// Previously only tried acceptAgentStep (Silent Success) — now tries ALL 7
|
||||||
|
|
||||||
|
// Try to focus the panel first (required for command.accept / acceptAgentStep)
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-2] focusing panel...`);
|
||||||
|
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
await vscode.commands.executeCommand('antigravity.agentSidePanel.focus');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
logToFile(`[APPROVAL-2] panel focus attempted (agentPanel + agentSidePanel)`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-2] panel focus failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All 7 approval commands in priority order (terminal first for Run button)
|
||||||
|
const commands = approved
|
||||||
|
? [
|
||||||
|
// Terminal commands (Run button) — UNTESTED INDIVIDUALLY (devlog-004)
|
||||||
|
'antigravity.terminalCommand.run', // SDK: TERMINAL_RUN
|
||||||
|
'antigravity.terminalCommand.accept', // SDK: TERMINAL_ACCEPT
|
||||||
|
// General command approval
|
||||||
|
'antigravity.command.accept', // SDK: COMMAND_ACCEPT
|
||||||
|
// Agent step approval (known: Silent Success with focus)
|
||||||
|
'antigravity.agent.acceptAgentStep', // SDK: ACCEPT_AGENT_STEP
|
||||||
|
// Cascade action (experimental)
|
||||||
|
// 'antigravity.executeCascadeAction', // SDK: needs action param — skip
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'antigravity.terminalCommand.reject', // SDK: TERMINAL_REJECT
|
||||||
|
'antigravity.command.reject', // SDK: COMMAND_REJECT
|
||||||
|
'antigravity.agent.rejectAgentStep', // SDK: REJECT_AGENT_STEP
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
const cmd = commands[i];
|
||||||
|
try {
|
||||||
|
const t0 = Date.now();
|
||||||
|
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] executing: ${cmd}`);
|
||||||
|
const result = await vscode.commands.executeCommand(cmd);
|
||||||
|
const dt = Date.now() - t0;
|
||||||
|
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] returned: ${JSON.stringify(result)} (${dt}ms)`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] ❌ FAIL: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Strategy 3: ResolveOutstandingSteps (REJECT ONLY — this CANCELS!) ──
|
||||||
|
if (!approved && sdk) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-3] ResolveOutstandingSteps (REJECT/CANCEL only!)`);
|
||||||
|
const rpcResult = await sdk.ls.rawRPC('ResolveOutstandingSteps', {
|
||||||
|
cascadeId: sessionId,
|
||||||
|
});
|
||||||
|
logToFile(`[APPROVAL-3] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
|
return `RPC-3:ResolveOutstandingSteps(cancel)`;
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-3] ❌ FAIL: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
|
||||||
|
return `ALL_ATTEMPTED:${action}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Activation ───
|
// ─── Activation ───
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
|
|||||||
Reference in New Issue
Block a user