fix(extension): step_type mapping bug + diff_review handler refactor

- Separate read tools (file_permission) from write tools (code_edit)
- write_to_file/replace_file_content now use AcknowledgeCascadeCodeEdit RPC
- diff_review: 2-strategy approach (RPC first, openReviewChanges fallback)
- Track modified_files and edit_step_indices in diff_review pending
- known-issues: 3 new entries (pending accumulation, step_type bug, isDirty failure)
This commit is contained in:
Variet Worker
2026-03-16 13:52:02 +09:00
parent 078f721187
commit d521dd5fa3
6 changed files with 252 additions and 63 deletions

View File

@@ -539,4 +539,20 @@
- **해결**: `recentPendingSteps` Map (TTL 60초) 추가. `${conversationId}:${stepIndex}`를 키로 사용. 파일 삭제 후에도 메모리에서 차단. delta>0에서 해당 세션 항목 클리어 - **해결**: `recentPendingSteps` Map (TTL 60초) 추가. `${conversationId}:${stepIndex}`를 키로 사용. 파일 삭제 후에도 메모리에서 차단. delta>0에서 해당 세션 항목 클리어
- **주의**: DOM observer HTTP `/pending` 경로(L738-812)는 `writePendingApproval()`을 우회하므로 이 메모리 dedup 미적용. "Run" 필터(L757)와 file_permission dedup(L786-800)이 대신 방어 - **주의**: DOM observer HTTP `/pending` 경로(L738-812)는 `writePendingApproval()`을 우회하므로 이 메모리 dedup 미적용. "Run" 필터(L757)와 file_permission dedup(L786-800)이 대신 방어
### [2026-03-16] pending 파일 무한 누적 — Collector/Bot이 로컬 파일 삭제 안 함
- **증상**: `bridge/pending/` 디렉토리에 56개 파일 누적 (auto_resolved 36개 + pending 20개)
- **원인**: `remote` 모드에서 Bot은 Gateway 서버에서 동작하여 로컬 pending 파일 직접 삭제 불가. Collector는 pending을 Gateway로 전달하지만 auto_resolved/expired 상태 파일을 로컬에서 삭제하는 로직 없음
- **해결**: (미완료) Collector에 로컬 pending cleanup 로직 추가 필요. 리팩토링 시 파일 기반 IPC 자체를 제거하면 근본 해결
- **주의**: pending 파일 누적은 Collector `_forward_pending_loop`의 매 사이클 읽기/해싱을 느리게 하여 새 pending 전달 지연 유발
### [2026-03-16] step_type 매핑 버그 — write_to_file이 file_permission으로 잘못 매핑
- **증상**: Discord에서 코드 편집 승인 시 `filePermission` RPC 전송 → 파일 읽기용 interaction이 전송됨
- **원인**: L2060/L2121에서 `write_to_file`, `replace_file_content`, `multi_replace_file_content`가 읽기 도구와 함께 `file_permission`으로 매핑됨. `tryApprovalStrategies``AcknowledgeCascadeCodeEdit` 경로(L2907)는 `typeLower.includes('write_to_file')` 체크 → 실제 값이 `file_permission`이라 영영 도달 불가
- **해결**: 읽기 도구(`view_file`, `list_dir` 등)와 쓰기 도구(`write_to_file`, `replace_file_content`, `multi_replace_file_content`)를 분리. 쓰기 도구는 `code_edit` step_type 사용. `tryApprovalStrategies``code_edit` 분기 추가
- **주의**: AG는 대부분 파일 쓰기에 WAITING을 안 만듦 (한번 승인하면 이후 자동). 실제 WAITING은 새 대화 첫 파일 접근 시에만 발생
### [2026-03-16] diff_review isDirty 실패 — AG diff는 VS Code dirty 아님
- **증상**: Discord diff_review Accept 클릭 → `agentAcceptAllInFile` 실행 → `isDirty` 문서 0개 → 대상 없이 실행 → 효과 없음
- **원인**: AG의 stacked code review는 VS Code의 `isDirty` 상태와 무관. AG 자체 diff 시스템으로 관리되며, 파일은 이미 디스크에 저장됨
- **해결**: (1차) `AcknowledgeCascadeCodeEdit` RPC로 직접 protocol 수락 시도 → (2차 fallback) `openReviewChanges` 패널 열기 + 수정 파일 focus + `agentAcceptAllInFile` 실행
- **주의**: diff_review pending에 `modified_files` (전체 경로)와 `edit_step_indices` (step 번호) 포함 필수. `agentAcceptAllInFile`은 diff 에디터가 포커스된 상태에서만 동작할 수 있음

