Compare commits
29 Commits
139ad3ee93
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32cf69469c | ||
|
|
7c8891b99c | ||
|
|
3cc3442fda | ||
|
|
e95e7791f9 | ||
|
|
2bf1eb41d1 | ||
|
|
cf1352eefa | ||
|
|
6aea48e2e9 | ||
|
|
bd5a7ca8b9 | ||
|
|
8ada5f7daf | ||
|
|
4f2be831a1 | ||
|
|
cbfd137dcb | ||
|
|
a99a1e3f54 | ||
|
|
ad4ed623bd | ||
|
|
64800d3c20 | ||
|
|
70c83b4226 | ||
|
|
bb54802c06 | ||
|
|
bf53072f3c | ||
|
|
02b4b03699 | ||
|
|
db805c6fde | ||
|
|
7f33a20e43 | ||
|
|
ef788e6ecc | ||
|
|
cd00986274 | ||
|
|
12095f36a4 | ||
|
|
498683c977 | ||
|
|
1662ac4f6b | ||
|
|
d027562f17 | ||
|
|
cc261011d6 | ||
|
|
37fbb9657e | ||
|
|
965f619664 |
@@ -21,6 +21,18 @@
|
|||||||
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
|
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 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)
|
### [2026-04-18] [Extension] ★ WS response 파일이 processResponseFile에 의해 삭제 → Observer pollResponseGroup 실패 (v0.5.78)
|
||||||
- **증상**: `!auto` Retry가 작동하지 않음. Observer가 `/response/{rid}`를 폴링하지만 항상 `{waiting: true}` 반환.
|
- **증상**: `!auto` Retry가 작동하지 않음. Observer가 `/response/{rid}`를 폴링하지만 항상 `{waiting: true}` 반환.
|
||||||
- **원인**: extension.ts의 WS 응답 핸들러가 `response/{rid}.json` 파일 작성 → 300ms 후 response watcher(`processResponseFile`)가 파일 감지 → pending 파일이 없어 `isDomObserver=false` → `fs.unlinkSync()` 실행 → Observer가 폴링할 때 파일이 이미 삭제됨.
|
- **원인**: extension.ts의 WS 응답 핸들러가 `response/{rid}.json` 파일 작성 → 300ms 후 response watcher(`processResponseFile`)가 파일 감지 → pending 파일이 없어 `isDomObserver=false` → `fs.unlinkSync()` 실행 → Observer가 폴링할 때 파일이 이미 삭제됨.
|
||||||
|
|||||||
205
.agents/references/observer-dev-guide.md
Normal file
205
.agents/references/observer-dev-guide.md
Normal 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 |
|
||||||
@@ -55,7 +55,7 @@ extension.ts activate()
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `scanChatBodies()` | 3초마다 실행, conversation view에서 메시지 블록 탐색 |
|
| `scanChatBodies()` | 3초마다 실행, conversation view에서 메시지 블록 탐색 |
|
||||||
| `extractCleanStepText(el)` | DOM 클론 → style/script/button 제거 → textContent 추출 |
|
| `extractCleanStepText(el)` | DOM 클론 → style/script/button 제거 → textContent 추출 |
|
||||||
| `extractContextFromNearby(btn)` | 승인 버튼 주변 DOM에서 명령어 텍스트 추출 |
|
| `extractContextFromNearby(btn)` | 승인 버튼 주변 DOM에서 명령어 텍스트 추출 (v23: sibling 탐색 포함) |
|
||||||
| `pollResponseGroup(rid, btnRefs)` | response 파일 polling → 버튼 자동 클릭 |
|
| `pollResponseGroup(rid, btnRefs)` | response 파일 polling → 버튼 자동 클릭 |
|
||||||
|
|
||||||
### 2.3 AI 응답 감지 셀렉터
|
### 2.3 AI 응답 감지 셀렉터
|
||||||
@@ -115,6 +115,7 @@ Observer DOM scan
|
|||||||
1. rawDesc에서 > 프롬프트 마커 찾기 → ✅ 성공 (buttons=2일 때 desc에 프롬프트 포함)
|
1. rawDesc에서 > 프롬프트 마커 찾기 → ✅ 성공 (buttons=2일 때 desc에 프롬프트 포함)
|
||||||
2. rawDesc 최장 라인 사용 → buttons=1일 때 desc="Always run"이라 실패
|
2. rawDesc 최장 라인 사용 → buttons=1일 때 desc="Always run"이라 실패
|
||||||
3. v20 fallback: bridge/pending/ 최신 파일에서 command 읽기 → Step Probe pending 있을 때만
|
3. v20 fallback: bridge/pending/ 최신 파일에서 command 읽기 → Step Probe pending 있을 때만
|
||||||
|
4. v23 sibling: Observer가 footer 형제 요소에서 pre.font-mono 탐색 → ✅ 성공
|
||||||
→ response 파일 작성 → Observer pollResponseGroup → 버튼 클릭
|
→ response 파일 작성 → Observer pollResponseGroup → 버튼 클릭
|
||||||
→ WS sendPending { status: 'auto_approved', command: displayCmd }
|
→ WS sendPending { status: 'auto_approved', command: displayCmd }
|
||||||
→ Discord embed 표시
|
→ 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.70 | heartbeat 로깅 강화 | API step count 동결 확인 |
|
||||||
| v0.5.71 | heartbeat 3 poll마다 실행, HB-CAPTURE 추가 | API가 진행중 step 미반환 재확인 |
|
| v0.5.71 | heartbeat 3 poll마다 실행, HB-CAPTURE 추가 | API가 진행중 step 미반환 재확인 |
|
||||||
| v0.5.72 | Observer DOM relay 재활성화 | AG 재시작 필요 (Observer HTML 캐시) |
|
| 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] AG 재시작하여 Observer 반영 확인 — ✅ v0.5.72 작동 확인
|
||||||
- [x] Observer의 AI 응답 릴레이가 작동하는지 Discord에서 확인 — ✅ 작동
|
- [x] Observer의 AI 응답 릴레이가 작동하는지 Discord에서 확인 — ✅ 작동
|
||||||
- [ ] v0.5.73 설치 (MSG-BLOCKS 로그 추가) → AG 재시작 → 사용자 메시지 DOM 클래스 식별
|
- [x] 사용자 메시지 셀렉터 추가 — ✅ v0.5.74
|
||||||
- [ ] 사용자 메시지 셀렉터 추가 후 테스트
|
- [x] Retry auto-approve 흐름 복구 — ✅ v0.5.78 (_from_ws 마커)
|
||||||
- [ ] "Always run" enrichment 개선 — buttons=1일 때 code 블록 추출 개선
|
- [x] 명령어 컨텍스트 sibling 탐색 — ✅ v0.5.79
|
||||||
|
- [x] Thinking 블록 필터링 — ✅ v0.5.79
|
||||||
|
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패) #636
|
||||||
|
- [ ] Observer pollResponseGroup 미시작 케이스 (trigger-click 선점)
|
||||||
- [ ] AI 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)
|
- [ ] AI 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)
|
||||||
|
|||||||
26
docs/devlog/2026-04-19.md
Normal file
26
docs/devlog/2026-04-19.md
Normal 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" 버튼 자동 소멸 확인 ✅
|
||||||
@@ -520,6 +520,7 @@ async function activate(context) {
|
|||||||
get sessionStalled() { return (0, step_probe_1.getStepProbeContext)().sessionStalled; },
|
get sessionStalled() { return (0, step_probe_1.getStepProbeContext)().sessionStalled; },
|
||||||
get lastPendingStepIndex() { return (0, step_probe_1.getStepProbeContext)().lastPendingStepIndex; },
|
get lastPendingStepIndex() { return (0, step_probe_1.getStepProbeContext)().lastPendingStepIndex; },
|
||||||
writeChatSnapshot,
|
writeChatSnapshot,
|
||||||
|
getLastWaitingCommand: step_probe_1.getLastWaitingCommand,
|
||||||
};
|
};
|
||||||
const bridgePort = await (0, http_bridge_1.startHttpBridge)(httpBridgeCtx, sdk);
|
const bridgePort = await (0, http_bridge_1.startHttpBridge)(httpBridgeCtx, sdk);
|
||||||
let localPort = bridgePort;
|
let localPort = bridgePort;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
4
extension/package-lock.json
generated
4
extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"version": "0.5.79",
|
"version": "0.5.103",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"version": "0.5.79",
|
"version": "0.5.103",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
||||||
"version": "0.5.79",
|
"version": "0.5.103",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
|
|||||||
98
extension/scratch/analyze_all_dumps.js
Normal file
98
extension/scratch/analyze_all_dumps.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
extension/scratch/diff_test.py
Normal file
2
extension/scratch/diff_test.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# diff_review detection test v2
|
||||||
|
test_value = "hello"
|
||||||
26
extension/scratch/print_dom_tree.js
Normal file
26
extension/scratch/print_dom_tree.js
Normal 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);
|
||||||
22
extension/scratch/test_accept.js
Normal file
22
extension/scratch/test_accept.js
Normal 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)));
|
||||||
114
extension/scratch/verify_final.js
Normal file
114
extension/scratch/verify_final.js
Normal 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 ===');
|
||||||
43
extension/scratch/verify_junk.js
Normal file
43
extension/scratch/verify_junk.js
Normal 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);
|
||||||
|
}
|
||||||
42
extension/scratch/verify_regex.js
Normal file
42
extension/scratch/verify_regex.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
122
extension/scratch/verify_v096.js
Normal file
122
extension/scratch/verify_v096.js
Normal 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 ===');
|
||||||
52
extension/scratch/verify_v32.js
Normal file
52
extension/scratch/verify_v32.js
Normal 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'));
|
||||||
@@ -212,6 +212,14 @@ async function processResponseFile(filePath: string) {
|
|||||||
// Without this skip, the watcher deletes the file before Observer can poll it
|
// Without this skip, the watcher deletes the file before Observer can poll it
|
||||||
// (since no pending file exists for the isDomObserver check).
|
// (since no pending file exists for the isDomObserver check).
|
||||||
if (resp._from_ws) {
|
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}`);
|
ctx.logToFile(`[RESPONSE] SKIP _from_ws file (for Observer pollResponseGroup): ${resp.request_id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import * as path from 'path';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as cp from 'child_process';
|
import * as cp from 'child_process';
|
||||||
import { WSBridgeClient, WSResponseData, WSCommandData } from './ws-client';
|
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 { startHttpBridge, getDeterministicPort, HttpBridgeContext } from './http-bridge';
|
||||||
import { setupApprovalObserver } from './html-patcher';
|
import { setupApprovalObserver } from './html-patcher';
|
||||||
import { watchCommandsDir, handleWSCommand, disposeCommandsWatcher, CommandHandlerContext } from './command-handler';
|
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 sessionStalled() { return getStepProbeContext().sessionStalled; },
|
||||||
get lastPendingStepIndex() { return getStepProbeContext().lastPendingStepIndex; },
|
get lastPendingStepIndex() { return getStepProbeContext().lastPendingStepIndex; },
|
||||||
writeChatSnapshot,
|
writeChatSnapshot,
|
||||||
|
getLastWaitingCommand,
|
||||||
};
|
};
|
||||||
const bridgePort = await startHttpBridge(httpBridgeCtx, sdk);
|
const bridgePort = await startHttpBridge(httpBridgeCtx, sdk);
|
||||||
let localPort = bridgePort;
|
let localPort = bridgePort;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface HttpBridgeContext {
|
|||||||
lastPendingStepIndex: number;
|
lastPendingStepIndex: number;
|
||||||
logToFile: (msg: string) => void;
|
logToFile: (msg: string) => void;
|
||||||
writeChatSnapshot?: (text: string) => void;
|
writeChatSnapshot?: (text: string) => void;
|
||||||
|
getLastWaitingCommand?: () => { cmd: string; desc: string; ts: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Module-level state ───
|
// ─── 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
|
// 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)
|
// 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;
|
let alwaysRunBtnIndex = alwaysRunDetected ? 0 : -1;
|
||||||
if (!alwaysRunDetected && Array.isArray(data.buttons)) {
|
if (!alwaysRunDetected && Array.isArray(data.buttons)) {
|
||||||
for (let bi = 0; bi < data.buttons.length; bi++) {
|
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;
|
alwaysRunDetected = true;
|
||||||
alwaysRunBtnIndex = bi;
|
alwaysRunBtnIndex = bi;
|
||||||
break;
|
break;
|
||||||
@@ -296,6 +299,27 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (alwaysRunDetected) {
|
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
|
// Try enrichment for better Discord display text
|
||||||
let displayCmd = rawCmd;
|
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)}`);
|
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}`); }
|
} 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();
|
const rid = data.request_id || Date.now().toString();
|
||||||
ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunBtnIndex}): cmd="${displayCmd.substring(0, 80)}"`);
|
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');
|
const responseDir = path.join(ctx.bridgePath, 'response');
|
||||||
if (!fs.existsSync(responseDir)) {
|
if (!fs.existsSync(responseDir)) {
|
||||||
fs.mkdirSync(responseDir, { recursive: true });
|
fs.mkdirSync(responseDir, { recursive: true });
|
||||||
@@ -347,26 +379,56 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
button_index: alwaysRunBtnIndex >= 0 ? alwaysRunBtnIndex : 0,
|
button_index: alwaysRunBtnIndex >= 0 ? alwaysRunBtnIndex : 0,
|
||||||
step_type: data.step_type || 'command',
|
step_type: data.step_type || 'command',
|
||||||
project_name: ctx.projectName,
|
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(
|
fs.writeFileSync(
|
||||||
path.join(responseDir, `${rid}.json`),
|
path.join(responseDir, `${rid}.json`),
|
||||||
JSON.stringify(respPayload),
|
JSON.stringify(respPayload),
|
||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
// Notify Discord (non-interactive "자동 승인" embed)
|
// v29: Respond to Observer immediately (don't block button click)
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true }));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
|
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export function generateApprovalObserverScript(_port: number): string {
|
export function generateApprovalObserverScript(_port: number): string {
|
||||||
return `
|
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
|
// v17: "Always run" auto-approve at bridge level + Retry button relay to Discord
|
||||||
(function(){
|
(function(){
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -12,11 +12,11 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
function log(m){
|
function log(m){
|
||||||
console.log('[GB Observer] '+m);
|
console.log('[GB Observer] '+m);
|
||||||
// v19: Relay important logs to extension via HTTP so they appear in extension.log
|
// 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){}
|
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
|
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
|
||||||
try {
|
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(
|
var NOISE_RE = new RegExp(
|
||||||
'^(' +
|
'^(' +
|
||||||
'chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|' +
|
'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;
|
return type+'|'+txt+'|'+idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||||||
// v7: STEP-AWARE CONTEXT EXTRACTION
|
// v7: STEP-AWARE CONTEXT EXTRACTION
|
||||||
// Find the closest [data-step-index] ancestor, extract step info
|
// Find the closest [data-step-index] ancestor, extract step info
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||||||
|
|
||||||
function getStepContainer(el) {
|
function getStepContainer(el) {
|
||||||
var node = el;
|
var node = el;
|
||||||
@@ -129,9 +129,9 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// v14: Climb DOM tree to find context near the button
|
// 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.
|
// 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*$/;
|
var PROMPT_ONLY_RE = /^[^\\n]*[\\/\>\\xbb$#]\\s*$/;
|
||||||
// v14: Detect CSS rules, JS source code, or extension internals in code text
|
// 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)/;
|
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 _bestCodeHeader = '';
|
||||||
var _sawCodeEls = false;
|
var _sawCodeEls = false;
|
||||||
var _allSkipped = true;
|
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++) {
|
for (var depth = 0; depth < 10 && node; depth++) {
|
||||||
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
|
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)
|
// 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 (!codeText || codeText.length <= 5) continue;
|
||||||
if (/^Running\\s*\\d/i.test(codeText)) continue;
|
if (/^Running\\s*\\d/i.test(codeText)) continue;
|
||||||
_sawCodeEls = true;
|
_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())) {
|
if (PROMPT_ONLY_RE.test(codeText.trim())) {
|
||||||
_debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30));
|
_debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30));
|
||||||
continue;
|
continue;
|
||||||
@@ -189,6 +203,57 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
_lastContextDebug = _debugTrail.join(' > ');
|
_lastContextDebug = _debugTrail.join(' > ');
|
||||||
return parts.join(' \u2014 ');
|
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
|
// v23: Also search sibling elements at each level
|
||||||
// AG Native's command display (pre.font-mono) is a SIBLING of footer, not ancestor
|
// AG Native's command display (pre.font-mono) is a SIBLING of footer, not ancestor
|
||||||
if (node && node.parentElement) {
|
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));
|
_debugTrail.push('sibling_d'+depth+':tag='+siblings[si].tagName.toLowerCase()+',code='+sibCode.substring(0,40));
|
||||||
_bestCodeText = sibCode;
|
_bestCodeText = sibCode;
|
||||||
_allSkipped = false;
|
_allSkipped = false;
|
||||||
// Found in sibling — return immediately
|
// Found in sibling ??return immediately
|
||||||
var sibParts = [];
|
var sibParts = [];
|
||||||
var sibHdr = siblings[si].querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]');
|
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)));
|
if (sibHdr) sibParts.push(cleanLines((sibHdr.textContent || '').trim().substring(0, 200)));
|
||||||
@@ -234,7 +299,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
function extractStepContext(btn) {
|
function extractStepContext(btn) {
|
||||||
var stepEl = getStepContainer(btn);
|
var stepEl = getStepContainer(btn);
|
||||||
if (!stepEl) {
|
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);
|
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 (codeText && !headerText.includes(codeText.substring(0, 20))) parts.push(codeText);
|
||||||
if (ariaLabel && ariaLabel.length > 5 && !headerText.includes(ariaLabel)) parts.push(ariaLabel);
|
if (ariaLabel && ariaLabel.length > 5 && !headerText.includes(ariaLabel)) parts.push(ariaLabel);
|
||||||
|
|
||||||
var result = parts.join(' — ');
|
var result = parts.join(' ??');
|
||||||
if (!result) result = cleanButtonText(btn);
|
if (!result) result = cleanButtonText(btn);
|
||||||
return 'Step #' + stepIdx + ': ' + result;
|
return 'Step #' + stepIdx + ': ' + result;
|
||||||
}
|
}
|
||||||
@@ -286,7 +351,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
for(var i=0; i<ACTION_WORDS.length; i++) {
|
for(var i=0; i<ACTION_WORDS.length; i++) {
|
||||||
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
function isRejectBtn(txt) {
|
function isRejectBtn(txt) {
|
||||||
@@ -366,18 +431,18 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
startObserver();
|
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
|
// 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
|
// Auto-triggers at 5s, 15s, 60s after load to capture React-rendered state
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||||||
|
|
||||||
var _dumpCount=0;
|
var _dumpCount=0;
|
||||||
var MAX_DUMPS=8;
|
var MAX_DUMPS=8;
|
||||||
var _conversationDumpCount=0;
|
var _conversationDumpCount=0;
|
||||||
|
|
||||||
function walkNode(el, depth, maxDepth, maxChildren) {
|
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;
|
if (!el || !el.tagName) return null;
|
||||||
var info = {
|
var info = {
|
||||||
tag: el.tagName ? el.tagName.toLowerCase() : '#text',
|
tag: el.tagName ? el.tagName.toLowerCase() : '#text',
|
||||||
@@ -413,7 +478,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
if (childInfo) info.children.push(childInfo);
|
if (childInfo) info.children.push(childInfo);
|
||||||
}
|
}
|
||||||
if (el.children.length > limit) {
|
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;
|
return info;
|
||||||
@@ -508,11 +573,11 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
setTimeout(function(){ log('Auto-dump @60s'); dumpDOMStructure(); }, 60000);
|
setTimeout(function(){ log('Auto-dump @60s'); dumpDOMStructure(); }, 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||||||
// v15: AG-NATIVE + CASCADE DUAL CHAT BODY SCANNING
|
// v15: AG-NATIVE + CASCADE DUAL CHAT BODY SCANNING
|
||||||
// AG Native: #conversation > ... > .leading-relaxed.select-text
|
// AG Native: #conversation > ... > .leading-relaxed.select-text
|
||||||
// Cascade: [data-testid="conversation-view"] > [data-step-index]
|
// Cascade: [data-testid="conversation-view"] > [data-step-index]
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||||||
|
|
||||||
var _lastScrapedStepIndex = -1;
|
var _lastScrapedStepIndex = -1;
|
||||||
var _lastStepText = '';
|
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.
|
// 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
|
// Detect consecutive lines with pipe separators (| col1 | col2 |) and wrap in code block fences
|
||||||
var bt = String.fromCharCode(96, 96, 96);
|
var bt = String.fromCharCode(96, 96, 96);
|
||||||
@@ -749,12 +814,12 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
// One-time DOM dump
|
// One-time DOM dump
|
||||||
dumpDOMStructure();
|
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');
|
var cv = document.querySelector('#conversation');
|
||||||
if (!cv) {
|
if (!cv) {
|
||||||
cv = document.querySelector('.antigravity-agent-side-panel');
|
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) {
|
if (!cv) {
|
||||||
var probe = document.querySelector('.leading-relaxed.select-text') || document.querySelector('.text-ide-message-block-bot-color');
|
var probe = document.querySelector('.leading-relaxed.select-text') || document.querySelector('.text-ide-message-block-bot-color');
|
||||||
if (probe) {
|
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)
|
// 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');
|
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, .select-text.rounded-lg');
|
||||||
|
|
||||||
if (responseBlocks.length > 0) {
|
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
|
// These are internal AI reasoning and should NOT be relayed to Discord
|
||||||
var filteredBlocks = [];
|
var filteredBlocks = [];
|
||||||
for (var fbi = 0; fbi < responseBlocks.length; fbi++) {
|
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') {
|
if (lastBlock.dataset.agChatScraped === 'true' || lastBlock.dataset.agChatScraped === 'pending') {
|
||||||
// Check for NEW blocks since last scrape
|
// Check for NEW blocks since last scrape
|
||||||
if (filteredBlocks.length > _lastResponseBlockCount) {
|
if (filteredBlocks.length > _lastResponseBlockCount) {
|
||||||
// New block appeared — process it
|
// New block appeared ??process it
|
||||||
for (var rbi = filteredBlocks.length - 1; rbi >= 0; rbi--) {
|
for (var rbi = filteredBlocks.length - 1; rbi >= 0; rbi--) {
|
||||||
if (filteredBlocks[rbi].dataset.agChatScraped !== 'true' && filteredBlocks[rbi].dataset.agChatScraped !== 'pending') {
|
if (filteredBlocks[rbi].dataset.agChatScraped !== 'true' && filteredBlocks[rbi].dataset.agChatScraped !== 'pending') {
|
||||||
lastBlock = filteredBlocks[rbi];
|
lastBlock = filteredBlocks[rbi];
|
||||||
@@ -896,7 +961,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
var waitTime = isUser ? 500 : 3000;
|
var waitTime = isUser ? 500 : 3000;
|
||||||
if (Date.now() - _lastStepTextTime < waitTime) return; // Still waiting
|
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.
|
// 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.
|
// Observer DOM extraction is the ONLY real-time path for AI response relay.
|
||||||
_lastStepTextSent = true;
|
_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); });
|
.catch(function(e) { el.dataset.agChatScraped = 'false'; log('AG-Native chat send error: ' + e.message); });
|
||||||
})(lastBlock, blockText, filteredBlocks.length, role);
|
})(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"]');
|
cv = document.querySelector('[data-testid="conversation-view"]');
|
||||||
if (!cv) {
|
if (!cv) {
|
||||||
// FALLBACK: Try older selectors
|
// FALLBACK: Try older selectors
|
||||||
@@ -989,7 +1054,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
if (_lastStepTextSent) continue;
|
if (_lastStepTextSent) continue;
|
||||||
if (Date.now() - _lastStepTextTime < 3000) break; // Still waiting
|
if (Date.now() - _lastStepTextTime < 3000) break; // Still waiting
|
||||||
|
|
||||||
// Content is stable — send it
|
// Content is stable ??send it
|
||||||
_lastStepTextSent = true;
|
_lastStepTextSent = true;
|
||||||
_lastScrapedStepIndex = stepIdx;
|
_lastScrapedStepIndex = stepIdx;
|
||||||
stepEl.dataset.agChatScraped = 'pending';
|
stepEl.dataset.agChatScraped = 'pending';
|
||||||
@@ -1007,34 +1072,52 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||||||
// BUTTON SCANNING (approval detection)
|
// BUTTON SCANNING (approval detection)
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||||||
|
|
||||||
function scan(){
|
function scan(){
|
||||||
if(!_ready)return;
|
if(!_ready)return;
|
||||||
scanChatBodies();
|
scanChatBodies();
|
||||||
var now=Date.now();
|
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;
|
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++){
|
for(var j=0;j<allBtns.length;j++){
|
||||||
var b=allBtns[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);
|
var txt=cleanButtonText(b);
|
||||||
if(txt.length <= 1) continue;
|
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;
|
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;
|
if(!isActionBtn(txt)) continue;
|
||||||
// Skip inline code lens buttons
|
// Skip inline code lens buttons
|
||||||
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
|
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
|
||||||
continue;
|
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
|
// v7: Use step-index for more unique group key
|
||||||
var stepContainer = getStepContainer(b);
|
var stepContainer = getStepContainer(b);
|
||||||
@@ -1059,12 +1142,88 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
|
|
||||||
var desc=extractContext(b);
|
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);
|
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
|
||||||
|
|
||||||
_sent[groupKey]={rid:rid,ts:now};
|
_sent[groupKey]={rid:rid,ts:now};
|
||||||
for(var mk=0;mk<bidList.length;mk++)_sent[bidList[mk]]={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){
|
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
|
||||||
var payload={
|
var payload={
|
||||||
@@ -1091,6 +1250,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
||||||
});
|
});
|
||||||
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
|
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
|
||||||
|
} // end else (immediate send)
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1174,7 +1334,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
|
|
||||||
function startObserver(){
|
function startObserver(){
|
||||||
if(_obs)return;
|
if(_obs)return;
|
||||||
log('startObserver() — scheduling auto-dumps and mutation observer');
|
log('startObserver() ??scheduling auto-dumps and mutation observer');
|
||||||
scheduleAutoDumps();
|
scheduleAutoDumps();
|
||||||
new MutationObserver(function(mutations){
|
new MutationObserver(function(mutations){
|
||||||
for(var i=0;i<mutations.length;i++){
|
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});
|
}).observe(document.body,{childList:true,subtree:true});
|
||||||
setInterval(scheduleScan,3000);
|
setInterval(scheduleScan,3000);
|
||||||
|
|
||||||
// ── TRIGGER-CLICK POLLING ──
|
// ?? TRIGGER-CLICK POLLING ??
|
||||||
(function pollTriggerClick(){
|
(function pollTriggerClick(){
|
||||||
if(_ready&&BASE){
|
if(_ready&&BASE){
|
||||||
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
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);
|
setTimeout(pollTriggerClick, 2000);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ── DEEP-INSPECT POLLING (v8: full body dump) ──
|
// ?? DEEP-INSPECT POLLING (v8: full body dump) ??
|
||||||
(function pollDeepInspect(){
|
(function pollDeepInspect(){
|
||||||
if(_ready&&BASE){
|
if(_ready&&BASE){
|
||||||
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
||||||
if(!d.inspect)return;
|
if(!d.inspect)return;
|
||||||
log('Deep inspect triggered — full body dump');
|
log('Deep inspect triggered ??full body dump');
|
||||||
// Force a fresh DOM dump
|
// Force a fresh DOM dump
|
||||||
_dumpCount = Math.max(0, _dumpCount - 1); // allow one more dump
|
_dumpCount = Math.max(0, _dumpCount - 1); // allow one more dump
|
||||||
dumpDOMStructure();
|
dumpDOMStructure();
|
||||||
@@ -1264,3 +1424,4 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ let activeTrajectoryId = '';
|
|||||||
const recentPendingSteps = new Map<string, number>();
|
const recentPendingSteps = new Map<string, number>();
|
||||||
const PENDING_MEMORY_TTL_MS = 30_000;
|
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
|
// generateApprovalObserverScript → extracted to ./observer-script.ts
|
||||||
const lastSnapshotText = new Map<string, string>();
|
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.
|
* Reset pending state after successful approval.
|
||||||
* Called after WS response triggers approval in extension.ts.
|
* 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);
|
const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIdx, sType || '', toolCall);
|
||||||
|
|
||||||
ctx.logToFile(`[STEP-PROBE] ★ WAITING (RT)! step=${actualIdx} type=${sType} cmd='${command}'`);
|
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) {
|
if (actualIdx !== ctx.lastPendingStepIndex) {
|
||||||
ctx.stallProbed = true;
|
ctx.stallProbed = true;
|
||||||
@@ -795,6 +808,19 @@ function setupMonitor() {
|
|||||||
source: 'step_probe_offset',
|
source: 'step_probe_offset',
|
||||||
safe_to_auto_run: isSafeToAutoRun,
|
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
|
// NOTE: no break — process ALL parallel WAITING steps
|
||||||
@@ -848,6 +874,20 @@ function setupMonitor() {
|
|||||||
source: 'step_probe',
|
source: 'step_probe',
|
||||||
safe_to_auto_run: isSafeToAutoRun,
|
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
|
// NOTE: no break — process ALL parallel WAITING steps
|
||||||
|
|||||||
Reference in New Issue
Block a user