Compare commits

..

29 Commits

Author SHA1 Message Date
Variet Worker
32cf69469c docs: session end — known-issues 2건 추가 + observer-dev-guide 3.3 업데이트 + devlog #019
[2026-04-19] Accept all span 렌더링 감지 실패 (v0.5.101)
[2026-04-19] auto-approve _from_ws 마커 누락 (v0.5.103)
observer-dev-guide 3.3: Accept all Observer 접근 가능으로 변경
Vikunja: #638 done, #639 done
2026-04-20 04:58:13 +09:00
Variet Worker
7c8891b99c fix(bridge): v38 auto-approve response에 _from_ws 마커 추가 — Observer polling 실패 수정 (v0.5.103)
근본 원인: auto-approve response 파일에 _from_ws 마커 없음.
processResponseFile(response watcher)이 Observer보다 먼저 파일을 읽고 삭제.
Observer의 GET /response/{rid} polling이 항상 {waiting:true} 반환.
known-issues [2026-04-18] WS response 파일 삭제 버그와 동일 패턴.
2026-04-20 04:43:56 +09:00
Variet Worker
3cc3442fda fix(bridge): v37 openReviewChanges 선호출 — agentAcceptAllInFile 실효성 보장 (v0.5.102)
agentAcceptAllInFile은 diff review 패널이 포커스되어야 동작.
v34에서 직접 호출만 했더니 SUCCESS 반환하지만 실제 효과 없음.
openReviewChanges → 500ms 대기 → agentAcceptAllInFile 순서로 수정.
2026-04-20 04:34:44 +09:00
Variet Worker
e95e7791f9 fix(observer): v36 Accept all 감지 — span.cursor-pointer 선택자 추가 (v0.5.101)
근본 원인: AG Native에서 Accept all 버튼이 <button>이 아닌 <span> 태그.
Observer의 allBtns 선택자가 button만 스캔하여 Accept all 미감지.
ACCEPT-SCAN 로그에서 tag=SPAN cls=cursor-pointer 확인.
span.cursor-pointer 추가로 diff review 버튼 감지 복구.
2026-04-20 04:26:58 +09:00
Variet Worker
2bf1eb41d1 feat(probe): v35 code_edit 자동 Accept — agentAcceptAllInFile 직접 호출 (v0.5.100)
Accept all 버튼이 Observer DOM에 없음 확인 (에디터 레이어).
step_probe에서 WAITING code_edit 감지 시 500ms 후 agentAcceptAllInFile 직접 실행.
Observer relay 필터에 ACCEPT 추가.
2026-04-20 04:05:47 +09:00
Variet Worker
cf1352eefa fix(bridge): v34 Accept all 이중 보장 — agentAcceptAllInFile 직접 호출 + ACCEPT 로그 relay (v0.5.99)
v34: Accept all/diff_review 감지 시 Observer DOM 클릭 + extension host agentAcceptAllInFile 이중 실행.
Observer relay 필터에 ACCEPT 키워드 추가로 ACCEPT-SCAN 진단 로그 활성화.
2026-04-20 00:11:07 +09:00
Variet Worker
6aea48e2e9 feat(bridge): v33 Accept all 자동승인 — diff review 버튼도 auto-approve (v0.5.98)
Always run과 동일하게 Accept all / Accept 버튼도 즉시 자동 승인.
Observer가 step_type=diff_review로 전송 → bridge에서 AUTO_APPROVE_RE 매칭 → 즉시 응답.
2026-04-19 21:27:18 +09:00
Variet Worker
bd5a7ca8b9 fix(observer): v32 터미널 프롬프트 조기감지 — JUNK/PROMPT 필터 전에 명령어 추출 (v0.5.97)
근본원인: code 요소의 textContent '❯ project > command'가 PowerShell 키워드
(return, function, const)를 포함할 수 있어 JUNK_CODE_RE에 잘못 걸림.
v32: ❯ 마커로 시작하는 code 텍스트는 JUNK/PROMPT 전에 명령어 직접 추출.
14/14 E2E 테스트 통과.
2026-04-19 19:55:44 +09:00
Variet Worker
8ada5f7daf fix(observer): v31 — content_copy 아이콘 필터 + 후보 길이순 정렬 + trailing icon strip (v0.5.96) 2026-04-19 15:18:11 +09:00
Variet Worker
4f2be831a1 diag(observer): v30 SCAN 진단 로그 — div textContent 덤프 (v0.5.95) 2026-04-19 15:08:56 +09:00
Variet Worker
cbfd137dcb fix(observer): v30 Running command 추출 — includes 매칭 + raw fallback + 디버그 로그 (v0.5.94) 2026-04-19 15:03:09 +09:00
Variet Worker
a99a1e3f54 docs: 배포 전 체크리스트에 log relay 필터/regex E2E/가정 검증 항목 추가 2026-04-19 14:56:40 +09:00
Variet Worker
ad4ed623bd docs: observer-dev-guide DOM 구조 BTN-DOM-DUMP 기반 갱신 + log relay 필터 규칙 추가 2026-04-19 14:48:42 +09:00
Variet Worker
64800d3c20 fix(observer): 'Running command' div에서 명령어 추출 — pre/code 대신 plain div 탐색 (v0.5.93) 2026-04-19 14:40:05 +09:00
Variet Worker
70c83b4226 fix(observer): log 필터에 CONTEXT/DEFERRED/DETECTED/BTN-DOM 추가 — 진단 로그 relay 누락 수정 (v0.5.92) 2026-04-19 14:10:59 +09:00
Variet Worker
bb54802c06 feat(enrichment): Discord 알림 지연 + Step Probe 폴링 — generic Always run 커맨드 100% 보강 (v0.5.91) 2026-04-19 10:25:55 +09:00
Variet Worker
bf53072f3c feat(enrichment): Step Probe API 메모리 기반 명령어 보강 — Always run 표시 개선 (v0.5.90) 2026-04-19 09:59:20 +09:00
Variet Worker
02b4b03699 diag(observer): Always run 버튼 주변 DOM 구조 원샷 덤프 추가 (v0.5.89) 2026-04-19 09:19:31 +09:00
Variet Worker
db805c6fde fix(observer): matchedType 대소문자 무시 — Always run이 permission으로 잘못 분류되는 문제 수정 (v0.5.88) 2026-04-19 08:01:47 +09:00
Variet Worker
7f33a20e43 docs: 배포 전 자기검증 체크리스트 추가 — 재시작 요구 최소화 정책 2026-04-19 07:58:14 +09:00
Variet Worker
ef788e6ecc docs: Observer 개발 가이드 SSOT 문서 생성 — 제약사항/배포/DOM구조/교훈 종합 2026-04-19 07:52:53 +09:00
Variet Worker
cd00986274 fix(observer): 깨진 문자열 리터럴 2건 수정 — walkNode 크래시 해결 (v0.5.87) 2026-04-19 07:51:29 +09:00
Variet Worker
12095f36a4 fix(observer): function declaration → var expression — strict mode 크래시 수정 (v0.5.86) 2026-04-19 07:41:04 +09:00
Variet Worker
498683c977 fix(approval): _from_ws 파일 60초 TTL 자동 삭제 — stale SKIP 스팸 방지 (v0.5.85) 2026-04-19 07:32:49 +09:00
Variet Worker
1662ac4f6b fix(observer): regex → 문자열 비교로 isGenericDesc 수정 — template literal escaping 회피 (v0.5.84) 2026-04-19 07:02:04 +09:00
Variet Worker
d027562f17 fix(observer): 500ms 딜레이드 컨텍스트 추출 + 버튼 셀렉터 확장 (v0.5.83)
- Always run 감지 시 desc가 generic이면 500ms 딜레이 후 재추출
- 버튼 셀렉터에 role=button, monaco-button, vscode-button 추가
- ACCEPT-SCAN 디버그 로그 (30초 간격)
2026-04-19 06:53:54 +09:00
Variet Worker
cc261011d6 fix(observer): 구 visibility 체크 제거 — Accept all 버튼 감지 차단 수정 (v0.5.81) 2026-04-19 04:18:35 +09:00
Variet Worker
37fbb9657e fix(observer): diff_review Accept all 버튼 감지 — offsetParent 체크 완화 (v0.5.80) 2026-04-19 03:53:03 +09:00
Variet Worker
965f619664 docs: relay-architecture + known-issues 업데이트 (v0.5.78-79 변경사항 반영) 2026-04-19 03:48:31 +09:00
22 changed files with 1111 additions and 65 deletions

View File

@@ -21,6 +21,18 @@
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
### [2026-04-19] [Observer] ★ Accept all 버튼이 `<span>`으로 렌더링 — Observer 감지 실패 (v0.5.101)
- **증상**: "Accept all" diff review 버튼이 화면에 보이지만 Observer가 감지하지 못함. Discord에 "Accept all" 자동 승인 알림이 안 옴.
- **원인**: AG Native UI 업데이트로 "Accept all"이 `<button>`이 아닌 `<span class="cursor-pointer">`로 렌더링됨. Observer의 `allBtns = querySelectorAll('button, [role="button"]')`에 span이 포함되지 않음. ACCEPT-SCAN 로그: `tag=SPAN cls=hover:text-ide-button-hover-color cursor-po txt=Accept all`.
- **해결 (v0.5.101)**: `allBtns` 선택자에 `span.cursor-pointer` 추가.
- **주의**: observer-dev-guide 섹션 3.3 "Accept all — Observer 접근 불가"는 outdated. UI 변경으로 chat panel footer에 Accept all이 표시됨. 가이드 업데이트 필요.
### [2026-04-19] [Bridge] ★ auto-approve response 파일에 `_from_ws` 마커 누락 — Observer polling 실패 (v0.5.103)
- **증상**: Observer가 "Accept all"을 감지하고 bridge가 자동 승인했지만, Observer의 `pollResponseGroup` GET `/response/{rid}`가 항상 `{waiting: true}` 반환. 버튼 클릭이 실행되지 않음.
- **원인**: http-bridge의 auto-approve 경로에서 response JSON 파일에 `_from_ws: true` 마커가 없음 → `processResponseFile`(response watcher)이 Observer보다 먼저 파일을 읽고 삭제 → Observer polling 시 파일 부재. known-issues [2026-04-18] WS response 삭제 버그와 동일 패턴.
- **해결 (v0.5.103)**: auto-approve response에 `_from_ws: true` + `_auto_approve_ttl` 마커 추가.
- **주의**: **response 디렉토리에 파일을 쓰는 모든 경로**는 반드시 `_from_ws: true` 마커를 포함해야 함. processResponseFile이 먼저 소비하는 race condition 항상 존재.
### [2026-04-18] [Extension] ★ WS response 파일이 processResponseFile에 의해 삭제 → Observer pollResponseGroup 실패 (v0.5.78)
- **증상**: `!auto` Retry가 작동하지 않음. Observer가 `/response/{rid}`를 폴링하지만 항상 `{waiting: true}` 반환.
- **원인**: extension.ts의 WS 응답 핸들러가 `response/{rid}.json` 파일 작성 → 300ms 후 response watcher(`processResponseFile`)가 파일 감지 → pending 파일이 없어 `isDomObserver=false``fs.unlinkSync()` 실행 → Observer가 폴링할 때 파일이 이미 삭제됨.