View File

@@ -3,3 +3,4 @@
| # | 시간 | 작업 | 커밋 | 상태 | | # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------| |---|------|------|------|------|
| 001 | 07:30~11:10 | 승인 상태 관리 근본 원인 분석 + v0.3.12 수정 (sawRunningAfterPending gate) + approval-flow.md 시스템 Flow 문서 + known-issues 2건 추가 | `2d9fe96` | ✅ | | 001 | 07:30~11:10 | 승인 상태 관리 근본 원인 분석 + v0.3.12 수정 (sawRunningAfterPending gate) + approval-flow.md 시스템 Flow 문서 + known-issues 2건 추가 | `2d9fe96` | ✅ |
| 002 | 13:25~13:48 | DOM Observer 전체 분석 + step_type 매핑 버그 수정 (write→code_edit) + diff_review 핸들러 리팩토링 (AcknowledgeCascadeCodeEdit RPC + openReviewChanges fallback) + known-issues 3건 추가 | `` | 🔧 |

View File

@@ -0,0 +1,29 @@
# DOM Observer 분석 + diff_review 수정
- **시간**: 2026-03-16 13:25~13:48
- **Commit**: ``
- **Vikunja**: 미정
## 분석 결과
extension.ts 3,166줄 전체를 5개 기능 영역으로 분류:
1. AG 정보 추출 (SDK, 세션 모니터, step probe) ~700줄
2. 승인 실행 (RPC) ~200줄
3. 파일 기반 IPC (리팩토링 대상) ~500줄
4. DOM Observer 인프라 (제거 검토) ~1,100줄
5. 기타 유틸 ~200줄
**DOM Observer는 제거 가능하다** — RPC가 모든 승인을 커버하며, diff_review도 VS Code 명령으로 처리 가능.
단, diff_review 원격 실행이 먼저 동작해야 DOM 제거에 동의 가능 (사용자 요구).
## 결정 사항
- write 도구를 `code_edit` step_type으로 분리 (기존: `file_permission`에 혼재)
- diff_review 핸들러를 2-strategy 방식으로 리팩토링:
1. `AcknowledgeCascadeCodeEdit` RPC (UI 조작 불필요)
2. `openReviewChanges` + 파일 포커스 + `agentAcceptAllInFile` (fallback)
- pending에 `modified_files`(전체경로)와 `edit_step_indices`(step 번호) 포함
## 미완료
- diff_review 실제 테스트 (AG 재시작 후 Accept all 버튼 동작 확인 필요)
- `AcknowledgeCascadeCodeEdit` RPC가 stepIndices 없이도 전체 수락하는지 확인 필요
- DOM Observer 제거 리팩토링 → diff_review 동작 확인 후 진행

View File