View File

@@ -0,0 +1,205 @@
# Observer Script 개발 가이드 — SSOT
> **이 문서는 Observer 코드 변경 전 반드시 확인하는 SSOT입니다.**
> 모든 Observer 관련 설계, 배포, 제약사항이 이 문서에 있습니다.
---
## 1. Observer 코드 특성
### 1.1 실행 환경
- Observer는 **workbench-jetski-agent.html**에 인라인 `<script>`로 삽입됨
- **AG Native의 Electron 렌더러 프로세스**에서 실행 (VS Code extension host가 아님)
- 렌더러는 **strict mode 아님** (확인 필요), 하지만 V8 parser는 일부 strict-like 규칙 적용
- `generateApprovalObserverScript(port)` 함수가 **TypeScript template literal**로 스크립트 생성
### 1.2 코드 작성 규칙 (위반 시 Observer 전체 크래시)
| 규칙 | 이유 | 예시 |
|------|------|------|
| **for 루프 안에 function 선언 금지** | V8 strict mode error | `var fn = function(){}` 사용 |
| **문자열 리터럴에 특수문자 금지** | template literal 이스케이핑 깨짐 | `'??'``'MAX'` |
| **regex에 `\\s` 등 이스케이프 금지** | template literal이 `\\\\s``\\s` (리터럴) | 문자열 비교 사용 |
| **ES6+ 구문 금지** | 구 V8 호환 | `var` 사용, `let/const/arrow` 금지 |
| **배포 전 SYNTAX CHECK 필수** | Observer 크래시 방지 | 아래 검증 명령어 참조 |
### 1.3 필수 검증 명령어 (모든 빌드 전 실행)
```powershell
npm.cmd run compile; node -e "const {generateApprovalObserverScript}=require('./out/observer-script'); let s=generateApprovalObserverScript(18080); try { new Function(s); console.log('SYNTAX OK'); } catch(e) { console.log('ERROR:', e.message); }"
```
**SYNTAX OK가 나오지 않으면 절대 배포하지 않는다.**
### 1.4 배포 전 자기검증 체크리스트 (MANDATORY)
**재시작을 요구하기 전 반드시 다음을 모두 통과해야 한다:**
1. [ ] **SYNTAX CHECK 통과**: `new Function(s)``SYNTAX OK`
2. [ ] **수정 방향 검증**: 이 수정이 문제를 해결하는 올바른 접근인지 스스로 2번 재검증
3. [ ] **template literal 규칙 위반 없음**: regex 이스케이프, 특수문자, function 선언 등
4. [ ] **변경 범위 최소화**: 불필요한 코드 포함 여부 확인
5. [ ] **재시작 사유 명시**: 사용자에게 (a) 무엇을 수정했고 (b) 왜 재시작이 필요한지 1~2줄로 설명
6. [ ] **재시작 횟수 명시**: Observer 변경 = 2회, Extension host만 변경 = 1회
7. [ ] **log() relay 필터 확인**: 새 로그 키워드 추가 시 log() 함수의 키워드 필터에도 추가했는지 확인 (섹션 3.5 참조)
8. [ ] **regex E2E 테스트**: Observer에서 사용하는 새 regex는 생성된 코드에서 직접 실행하여 매칭 검증
9. [ ] **구현 전 가정 검증**: 새 접근을 코딩하기 전에, 핵심 가정이 성립하는지 로그 1줄로 먼저 확인 (예: "Step Probe가 WAITING을 볼 수 있는가?" → `STEP-PROBE.*WAITING` 로그 검색)
**정당한 사유 없이 재시작을 요구하지 않는다.**
**DOM 구조를 먼저 파악하고 설계한 후 코드를 작성한다.**
**시행착오식(trial-and-error) 접근을 하지 않는다.**
**추측으로 코딩하지 않는다. 로그/데이터로 확인한 사실에 기반하여 코딩한다.**
---
## 2. 배포 프로세스
### 2.1 Observer 코드가 포함된 변경
Observer 코드 변경은 **extension host 코드 변경보다 비용이 높다**:
```
VSIX 빌드 → VSIX 설치 → AG 재시작 #1 (extension이 HTML 패치)
→ AG 재시작 #2 (패치된 HTML 로드) → Observer 실행
```
**총 2번 AG 재시작 필요**
### 2.2 Extension host 코드만 변경 (approval-handler, http-bridge 등)
```
VSIX 빌드 → VSIX 설치 → AG 재시작 #1 → 즉시 적용
```
**1번 AG 재시작 필요**
### 2.3 VSIX 설치 확인
```powershell
Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Filter "*gravity*" -Directory |
ForEach-Object { $j = Get-Content (Join-Path $_.FullName "package.json") | ConvertFrom-Json; "$($_.Name): v$($j.version)" }
```
### 2.4 HTML 패치 확인 (Observer 코드가 반영되었는지)
```powershell
Select-String -Path "$env:LOCALAPPDATA\Programs\Antigravity\resources\app\out\vs\code\electron-browser\workbench\workbench-jetski-agent.html" -Pattern "검색할_함수명" -Quiet
```
---
## 3. AG Native DOM 구조
### 3.1 Chat Panel (Observer가 접근 가능)
- 위치: `document.querySelector('#conversation')` 또는 `document.querySelector('[class*="conversation"]')`
- AI 응답 블록: `.leading-relaxed.select-text`
- 사용자 메시지 블록: `.select-text.rounded-lg` (v0.5.74+)
- Thinking 블록: 조상에 `max-h-[200px]` 클래스 있음 → 필터링
### 3.2 승인 버튼 — "Always run" (v0.5.93 BTN-DOM-DUMP 확인)
실제 DOM 구조 (v0.5.92 로그로 확인):
- d0: button.flex.cursor-pointer (Always run 버튼)
- d1: div.min-w-0
- d2: div.flex.items-center.justify-between.rounded-b.border-t (버튼 바)
- d3: div (이름 없는 컨테이너)
- div.mb-1 = "Running command" (헤더)
- div.flex = " gravity_control > ..." (실제 명령어, plain div!)
- div.flex = "Always run Cancel" (버튼들)
> 명령어는 pre.font-mono나 code가 아닌 plain div.flex에 있음.
> v30: "Running command" div의 형제를 탐색하여 프롬프트 마커 뒤의 명령어 추출.
- "Retry" 버튼: button 태그, chat panel 내
### 3.3 Diff Review (Accept all / Reject all) — Observer 접근 가능 (v0.5.101+)
- **v0.5.101 이전**: 에디터 webview에 렌더링, Observer document에서 접근 불가
- **v0.5.101 이후**: AG UI 업데이트로 chat panel footer에 `<span class="cursor-pointer">` 태그로 렌더링
- Observer의 `allBtns` 선택자에 `span.cursor-pointer` 포함 필수
- `matchedType = 'diff_review'`로 분류됨 (L1120: `txt.includes('Accept')`)
- auto-approve response 파일에 `_from_ws: true` 마커 필수 (processResponseFile race condition 방지)
### 3.4 DOM 렌더링 타이밍
- "Always run" 버튼이 DOM에 나타날 때 명령어 div도 함께 렌더링됨
- v30의 "Running command" div 탐색은 즉시 성공
### 3.5 log() relay 필터 규칙
Observer의 log() 함수는 키워드 필터로 일부 로그만 extension.log에 relay.
새 로그 키워드 추가 시 반드시 필터도 함께 수정해야 함.
현재 필터 키워드 (v0.5.92+):
CV-CLASSES, CV-CHILDREN, child[, CV found, Conversation view,
BEACON, ERROR, chat relay, user-cls,
CONTEXT, BTN-DOM, DEFERRED, DETECTED
---
## 4. 파일 경로 매핑
| 항목 | 경로 |
|------|------|
| AG 설치 경로 | `$env:LOCALAPPDATA\Programs\Antigravity\` |
| workbench HTML | `...\resources\app\out\vs\code\electron-browser\workbench\workbench-jetski-agent.html` |
| Extension 로그 | `$env:USERPROFILE\.gemini\antigravity\bridge\extension.log` |
| Pending 파일 | `$env:USERPROFILE\.gemini\antigravity\bridge\pending\*.json` |
| Response 파일 | `$env:USERPROFILE\.gemini\antigravity\bridge\response\*.json` |
| VSIX extensions | `$env:USERPROFILE\.vscode\extensions\variet.gravity-bridge-*\` |
| HTTP Bridge 포트 | 34332 (또는 `getDeterministicPort('gravity_control')`) |
| Discord 채널 | `#ag-gravity_control` (ID: 1483082084540223663) |
---
## 5. 과거 실수 및 교훈
### 5.1 VSIX 미설치 (v0.5.78~83)
- **증상**: 빌드만 하고 `code --install-extension` 실행 안 함
- **결과**: 설치된 버전이 v0.5.50, 모든 수정사항 미적용
- **교훈**: 빌드 후 반드시 `code --install-extension *.vsix --force` 실행
- **확인**: `Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Filter "*gravity*"`
### 5.2 function 선언 → Observer 크래시 (v0.5.84~86)
- **증상**: `function _isGenericDesc(d){}` 를 for 루프 내부에 선언
- **결과**: Observer 전체 크래시, chat relay + auto-approve 중단
- **교훈**: `var fn = function(){}` 사용, 배포 전 SYNTAX CHECK 필수
### 5.3 깨진 문자열 리터럴 (v0.5.86)
- **증상**: `{tag:'??',...}` (특수문자가 따옴표 깨뜨림)
- **결과**: SYNTAX ERROR, Observer 미작동
- **교훈**: template literal 안에서 특수문자/이모지 사용 주의
### 5.4 regex 이스케이핑 실패 (v0.5.83~84)
- **증상**: `/Always\\s+run/` → 생성 시 `\\s` (리터럴 백슬래시+s)로 출력
- **결과**: "Always run" 매칭 실패
- **교훈**: template literal 안에서 regex 대신 **문자열 비교** 사용
### 5.5 _from_ws 파일 무한 누적 (v0.5.78~84)
- **증상**: response 파일에 `_from_ws: true` 마커 → processResponseFile이 스킵 → 영원히 삭제 안 됨
- **결과**: 3초마다 4개 파일 × SKIP 로그 → 로그 스팸, 다른 처리 방해
- **교훈**: 보존 파일에는 반드시 **TTL(자동 만료)** 추가
---
## 6. 디버깅 체크리스트
### Observer가 작동하지 않을 때
1. `extension.log`에서 `setup complete` 확인 → Observer 로드 여부
2. `OBSERVER-LOG` 패턴 검색 → 스캔 활동 여부
3. `HTTP-REQ` 검색 → HTTP bridge에 요청 도달 여부
4. **SYNTAX CHECK** 실행 → 생성 스크립트 문법 검증
5. `SKIP _from_ws` 반복 확인 → stale response 파일 정리
### Discord에 메시지가 안 올 때
1. `POST /chat` 검색 → chat relay 전송 여부
2. `WS.*send` 검색 → WebSocket 전송 여부
3. Discord API로 직접 확인: `node extension/scratch/discord_read.js`
4. stale response 파일 확인: `Get-ChildItem $env:USERPROFILE\.gemini\antigravity\bridge\response\*.json`
---
## 7. 버전 히스토리 요약
| 버전 | 핵심 변경 | 결과 |
|------|----------|------|
| v0.5.50 | 기본 릴레이 시스템 | ✅ 안정 |
| v0.5.78 | `_from_ws` 마커 (Retry 보존) | ✅ 작동 (TTL 미구현) |
| v0.5.79 | sibling 탐색 + thinking 필터 | ✅ 작동 |
| v0.5.80~81 | Accept all offsetParent 완화 | ❌ 구조적 불가 (에디터 webview) |
| v0.5.82 | 버튼 셀렉터 확장 + ACCEPT-SCAN | 진단용 |
| v0.5.83 | DEFERRED 컨텍스트 500ms | regex 이스케이핑 실패 |
| v0.5.84 | regex → 문자열 비교 | function 선언 크래시 |
| v0.5.85 | `_from_ws` TTL 60초 | ✅ stale 정리 |
| v0.5.86 | function → var expression | 깨진 문자열 미발견 |
| v0.5.87 | 깨진 문자열 2건 수정 | ✅ SYNTAX OK |

View File

@@ -55,7 +55,7 @@ extension.ts activate()
|------|------|
| `scanChatBodies()` | 3초마다 실행, conversation view에서 메시지 블록 탐색 |
| `extractCleanStepText(el)` | DOM 클론 → style/script/button 제거 → textContent 추출 |
| `extractContextFromNearby(btn)` | 승인 버튼 주변 DOM에서 명령어 텍스트 추출 |
| `extractContextFromNearby(btn)` | 승인 버튼 주변 DOM에서 명령어 텍스트 추출 (v23: sibling 탐색 포함) |
| `pollResponseGroup(rid, btnRefs)` | response 파일 polling → 버튼 자동 클릭 |
### 2.3 AI 응답 감지 셀렉터
@@ -115,6 +115,7 @@ Observer DOM scan
1. rawDesc에서 > 프롬프트 마커 찾기 → ✅ 성공 (buttons=2일 때 desc에 프롬프트 포함)
2. rawDesc 최장 라인 사용 → buttons=1일 때 desc="Always run"이라 실패
3. v20 fallback: bridge/pending/ 최신 파일에서 command 읽기 → Step Probe pending 있을 때만
4. v23 sibling: Observer가 footer 형제 요소에서 pre.font-mono 탐색 → ✅ 성공
→ response 파일 작성 → Observer pollResponseGroup → 버튼 클릭
→ WS sendPending { status: 'auto_approved', command: displayCmd }
→ Discord embed 표시
@@ -194,6 +195,11 @@ AUTO-APPROVE raw: cmd="Always run" desc="Always run" buttons=["Always run"]
| v0.5.70 | heartbeat 로깅 강화 | API step count 동결 확인 |
| v0.5.71 | heartbeat 3 poll마다 실행, HB-CAPTURE 추가 | API가 진행중 step 미반환 재확인 |
| v0.5.72 | Observer DOM relay 재활성화 | AG 재시작 필요 (Observer HTML 캐시) |
| v0.5.74 | 사용자 메시지 셀렉터 추가 (`.select-text.rounded-lg`) | ✅ 사용자 메시지 릴레이 작동 |
| v0.5.76 | DOM 탐색 depth 5→10, `pre.font-mono` 우선 탐색 | Observer HTML 업데이트 필요 |
| v0.5.77 | WS response 파일 작성 (pollResponseGroup용) | Retry 클릭 경로 추가 |
| v0.5.78 | `_from_ws` 마커로 processResponseFile 삭제 방지 | ✅ Retry auto-approve 작동 |
| v0.5.79 | sibling 탐색 추가 + thinking 블록 필터링 | ✅ 명령어 컨텍스트 부분 추출 |
---
@@ -201,7 +207,10 @@ AUTO-APPROVE raw: cmd="Always run" desc="Always run" buttons=["Always run"]
- [x] AG 재시작하여 Observer 반영 확인 — ✅ v0.5.72 작동 확인
- [x] Observer의 AI 응답 릴레이가 작동하는지 Discord에서 확인 — ✅ 작동
- [ ] v0.5.73 설치 (MSG-BLOCKS 로그 추가) → AG 재시작 → 사용자 메시지 DOM 클래스 식별
- [ ] 사용자 메시지 셀렉터 추가 후 테스트
- [ ] "Always run" enrichment 개선 — buttons=1일 때 code 블록 추출 개선
- [x] 사용자 메시지 셀렉터 추가 — ✅ v0.5.74
- [x] Retry auto-approve 흐름 복구 — ✅ v0.5.78 (_from_ws 마커)
- [x] 명령어 컨텍스트 sibling 탐색 — ✅ v0.5.79
- [x] Thinking 블록 필터링 — ✅ v0.5.79
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패) #636
- [ ] Observer pollResponseGroup 미시작 케이스 (trigger-click 선점)
- [ ] AI 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)

26
docs/devlog/2026-04-19.md Normal file
View File

@@ -0,0 +1,26 @@
# Devlog 2026-04-19
## 작업 인덱스
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 21:22 | v30-32 Observer 명령어 추출 안정화 (터미널 프롬프트 조기감지) | `bd5a7ca` | ✅ |
| 002 | 23:16 | v33 Accept all 자동승인 — diff review auto-approve | `6aea48e` | ✅ |
| 003 | 00:18 | v34 Accept all 이중 보장 — agentAcceptAllInFile 직접 호출 | `cf1352e` | ✅ |
| 004 | 00:34 | v35 code_edit 자동 Accept — step-probe 경로 | `2bf1eb4` | ✅ |
| 005 | 04:26 | v36 Accept all span 감지 — 근본 원인 발견 (button→span) | `e95e779` | ✅ |
| 006 | 04:34 | v37 openReviewChanges 선호출 — agentAcceptAllInFile 보조 | `3cc3442` | ✅ |
| 007 | 04:43 | v38 _from_ws 마커 추가 — Observer polling 실패 근본 수정 | `7c8891b` | ✅ |
## v0.5.103 — Accept all (Diff Review) 자동 승인 복구
### 근본 원인 (2가지)
1. **Observer 감지 실패**: AG UI가 "Accept all"을 `<button>`이 아닌 `<span class="cursor-pointer">`로 렌더링. Observer의 `allBtns` 선택자가 `button`만 스캔하여 미감지.
2. **Response 파일 race condition**: auto-approve response 파일에 `_from_ws: true` 마커 없음 → `processResponseFile`이 Observer보다 먼저 파일 삭제 → Observer polling 무한 실패.
### 검증 결과
- Observer ACCEPT-SCAN: `tag=SPAN cls=cursor-pointer txt=Accept all`
- `DETECTED diff_review: Accept all`
- `response served to renderer: ...approved=true` (이전 0건 → 7건) ✅
- Discord "자동 승인됨 Accept all" 표시 ✅
- 화면에서 "Accept all" 버튼 자동 소멸 확인 ✅

View File