@@ -1746,6 +1746,8 @@ function setupMonitor() {
let wasRunning = false; // track RUNNING→IDLE transition for response capture let wasRunning = false; // track RUNNING→IDLE transition for response capture
let lastUserInputStepIdx = -1; // track user input for response matching let lastUserInputStepIdx = -1; // track user input for response matching
let pendingModifiedFiles = []; // accumulate modified files during RUNNING let pendingModifiedFiles = []; // accumulate modified files during RUNNING
let pendingModifiedFilePaths = []; // full paths for diff review
let pendingEditStepIndices = []; // step indices for AcknowledgeCascadeCodeEdit
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
setInterval(async () => { setInterval(async () => {
pollCount++; pollCount++;
@@ -1885,6 +1887,8 @@ function setupMonitor() {
const bn = tf.split(/[\\/]/).pop() || tf; const bn = tf.split(/[\\/]/).pop() || tf;
if (!pendingModifiedFiles.includes(bn)) { if (!pendingModifiedFiles.includes(bn)) {
pendingModifiedFiles.push(bn); pendingModifiedFiles.push(bn);
pendingModifiedFilePaths.push(tf);
pendingEditStepIndices.push(actualIdx);
logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`); logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`);
} }
} }
@@ -2070,7 +2074,9 @@ function setupMonitor() {
conversation_id: activeSessionId, conversation_id: activeSessionId,
command, command,
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`, description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search', 'replace_file_content', 'write_to_file', 'multi_replace_file_content'].includes(toolName) ? 'file_permission' : toolName, step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
: toolName,
step_index: actualIndex, step_index: actualIndex,
source: 'step_probe_offset', source: 'step_probe_offset',
}); });
@@ -2133,7 +2139,9 @@ function setupMonitor() {
conversation_id: activeSessionId, conversation_id: activeSessionId,
command, command,
description, description,
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search', 'replace_file_content', 'write_to_file', 'multi_replace_file_content'].includes(toolName) ? 'file_permission' : toolName, step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
: toolName,
step_index: si, step_index: si,
source: 'step_probe', source: 'step_probe',
}); });
@@ -2437,8 +2445,12 @@ function setupMonitor() {
{ text: 'Accept all', index: 0 }, { text: 'Accept all', index: 0 },
{ text: 'Reject all', index: 1 }, { text: 'Reject all', index: 1 },
], ],
modified_files: pendingModifiedFilePaths.slice(0, 20),
edit_step_indices: pendingEditStepIndices.slice(0, 20),
}); });
pendingModifiedFiles = []; // reset after notification pendingModifiedFiles = []; // reset after notification
pendingModifiedFilePaths = [];
pendingEditStepIndices = [];
} }
wasRunning = isRunning; wasRunning = isRunning;
} }
@@ -2587,39 +2599,96 @@ async function processResponseFile(filePath) {
// DOM observer: renderer handles clicking via pollResponse // DOM observer: renderer handles clicking via pollResponse
// Step probe/stall: try RPC → VS Code commands → log results // Step probe/stall: try RPC → VS Code commands → log results
const approved = resp.approved; const approved = resp.approved;
// ── diff_review: Accept all / Reject all via VS Code commands ── // ── diff_review: Accept all / Reject all ──
if (pendingStepType === 'diff_review') { if (pendingStepType === 'diff_review') {
const btnIdx = resp.button_index ?? -1; const btnIdx = resp.button_index ?? -1;
const isAccept = btnIdx === 0 || (btnIdx === -1 && approved); const isAccept = btnIdx === 0 || (btnIdx === -1 && approved);
const cmd = isAccept const cmd = isAccept
? 'antigravity.prioritized.agentAcceptAllInFile' ? 'antigravity.prioritized.agentAcceptAllInFile'
: 'antigravity.prioritized.agentRejectAllInFile'; : 'antigravity.prioritized.agentRejectAllInFile';
logToFile(`[RESPONSE] diff_review → ${cmd} (btnIdx=${btnIdx})`); logToFile(`[RESPONSE] diff_review → ${isAccept ? 'ACCEPT' : 'REJECT'} (btnIdx=${btnIdx})`);
try { let diffReviewDone = false;
// Find dirty documents and focus each before executing const targetSession = sessionId || activeSessionId;
const dirtyDocs = vscode.workspace.textDocuments.filter(d => d.isDirty); // ── Strategy 1: AcknowledgeCascadeCodeEdit RPC ──
logToFile(`[RESPONSE] diff_review: ${dirtyDocs.length} dirty docs found`); // Accept/reject all pending code edits via protocol (no UI interaction needed)
if (dirtyDocs.length > 0) { if (sdk) {
for (const doc of dirtyDocs) { try {
try { // Get tracked step indices from pending data (or use all recent edit steps)
await vscode.window.showTextDocument(doc, { preview: false }); const trackedSteps = [];
await new Promise(r => setTimeout(r, 200)); // brief wait for focus const pendingDir = path.join(bridgePath, 'pending');
await vscode.commands.executeCommand(cmd); try {
logToFile(`[RESPONSE] diff_review: ${cmd} on ${doc.fileName.split(/[\\/]/).pop()} OK`); const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
} if (fs.existsSync(pendingFile)) {
catch (perFileErr) { const pd = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
logToFile(`[RESPONSE] diff_review per-file error: ${perFileErr.message?.substring(0, 80)}`); if (pd.edit_step_indices)
trackedSteps.push(...pd.edit_step_indices);
} }
} }
catch { }
// If no tracked steps, use the step_index from the pending
if (trackedSteps.length === 0 && pendingStepIndex > 0) {
trackedSteps.push(pendingStepIndex);
}
logToFile(`[DIFF-REVIEW-RPC] AcknowledgeCascadeCodeEdit(session=${targetSession.substring(0, 8)}, accept=${isAccept}, steps=[${trackedSteps.join(',')}])`);
const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
cascadeId: targetSession,
accept: isAccept,
...(trackedSteps.length > 0 ? { stepIndices: trackedSteps } : {}),
});
logToFile(`[DIFF-REVIEW-RPC] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
diffReviewDone = true;
} }
else { catch (rpcErr) {
// No dirty docs — try command anyway logToFile(`[DIFF-REVIEW-RPC] ❌ ${rpcErr.message.substring(0, 200)}`);
await vscode.commands.executeCommand(cmd);
logToFile(`[RESPONSE] diff_review: no dirty docs, command executed anyway`);
} }
} }
catch (cmdErr) { // ── Strategy 2: Open review panel + focus file + VS Code command ──
logToFile(`[RESPONSE] diff_review command error: ${cmdErr.message}`); if (!diffReviewDone) {
try {
// Step 2a: Open the Review Changes panel
try {
await vscode.commands.executeCommand('antigravity.openReviewChanges');
logToFile(`[DIFF-REVIEW-CMD] openReviewChanges OK`);
await new Promise(r => setTimeout(r, 500));
}
catch { }
// Step 2b: Find modified files from pending data
let modifiedFiles = [];
try {
const pendingFile = path.join(bridgePath, 'pending', `${resp.request_id}.json`);
if (fs.existsSync(pendingFile)) {
const pd = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pd.modified_files)
modifiedFiles = pd.modified_files;
}
}
catch { }
// Step 2c: Open and focus each modified file, then execute
if (modifiedFiles.length > 0) {
for (const filePath of modifiedFiles) {
try {
const uri = vscode.Uri.file(filePath);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await new Promise(r => setTimeout(r, 300));
await vscode.commands.executeCommand(cmd);
logToFile(`[DIFF-REVIEW-CMD] ${cmd} on ${filePath.split(/[\\/]/).pop()} OK`);
}
catch (e) {
logToFile(`[DIFF-REVIEW-CMD] per-file error on ${filePath}: ${e.message?.substring(0, 80)}`);
}
}
}
else {
// No file list — just execute command (best effort)
await vscode.commands.executeCommand(cmd);
logToFile(`[DIFF-REVIEW-CMD] ${cmd} executed (no file list)`);
}
diffReviewDone = true;
}
catch (cmdErr) {
logToFile(`[DIFF-REVIEW-CMD] error: ${cmdErr.message}`);
}
} }
} }
else if (isDomObserver) { else if (isDomObserver) {
@@ -2949,21 +3018,23 @@ async function tryApprovalStrategies(approved, sessionId, stepType = '', stepInd
// Build interaction sub-message based on step_type // Build interaction sub-message based on step_type
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', ''); const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
let interactionPayload = {}; let interactionPayload = {};
if (typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) { if (typeLower.includes('code_edit') || typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
// CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC // CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC
try { try {
logToFile(`[APPROVAL-PROTO-ACK] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0, 8)}, accept=true, stepIndices=[${effectiveStepIndex}])`); logToFile(`[APPROVAL-CODE-EDIT] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', { const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
cascadeId: sessionId, cascadeId: sessionId,
accept: true, accept: approved,
stepIndices: [effectiveStepIndex], stepIndices: [effectiveStepIndex],
}); });
logToFile(`[APPROVAL-PROTO-ACK] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`); logToFile(`[APPROVAL-CODE-EDIT] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
return `RPC-PROTO-ACK:AcknowledgeCascadeCodeEdit`; return `RPC:AcknowledgeCascadeCodeEdit(accept=${approved})`;
} }
catch (e) { catch (e) {
logToFile(`[APPROVAL-PROTO-ACK] ❌ ${e.message.substring(0, 200)}`); logToFile(`[APPROVAL-CODE-EDIT] ❌ ${e.message.substring(0, 200)}`);
// Fall through to HandleCascadeUserInteraction // Fallback: try HandleCascadeUserInteraction with runCommand
logToFile(`[APPROVAL-CODE-EDIT] falling back to HandleCascadeUserInteraction`);
interactionPayload = { runCommand: { confirm: true } };
} }
} }
// Map step_type to interaction sub-message field // Map step_type to interaction sub-message field

File diff suppressed because one or more lines are too long

View File

@@ -1739,6 +1739,8 @@ function setupMonitor() {
let wasRunning = false; // track RUNNING→IDLE transition for response capture let wasRunning = false; // track RUNNING→IDLE transition for response capture
let lastUserInputStepIdx = -1; // track user input for response matching let lastUserInputStepIdx = -1; // track user input for response matching
let pendingModifiedFiles: string[] = []; // accumulate modified files during RUNNING let pendingModifiedFiles: string[] = []; // accumulate modified files during RUNNING
let pendingModifiedFilePaths: string[] = []; // full paths for diff review
let pendingEditStepIndices: number[] = []; // step indices for AcknowledgeCascadeCodeEdit
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
setInterval(async () => { setInterval(async () => {
@@ -1884,6 +1886,8 @@ function setupMonitor() {
const bn = tf.split(/[\\/]/).pop() || tf; const bn = tf.split(/[\\/]/).pop() || tf;
if (!pendingModifiedFiles.includes(bn)) { if (!pendingModifiedFiles.includes(bn)) {
pendingModifiedFiles.push(bn); pendingModifiedFiles.push(bn);
pendingModifiedFilePaths.push(tf);
pendingEditStepIndices.push(actualIdx);
logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`); logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`);
} }
} }
@@ -2057,7 +2061,9 @@ function setupMonitor() {
conversation_id: activeSessionId, conversation_id: activeSessionId,
command, command,
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`, description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search', 'replace_file_content', 'write_to_file', 'multi_replace_file_content'].includes(toolName) ? 'file_permission' : toolName, step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
: toolName,
step_index: actualIndex, step_index: actualIndex,
source: 'step_probe_offset', source: 'step_probe_offset',
}); });
@@ -2118,7 +2124,9 @@ function setupMonitor() {
conversation_id: activeSessionId, conversation_id: activeSessionId,
command, command,
description, description,
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search', 'replace_file_content', 'write_to_file', 'multi_replace_file_content'].includes(toolName) ? 'file_permission' : toolName, step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
: toolName,
step_index: si, step_index: si,
source: 'step_probe', source: 'step_probe',
}); });
@@ -2406,8 +2414,12 @@ function setupMonitor() {
{ text: 'Accept all', index: 0 }, { text: 'Accept all', index: 0 },
{ text: 'Reject all', index: 1 }, { text: 'Reject all', index: 1 },
], ],
}); modified_files: pendingModifiedFilePaths.slice(0, 20),
edit_step_indices: pendingEditStepIndices.slice(0, 20),
} as any);
pendingModifiedFiles = []; // reset after notification pendingModifiedFiles = []; // reset after notification
pendingModifiedFilePaths = [];
pendingEditStepIndices = [];
} }
wasRunning = isRunning; wasRunning = isRunning;
} catch (e: any) { } catch (e: any) {
@@ -2556,36 +2568,94 @@ async function processResponseFile(filePath: string) {
const approved = resp.approved; const approved = resp.approved;
// ── diff_review: Accept all / Reject all via VS Code commands ── // ── diff_review: Accept all / Reject all ──
if (pendingStepType === 'diff_review') { if (pendingStepType === 'diff_review') {
const btnIdx = resp.button_index ?? -1; const btnIdx = resp.button_index ?? -1;
const isAccept = btnIdx === 0 || (btnIdx === -1 && approved); const isAccept = btnIdx === 0 || (btnIdx === -1 && approved);
const cmd = isAccept const cmd = isAccept
? 'antigravity.prioritized.agentAcceptAllInFile' ? 'antigravity.prioritized.agentAcceptAllInFile'
: 'antigravity.prioritized.agentRejectAllInFile'; : 'antigravity.prioritized.agentRejectAllInFile';
logToFile(`[RESPONSE] diff_review → ${cmd} (btnIdx=${btnIdx})`); logToFile(`[RESPONSE] diff_review → ${isAccept ? 'ACCEPT' : 'REJECT'} (btnIdx=${btnIdx})`);
try {
// Find dirty documents and focus each before executing let diffReviewDone = false;
const dirtyDocs = vscode.workspace.textDocuments.filter(d => d.isDirty); const targetSession = sessionId || activeSessionId;
logToFile(`[RESPONSE] diff_review: ${dirtyDocs.length} dirty docs found`);
if (dirtyDocs.length > 0) { // ── Strategy 1: AcknowledgeCascadeCodeEdit RPC ──
for (const doc of dirtyDocs) { // Accept/reject all pending code edits via protocol (no UI interaction needed)
try { if (sdk) {
await vscode.window.showTextDocument(doc, { preview: false }); try {
await new Promise(r => setTimeout(r, 200)); // brief wait for focus // Get tracked step indices from pending data (or use all recent edit steps)
await vscode.commands.executeCommand(cmd); const trackedSteps: number[] = [];
logToFile(`[RESPONSE] diff_review: ${cmd} on ${doc.fileName.split(/[\\/]/).pop()} OK`); const pendingDir = path.join(bridgePath, 'pending');
} catch (perFileErr: any) { try {
logToFile(`[RESPONSE] diff_review per-file error: ${perFileErr.message?.substring(0, 80)}`); const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
if (fs.existsSync(pendingFile)) {
const pd = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pd.edit_step_indices) trackedSteps.push(...pd.edit_step_indices);
} }
} catch { }
// If no tracked steps, use the step_index from the pending
if (trackedSteps.length === 0 && pendingStepIndex > 0) {
trackedSteps.push(pendingStepIndex);
} }
} else {
// No dirty docs — try command anyway logToFile(`[DIFF-REVIEW-RPC] AcknowledgeCascadeCodeEdit(session=${targetSession.substring(0, 8)}, accept=${isAccept}, steps=[${trackedSteps.join(',')}])`);
await vscode.commands.executeCommand(cmd); const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
logToFile(`[RESPONSE] diff_review: no dirty docs, command executed anyway`); cascadeId: targetSession,
accept: isAccept,
...(trackedSteps.length > 0 ? { stepIndices: trackedSteps } : {}),
});
logToFile(`[DIFF-REVIEW-RPC] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
diffReviewDone = true;
} catch (rpcErr: any) {
logToFile(`[DIFF-REVIEW-RPC] ❌ ${rpcErr.message.substring(0, 200)}`);
}
}
// ── Strategy 2: Open review panel + focus file + VS Code command ──
if (!diffReviewDone) {
try {
// Step 2a: Open the Review Changes panel
try {
await vscode.commands.executeCommand('antigravity.openReviewChanges');
logToFile(`[DIFF-REVIEW-CMD] openReviewChanges OK`);
await new Promise(r => setTimeout(r, 500));
} catch { }
// Step 2b: Find modified files from pending data
let modifiedFiles: string[] = [];
try {
const pendingFile = path.join(bridgePath, 'pending', `${resp.request_id}.json`);
if (fs.existsSync(pendingFile)) {
const pd = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pd.modified_files) modifiedFiles = pd.modified_files;
}
} catch { }
// Step 2c: Open and focus each modified file, then execute
if (modifiedFiles.length > 0) {
for (const filePath of modifiedFiles) {
try {
const uri = vscode.Uri.file(filePath);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await new Promise(r => setTimeout(r, 300));
await vscode.commands.executeCommand(cmd);
logToFile(`[DIFF-REVIEW-CMD] ${cmd} on ${filePath.split(/[\\/]/).pop()} OK`);
} catch (e: any) {
logToFile(`[DIFF-REVIEW-CMD] per-file error on ${filePath}: ${e.message?.substring(0, 80)}`);
}
}
} else {
// No file list — just execute command (best effort)
await vscode.commands.executeCommand(cmd);
logToFile(`[DIFF-REVIEW-CMD] ${cmd} executed (no file list)`);
}
diffReviewDone = true;
} catch (cmdErr: any) {
logToFile(`[DIFF-REVIEW-CMD] error: ${cmdErr.message}`);
} }
} catch (cmdErr: any) {
logToFile(`[RESPONSE] diff_review command error: ${cmdErr.message}`);
} }
} else if (isDomObserver) { } else if (isDomObserver) {
// DOM observer path: ALSO try RPC strategies (renderer click is unreliable) // DOM observer path: ALSO try RPC strategies (renderer click is unreliable)
@@ -2904,20 +2974,22 @@ async function tryApprovalStrategies(approved: boolean, sessionId: string, stepT
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', ''); const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
let interactionPayload: Record<string, any> = {}; let interactionPayload: Record<string, any> = {};
if (typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) { if (typeLower.includes('code_edit') || typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
// CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC // CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC
try { try {
logToFile(`[APPROVAL-PROTO-ACK] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0,8)}, accept=true, stepIndices=[${effectiveStepIndex}])`); logToFile(`[APPROVAL-CODE-EDIT] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0,8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', { const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
cascadeId: sessionId, cascadeId: sessionId,
accept: true, accept: approved,
stepIndices: [effectiveStepIndex], stepIndices: [effectiveStepIndex],
}); });
logToFile(`[APPROVAL-PROTO-ACK] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`); logToFile(`[APPROVAL-CODE-EDIT] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
return `RPC-PROTO-ACK:AcknowledgeCascadeCodeEdit`; return `RPC:AcknowledgeCascadeCodeEdit(accept=${approved})`;
} catch (e: any) { } catch (e: any) {
logToFile(`[APPROVAL-PROTO-ACK] ❌ ${e.message.substring(0, 200)}`); logToFile(`[APPROVAL-CODE-EDIT] ❌ ${e.message.substring(0, 200)}`);
// Fall through to HandleCascadeUserInteraction // Fallback: try HandleCascadeUserInteraction with runCommand
logToFile(`[APPROVAL-CODE-EDIT] falling back to HandleCascadeUserInteraction`);
interactionPayload = { runCommand: { confirm: true } };
} }
} }