@@ -520,6 +520,7 @@ async function activate(context) {
get sessionStalled() { return (0, step_probe_1.getStepProbeContext)().sessionStalled; },
get lastPendingStepIndex() { return (0, step_probe_1.getStepProbeContext)().lastPendingStepIndex; },
writeChatSnapshot,
getLastWaitingCommand: step_probe_1.getLastWaitingCommand,
};
const bridgePort = await (0, http_bridge_1.startHttpBridge)(httpBridgeCtx, sdk);
let localPort = bridgePort;

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
{
"name": "gravity-bridge",
"version": "0.5.79",
"version": "0.5.103",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gravity-bridge",
"version": "0.5.79",
"version": "0.5.103",
"dependencies": {
"cheerio": "^1.2.0",
"ws": "^8.19.0"

View File

@@ -2,7 +2,7 @@
"name": "gravity-bridge",
"displayName": "Gravity Bridge",
"description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.79",
"version": "0.5.103",
"publisher": "variet",
"engines": {
"vscode": "^1.100.0"

View File

@@ -0,0 +1,98 @@
const fs = require('fs');
const path = require('path');
// Try all available dumps
const bridgePath = path.join(process.env.USERPROFILE, '.gemini/antigravity/bridge');
const dumpFiles = ['dump_html_1.json', 'dump_html_2.json', 'dump_html_3.json', 'dump_html_4.json', 'dump_html_5.json', 'deep-inspect-result.json', 'deep-inspect-manual.json'];
function printTree(node, indent, maxDepth) {
if (!node || indent > maxDepth) return;
const tag = (node.tag || node.tagName || '?').toLowerCase();
const clsArr = (node.cls || node.className || '').split(' ').filter(c => c.length > 0);
const text = (node.text || node.textContent || '').substring(0, 50).replace(/[\n\r]+/g, ' ');
const childCount = (node.children || []).length;
let line = ' '.repeat(indent) + tag;
if (clsArr.length > 0) line += '.' + clsArr[0];
if (childCount) line += ' [' + childCount + ']';
if (text && childCount === 0) line += ' = "' + text + '"';
console.log(line);
if (node.children) {
for (const c of node.children) printTree(c, indent + 1, maxDepth);
}
}
// Find the conversation area in each dump
function findConvo(node, depth) {
if (!node || depth > 20) return null;
const cls = node.cls || node.className || '';
if (cls.includes('bg-agent-convo-background') || cls.includes('agent-convo')) return node;
if (node.children) {
for (const c of node.children) {
const r = findConvo(c, depth + 1);
if (r) return r;
}
}
return null;
}
// Find buttons (Run, Allow, Always run, Accept)
function findButtons(node, depth, results) {
if (!node || depth > 25) return;
const tag = (node.tag || node.tagName || '').toLowerCase();
const text = (node.text || node.textContent || '');
if (tag === 'button' && /Run|Allow|Accept|Always/i.test(text) && text.length < 50) {
results.push({ text: text.trim(), depth });
}
if (node.children) {
for (const c of node.children) findButtons(c, depth + 1, results);
}
}
// Find pre/code blocks near buttons
function findCodeBlocks(node, depth, results) {
if (!node || depth > 25) return;
const tag = (node.tag || node.tagName || '').toLowerCase();
const cls = node.cls || node.className || '';
if ((tag === 'pre' || tag === 'code') && cls.includes('font-mono')) {
const text = (node.text || node.textContent || '').substring(0, 80);
results.push({ tag, cls: cls.substring(0, 50), text, depth });
}
if (node.children) {
for (const c of node.children) findCodeBlocks(c, depth + 1, results);
}
}
for (const df of dumpFiles) {
const fp = path.join(bridgePath, df);
if (!fs.existsSync(fp)) continue;
try {
const dump = JSON.parse(fs.readFileSync(fp, 'utf8'));
const root = dump.bodyTree || dump.body || dump;
console.log('\n=== ' + df + ' ===');
// Find conversation area
const convo = findConvo(root, 0);
if (convo) {
console.log('>> Conversation area found, tree (depth 12):');
printTree(convo, 0, 12);
}
// Find buttons
const btns = [];
findButtons(root, 0, btns);
if (btns.length > 0) {
console.log('>> Buttons found:');
for (const b of btns) console.log(' d' + b.depth + ': ' + b.text);
}
// Find code blocks
const codes = [];
findCodeBlocks(root, 0, codes);
if (codes.length > 0) {
console.log('>> Code blocks (font-mono):');
for (const c of codes) console.log(' d' + c.depth + ': <' + c.tag + '> cls=' + c.cls + ' text="' + c.text + '"');
}
} catch (e) {
console.log('ERROR reading ' + df + ': ' + e.message);
}
}

View File

@@ -0,0 +1,2 @@
# diff_review detection test v2
test_value = "hello"

View File

@@ -0,0 +1,26 @@
const fs = require('fs');
const path = require('path');
const dump = JSON.parse(fs.readFileSync(
path.join(process.env.USERPROFILE, '.gemini/antigravity/bridge/deep-inspect-result.json'), 'utf8'
));
function printTree(node, indent, maxDepth) {
if (!node || indent > maxDepth) return;
const tag = (node.tag || '?').toLowerCase();
const clsArr = (node.cls || '').split(' ').filter(c => c.length > 0);
const clsShort = clsArr.slice(0, 3).join(' ');
const text = (node.text || '').substring(0, 40).replace(/[\n\r]+/g, ' ');
const childCount = (node.children || []).length;
let line = ' '.repeat(indent) + tag;
if (clsShort) line += '.' + clsArr[0];
if (childCount) line += ' [' + childCount + ' children]';
if (text && childCount === 0) line += ' = "' + text + '"';
console.log(line);
if (node.children) {
for (const c of node.children) printTree(c, indent + 1, maxDepth);
}
}
console.log('=== FULL DOM TREE (depth 8) ===');
printTree(dump.bodyTree || dump.body, 0, 8);

View File

@@ -0,0 +1,22 @@
// Test: call agentAcceptAllInFile via extension's HTTP bridge trigger-click
// This simulates what the approval handler does
const http = require('http');
const PORT = 34332; // from observer setup log
// Write a trigger-click file to make Observer click "Accept all"
const fs = require('fs');
const path = require('path');
const bridgePath = path.join(process.env.USERPROFILE, '.gemini', 'antigravity', 'bridge');
// Check if there's a trigger_click.json
const triggerFile = path.join(bridgePath, 'trigger_click.json');
console.log('Writing trigger_click.json for accept...');
fs.writeFileSync(triggerFile, JSON.stringify({ action: 'approve', type: 'diff_review', ts: Date.now() }), 'utf-8');
console.log('Done. Check if Accept all was clicked.');
// Also check extension log for recent entries
const logFile = path.join(bridgePath, 'extension.log');
const lines = fs.readFileSync(logFile, 'utf-8').split('\n');
const recent = lines.slice(-5);
recent.forEach(l => console.log(l.substring(0, 200)));

View File

@@ -0,0 +1,114 @@
// Final simulation: exact v0.5.96 flow with realistic DOM
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
try { new Function(s); console.log('SYNTAX: OK'); } catch(e) { console.log('SYNTAX ERROR:', e.message); process.exit(1); }
let promptRe = /[\u003e\u00bb\u276f]\s+(.+)/;
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
let iconFilterRe = /^(content_copy|content_paste|play_arrow|check_circle|chevron_|keyboard_arrow|more_horiz|more_vert|expand_|alternate_email|arrow_drop)/;
// Realistic scenario: "Running command" div has siblings including a copy button
// The actual DOM probably has a structure like:
// div "Running command"
// span/div with the copy icon (textContent = "> content_copy" or just "content_copy")
// div with the actual prompt+command
// div with the buttons
function v31_simulate(name, siblings) {
console.log('\n=== ' + name + ' ===');
// Step 1: Find "Running command" header
let rcIdx = -1;
for (let i = 0; i < siblings.length; i++) {
let t = siblings[i].trim();
if (t === 'Running command' || (t.indexOf('Running command') !== -1 && t.length < 30)) {
rcIdx = i;
break;
}
}
if (rcIdx < 0) { console.log(' NO RC HEADER'); return; }
// Step 2: Collect candidates (filter icons and buttons)
let cands = [];
for (let i = 0; i < siblings.length; i++) {
if (i === rcIdx) continue;
let t = siblings[i].trim();
if (t.length < 5) continue;
if (iconFilterRe.test(t)) { console.log(' FILTER icon: "' + t.substring(0,40) + '"'); continue; }
if (/^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i.test(t)) { console.log(' FILTER btn: "' + t.substring(0,40) + '"'); continue; }
if (t.indexOf('Always run') !== -1 && t.indexOf('Cancel') !== -1) { console.log(' FILTER btn-bar: "' + t.substring(0,40) + '"'); continue; }
cands.push(t);
console.log(' CANDIDATE: "' + t.substring(0,60) + '" (len=' + t.length + ')');
}
// Step 3: Sort by length (longest first)
cands.sort((a,b) => b.length - a.length);
// Step 4: Extract command from best candidate
for (let cand of cands) {
let m = promptRe.exec(cand);
if (m && m[1].trim().length > 3) {
let cmdV = m[1].trim().replace(stripRe, '').trim();
if (cmdV.length < 3) { console.log(' SKIP (too short after strip): "' + cmdV + '"'); continue; }
if (/^(Always|Run|Allow|Cancel|Deny)/i.test(cmdV)) continue;
console.log(' EXTRACTED: "' + cmdV + '"');
return;
}
if (cand.length > 10 && /[\u276f\u003e]/.test(cand)) {
let raw = cand.replace(stripRe, '').trim();
console.log(' EXTRACTED (raw): "' + raw + '"');
return;
}
}
console.log(' NO MATCH - will fallback to "Always run"');
}
// Scenario A: What ACTUALLY happened (3 siblings, "content_copy" mixed in command text)
v31_simulate('A: Icon in command text', [
'Running command',
'\u276f gravity_control > Start-Sleep 12; $logFile content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Scenario B: Copy button as separate small div
v31_simulate('B: Icon as separate div + command div', [
'Running command',
'> content_copy',
'\u276f gravity_control > npm run compile',
'Always run Cancel'
]);
// Scenario C: Just "content_copy" standalone (no >)
v31_simulate('C: Standalone icon + command', [
'Running command',
'content_copy',
'\u276f gravity_control > git push origin main',
'Always run Cancel'
]);
// Scenario D: Multiple icons mixed
v31_simulate('D: Multiple icons + command', [
'Running command',
'play_arrow',
'> content_copy',
'\u276f gravity_control > node -e "console.log(1)" content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Scenario E: Edge - no command, only prompt
v31_simulate('E: Prompt only', [
'Running command',
'\u276f gravity_control > ',
'Always run Cancel'
]);
// Scenario F: The v0.5.95 cmdV=content_copy case
// This implies regex matched "content_copy" from a "> content_copy" sibling
// and there was no longer sibling
v31_simulate('F: Only icon sibling (worst case)', [
'Running command',
'> content_copy',
'Always run Cancel'
]);
console.log('\n=== SIMULATION COMPLETE ===');

View File

@@ -0,0 +1,43 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Extract the regex strings
let junkMatch = s.match(/JUNK_CODE_RE\s*=\s*(\/[^;]+)/);
let promptMatch = s.match(/PROMPT_ONLY_RE\s*=\s*(\/[^;]+)/);
console.log('JUNK_CODE_RE:', junkMatch[1].substring(0, 100));
console.log('PROMPT_ONLY_RE:', promptMatch[1]);
// Use eval to construct the actual regexes
let JUNK = eval(junkMatch[1]);
let PROMPT = eval(promptMatch[1]);
let tests = [
['\u276f gravity_control > ', 'prompt only (no command)'],
['\u276f extension > ', 'prompt only (extension)'],
['\u276f gravity_control > $logFile = Join-Path $env:USERPROFILE', 'PS var assignment'],
['\u276f extension > npm.cmd run compile', 'npm compile'],
['\u276f gravity_control > Start-Sleep 12', 'Start-Sleep'],
['\u276f gravity_control > git add -A; git commit -m "test"', 'git commit'],
['\u276f gravity_control > node -e "const {gen}=require()"', 'node with require'],
['\u276f gravity_control > Get-Content file.txt', 'Get-Content'],
['\u276f gravity_control > npm.cmd version patch', 'npm version'],
['function test() { return 1; }', 'JS function (should be JUNK)'],
['const x = require("fs")', 'JS const (should be JUNK)'],
['import { foo } from "bar"', 'JS import (should be JUNK)'],
];
console.log('\n=== CODE ELEMENT FILTER ANALYSIS ===');
for (let [text, desc] of tests) {
let isJunk = JUNK.test(text);
let isPrompt = PROMPT.test(text.trim());
let junkPart = isJunk ? text.match(JUNK)[0] : null;
let status;
if (isPrompt) status = 'SKIP-PROMPT';
else if (isJunk) status = 'SKIP-JUNK (' + junkPart + ')';
else status = 'PASS';
let isBug = (isJunk || isPrompt) && text.indexOf('\u276f') !== -1 && text.trim().length > 25;
console.log((isBug ? 'BUG ' : ' ') + status.padEnd(40) + ' | ' + desc);
}

View File

@@ -0,0 +1,42 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Find the regex used in v30 candidate matching
let idx = s.indexOf('candT.match(');
if (idx < 0) idx = s.indexOf('sibT.match(');
let reStr = s.substring(idx, s.indexOf(');', idx) + 1);
console.log('Match code:', reStr.substring(0, 60));
// Extract just the regex part
let reMatch = reStr.match(/\/(.*?)\//);
let reSource = reMatch ? reMatch[0] : 'NOT FOUND';
console.log('Regex source:', reSource);
// Build and test the actual regex
let re = new RegExp(reMatch[1]);
console.log('Regex object:', re);
// Test with the EXACT patterns from logs
let tests = [
['Normal', '\u276f gravity_control > Start-Sleep 12 content_copy'],
['Git cmd', '\u276f gravity_control > git add -A; git commit -m "test"'],
['Short', '> content_copy'],
['Prompt only', '\u276f gravity_control > '],
['Dir cmd', '\u276f gravity_control > dir content_copy'],
];
console.log('\n=== REGEX TESTS ===');
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
for (let [name, text] of tests) {
let m = re.exec(text);
if (m) {
let raw = m[1].trim();
let cleaned = raw.replace(stripRe, '').trim();
console.log(name + ':');
console.log(' raw match[1]: "' + raw + '"');
console.log(' after strip: "' + cleaned + '"');
console.log(' length ok: ' + (cleaned.length >= 3));
} else {
console.log(name + ': NO MATCH');
}
}

View File

@@ -0,0 +1,122 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// 1. SYNTAX CHECK
try { new Function(s); console.log('[1] SYNTAX: OK'); } catch(e) { console.log('[1] SYNTAX ERROR:', e.message); process.exit(1); }
// 2. v30 block exists
let v30Start = s.indexOf('// v30:');
let v30End = s.indexOf('// v23:', v30Start);
console.log('[2] v30 block:', v30Start > 0 && v30End > v30Start ? 'OK' : 'MISSING');
// 3. Key features present
console.log('[3] rcCands:', s.indexOf('rcCands') > 0 ? 'OK' : 'MISSING');
console.log('[4] content_copy filter:', s.indexOf('content_copy|content_paste') > 0 ? 'OK' : 'MISSING');
console.log('[5] sort by length:', s.indexOf('.sort(') > 0 ? 'OK' : 'MISSING');
console.log('[6] icon strip replace:', (s.match(/content_copy/g)||[]).length >= 2 ? 'OK (filter+strip)' : 'CHECK');
// 4. Simulate exact DOM from BTN-DOM-DUMP + CONTEXT logs
let promptRe = /[\u003e\u00bb\u276f]\s+(.+)/;
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
let iconFilterRe = /^(content_copy|content_paste|play_arrow|check_circle|chevron_|keyboard_arrow|more_horiz|more_vert|expand_|alternate_email|arrow_drop)/;
let btnFilterRe = /^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i;
function simulate(name, siblings) {
console.log('\n=== ' + name + ' ===');
let rcFound = false;
for (let sib of siblings) {
if (sib === 'Running command' || (sib.indexOf('Running command') !== -1 && sib.length < 30)) {
rcFound = true;
break;
}
}
if (!rcFound) { console.log(' RC header NOT FOUND'); return null; }
let cands = [];
for (let sib of siblings) {
if (sib === 'Running command') continue;
if (sib.length < 5) continue;
if (iconFilterRe.test(sib)) { console.log(' SKIP icon: "' + sib.substring(0,30) + '"'); continue; }
if (btnFilterRe.test(sib)) { console.log(' SKIP btn: "' + sib.substring(0,30) + '"'); continue; }
if (sib.indexOf('Always run') !== -1 && sib.indexOf('Cancel') !== -1) { console.log(' SKIP btn-bar: "' + sib.substring(0,30) + '"'); continue; }
cands.push(sib);
}
cands.sort((a,b) => b.length - a.length);
console.log(' Candidates: ' + cands.length);
for (let i = 0; i < cands.length; i++) {
console.log(' [' + i + '] len=' + cands[i].length + ': "' + cands[i].substring(0,80) + '"');
}
for (let cand of cands) {
let m = promptRe.exec(cand);
if (m && m[1].trim().length > 3) {
let cmdV = m[1].trim().replace(stripRe, '').trim();
if (cmdV.length < 3) continue;
console.log(' RESULT: "' + cmdV + '"');
return cmdV;
}
if (cand.length > 10 && /[\u276f\u003e]/.test(cand)) {
let raw = cand.replace(stripRe, '').trim();
console.log(' RESULT (raw): "' + raw + '"');
return raw;
}
}
console.log(' RESULT: NO MATCH');
return null;
}
// Case 1: From BTN-DOM-DUMP (3 siblings, command + content_copy icon)
simulate('Case1: Normal command with icon', [
'Running command',
'\u276f gravity_control > Start-Sleep 12; $logFile content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Case 2: content_copy as standalone sibling
simulate('Case2: Icon as separate div', [
'Running command',
'content_copy',
'\u276f gravity_control > npm run compile',
'Always run Cancel'
]);
// Case 3: No icon appended
simulate('Case3: Clean command', [
'Running command',
'\u276f gravity_control > git add -A; git commit -m "test"',
'Always run Cancel'
]);
// Case 4: Very long command
simulate('Case4: Long command', [
'Running command',
'\u276f gravity_control > Select-String -Path "$env:USERPROFILE\\.gemini\\antigravity\\bridge\\extension.log" -Pattern "CONTEXT" content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Case 5: Prompt only (no command yet)
simulate('Case5: Prompt only', [
'Running command',
'\u276f gravity_control > ',
'Always run Cancel'
]);
// Case 6: Multiple icon texts
simulate('Case6: Multiple icons', [
'Running command',
'play_arrow',
'content_copy',
'\u276f gravity_control > dir content_copy',
'Always run Cancel'
]);
// Case 7: Observed log pattern - "content_copy" was cmdV
// This means the regex matched on just "content_copy" with a > before it
// Possible: the sibling text is "> content_copy" (very short prompt)
simulate('Case7: Short prompt with icon only', [
'Running command',
'> content_copy',
'Always run Cancel'
]);
console.log('\n=== ALL TESTS COMPLETE ===');

View File

@@ -0,0 +1,52 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Extract the terminal prompt regex from generated code
let idx = s.indexOf('_termPromptMatch');
let reCtx = s.substring(idx, s.indexOf(');', idx) + 1);
console.log('v32 code:', reCtx.substring(0, 80));
// Extract regex
let reMatch = reCtx.match(/\/(.+?)\//);
let termRe = new RegExp(reMatch[1]);
console.log('v32 regex:', termRe);
let stripRe = /\s*(content_copy|content_paste|play_arrow)\s*$/;
let tests = [
// Should MATCH (terminal commands)
['\u276f gravity_control > Start-Sleep 12', true, 'Start-Sleep'],
['\u276f gravity_control > npm.cmd run compile', true, 'npm compile'],
['\u276f gravity_control > $logFile = Join-Path $env:USERPROFILE', true, 'PS variable (had JUNK match)'],
['\u276f gravity_control > git add -A; git commit -m "test"', true, 'git commit'],
['\u276f gravity_control > node -e "const {gen}=require(\'./out\')"', true, 'node with const (was JUNK)'],
['\u276f extension > npm.cmd run compile', true, 'extension npm'],
['\u276f gravity_control > Start-Sleep 12 content_copy', true, 'with icon (strip)'],
['\u276f gravity_control > Get-Content f.txt | Select-Object -Last 5', true, 'Get-Content'],
// Should NOT match (prompt only, no command)
['\u276f gravity_control > ', false, 'prompt only'],
['\u276f extension > ', false, 'prompt only ext'],
// Should NOT match (not terminal - JS code)
['function test() { return 1; }', false, 'JS function'],
['const x = require("fs")', false, 'JS const'],
['import { foo } from "bar"', false, 'JS import'],
// Should NOT match (no prompt marker)
['gravity_control > dir', false, 'no marker'],
];
console.log('\n=== v32 TERMINAL PROMPT REGEX TESTS ===');
let pass = 0, fail = 0;
for (let [text, shouldMatch, desc] of tests) {
let m = termRe.exec(text);
let matched = false;
let cmdV = null;
if (m && m[1] && m[1].trim().length > 2) {
cmdV = m[1].trim().replace(stripRe, '').trim();
matched = cmdV.length > 2;
}
let ok = matched === shouldMatch;
if (ok) pass++; else fail++;
console.log((ok ? 'PASS' : 'FAIL') + ' | ' + (matched ? 'MATCH' : 'SKIP ').padEnd(5) + ' | ' + desc);
if (matched && cmdV) console.log(' cmd: "' + cmdV + '"');
}
console.log('\nResult: ' + pass + '/' + (pass+fail) + ' passed' + (fail > 0 ? ' (' + fail + ' FAILED!)' : ' ALL OK'));

View File

@@ -212,6 +212,14 @@ async function processResponseFile(filePath: string) {
// Without this skip, the watcher deletes the file before Observer can poll it
// (since no pending file exists for the isDomObserver check).
if (resp._from_ws) {
// v26: TTL — delete stale _from_ws files after 60s to prevent infinite SKIP spam
const wsRidTs = parseInt((resp.request_id || '').split('_')[0], 10);
const wsAge = isNaN(wsRidTs) ? 999999 : Date.now() - wsRidTs;
if (wsAge > 60_000) {
ctx.logToFile(`[RESPONSE] CLEANUP stale _from_ws file: ${resp.request_id} age=${Math.round(wsAge / 1000)}s`);
try { fs.unlinkSync(filePath); } catch { }
return;
}
ctx.logToFile(`[RESPONSE] SKIP _from_ws file (for Observer pollResponseGroup): ${resp.request_id}`);
return;
}

View File

@@ -16,7 +16,7 @@ import * as path from 'path';
import * as os from 'os';
import * as cp from 'child_process';
import { WSBridgeClient, WSResponseData, WSCommandData } from './ws-client';
import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, resetPendingStateForReconnect, handleDiffReviewResponse, getActiveSessionId as getStepProbeSessionId, getStepProbeContext } from './step-probe';
import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, resetPendingStateForReconnect, handleDiffReviewResponse, getActiveSessionId as getStepProbeSessionId, getStepProbeContext, getLastWaitingCommand } from './step-probe';
import { startHttpBridge, getDeterministicPort, HttpBridgeContext } from './http-bridge';
import { setupApprovalObserver } from './html-patcher';
import { watchCommandsDir, handleWSCommand, disposeCommandsWatcher, CommandHandlerContext } from './command-handler';
@@ -521,6 +521,7 @@ export async function activate(context: vscode.ExtensionContext) {
get sessionStalled() { return getStepProbeContext().sessionStalled; },
get lastPendingStepIndex() { return getStepProbeContext().lastPendingStepIndex; },
writeChatSnapshot,
getLastWaitingCommand,
};
const bridgePort = await startHttpBridge(httpBridgeCtx, sdk);
let localPort = bridgePort;

View File

@@ -26,6 +26,7 @@ export interface HttpBridgeContext {
lastPendingStepIndex: number;
logToFile: (msg: string) => void;
writeChatSnapshot?: (text: string) => void;
getLastWaitingCommand?: () => { cmd: string; desc: string; ts: number };
}
// ─── Module-level state ───
@@ -284,11 +285,13 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
// v19: "Always run" auto-approve — MUST run BEFORE any filter can reject it
// Detects from rawCmd OR from buttons array (Observer may detect sibling first)
let alwaysRunDetected = /^Always\s+run$/i.test(rawCmd);
// v33: Also auto-approve "Accept all" (diff review) and "Accept" buttons
const AUTO_APPROVE_RE = /^(Always\s+run|Accept\s+all|Accept)$/i;
let alwaysRunDetected = AUTO_APPROVE_RE.test(rawCmd);
let alwaysRunBtnIndex = alwaysRunDetected ? 0 : -1;
if (!alwaysRunDetected && Array.isArray(data.buttons)) {
for (let bi = 0; bi < data.buttons.length; bi++) {
if (/^Always\s+run$/i.test((data.buttons[bi].text || '').trim())) {
if (AUTO_APPROVE_RE.test((data.buttons[bi].text || '').trim())) {
alwaysRunDetected = true;
alwaysRunBtnIndex = bi;
break;
@@ -296,6 +299,27 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
}
}
if (alwaysRunDetected) {
// v34: If this is "Accept all" / "Accept", also call agentAcceptAllInFile directly
const isAcceptAll = /^Accept/i.test(rawCmd) || (data.step_type === 'diff_review');
if (isAcceptAll) {
ctx.logToFile(`[HTTP] AUTO-APPROVE Accept all → opening review + agentAcceptAllInFile`);
try {
const vscode = require('vscode');
// v37: Must focus diff review panel BEFORE calling agentAcceptAllInFile
// Without this, the command succeeds but has no effect
(async () => {
try {
await vscode.commands.executeCommand('antigravity.openReviewChanges');
ctx.logToFile(`[HTTP] openReviewChanges OK`);
await new Promise((r: any) => setTimeout(r, 500));
await vscode.commands.executeCommand('antigravity.prioritized.agentAcceptAllInFile');
ctx.logToFile(`[HTTP] ✅ agentAcceptAllInFile SUCCESS`);
} catch (e: any) {
ctx.logToFile(`[HTTP] ❌ agentAcceptAllInFile: ${e.message?.substring(0, 100)}`);
}
})();
} catch (e: any) { ctx.logToFile(`[HTTP] ❌ vscode require failed: ${e.message}`); }
}
// Try enrichment for better Discord display text
let displayCmd = rawCmd;
ctx.logToFile(`[HTTP] AUTO-APPROVE raw: cmd="${rawCmd}" desc="${rawDesc.substring(0, 120)}" buttons=${JSON.stringify((data.buttons || []).map((b: any) => b.text)).substring(0, 200)}`);
@@ -334,9 +358,17 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
}
} catch (e: any) { ctx.logToFile(`[HTTP] AUTO-APPROVE pending lookup error: ${e.message}`); }
}
// v29: Final-final fallback — Step Probe API memory
if (displayCmd === rawCmd && GENERIC_BTN_RE.test(displayCmd) && ctx.getLastWaitingCommand) {
const wc = ctx.getLastWaitingCommand();
if (wc.cmd && wc.cmd.length > 3 && !GENERIC_BTN_RE.test(wc.cmd) && (Date.now() - wc.ts) < 30_000) {
displayCmd = wc.desc && wc.desc.length > wc.cmd.length ? wc.desc.substring(0, 200) : wc.cmd.substring(0, 200);
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from step-probe memory: "${displayCmd.substring(0, 80)}"`);
}
}
const rid = data.request_id || Date.now().toString();
ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunBtnIndex}): cmd="${displayCmd.substring(0, 80)}"`);
// Write response file so observer's pollResponseGroup picks it up and clicks the button
// Write response file IMMEDIATELY so observer clicks the button with zero delay
const responseDir = path.join(ctx.bridgePath, 'response');
if (!fs.existsSync(responseDir)) {
fs.mkdirSync(responseDir, { recursive: true });
@@ -347,26 +379,56 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
button_index: alwaysRunBtnIndex >= 0 ? alwaysRunBtnIndex : 0,
step_type: data.step_type || 'command',
project_name: ctx.projectName,
_from_ws: true, // v38: prevent processResponseFile from consuming before Observer polls
_auto_approve_ttl: Date.now() + 60_000, // auto-expire after 60s
};
fs.writeFileSync(
path.join(responseDir, `${rid}.json`),
JSON.stringify(respPayload),
'utf-8'
);
// Notify Discord (non-interactive "자동 승인" embed)
if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
ctx.wsBridge.sendPending({
request_id: rid,
command: displayCmd,
description: rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd,
step_type: data.step_type || 'command',
status: 'auto_approved',
buttons: data.buttons,
project_name: ctx.projectName,
});
}
// v29: Respond to Observer immediately (don't block button click)
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true }));
// v29: Discord notification — if displayCmd is generic, poll Step Probe for real command
const isGenericDisplay = GENERIC_BTN_RE.test(displayCmd);
const sendDiscord = (finalCmd: string, finalDesc: string) => {
if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
ctx.wsBridge.sendPending({
request_id: rid,
command: finalCmd,
description: finalDesc,
step_type: data.step_type || 'command',
status: 'auto_approved',
buttons: data.buttons,
project_name: ctx.projectName,
});
}
};
if (isGenericDisplay && ctx.getLastWaitingCommand) {
// Poll Step Probe memory for up to 6s (API polls every 5s)
let pollAttempt = 0;
const maxAttempts = 30; // 30 * 200ms = 6s
const pollTimer = setInterval(() => {
pollAttempt++;
const wc = ctx.getLastWaitingCommand!();
if (wc.cmd && wc.cmd.length > 3 && !GENERIC_BTN_RE.test(wc.cmd) && (Date.now() - wc.ts) < 15_000) {
clearInterval(pollTimer);
const enrichedCmd = wc.desc && wc.desc.length > wc.cmd.length ? wc.desc.substring(0, 200) : wc.cmd.substring(0, 200);
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched (delayed ${pollAttempt * 200}ms): "${enrichedCmd.substring(0, 80)}"`);
sendDiscord(enrichedCmd, `[${rawCmd}] ${enrichedCmd}`);
} else if (pollAttempt >= maxAttempts) {
clearInterval(pollTimer);
ctx.logToFile(`[HTTP] AUTO-APPROVE no enrichment after ${maxAttempts * 200}ms — sending generic`);
sendDiscord(displayCmd, rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd);
}
}, 200);
} else {
// Already enriched or no Step Probe — send immediately
sendDiscord(displayCmd, rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd);
}
return;
}
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {

View File

@@ -1,6 +1,6 @@
export function generateApprovalObserverScript(_port: number): string {
return `
// ── Gravity Bridge v17: Always Run Auto-Approve + Retry Detection ──
// ?€?€ Gravity Bridge v17: Always Run Auto-Approve + Retry Detection ?€?€
// v17: "Always run" auto-approve at bridge level + Retry button relay to Discord
(function(){
'use strict';
@@ -12,11 +12,11 @@ export function generateApprovalObserverScript(_port: number): string {
function log(m){
console.log('[GB Observer] '+m);
// v19: Relay important logs to extension via HTTP so they appear in extension.log
if (BASE && (m.indexOf('CV-CLASSES')!==-1 || m.indexOf('CV-CHILDREN')!==-1 || m.indexOf('child[')!==-1 || m.indexOf('CV found')!==-1 || m.indexOf('Conversation view')!==-1 || m.indexOf('BEACON')!==-1 || m.indexOf('ERROR')!==-1 || m.indexOf('chat relay')!==-1 || m.indexOf('user-cls')!==-1)) {
if (BASE && (m.indexOf('CV-CLASSES')!==-1 || m.indexOf('CV-CHILDREN')!==-1 || m.indexOf('child[')!==-1 || m.indexOf('CV found')!==-1 || m.indexOf('Conversation view')!==-1 || m.indexOf('BEACON')!==-1 || m.indexOf('ERROR')!==-1 || m.indexOf('chat relay')!==-1 || m.indexOf('user-cls')!==-1 || m.indexOf('CONTEXT')!==-1 || m.indexOf('BTN-DOM')!==-1 || m.indexOf('DEFERRED')!==-1 || m.indexOf('DETECTED')!==-1 || m.indexOf('ACCEPT')!==-1)) {
try { fetch(BASE+'/log', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({msg:m.substring(0,2000)})}); } catch(e){}
}
}
log('v17 Script loaded Always Run Auto-Approve + Retry Detection');
log('v17 Script loaded ??Always Run Auto-Approve + Retry Detection');
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
try {
@@ -39,7 +39,7 @@ export function generateApprovalObserverScript(_port: number): string {
}
}
// ── Noise filter: lines that are UI artifacts, not real content ──
// ?€?€ Noise filter: lines that are UI artifacts, not real content ?€?€
var NOISE_RE = new RegExp(
'^(' +
'chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|' +
@@ -114,10 +114,10 @@ export function generateApprovalObserverScript(_port: number): string {
return type+'|'+txt+'|'+idx;
}
// ══════════════════════════════════════════════════════════════════
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
// v7: STEP-AWARE CONTEXT EXTRACTION
// Find the closest [data-step-index] ancestor, extract step info
// ══════════════════════════════════════════════════════════════════
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
function getStepContainer(el) {
var node = el;
@@ -129,9 +129,9 @@ export function generateApprovalObserverScript(_port: number): string {
}
// v14: Climb DOM tree to find context near the button
// STRICT SCOPE: Only 5 levels up beyond that we're in unrelated UI territory.
// STRICT SCOPE: Only 5 levels up ??beyond that we're in unrelated UI territory.
// JUNK FILTERS: CSS rules, source code, Material icon gluing are all rejected.
// NO FALLBACK: span/div/p text collection is removed entirely it always grabs chat/UI text.
// NO FALLBACK: span/div/p text collection is removed entirely ??it always grabs chat/UI text.
var PROMPT_ONLY_RE = /^[^\\n]*[\\/\>\\xbb$#]\\s*$/;
// v14: Detect CSS rules, JS source code, or extension internals in code text
var JUNK_CODE_RE = /(!important|::selection|background-color:|var\\(--|font-size:|border-[a-z]|padding:|margin:|display:\\s|\\{[^}]*:[^}]*\\}|===|!==|\\|\\||\\bfunction\\s*\\(|\\bconst\\s+\\w+\\s*=|\\bvar\\s+\\w+\\s*=|\\bif\\s*\\(|\\breturn\\b|\\bimport\\s|\\bexport\\s|\\bclass\\s+\\w|\\bnew\\s+\\w|\\.test\\(|\\.match\\(|\\.replace\\(|_RE[.\\s]|\\brawDesc\\b|\\brawCmd\\b|\\benrichedCmd\\b|\\bquerySelector)/;
@@ -144,7 +144,7 @@ export function generateApprovalObserverScript(_port: number): string {
var _bestCodeHeader = '';
var _sawCodeEls = false;
var _allSkipped = true;
// v22: Increased from 5 to 10 AG Native command display (SRi) can be many levels up
// v22: Increased from 5 to 10 ??AG Native command display (SRi) can be many levels up
for (var depth = 0; depth < 10 && node; depth++) {
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
// v22: Prioritize pre.font-mono (AG Native command line display from SRi component)
@@ -155,6 +155,20 @@ export function generateApprovalObserverScript(_port: number): string {
if (!codeText || codeText.length <= 5) continue;
if (/^Running\\s*\\d/i.test(codeText)) continue;
_sawCodeEls = true;
// v32: Terminal prompt detection — extract command BEFORE JUNK/PROMPT filters
// PS/bash commands can contain JS keywords (return, function, const) → false JUNK matches
var _termPromptMatch = codeText.match(/^[\\u276f\\u00bb]\\s+[^\\n]*[\\u003e]\\s+(.+)/);
if (_termPromptMatch && _termPromptMatch[1].trim().length > 2) {
var _termCmd = _termPromptMatch[1].trim();
_termCmd = _termCmd.replace(/\\s*(content_copy|content_paste|play_arrow)\\s*$/, '').trim();
if (_termCmd.length > 2) {
_allSkipped = false;
_bestCodeText = 'Running command: ' + _termCmd;
log('CONTEXT-OK d='+depth+' src=terminal-prompt cmd='+_termCmd.substring(0,80));
_lastContextDebug = _debugTrail.join(' ');
return _bestCodeText;
}
}
if (PROMPT_ONLY_RE.test(codeText.trim())) {
_debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30));
continue;
@@ -189,6 +203,57 @@ export function generateApprovalObserverScript(_port: number): string {
_lastContextDebug = _debugTrail.join(' > ');
return parts.join(' \u2014 ');
}
// v30: Command text is in plain divs near "Running command" header, not pre/code
var rcDivs = node.querySelectorAll('div');
// v30 diagnostic: log what we find at each depth where code was skipped
if (_sawCodeEls && rcDivs.length > 0 && depth <= 5) {
var rcSample = [];
for (var rdi = 0; rdi < Math.min(rcDivs.length, 8); rdi++) {
var rdt = (rcDivs[rdi].textContent || '').trim().substring(0,40);
var rdc = rcDivs[rdi].children ? rcDivs[rdi].children.length : 0;
rcSample.push('ch'+rdc+':"'+rdt+'"');
}
log('CONTEXT-v30-SCAN d='+depth+' divs='+rcDivs.length+' ['+rcSample.join(', ')+']');
}
for (var rci = 0; rci < rcDivs.length; rci++) {
var rcEl = rcDivs[rci];
var rcChildCount = rcEl.children ? rcEl.children.length : 0;
var rcTxt = (rcEl.textContent || '').trim();
if ((rcTxt === 'Running command' || (rcChildCount === 0 && rcTxt.indexOf('Running command') !== -1 && rcTxt.length < 30)) && rcEl.parentElement) {
var rcP = rcEl.parentElement;
var rcCands = [];
for (var rcsi = 0; rcsi < rcP.children.length; rcsi++) {
if (rcP.children[rcsi] === rcEl) continue;
var sibT = (rcP.children[rcsi].textContent || '').trim();
if (sibT.length < 5) continue;
if (/^(content_copy|content_paste|play_arrow|check_circle|chevron_|keyboard_arrow|more_horiz|more_vert|expand_|alternate_email|arrow_drop)/.test(sibT)) continue;
if (/^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i.test(sibT)) continue;
if (sibT.indexOf('Always run') !== -1 && sibT.indexOf('Cancel') !== -1) continue;
rcCands.push(sibT);
}
rcCands.sort(function(a,b){ return b.length - a.length; });
log('CONTEXT-v30 RC d='+depth+' cands='+rcCands.length+(rcCands.length>0?' best="'+rcCands[0].substring(0,60)+'"':''));
for (var rcci = 0; rcci < rcCands.length; rcci++) {
var candT = rcCands[rcci];
var pM = candT.match(/[\\u003e\\u00bb\\u276f]\\s+(.+)/);
if (pM && pM[1].trim().length > 3) {
var cmdV = pM[1].trim();
cmdV = cmdV.replace(/\\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\\s*$/, '').trim();
if (cmdV.length < 3) continue;
if (/^(Always|Run|Allow|Cancel|Deny)/i.test(cmdV)) continue;
log('CONTEXT-OK d='+depth+' src=running-cmd cmdV='+cmdV.substring(0,80));
_lastContextDebug = _debugTrail.join(' ');
return 'Running command: ' + cmdV.substring(0, 300);
}
if (candT.length > 10 && /[\\u276f\\u003e]/.test(candT)) {
var rawC = candT.replace(/\\s*(content_copy|content_paste|play_arrow)\\s*$/, '').trim();
log('CONTEXT-OK d='+depth+' src=running-cmd-raw rawC='+rawC.substring(0,80));
_lastContextDebug = _debugTrail.join(' ');
return rawC.substring(0, 300);
}
}
}
}
// v23: Also search sibling elements at each level
// AG Native's command display (pre.font-mono) is a SIBLING of footer, not ancestor
if (node && node.parentElement) {
@@ -205,7 +270,7 @@ export function generateApprovalObserverScript(_port: number): string {
_debugTrail.push('sibling_d'+depth+':tag='+siblings[si].tagName.toLowerCase()+',code='+sibCode.substring(0,40));
_bestCodeText = sibCode;
_allSkipped = false;
// Found in sibling return immediately
// Found in sibling ??return immediately
var sibParts = [];
var sibHdr = siblings[si].querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]');
if (sibHdr) sibParts.push(cleanLines((sibHdr.textContent || '').trim().substring(0, 200)));
@@ -234,7 +299,7 @@ export function generateApprovalObserverScript(_port: number): string {
function extractStepContext(btn) {
var stepEl = getStepContainer(btn);
if (!stepEl) {
// v9 FALLBACK: no data-step-index climb DOM for pre/code blocks
// v9 FALLBACK: no data-step-index ??climb DOM for pre/code blocks
return extractContextFromNearby(btn);
}
@@ -270,7 +335,7 @@ export function generateApprovalObserverScript(_port: number): string {
if (codeText && !headerText.includes(codeText.substring(0, 20))) parts.push(codeText);
if (ariaLabel && ariaLabel.length > 5 && !headerText.includes(ariaLabel)) parts.push(ariaLabel);
var result = parts.join(' ');
var result = parts.join(' ??');
if (!result) result = cleanButtonText(btn);
return 'Step #' + stepIdx + ': ' + result;
}
@@ -286,7 +351,7 @@ export function generateApprovalObserverScript(_port: number): string {
for(var i=0; i<ACTION_WORDS.length; i++) {
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
}
// v9: Removed "Running N commands" it's a group header, not an approval button
// v9: Removed "Running N commands" ??it's a group header, not an approval button
return false;
}
function isRejectBtn(txt) {
@@ -366,18 +431,18 @@ export function generateApprovalObserverScript(_port: number): string {
startObserver();
});
// ══════════════════════════════════════════════════════════════════
// v8: FULL DOM STRUCTURE DUMP (unconditional no selector dependency)
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
// v8: FULL DOM STRUCTURE DUMP (unconditional ??no selector dependency)
// Dumps entire document.body tree to /dump-html for real DOM analysis
// Auto-triggers at 5s, 15s, 60s after load to capture React-rendered state
// ══════════════════════════════════════════════════════════════════
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
var _dumpCount=0;
var MAX_DUMPS=8;
var _conversationDumpCount=0;
function walkNode(el, depth, maxDepth, maxChildren) {
if (depth > maxDepth) return {tag:'',text:'depth limit'};
if (depth > maxDepth) return {tag:'MAX',text:'depth limit'};
if (!el || !el.tagName) return null;
var info = {
tag: el.tagName ? el.tagName.toLowerCase() : '#text',
@@ -413,7 +478,7 @@ export function generateApprovalObserverScript(_port: number): string {
if (childInfo) info.children.push(childInfo);
}
if (el.children.length > limit) {
info.children.push({tag: '', text: '+' + (el.children.length - limit) + ' more children'});
info.children.push({tag: 'TRUNC', text: '+' + (el.children.length - limit) + ' more children'});
}
}
return info;
@@ -508,11 +573,11 @@ export function generateApprovalObserverScript(_port: number): string {
setTimeout(function(){ log('Auto-dump @60s'); dumpDOMStructure(); }, 60000);
}
// ══════════════════════════════════════════════════════════════════
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
// v15: AG-NATIVE + CASCADE DUAL CHAT BODY SCANNING
// AG Native: #conversation > ... > .leading-relaxed.select-text
// Cascade: [data-testid="conversation-view"] > [data-step-index]
// ══════════════════════════════════════════════════════════════════
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
var _lastScrapedStepIndex = -1;
var _lastStepText = '';
@@ -684,7 +749,7 @@ export function generateApprovalObserverScript(_port: number): string {
}
}
// v19: Post-process wrap markdown table patterns in code blocks for Discord
// v19: Post-process ??wrap markdown table patterns in code blocks for Discord
// AG Native renders tables as divs, not <table> HTML, so DOM-level handler can't catch them.
// Detect consecutive lines with pipe separators (| col1 | col2 |) and wrap in code block fences
var bt = String.fromCharCode(96, 96, 96);
@@ -749,12 +814,12 @@ export function generateApprovalObserverScript(_port: number): string {
// One-time DOM dump
dumpDOMStructure();
// ── STRATEGY 1: AG Native #conversation or .antigravity-agent-side-panel ──
// ?€?€ STRATEGY 1: AG Native ??#conversation or .antigravity-agent-side-panel ?€?€
var cv = document.querySelector('#conversation');
if (!cv) {
cv = document.querySelector('.antigravity-agent-side-panel');
}
// v19: Fallback find conversation by tracing from known content elements
// v19: Fallback ??find conversation by tracing from known content elements
if (!cv) {
var probe = document.querySelector('.leading-relaxed.select-text') || document.querySelector('.text-ide-message-block-bot-color');
if (probe) {
@@ -818,11 +883,11 @@ export function generateApprovalObserverScript(_port: number): string {
}
// v22: AI response = .leading-relaxed.select-text, User message = .select-text.rounded-lg (Esn component, msn class)
// Source: jetskiAgent/main.js msn="bg-gray-500/10 border border-gray-500/20 p-2 rounded-lg w-full text-sm select-text"
// Source: jetskiAgent/main.js ??msn="bg-gray-500/10 border border-gray-500/20 p-2 rounded-lg w-full text-sm select-text"
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, .select-text.rounded-lg');
if (responseBlocks.length > 0) {
// v22: Filter out thinking/reasoning blocks they have ancestor with max-h-[200px]
// v22: Filter out thinking/reasoning blocks ??they have ancestor with max-h-[200px]
// These are internal AI reasoning and should NOT be relayed to Discord
var filteredBlocks = [];
for (var fbi = 0; fbi < responseBlocks.length; fbi++) {
@@ -847,7 +912,7 @@ export function generateApprovalObserverScript(_port: number): string {
if (lastBlock.dataset.agChatScraped === 'true' || lastBlock.dataset.agChatScraped === 'pending') {
// Check for NEW blocks since last scrape
if (filteredBlocks.length > _lastResponseBlockCount) {
// New block appeared process it
// New block appeared ??process it
for (var rbi = filteredBlocks.length - 1; rbi >= 0; rbi--) {
if (filteredBlocks[rbi].dataset.agChatScraped !== 'true' && filteredBlocks[rbi].dataset.agChatScraped !== 'pending') {
lastBlock = filteredBlocks[rbi];
@@ -896,7 +961,7 @@ export function generateApprovalObserverScript(_port: number): string {
var waitTime = isUser ? 500 : 3000;
if (Date.now() - _lastStepTextTime < waitTime) return; // Still waiting
// v21: DOM-based chat relay RE-ENABLED GetCascadeTrajectorySteps does NOT
// v21: DOM-based chat relay RE-ENABLED ??GetCascadeTrajectorySteps does NOT
// return steps for in-progress cascades, making Step Probe RT-CAPTURE useless.
// Observer DOM extraction is the ONLY real-time path for AI response relay.
_lastStepTextSent = true;
@@ -913,11 +978,11 @@ export function generateApprovalObserverScript(_port: number): string {
.catch(function(e) { el.dataset.agChatScraped = 'false'; log('AG-Native chat send error: ' + e.message); });
})(lastBlock, blockText, filteredBlocks.length, role);
}
return; // AG Native path handled don't fall through to Cascade path
return; // AG Native path handled ??don't fall through to Cascade path
}
}
// ── STRATEGY 2: Cascade [data-testid="conversation-view"] ──
// ?€?€ STRATEGY 2: Cascade ??[data-testid="conversation-view"] ?€?€
cv = document.querySelector('[data-testid="conversation-view"]');
if (!cv) {
// FALLBACK: Try older selectors
@@ -989,7 +1054,7 @@ export function generateApprovalObserverScript(_port: number): string {
if (_lastStepTextSent) continue;
if (Date.now() - _lastStepTextTime < 3000) break; // Still waiting
// Content is stable send it
// Content is stable ??send it
_lastStepTextSent = true;
_lastScrapedStepIndex = stepIdx;
stepEl.dataset.agChatScraped = 'pending';
@@ -1007,34 +1072,52 @@ export function generateApprovalObserverScript(_port: number): string {
}
}
// ══════════════════════════════════════════════════════════════════
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
// BUTTON SCANNING (approval detection)
// ══════════════════════════════════════════════════════════════════
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
function scan(){
if(!_ready)return;
scanChatBodies();
var now=Date.now();
var allBtns=document.querySelectorAll('button');
var allBtns=document.querySelectorAll('button, [role="button"], a.monaco-button, .monaco-text-button, vscode-button, span.cursor-pointer');
if(!allBtns.length)return;
// v25: One-shot debug ??find Accept/Reject elements in ANY tag (run once per 30s)
if (!scan._lastAcceptScan || now - scan._lastAcceptScan > 30000) {
scan._lastAcceptScan = now;
var allEls = document.querySelectorAll('button, a, div, span, [role="button"]');
for (var ai = 0; ai < allEls.length; ai++) {
var aTxt = (allEls[ai].textContent || '').trim();
if (aTxt.length > 2 && aTxt.length < 30 && /Accept|Reject all/i.test(aTxt)) {
log('ACCEPT-SCAN tag=' + allEls[ai].tagName + ' cls=' + (allEls[ai].className || '').substring(0,80) + ' txt=' + aTxt.substring(0,40) + ' oP=' + !!allEls[ai].offsetParent + ' dis=' + allEls[ai].disabled + ' hid=' + allEls[ai].hidden);
}
}
}
for(var j=0;j<allBtns.length;j++){
var b=allBtns[j];
if(b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed'))continue;
// v24: Visibility check moved after txt extraction (see isDiffReviewBtn below)
var txt=cleanButtonText(b);
if(txt.length <= 1) continue;
// v9: Skip group header buttons not approval buttons
// v9: Skip group header buttons ??not approval buttons
if (/^Running\\s*\\d+\\s*commands?$/i.test(txt)) continue;
// v24: Relaxed visibility check ??Accept all/Reject all buttons in AG Native
// editor bottom bar may have offsetParent===null (different rendering layer)
var isDiffReviewBtn = txt.includes('Accept') || txt === 'Reject all';
if(!isDiffReviewBtn && (b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed')))continue;
if(!isActionBtn(txt)) continue;
// Skip inline code lens buttons
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
continue;
}
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt === 'Retry' ? 'retry' : (txt.includes('Run') || txt.includes('Allow') ? 'command' : 'permission'));
var txtLow = txt.toLowerCase();
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt === 'Retry' ? 'retry' : (txtLow.includes('run') || txtLow.includes('allow') ? 'command' : 'permission'));
// v7: Use step-index for more unique group key
var stepContainer = getStepContainer(b);
@@ -1059,12 +1142,88 @@ export function generateApprovalObserverScript(_port: number): string {
var desc=extractContext(b);
// v28: One-shot DOM structure dump for button context analysis
if (!window._btnDomDumped && txtLow.includes('run')) {
window._btnDomDumped = true;
var dumpLines = [];
var cur = b;
for (var dd = 0; dd < 10 && cur; dd++) {
var childSummary = [];
if (cur.children) {
for (var ci2 = 0; ci2 < Math.min(cur.children.length, 8); ci2++) {
var ch = cur.children[ci2];
var chTag = (ch.tagName || '?').toLowerCase();
var chCls = (typeof ch.className === 'string' ? ch.className : '').substring(0, 40);
var chText = (ch.textContent || '').substring(0, 30).replace(/\\n/g, ' ');
childSummary.push(chTag + '.' + chCls.split(' ')[0] + '=' + chText);
}
}
var sibSummary = [];
if (cur.parentElement) {
for (var si2 = 0; si2 < Math.min(cur.parentElement.children.length, 6); si2++) {
var sib = cur.parentElement.children[si2];
var sibTag = (sib.tagName || '?').toLowerCase();
var sibCls = (typeof sib.className === 'string' ? sib.className : '').substring(0, 30);
sibSummary.push(sibTag + '.' + sibCls.split(' ')[0] + (sib === cur ? '*' : ''));
}
}
dumpLines.push('d' + dd + ':' + (cur.tagName || '?').toLowerCase() + ' cls=' + (typeof cur.className === 'string' ? cur.className : '').substring(0, 50) + ' | children=[' + childSummary.join(', ') + '] | siblings=[' + sibSummary.join(', ') + ']');
cur = cur.parentElement;
}
log('BTN-DOM-DUMP txt=' + txt + ' desc=' + desc.substring(0, 40));
for (var ddi = 0; ddi < dumpLines.length; ddi++) {
log('BTN-DOM-DUMP ' + dumpLines[ddi]);
}
}
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
_sent[groupKey]={rid:rid,ts:now};
for(var mk=0;mk<bidList.length;mk++)_sent[bidList[mk]]={rid:rid,ts:now};
log('DETECTED '+matchedType+': '+txt+' ['+desc.substring(0,80)+'] step='+stepIdx);
// v26: Deferred context (string match, not regex)
var _isGenericDesc = function(d) {
var t = d.trim().toLowerCase();
return t === 'always run' || t === 'run' || t === 'allow' || t === 'accept' || t === 'retry' || t === txt.toLowerCase();
};
// v26: Deferred context ??if desc is generic ("Always run", button text only),
// delay 500ms and re-extract to allow DOM rendering to complete
var isGenericDesc = _isGenericDesc(desc);
if (isGenericDesc && matchedType === 'command') {
log('DEFERRED-CONTEXT: desc="' + desc.substring(0,30) + '" ??waiting 500ms for DOM render');
(function(b2, rid2, btnRefs2, bidList2, groupKey2, txt2, type2, buttonsArr2) {
setTimeout(function() {
var retryDesc = extractContext(b2);
var finalDesc = _isGenericDesc(retryDesc) ? desc : retryDesc;
log('DEFERRED-RESULT: "' + finalDesc.substring(0,80) + '"');
var payload = {
request_id: rid2,
command: txt2,
description: finalDesc,
step_type: type2,
buttons: buttonsArr2,
_debug_trail: _lastContextDebug || ''
};
fetch(BASE+'/pending',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload)
}).then(function(r){return r.json();}).then(function(d){
if (!d.ok || d.filtered) {
delete _sent[groupKey2];
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
return;
}
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
}).catch(function(e){
delete _sent[groupKey2];
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
});
}, 500);
})(b, rid, btnRefs, bidList, groupKey, txt, matchedType, buttonsArr);
} else {
// Original immediate send path
log('DETECTED '+matchedType+': '+txt+' ['+desc.substring(0,80)+'] step='+stepIdx);
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
var payload={
@@ -1091,6 +1250,7 @@ export function generateApprovalObserverScript(_port: number): string {
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
});
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
} // end else (immediate send)
break;
}
@@ -1174,7 +1334,7 @@ export function generateApprovalObserverScript(_port: number): string {
function startObserver(){
if(_obs)return;
log('startObserver() scheduling auto-dumps and mutation observer');
log('startObserver() ??scheduling auto-dumps and mutation observer');
scheduleAutoDumps();
new MutationObserver(function(mutations){
for(var i=0;i<mutations.length;i++){
@@ -1186,7 +1346,7 @@ export function generateApprovalObserverScript(_port: number): string {
}).observe(document.body,{childList:true,subtree:true});
setInterval(scheduleScan,3000);
// ── TRIGGER-CLICK POLLING ──
// ?€?€ TRIGGER-CLICK POLLING ?€?€
(function pollTriggerClick(){
if(_ready&&BASE){
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
@@ -1209,12 +1369,12 @@ export function generateApprovalObserverScript(_port: number): string {
setTimeout(pollTriggerClick, 2000);
})();
// ── DEEP-INSPECT POLLING (v8: full body dump) ──
// ?€?€ DEEP-INSPECT POLLING (v8: full body dump) ?€?€
(function pollDeepInspect(){
if(_ready&&BASE){
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(!d.inspect)return;
log('Deep inspect triggered full body dump');
log('Deep inspect triggered ??full body dump');
// Force a fresh DOM dump
_dumpCount = Math.max(0, _dumpCount - 1); // allow one more dump
dumpDOMStructure();
@@ -1264,3 +1424,4 @@ export function generateApprovalObserverScript(_port: number): string {
})();
`;
}

View File

@@ -41,6 +41,9 @@ let activeTrajectoryId = '';
const recentPendingSteps = new Map<string, number>();
const PENDING_MEMORY_TTL_MS = 30_000;
// v29: Last WAITING command from API — used by http-bridge for Always run enrichment
let lastWaitingCommand = { cmd: '', desc: '', ts: 0 };
// generateApprovalObserverScript → extracted to ./observer-script.ts
const lastSnapshotText = new Map<string, string>();
@@ -79,6 +82,14 @@ export function getStepProbeContext(): { activeSessionId: string; sessionStalled
};
}
/**
* v29: Get last WAITING command from Step Probe API.
* Used by http-bridge as fallback when Observer's extractContext returns generic "Always run".
*/
export function getLastWaitingCommand(): { cmd: string; desc: string; ts: number } {
return { ...lastWaitingCommand };
}
/**
* Reset pending state after successful approval.
* Called after WS response triggers approval in extension.ts.
@@ -608,6 +619,8 @@ function setupMonitor() {
const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIdx, sType || '', toolCall);
ctx.logToFile(`[STEP-PROBE] ★ WAITING (RT)! step=${actualIdx} type=${sType} cmd='${command}'`);
// v29: Save for http-bridge enrichment
lastWaitingCommand = { cmd: command, desc: description, ts: Date.now() };
if (actualIdx !== ctx.lastPendingStepIndex) {
ctx.stallProbed = true;
@@ -795,6 +808,19 @@ function setupMonitor() {
source: 'step_probe_offset',
safe_to_auto_run: isSafeToAutoRun,
});
// v35: Auto-accept code edits (offset path)
if (['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName)) {
ctx.logToFile(`[STEP-PROBE] v35: code_edit (offset) → auto-accepting in 500ms`);
setTimeout(async () => {
try {
const vscode = require('vscode');
await vscode.commands.executeCommand('antigravity.prioritized.agentAcceptAllInFile');
ctx.logToFile(`[STEP-PROBE] ✅ agentAcceptAllInFile (offset) SUCCESS`);
} catch (e: any) {
ctx.logToFile(`[STEP-PROBE] ❌ agentAcceptAllInFile (offset): ${e.message?.substring(0, 100)}`);
}
}, 500);
}
}
}
// NOTE: no break — process ALL parallel WAITING steps
@@ -848,6 +874,20 @@ function setupMonitor() {
source: 'step_probe',
safe_to_auto_run: isSafeToAutoRun,
});
// v35: Auto-accept code edits via agentAcceptAllInFile
// Observer can't see "Accept all" button (different DOM layer)
if (['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName)) {
ctx.logToFile(`[STEP-PROBE] v35: code_edit detected → auto-accepting in 500ms`);
setTimeout(async () => {
try {
const vscode = require('vscode');
await vscode.commands.executeCommand('antigravity.prioritized.agentAcceptAllInFile');
ctx.logToFile(`[STEP-PROBE] ✅ agentAcceptAllInFile SUCCESS`);
} catch (e: any) {
ctx.logToFile(`[STEP-PROBE] ❌ agentAcceptAllInFile: ${e.message?.substring(0, 100)}`);
}
}, 500);
}
}
}
// NOTE: no break — process ALL parallel WAITING steps