Compare commits
36 Commits
13e569f426
...
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 | ||
|
|
139ad3ee93 | ||
|
|
08fd08b9a6 | ||
|
|
326454be40 | ||
|
|
98b7585e3c | ||
|
|
c7f939ce85 | ||
|
|
7a1675fd5d | ||
|
|
6b9f1188c3 |
@@ -21,6 +21,56 @@
|
|||||||
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 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)
|
||||||
|
- **증상**: `!auto` Retry가 작동하지 않음. Observer가 `/response/{rid}`를 폴링하지만 항상 `{waiting: true}` 반환.
|
||||||
|
- **원인**: extension.ts의 WS 응답 핸들러가 `response/{rid}.json` 파일 작성 → 300ms 후 response watcher(`processResponseFile`)가 파일 감지 → pending 파일이 없어 `isDomObserver=false` → `fs.unlinkSync()` 실행 → Observer가 폴링할 때 파일이 이미 삭제됨.
|
||||||
|
- **해결 (v0.5.78)**: WS 응답 파일에 `_from_ws: true` 마커 추가. `processResponseFile`에서 `_from_ws` 파일 스킵 (WS 핸들러에서 이미 `tryApprovalStrategies` 실행하므로 중복 방지도 함께 해결).
|
||||||
|
- **주의**: http-bridge의 `_handlePending`는 pending 파일을 생성하지 않음 (WS 전송만 수행). 따라서 `processResponseFile`의 `isDomObserver` 판별이 실패함. WS 경로로 들어오는 모든 response는 반드시 마커로 구분해야 함.
|
||||||
|
|
||||||
|
### [2026-04-18] [Extension] ★ extractContextFromNearby 조상 탐색만으로는 명령어 추출 불가 (v0.5.79)
|
||||||
|
- **증상**: Discord auto-approve 알림에 "Always run"만 표시되고 실제 명령어가 안 보임. depth를 10으로 늘려도 동일.
|
||||||
|
- **원인**: AG Native DOM 구조에서 "Always run" 버튼은 `footer` 내부에 있고, 실제 명령어(`pre.font-mono`)는 `footer`의 **형제(sibling)** 요소에 있음. 조상 탐색(parentElement)으로는 도달 불가. trail: `d0:button → d1:div → d2:footer` (footer.parentElement가 null).
|
||||||
|
- **해결 (v0.5.79)**: `extractContextFromNearby`에 형제 탐색 로직 추가. 각 depth에서 `node.parentElement.children`을 순회하며 `pre.font-mono, pre, code`를 찾음 → `CONTEXT-OK src=sibling` 성공.
|
||||||
|
- **주의**: Observer 코드 변경은 HTML 인라인 스크립트이므로 AG 2번 재시작(Quit + Relaunch × 2) 필요.
|
||||||
|
|
||||||
|
### [2026-04-18] [Extension] Thinking 블록이 AI 응답으로 릴레이됨
|
||||||
|
- **증상**: AI의 내부 사고 과정(thinking/reasoning)이 Discord에 릴레이됨.
|
||||||
|
- **원인**: Observer의 `scanChatBodies`가 `.leading-relaxed.select-text` 블록을 전부 캡처하는데, thinking 블록도 이 셀렉터에 매칭됨.
|
||||||
|
- **해결**: thinking 블록의 조상에 `max-h-[200px]` 클래스가 있는지 확인하여 필터링.
|
||||||
|
- **주의**: AG UI 업데이트로 thinking 블록의 클래스가 변경될 수 있음.
|
||||||
|
|
||||||
|
|
||||||
|
- **증상**: Step Probe의 RT-CAPTURE, HB-CAPTURE가 현재 대화 중에 전혀 발동하지 않음. POLL에서 `status=IDLE, steps=928, delta=0` 고정. Heartbeat probe에서도 `real=928 known=928` 불변.
|
||||||
|
- **원인**: AG Native의 `GetCascadeTrajectorySteps` API는 **cascade가 완전히 종료(IDLE 전환)된 후에만** 새 step을 반환합니다. 진행 중인 cascade에서는 step count가 동결됩니다. `GetAllCascadeTrajectories`의 `stepCount`도 마찬가지.
|
||||||
|
- **영향**: Step Probe 기반의 모든 실시간 캡처(RT-CAPTURE, HB-CAPTURE, USER_INPUT 감지)가 **구조적으로 불가능**. Observer DOM이 유일한 실시간 데이터 경로.
|
||||||
|
- **해결**: Observer DOM 기반 chat relay를 재활성화 (v0.5.72). Step Probe는 cascade 완료 후 보완 용도로만 사용.
|
||||||
|
- **주의**: 이 제약은 AG Native 아키텍처의 근본적 특성. Polling 주기나 heartbeat 빈도를 변경해도 해결 불가. **실시간 릴레이는 반드시 Observer DOM 경로를 사용해야 함.**
|
||||||
|
- **참조**: `.agents/references/relay-architecture.md` (상세 분석)
|
||||||
|
|
||||||
|
### [2026-04-18] [Extension] Observer의 /pending POST에 명령어 컨텍스트가 없음 (Always run만 전달)
|
||||||
|
- **증상**: Discord auto-approve 알림에 "Always run"만 표시되고 실제 명령어가 안 보임.
|
||||||
|
- **원인**: Observer가 `/pending` POST 시 `command: "Always run"`, `description: "Always run"`만 보냄. `extractContextFromNearby(btn)`이 버튼 주변 DOM에서 유의미한 텍스트를 찾지 못함.
|
||||||
|
- **해결 (부분)**: v0.5.69에서 bridge/pending/ 디렉토리의 최신 Step Probe pending 파일에서 명령어를 읽는 fallback 추가. Step Probe pending이 있을 때만 작동.
|
||||||
|
- **주의**: Step Probe WAITING 감지가 진행 중 cascade에서 불가하므로 (위 이슈 참조), 현재 대부분의 경우 fallback도 실패. Observer의 DOM 컨텍스트 추출 개선이 필요.
|
||||||
|
|
||||||
|
### [2026-04-18] [Extension] Observer의 사용자 메시지 셀렉터 미매칭
|
||||||
|
- **증상**: 사용자가 AG에서 입력한 메시지가 Discord에 전혀 전달되지 않음.
|
||||||
|
- **원인**: Observer의 셀렉터(`.text-ide-message-block-user-color`, `[data-message-role="user"]` 등)가 AG Native DOM에서 매칭되지 않음. AI 응답만 `.leading-relaxed.select-text`로 매칭됨.
|
||||||
|
- **해결**: DOM 덤프에서 사용자 메시지 블록의 실제 CSS 클래스를 식별 후 셀렉터 추가 필요.
|
||||||
|
- **주의**: Step Probe의 USER_INPUT 캡처도 진행 중 cascade에서 불가 (위 이슈 참조).
|
||||||
|
|
||||||
### [2026-04-16] [Extension] ★ AG Native 세션 AI 응답이 Discord에 CSS로 전달됨 (v0.5.52 수정, #632)
|
### [2026-04-16] [Extension] ★ AG Native 세션 AI 응답이 Discord에 CSS로 전달됨 (v0.5.52 수정, #632)
|
||||||
- **증상**: Discord에 AI 대화 응답 대신 **CSS 스타일시트 코드** (`remark-github-blockquote-alert/alert.css`)가 전달됨. `scanChatBodies()` → POST /chat 경로는 작동하지만 내용이 CSS.
|
- **증상**: Discord에 AI 대화 응답 대신 **CSS 스타일시트 코드** (`remark-github-blockquote-alert/alert.css`)가 전달됨. `scanChatBodies()` → POST /chat 경로는 작동하지만 내용이 CSS.
|
||||||
- **원인**: `extractCleanStepText()`에서 clone한 DOM에서 버튼/SVG/아이콘은 제거하지만 **`<style>` 태그는 제거하지 않음**. AG Native 마크다운 렌더러가 `.leading-relaxed.select-text` 내부에 `<style>` 블록을 주입하는데, 이 CSS textContent가 AI 응답 텍스트로 추출됨.
|
- **원인**: `extractCleanStepText()`에서 clone한 DOM에서 버튼/SVG/아이콘은 제거하지만 **`<style>` 태그는 제거하지 않음**. AG Native 마크다운 렌더러가 `.leading-relaxed.select-text` 내부에 `<style>` 블록을 주입하는데, 이 CSS textContent가 AI 응답 텍스트로 추출됨.
|
||||||
|
|||||||
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 |
|
||||||
216
.agents/references/relay-architecture.md
Normal file
216
.agents/references/relay-architecture.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# AG Native 릴레이 아키텍처 분석
|
||||||
|
|
||||||
|
> **이 문서는 AG Native ↔ Discord 릴레이의 데이터 흐름 SSOT입니다.**
|
||||||
|
> 구현/디버깅 전 반드시 확인합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 데이터 경로 요약
|
||||||
|
|
||||||
|
AG Native에서 Discord로 메시지를 전달하는 경로는 크게 2개:
|
||||||
|
|
||||||
|
| # | 경로 | 소스 | 실시간? | 상태 |
|
||||||
|
|---|------|------|---------|------|
|
||||||
|
| 1 | **Observer DOM** | workbench.html 인라인 스크립트 → DOM 관찰 → HTTP POST → http-bridge | ✅ 실시간 | AI 응답: ✅ 작동 (v0.5.72+), 사용자 메시지: ✅ 작동 (v0.5.74+) |
|
||||||
|
| 2 | **Step Probe (trajectory API)** | LS RPC `GetCascadeTrajectorySteps` → step 분석 | ❌ cascade 완료 후에만 | AI 응답: ❌ 실시간 불가, 사용자 메시지: ❌ 실시간 불가 |
|
||||||
|
|
||||||
|
### 1.1 핵심 API 제약 (2026-04-18 확인)
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **`GetCascadeTrajectorySteps`는 진행 중인 cascade의 step을 실시간으로 반환하지 않습니다.**
|
||||||
|
> step count는 cascade가 **완전히 종료**(IDLE 전환)된 후에만 업데이트됩니다.
|
||||||
|
> 따라서 Step Probe의 RT-CAPTURE, HB-CAPTURE 모두 **현재 진행 중인 대화에서는 작동하지 않습니다.**
|
||||||
|
|
||||||
|
**검증 데이터**:
|
||||||
|
- POLL에서 `status=CASCADE_RUN_STATUS_IDLE`, `steps=928`, `delta=0` 고정
|
||||||
|
- HEARTBEAT probe: `offset=927 got=1 real=928 known=928` → 변함 없음
|
||||||
|
- 실제로 수십 개의 tool call이 실행되었지만 step count 불변
|
||||||
|
- Cascade 종료 후 다음 poll에서 step count가 점프 (예: 733 → 865 → 928)
|
||||||
|
|
||||||
|
### 1.2 AG Native SDK EventMonitor
|
||||||
|
|
||||||
|
SDK에 이벤트 시스템이 있으나 **모두 polling 기반**:
|
||||||
|
- `EventMonitor.onStepCountChanged` — getDiagnostics 기반 polling
|
||||||
|
- `EventMonitor.onActiveSessionChanged` — state.vscdb 기반 polling
|
||||||
|
- **실시간 push (WebSocket/SSE)는 없음**
|
||||||
|
- 현재 상태: ERR_CONNECTION_REFUSED 문제로 비활성화됨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Observer DOM 경로 상세
|
||||||
|
|
||||||
|
### 2.1 Observer 스크립트 삽입 체인
|
||||||
|
|
||||||
|
```
|
||||||
|
extension.ts activate()
|
||||||
|
→ html-patcher.ts setupApprovalObserver()
|
||||||
|
→ observer-script.ts generateApprovalObserverScript(port)
|
||||||
|
→ workbench-jetski-agent.html에 인라인 <script> 삽입
|
||||||
|
→ AG 재시작 시 렌더러가 로드
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Observer 주요 함수
|
||||||
|
|
||||||
|
| 함수 | 역할 |
|
||||||
|
|------|------|
|
||||||
|
| `scanChatBodies()` | 3초마다 실행, conversation view에서 메시지 블록 탐색 |
|
||||||
|
| `extractCleanStepText(el)` | DOM 클론 → style/script/button 제거 → textContent 추출 |
|
||||||
|
| `extractContextFromNearby(btn)` | 승인 버튼 주변 DOM에서 명령어 텍스트 추출 (v23: sibling 탐색 포함) |
|
||||||
|
| `pollResponseGroup(rid, btnRefs)` | response 파일 polling → 버튼 자동 클릭 |
|
||||||
|
|
||||||
|
### 2.3 AI 응답 감지 셀렉터
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var responseBlocks = cv.querySelectorAll(
|
||||||
|
'.leading-relaxed.select-text, ' // ← AI 응답 마크다운 블록 (주력)
|
||||||
|
+ '.text-ide-message-block-user-color, ' // ← 사용자 메시지 (미매칭)
|
||||||
|
+ '.text-ide-message-block-bot-color, ' // ← NUX tooltip 전용 (오매칭)
|
||||||
|
+ '.bg-ide-message-block-user-background, '// ← 사용자 메시지 (미매칭)
|
||||||
|
+ '[data-message-role="user"], ' // ← 사용자 메시지 (미매칭)
|
||||||
|
+ '[data-role="user"]' // ← 사용자 메시지 (미매칭)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **v0.5.74에서 사용자 메시지 셀렉터가 추가되었습니다.**
|
||||||
|
> AG Native 소스(`jetskiAgent/main.js`)의 `Esn` 컴포넌트 분석으로
|
||||||
|
> 사용자 메시지 CSS 클래스(`msn = "bg-gray-500/10 border border-gray-500/20 p-2 rounded-lg w-full text-sm select-text"`)를 식별.
|
||||||
|
> 셀렉터: `.select-text.rounded-lg`, 역할 판별: `rounded-lg` 있고 `leading-relaxed` 없으면 → user
|
||||||
|
|
||||||
|
### 2.4 AI 응답 추출 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
scanChatBodies() 3초 간격
|
||||||
|
→ cv = document.querySelector('#conversation')
|
||||||
|
→ responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, ...')
|
||||||
|
→ lastBlock = responseBlocks[last] (가장 최근 블록)
|
||||||
|
→ 이미 scrape 됐으면 skip
|
||||||
|
→ blockText = extractCleanStepText(lastBlock)
|
||||||
|
→ 안정화 대기 (3초 동안 텍스트 변경 없으면)
|
||||||
|
→ POST /chat { text, source, block_index, role }
|
||||||
|
→ http-bridge → writeChatSnapshot() → WS → Discord
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Observer 업데이트 제약
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **Observer 코드는 workbench.html에 인라인 삽입됩니다.**
|
||||||
|
> extension reload만으로는 Observer 코드가 업데이트되지 않습니다.
|
||||||
|
> **AG 재시작 + V8 CachedData 삭제**가 필요합니다.
|
||||||
|
> (단, product.json 체크섬이 맞으면 CachedData 삭제 없이 AG 재시작만으로 충분할 수 있음)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 승인 버튼 (Auto-Approve) 경로
|
||||||
|
|
||||||
|
### 3.1 "Always run" 자동 승인 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
Observer DOM scan
|
||||||
|
→ "Always run" 버튼 텍스트 감지
|
||||||
|
→ POST /pending { command: "Always run", description: "...", buttons: [...] }
|
||||||
|
→ http-bridge _handlePending()
|
||||||
|
→ alwaysRunDetected = true
|
||||||
|
→ enrichment 시도:
|
||||||
|
1. rawDesc에서 > 프롬프트 마커 찾기 → ✅ 성공 (buttons=2일 때 desc에 프롬프트 포함)
|
||||||
|
2. rawDesc 최장 라인 사용 → buttons=1일 때 desc="Always run"이라 실패
|
||||||
|
3. v20 fallback: bridge/pending/ 최신 파일에서 command 읽기 → Step Probe pending 있을 때만
|
||||||
|
4. v23 sibling: Observer가 footer 형제 요소에서 pre.font-mono 탐색 → ✅ 성공
|
||||||
|
→ response 파일 작성 → Observer pollResponseGroup → 버튼 클릭
|
||||||
|
→ WS sendPending { status: 'auto_approved', command: displayCmd }
|
||||||
|
→ Discord embed 표시
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 명령어 enrichment 현황 (2026-04-18 검증)
|
||||||
|
|
||||||
|
| 조건 | 결과 | 빈도 |
|
||||||
|
|------|------|------|
|
||||||
|
| Observer가 buttons=2 (`["Always run","Cancel"]`)이고 desc에 `>` 포함 | ✅ 명령어 표시 | ~50% |
|
||||||
|
| Observer가 buttons=1 (`["Always run"]`)이고 desc="Always run" | ❌ "Always run" 표시 | ~50% |
|
||||||
|
|
||||||
|
**로그 증거** (04:25:59):
|
||||||
|
```
|
||||||
|
AUTO-APPROVE raw: cmd="Always run" desc="…\extension > npm.cmd run compile..." buttons=["Always run","Cancel"]
|
||||||
|
→ cmd="npm.cmd run compile 2>&1; npm.cmd version patch..." ✅ 성공
|
||||||
|
|
||||||
|
AUTO-APPROVE raw: cmd="Always run" desc="Always run" buttons=["Always run"]
|
||||||
|
→ cmd="Always run" ❌ 실패
|
||||||
|
```
|
||||||
|
|
||||||
|
> buttons=2인 경우("Always run" + "Cancel")는 Observer가 code 블록을 찾아 description에 포함.
|
||||||
|
> buttons=1인 경우는 code 블록이 DOM에서 아직 렌더링되지 않았거나 접근 불가.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 사용자 메시지 릴레이 상태
|
||||||
|
|
||||||
|
### 4.1 현재 상태: ✅ 작동 (v0.5.74+)
|
||||||
|
|
||||||
|
| 경로 | 상태 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| Observer DOM | ✅ | `.select-text.rounded-lg` 셀렉터로 캡처 (v0.5.74) |
|
||||||
|
| Step Probe (trajectory API) | ❌ | cascade 진행 중 step 조회 불가 |
|
||||||
|
| Step Probe (observer [USER-MSG]) | ❌ | `lastUserInputStepIndex`가 갱신되지 않음 |
|
||||||
|
|
||||||
|
### 4.2 해결 방안
|
||||||
|
|
||||||
|
1. **DOM 덤프에서 사용자 메시지 클래스 식별** → Observer 셀렉터 추가
|
||||||
|
2. **Cascade 완료 후** Step Probe HB-CAPTURE에서 `USER_INPUT` step 캡처 (지연 릴레이)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 파일/포트 매핑
|
||||||
|
|
||||||
|
| 항목 | 값 |
|
||||||
|
|------|-----|
|
||||||
|
| Observer 삽입 대상 | `workbench-jetski-agent.html` |
|
||||||
|
| HTTP Bridge 포트 | `getDeterministicPort('gravity_control')` = **18080** |
|
||||||
|
| Extension 로그 | `~/.gemini/antigravity/bridge/extension.log` |
|
||||||
|
| Pending 파일 | `~/.gemini/antigravity/bridge/pending/*.json` |
|
||||||
|
| Response 파일 | `~/.gemini/antigravity/bridge/response/*.json` |
|
||||||
|
| Chat Snapshot 파일 | `~/.gemini/antigravity/bridge/chat_snapshots/*.json` |
|
||||||
|
| Discord 채널 | `#ag-gravity_control` (ID: 1483082084540223663) |
|
||||||
|
| Discord Bot 토큰 | `.env` → `DISCORD_TOKEN` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 디버깅 도구
|
||||||
|
|
||||||
|
| 도구 | 경로 | 용도 |
|
||||||
|
|------|------|------|
|
||||||
|
| Discord 메시지 읽기 | `extension/scratch/discord_read.js` | API로 채널 최근 메시지 조회 |
|
||||||
|
| Discord 채널 목록 | `extension/scratch/discord_channels.js` | 서버 채널 목록 조회 |
|
||||||
|
| Extension 로그 확인 | `Select-String -Path $logFile -Pattern "패턴"` | 실시간 로그 분석 |
|
||||||
|
| DOM 구조 덤프 | Observer 자동 (CV-CHILDREN 로그) | AG Native DOM 클래스 식별 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 버전 히스토리 (v0.5.67~)
|
||||||
|
|
||||||
|
| 버전 | 변경 | 결과 |
|
||||||
|
|------|------|------|
|
||||||
|
| v0.5.67 | Observer DOM relay 비활성화, Step Probe RT-CAPTURE로 전환 | ❌ API가 진행중 step 미반환 |
|
||||||
|
| v0.5.68 | auto-approve enrichment 디버그 로그 추가, 조건 >10 → >3 완화 | Observer가 desc="Always run" 보냄 확인 |
|
||||||
|
| v0.5.69 | pending 파일 fallback으로 auto-approve 명령어 enrichment | 일부 개선 (Step Probe pending 있을 때만) |
|
||||||
|
| v0.5.70 | heartbeat 로깅 강화 | API step count 동결 확인 |
|
||||||
|
| v0.5.71 | heartbeat 3 poll마다 실행, HB-CAPTURE 추가 | API가 진행중 step 미반환 재확인 |
|
||||||
|
| v0.5.72 | Observer DOM relay 재활성화 | AG 재시작 필요 (Observer HTML 캐시) |
|
||||||
|
| v0.5.74 | 사용자 메시지 셀렉터 추가 (`.select-text.rounded-lg`) | ✅ 사용자 메시지 릴레이 작동 |
|
||||||
|
| v0.5.76 | DOM 탐색 depth 5→10, `pre.font-mono` 우선 탐색 | Observer HTML 업데이트 필요 |
|
||||||
|
| v0.5.77 | WS response 파일 작성 (pollResponseGroup용) | Retry 클릭 경로 추가 |
|
||||||
|
| v0.5.78 | `_from_ws` 마커로 processResponseFile 삭제 방지 | ✅ Retry auto-approve 작동 |
|
||||||
|
| v0.5.79 | sibling 탐색 추가 + thinking 블록 필터링 | ✅ 명령어 컨텍스트 부분 추출 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 남은 작업 (TODO)
|
||||||
|
|
||||||
|
- [x] AG 재시작하여 Observer 반영 확인 — ✅ v0.5.72 작동 확인
|
||||||
|
- [x] Observer의 AI 응답 릴레이가 작동하는지 Discord에서 확인 — ✅ 작동
|
||||||
|
- [x] 사용자 메시지 셀렉터 추가 — ✅ v0.5.74
|
||||||
|
- [x] Retry auto-approve 흐름 복구 — ✅ v0.5.78 (_from_ws 마커)
|
||||||
|
- [x] 명령어 컨텍스트 sibling 탐색 — ✅ v0.5.79
|
||||||
|
- [x] Thinking 블록 필터링 — ✅ v0.5.79
|
||||||
|
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패) #636
|
||||||
|
- [ ] Observer pollResponseGroup 미시작 케이스 (trigger-click 선점)
|
||||||
|
- [ ] AI 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)
|
||||||
5
docs/devlog/2026-04-17.md
Normal file
5
docs/devlog/2026-04-17.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 2026-04-17
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료 |
|
||||||
|
|-------|-------|----------|-----------|----------|
|
||||||
|
| 001 | 08:05 | v18 Observer DOM->Markdown 파서 개선(<a> 파싱 포함) 및 노이즈 필터 부작용(코드 블럭 잘림 방지) 해결, User 구문 추출 연동, v0.5.56 배포 | `미정` | ✅ |
|
||||||
5
docs/devlog/2026-04-18.md
Normal file
5
docs/devlog/2026-04-18.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Devlog — 2026-04-18
|
||||||
|
|
||||||
|
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||||
|
|---|------|----------|------|------|
|
||||||
|
| 001 | 09:20~23:50 | Retry auto-approve 흐름 복구 — WS response 파일 보존 (`_from_ws`), Observer 형제 탐색(sibling), thinking 블록 필터링 | `pending` | ✅ |
|
||||||
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" 버튼 자동 소멸 확인 ✅
|
||||||
17
docs/devlog/entries/20260417-001.md
Normal file
17
docs/devlog/entries/20260417-001.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# DOM Observer 마크다운 구조 복원 및 사용자 메시지 연동 (v0.5.56)
|
||||||
|
|
||||||
|
### 목표
|
||||||
|
DOM Observer(`observer-script.ts`)가 AI 채팅을 `innerText`로 추출하며 잃어버리는 마크다운 서식을 복원하고, 사용자(User) 메시지도 포착하여 함께 Discord 봇으로 보내기 (#634 이슈).
|
||||||
|
|
||||||
|
### 변경 사항
|
||||||
|
1. **`convertNodeToMarkdown` 파서 확장**:
|
||||||
|
- AI 채팅창의 DOM Tree를 순회하며 `<h1>`~`<h4>`, `<p>`, `<ul>`, `<ol>`, `<li>`, `<strong>`, `<em>`, `<code>`, `<pre>`, `<blockquote>` 등 대부분의 마크다운 요소를 파싱하는 로직 도입.
|
||||||
|
- 추가로 `<a>` 태그(Link) 속성을 지원하여 `[text](href)` 형태로 복원하도록 개선.
|
||||||
|
2. **파괴적인 `cleanLines()` 노이즈 필터 제거**:
|
||||||
|
- 이전에 사용되던 `cleanLines()`가 `}[공백]`이나 `import` 같은 코드를 UI 노이즈로 오인하여 삭제(Drop)하는 심각한 이슈를 발견. 전체 마크다운 문자열에는 해당 필터를 적용하지 않고 정규식을 통해 `Thought for X s` 형태의 메시지만 지우도록 수정.
|
||||||
|
3. **User 메시지 대상 추가**:
|
||||||
|
- `scanChatBodies()`의 탐색 Selector에 `.text-ide-message-block-user-color`, `.bg-ide-message-block-user-background` 등을 추가하여 사용자 메시지 블록도 대상에 포함.
|
||||||
|
- 데이터 전송 시 `role: 'user'` 정보를 보내고, `http-bridge.ts`에서 이를 구분하여 헤더를 `🧑💻 **[DOM 추출] 사용자 요청**`로 지정해 Discord로 릴레이.
|
||||||
|
|
||||||
|
### 결과
|
||||||
|
`v0.5.56` VSIX 배포 준비 완료 (v0.5.54/55 빌드는 테스트 과정 중 건너뜀). AG Native에서 확장 설치 캐시를 리셋하거나 직접 VSIX를 설치하면 적용됨.
|
||||||
31
docs/devlog/entries/20260418-001.md
Normal file
31
docs/devlog/entries/20260418-001.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Retry Auto-Approve 흐름 복구 및 Observer 고도화
|
||||||
|
|
||||||
|
- **시간**: 2026-04-18 09:20~23:50
|
||||||
|
- **Commit**: `pending`
|
||||||
|
- **Vikunja**: 신규 생성 예정
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
|
||||||
|
### 1. `_from_ws` 마커 기반 response 파일 보존
|
||||||
|
- **문제**: WS 응답 핸들러가 response 파일 작성 → processResponseFile이 300ms 후 삭제 → Observer pollResponseGroup 실패
|
||||||
|
- **선택**: response 파일에 `_from_ws: true` 마커 추가, processResponseFile에서 스킵
|
||||||
|
- **이유**: pending 파일 생성을 추가하는 것보다 단순하고, WS 핸들러에서 이미 tryApprovalStrategies를 실행하므로 중복 실행 방지도 함께 해결
|
||||||
|
|
||||||
|
### 2. 형제(sibling) DOM 탐색
|
||||||
|
- **문제**: "Always run" 버튼의 조상(parentElement) 탐색으로는 `pre.font-mono` 도달 불가 (footer.parentElement가 null)
|
||||||
|
- **선택**: 각 depth에서 `node.parentElement.children`을 순회하여 형제 요소의 code 블록 탐색
|
||||||
|
- **이유**: AG Native DOM 구조에서 명령어는 footer의 형제 요소에 있으므로 조상 탐색만으로는 구조적으로 불가
|
||||||
|
|
||||||
|
### 3. Thinking 블록 필터링
|
||||||
|
- **문제**: AI의 내부 사고 과정이 Discord에 릴레이됨
|
||||||
|
- **선택**: `max-h-[200px]` 조상 확인으로 thinking 블록 식별
|
||||||
|
- **이유**: thinking 블록은 접힌 상태에서 max-height가 200px로 제한되는 특징이 있음
|
||||||
|
|
||||||
|
## 시행착오
|
||||||
|
1. depth 5→10 증가만으로 해결 시도 → 실패 (조상이 아닌 형제에 명령어가 있었음)
|
||||||
|
2. Observer HTML 변경 후 Reload Window만 실행 → 실패 (AG 2번 재시작 필요)
|
||||||
|
3. response 파일이 삭제되는 원인을 clickTrigger 타이밍으로 오인 → 실제는 processResponseFile의 isDomObserver 판별 실패
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패)
|
||||||
|
- [ ] Observer pollResponseGroup이 시작되지 않는 케이스 (POST /pending 이전에 trigger-click 소비)
|
||||||
@@ -397,6 +397,23 @@ async function activate(context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Normal approval — tryApprovalStrategies
|
// Normal approval — tryApprovalStrategies
|
||||||
|
// v22: ALSO write response file so Observer's pollResponseGroup can click
|
||||||
|
// the correct button (with exact button_index). Without this, only the
|
||||||
|
// imprecise pollTriggerClick fallback was used for WS-path responses.
|
||||||
|
const responseDir = path.join(bridgePath, 'response');
|
||||||
|
if (!fs.existsSync(responseDir)) {
|
||||||
|
fs.mkdirSync(responseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const respPayload = {
|
||||||
|
request_id: data.request_id,
|
||||||
|
approved,
|
||||||
|
button_index: data.button_index ?? 0,
|
||||||
|
step_type: stepType,
|
||||||
|
project_name: projectName,
|
||||||
|
_from_ws: true,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(path.join(responseDir, `${data.request_id}.json`), JSON.stringify(respPayload), 'utf-8');
|
||||||
|
logToFile(`[WS-RESPONSE] Response file written for pollResponseGroup: ${data.request_id?.substring(0, 12)}`);
|
||||||
const approvalCtx = (0, step_probe_1.getApprovalContext)();
|
const approvalCtx = (0, step_probe_1.getApprovalContext)();
|
||||||
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
|
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
|
||||||
(0, step_probe_1.tryApprovalStrategies)(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
|
(0, step_probe_1.tryApprovalStrategies)(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
|
||||||
@@ -503,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.34",
|
"version": "0.5.103",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"version": "0.5.34",
|
"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.54",
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
extension/scratch/analyze_dump.js
Normal file
28
extension/scratch/analyze_dump.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const dump = JSON.parse(fs.readFileSync(
|
||||||
|
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
|
||||||
|
));
|
||||||
|
|
||||||
|
const bodyStr = JSON.stringify(dump.body);
|
||||||
|
|
||||||
|
// Find all unique tag names
|
||||||
|
const tagMatches = bodyStr.match(/"tag":"[a-z0-9]+"/g) || [];
|
||||||
|
const uniqueTags = [...new Set(tagMatches)];
|
||||||
|
console.log('=== Unique DOM tags ===');
|
||||||
|
console.log(uniqueTags.sort().join('\n'));
|
||||||
|
|
||||||
|
// Check for pipe characters (markdown table syntax)
|
||||||
|
console.log('\n=== Pipe | in text content ===');
|
||||||
|
const pipeMatches = [...bodyStr.matchAll(/"text":"[^"]*\|[^"]*"/g)];
|
||||||
|
console.log(`Found ${pipeMatches.length} text nodes with pipe |`);
|
||||||
|
pipeMatches.slice(0, 5).forEach(m => console.log(' ', m[0].substring(0, 120)));
|
||||||
|
|
||||||
|
// Check for table-related class names
|
||||||
|
console.log('\n=== Table-related classes ===');
|
||||||
|
const classMatches = bodyStr.match(/"cls":"[^"]*"/g) || [];
|
||||||
|
const tableClasses = classMatches.filter(c => /table|grid|cell|col|row/i.test(c));
|
||||||
|
console.log(`Found ${tableClasses.length} table-related classes`);
|
||||||
|
[...new Set(tableClasses)].slice(0, 10).forEach(c => console.log(' ', c));
|
||||||
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"
|
||||||
37
extension/scratch/discord_channels.js
Normal file
37
extension/scratch/discord_channels.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// List all channels in the guild
|
||||||
|
const https = require('https');
|
||||||
|
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
|
||||||
|
const GUILD_ID = '1478722210460991662';
|
||||||
|
|
||||||
|
function apiGet(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const opts = {
|
||||||
|
hostname: 'discord.com',
|
||||||
|
path: `/api/v10${path}`,
|
||||||
|
headers: { 'Authorization': `Bot ${TOKEN}` }
|
||||||
|
};
|
||||||
|
https.get(opts, res => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', c => data += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
||||||
|
});
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const channels = await apiGet(`/guilds/${GUILD_ID}/channels`);
|
||||||
|
if (!Array.isArray(channels)) {
|
||||||
|
console.log('Error:', channels);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Total channels: ${channels.length}\n`);
|
||||||
|
channels.sort((a,b) => (a.position||0) - (b.position||0));
|
||||||
|
channels.forEach(c => {
|
||||||
|
const type = ['TEXT','DM','VOICE','GROUP_DM','CATEGORY','ANNOUNCE','','','','','','THREAD','THREAD','THREAD','','FORUM','MEDIA'][c.type] || c.type;
|
||||||
|
console.log(`${c.id} | ${type.padEnd(10)} | #${c.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => console.error(e));
|
||||||
55
extension/scratch/discord_read.js
Normal file
55
extension/scratch/discord_read.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Read latest Discord messages from ag-gravity_control channel
|
||||||
|
const https = require('https');
|
||||||
|
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
|
||||||
|
const CHANNEL_ID = '1483082084540223663';
|
||||||
|
|
||||||
|
function apiGet(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const opts = {
|
||||||
|
hostname: 'discord.com',
|
||||||
|
path: `/api/v10${path}`,
|
||||||
|
headers: { 'Authorization': `Bot ${TOKEN}` }
|
||||||
|
};
|
||||||
|
https.get(opts, res => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', c => data += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
||||||
|
});
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const limit = process.argv[2] || 15;
|
||||||
|
const msgs = await apiGet(`/channels/${CHANNEL_ID}/messages?limit=${limit}`);
|
||||||
|
if (!Array.isArray(msgs)) {
|
||||||
|
console.log('Error:', JSON.stringify(msgs));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`=== #ag-gravity_control — Last ${msgs.length} messages ===\n`);
|
||||||
|
|
||||||
|
msgs.reverse().forEach(m => {
|
||||||
|
const time = new Date(m.timestamp).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
const author = m.author?.username || '?';
|
||||||
|
|
||||||
|
if (m.embeds?.length > 0) {
|
||||||
|
m.embeds.forEach(e => {
|
||||||
|
const title = e.title || '(no title)';
|
||||||
|
const desc = (e.description || '').substring(0, 300);
|
||||||
|
const colorHex = e.color ? `#${e.color.toString(16).padStart(6, '0')}` : 'default';
|
||||||
|
const footer = e.footer?.text || '';
|
||||||
|
console.log(`[${time}] 📦 EMBED [${colorHex}] ${title}`);
|
||||||
|
if (desc) console.log(` ${desc.replace(/\n/g, '\n ')}`);
|
||||||
|
if (footer) console.log(` 📎 ${footer}`);
|
||||||
|
});
|
||||||
|
} else if (m.content) {
|
||||||
|
const content = m.content.substring(0, 300);
|
||||||
|
console.log(`[${time}] 💬 ${author}: ${content}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => console.error(e));
|
||||||
29
extension/scratch/find_user_msg.js
Normal file
29
extension/scratch/find_user_msg.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const d = JSON.parse(fs.readFileSync(
|
||||||
|
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
|
||||||
|
));
|
||||||
|
const s = JSON.stringify(d.body);
|
||||||
|
|
||||||
|
console.log('title:', d.quickInfo.title);
|
||||||
|
console.log('Has id=conversation:', s.includes('"id":"conversation"'));
|
||||||
|
console.log('Has agent-side-panel:', s.includes('antigravity-agent-side-panel'));
|
||||||
|
|
||||||
|
// Find message-block patterns
|
||||||
|
const mb = [...s.matchAll(/message-block/g)];
|
||||||
|
console.log('message-block occurrences:', mb.length);
|
||||||
|
|
||||||
|
// Find user-related class patterns
|
||||||
|
const userPatterns = ['user-color', 'user-background', 'user-message', 'user-query', 'user-input', 'human'];
|
||||||
|
userPatterns.forEach(p => {
|
||||||
|
const cnt = [...s.matchAll(new RegExp(p, 'gi'))].length;
|
||||||
|
if (cnt > 0) console.log(` ${p}: ${cnt} occurrences`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show all unique classes that include 'message' or 'chat' or 'conversation'
|
||||||
|
const clsMatches = [...s.matchAll(/"cls":"([^"]*(?:message|chat|conversation|query|user|human)[^"]*)"/gi)];
|
||||||
|
console.log('\nClasses with message/chat/conversation/user/human:');
|
||||||
|
const uniq = [...new Set(clsMatches.map(m => m[1]))];
|
||||||
|
uniq.forEach(c => console.log(' ', c.substring(0, 120)));
|
||||||
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'));
|
||||||
@@ -205,6 +205,25 @@ async function processResponseFile(filePath: string) {
|
|||||||
}
|
}
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
const resp = JSON.parse(content);
|
const resp = JSON.parse(content);
|
||||||
|
|
||||||
|
// v22: Skip files written by the WS response handler (extension.ts onResponse).
|
||||||
|
// Those files exist ONLY for Observer's pollResponseGroup to read via HTTP.
|
||||||
|
// The WS handler already calls tryApprovalStrategies, so processing here is redundant.
|
||||||
|
// Without this skip, the watcher deletes the file before Observer can poll it
|
||||||
|
// (since no pending file exists for the isDomObserver check).
|
||||||
|
if (resp._from_ws) {
|
||||||
|
// v26: TTL — delete stale _from_ws files after 60s to prevent infinite SKIP spam
|
||||||
|
const wsRidTs = parseInt((resp.request_id || '').split('_')[0], 10);
|
||||||
|
const wsAge = isNaN(wsRidTs) ? 999999 : Date.now() - wsRidTs;
|
||||||
|
if (wsAge > 60_000) {
|
||||||
|
ctx.logToFile(`[RESPONSE] CLEANUP stale _from_ws file: ${resp.request_id} age=${Math.round(wsAge / 1000)}s`);
|
||||||
|
try { fs.unlinkSync(filePath); } catch { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.logToFile(`[RESPONSE] SKIP _from_ws file (for Observer pollResponseGroup): ${resp.request_id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved} step_type=${resp.step_type || '(missing)'} keys=[${Object.keys(resp).join(',')}]`;
|
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved} step_type=${resp.step_type || '(missing)'} keys=[${Object.keys(resp).join(',')}]`;
|
||||||
console.log(`Gravity Bridge: ${msg}`);
|
console.log(`Gravity Bridge: ${msg}`);
|
||||||
ctx.logToFile(msg);
|
ctx.logToFile(msg);
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -388,6 +388,28 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normal approval — tryApprovalStrategies
|
// Normal approval — tryApprovalStrategies
|
||||||
|
// v22: ALSO write response file so Observer's pollResponseGroup can click
|
||||||
|
// the correct button (with exact button_index). Without this, only the
|
||||||
|
// imprecise pollTriggerClick fallback was used for WS-path responses.
|
||||||
|
const responseDir = path.join(bridgePath, 'response');
|
||||||
|
if (!fs.existsSync(responseDir)) {
|
||||||
|
fs.mkdirSync(responseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const respPayload = {
|
||||||
|
request_id: data.request_id,
|
||||||
|
approved,
|
||||||
|
button_index: data.button_index ?? 0,
|
||||||
|
step_type: stepType,
|
||||||
|
project_name: projectName,
|
||||||
|
_from_ws: true,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(responseDir, `${data.request_id}.json`),
|
||||||
|
JSON.stringify(respPayload),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
logToFile(`[WS-RESPONSE] Response file written for pollResponseGroup: ${data.request_id?.substring(0, 12)}`);
|
||||||
|
|
||||||
const approvalCtx = getApprovalContext();
|
const approvalCtx = getApprovalContext();
|
||||||
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
|
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
|
||||||
tryApprovalStrategies(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
|
tryApprovalStrategies(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
|
||||||
@@ -499,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 ───
|
||||||
@@ -126,6 +127,21 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /log — renderer relays important diagnostic logs
|
||||||
|
if (req.method === 'POST' && url.pathname === '/log') {
|
||||||
|
let logBody = '';
|
||||||
|
req.setEncoding('utf8');
|
||||||
|
req.on('data', (c: string) => logBody += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const logData = JSON.parse(logBody);
|
||||||
|
ctx.logToFile(`[OBSERVER-LOG] ${logData.msg || logBody.substring(0, 500)}`);
|
||||||
|
} catch { ctx.logToFile(`[OBSERVER-LOG] ${logBody.substring(0, 500)}`); }
|
||||||
|
res.writeHead(200); res.end('ok');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === 'POST' && url.pathname === '/dump-html') {
|
if (req.method === 'POST' && url.pathname === '/dump-html') {
|
||||||
let dumpBody = '';
|
let dumpBody = '';
|
||||||
req.setEncoding('utf8');
|
req.setEncoding('utf8');
|
||||||
@@ -266,6 +282,155 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i;
|
const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i;
|
||||||
let enrichedCmd = rawCmd;
|
let enrichedCmd = rawCmd;
|
||||||
let enrichedDesc = rawDesc;
|
let enrichedDesc = rawDesc;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// v33: Also auto-approve "Accept all" (diff review) and "Accept" buttons
|
||||||
|
const AUTO_APPROVE_RE = /^(Always\s+run|Accept\s+all|Accept)$/i;
|
||||||
|
let alwaysRunDetected = AUTO_APPROVE_RE.test(rawCmd);
|
||||||
|
let alwaysRunBtnIndex = alwaysRunDetected ? 0 : -1;
|
||||||
|
if (!alwaysRunDetected && Array.isArray(data.buttons)) {
|
||||||
|
for (let bi = 0; bi < data.buttons.length; bi++) {
|
||||||
|
if (AUTO_APPROVE_RE.test((data.buttons[bi].text || '').trim())) {
|
||||||
|
alwaysRunDetected = true;
|
||||||
|
alwaysRunBtnIndex = bi;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alwaysRunDetected) {
|
||||||
|
// v34: If this is "Accept all" / "Accept", also call agentAcceptAllInFile directly
|
||||||
|
const isAcceptAll = /^Accept/i.test(rawCmd) || (data.step_type === 'diff_review');
|
||||||
|
if (isAcceptAll) {
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE Accept all → opening review + agentAcceptAllInFile`);
|
||||||
|
try {
|
||||||
|
const vscode = require('vscode');
|
||||||
|
// v37: Must focus diff review panel BEFORE calling agentAcceptAllInFile
|
||||||
|
// Without this, the command succeeds but has no effect
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await vscode.commands.executeCommand('antigravity.openReviewChanges');
|
||||||
|
ctx.logToFile(`[HTTP] openReviewChanges OK`);
|
||||||
|
await new Promise((r: any) => setTimeout(r, 500));
|
||||||
|
await vscode.commands.executeCommand('antigravity.prioritized.agentAcceptAllInFile');
|
||||||
|
ctx.logToFile(`[HTTP] ✅ agentAcceptAllInFile SUCCESS`);
|
||||||
|
} catch (e: any) {
|
||||||
|
ctx.logToFile(`[HTTP] ❌ agentAcceptAllInFile: ${e.message?.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} catch (e: any) { ctx.logToFile(`[HTTP] ❌ vscode require failed: ${e.message}`); }
|
||||||
|
}
|
||||||
|
// Try enrichment for better Discord display text
|
||||||
|
let displayCmd = rawCmd;
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE raw: cmd="${rawCmd}" desc="${rawDesc.substring(0, 120)}" buttons=${JSON.stringify((data.buttons || []).map((b: any) => b.text)).substring(0, 200)}`);
|
||||||
|
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 3 && rawDesc !== rawCmd) {
|
||||||
|
const promptMatch = rawDesc.match(/[>»]\s*(.+)/);
|
||||||
|
if (promptMatch && promptMatch[1].trim().length > 3) {
|
||||||
|
displayCmd = promptMatch[1].trim().substring(0, 200);
|
||||||
|
} else {
|
||||||
|
// v19: Fallback — use longest line from description as command text
|
||||||
|
const descLines = rawDesc.split(/\n/).map((l: string) => l.trim()).filter((l: string) => l.length > 3);
|
||||||
|
if (descLines.length > 0) {
|
||||||
|
descLines.sort((a: string, b: string) => b.length - a.length);
|
||||||
|
displayCmd = descLines[0].substring(0, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// v20: Final fallback — read latest Step Probe pending file for actual command
|
||||||
|
if (displayCmd === rawCmd && GENERIC_BTN_RE.test(displayCmd)) {
|
||||||
|
try {
|
||||||
|
const pendingDir = path.join(ctx.bridgePath, 'pending');
|
||||||
|
if (fs.existsSync(pendingDir)) {
|
||||||
|
const pFiles = fs.readdirSync(pendingDir)
|
||||||
|
.filter((f: string) => f.endsWith('.json'))
|
||||||
|
.map((f: string) => ({ name: f, time: fs.statSync(path.join(pendingDir, f)).mtimeMs }))
|
||||||
|
.sort((a: any, b: any) => b.time - a.time);
|
||||||
|
if (pFiles.length > 0 && (Date.now() - pFiles[0].time) < 30_000) {
|
||||||
|
const pData = JSON.parse(fs.readFileSync(path.join(pendingDir, pFiles[0].name), 'utf-8'));
|
||||||
|
if (pData.command && pData.command.length > 3 && !GENERIC_BTN_RE.test(pData.command)) {
|
||||||
|
displayCmd = pData.command.substring(0, 200);
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending file: "${displayCmd.substring(0, 80)}"`);
|
||||||
|
} else if (pData.description && pData.description.length > 5) {
|
||||||
|
displayCmd = pData.description.substring(0, 200);
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending desc: "${displayCmd.substring(0, 80)}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) { ctx.logToFile(`[HTTP] AUTO-APPROVE pending lookup error: ${e.message}`); }
|
||||||
|
}
|
||||||
|
// v29: Final-final fallback — Step Probe API memory
|
||||||
|
if (displayCmd === rawCmd && GENERIC_BTN_RE.test(displayCmd) && ctx.getLastWaitingCommand) {
|
||||||
|
const wc = ctx.getLastWaitingCommand();
|
||||||
|
if (wc.cmd && wc.cmd.length > 3 && !GENERIC_BTN_RE.test(wc.cmd) && (Date.now() - wc.ts) < 30_000) {
|
||||||
|
displayCmd = wc.desc && wc.desc.length > wc.cmd.length ? wc.desc.substring(0, 200) : wc.cmd.substring(0, 200);
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from step-probe memory: "${displayCmd.substring(0, 80)}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rid = data.request_id || Date.now().toString();
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunBtnIndex}): cmd="${displayCmd.substring(0, 80)}"`);
|
||||||
|
// Write response file IMMEDIATELY so observer clicks the button with zero delay
|
||||||
|
const responseDir = path.join(ctx.bridgePath, 'response');
|
||||||
|
if (!fs.existsSync(responseDir)) {
|
||||||
|
fs.mkdirSync(responseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const respPayload = {
|
||||||
|
request_id: rid,
|
||||||
|
approved: true,
|
||||||
|
button_index: alwaysRunBtnIndex >= 0 ? alwaysRunBtnIndex : 0,
|
||||||
|
step_type: data.step_type || 'command',
|
||||||
|
project_name: ctx.projectName,
|
||||||
|
_from_ws: true, // v38: prevent processResponseFile from consuming before Observer polls
|
||||||
|
_auto_approve_ttl: Date.now() + 60_000, // auto-expire after 60s
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(responseDir, `${rid}.json`),
|
||||||
|
JSON.stringify(respPayload),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
// v29: Respond to Observer immediately (don't block button click)
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true }));
|
||||||
|
|
||||||
|
// v29: Discord notification — if displayCmd is generic, poll Step Probe for real command
|
||||||
|
const isGenericDisplay = GENERIC_BTN_RE.test(displayCmd);
|
||||||
|
const sendDiscord = (finalCmd: string, finalDesc: string) => {
|
||||||
|
if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
|
||||||
|
ctx.wsBridge.sendPending({
|
||||||
|
request_id: rid,
|
||||||
|
command: finalCmd,
|
||||||
|
description: finalDesc,
|
||||||
|
step_type: data.step_type || 'command',
|
||||||
|
status: 'auto_approved',
|
||||||
|
buttons: data.buttons,
|
||||||
|
project_name: ctx.projectName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isGenericDisplay && ctx.getLastWaitingCommand) {
|
||||||
|
// Poll Step Probe memory for up to 6s (API polls every 5s)
|
||||||
|
let pollAttempt = 0;
|
||||||
|
const maxAttempts = 30; // 30 * 200ms = 6s
|
||||||
|
const pollTimer = setInterval(() => {
|
||||||
|
pollAttempt++;
|
||||||
|
const wc = ctx.getLastWaitingCommand!();
|
||||||
|
if (wc.cmd && wc.cmd.length > 3 && !GENERIC_BTN_RE.test(wc.cmd) && (Date.now() - wc.ts) < 15_000) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
const enrichedCmd = wc.desc && wc.desc.length > wc.cmd.length ? wc.desc.substring(0, 200) : wc.cmd.substring(0, 200);
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched (delayed ${pollAttempt * 200}ms): "${enrichedCmd.substring(0, 80)}"`);
|
||||||
|
sendDiscord(enrichedCmd, `[${rawCmd}] ${enrichedCmd}`);
|
||||||
|
} else if (pollAttempt >= maxAttempts) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
ctx.logToFile(`[HTTP] AUTO-APPROVE no enrichment after ${maxAttempts * 200}ms — sending generic`);
|
||||||
|
sendDiscord(displayCmd, rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
} else {
|
||||||
|
// Already enriched or no Step Probe — send immediately
|
||||||
|
sendDiscord(displayCmd, rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
|
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
|
||||||
// Extract the actual command from description (often includes terminal prompt)
|
// Extract the actual command from description (often includes terminal prompt)
|
||||||
// Pattern: "…\project_name > actual_command"
|
// Pattern: "…\project_name > actual_command"
|
||||||
@@ -361,44 +526,8 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
buttons: data.buttons,
|
buttons: data.buttons,
|
||||||
step_index: ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : undefined,
|
step_index: ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : undefined,
|
||||||
};
|
};
|
||||||
// v17: "Always run" auto-approve — click button immediately without Discord roundtrip
|
// v19: "Always run" auto-approve was already handled above (before filter chain)
|
||||||
// rawCmd is the original button text before enrichment. "Always run" means the user
|
// No need for duplicate check here.
|
||||||
// already trusts this command pattern, so we auto-approve at the bridge level.
|
|
||||||
if (/^Always\s+run$/i.test(rawCmd)) {
|
|
||||||
ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run": enriched="${enrichedCmd.substring(0, 80)}"`);
|
|
||||||
// Write response file so observer's pollResponseGroup picks it up and clicks the button
|
|
||||||
const responseDir = path.join(ctx.bridgePath, 'response');
|
|
||||||
if (!fs.existsSync(responseDir)) {
|
|
||||||
fs.mkdirSync(responseDir, { recursive: true });
|
|
||||||
}
|
|
||||||
const respPayload = {
|
|
||||||
request_id: rid,
|
|
||||||
approved: true,
|
|
||||||
button_index: 0, // "Always run" is always the first button
|
|
||||||
step_type: data.step_type || 'command',
|
|
||||||
project_name: ctx.projectName,
|
|
||||||
};
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(responseDir, `${rid}.json`),
|
|
||||||
JSON.stringify(respPayload),
|
|
||||||
'utf-8'
|
|
||||||
);
|
|
||||||
// Notify Discord (non-interactive "자동 승인" embed)
|
|
||||||
if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
|
|
||||||
ctx.wsBridge.sendPending({
|
|
||||||
request_id: rid,
|
|
||||||
command: enrichedCmd || rawCmd,
|
|
||||||
description: enrichedDesc,
|
|
||||||
step_type: pending.step_type,
|
|
||||||
status: 'auto_approved',
|
|
||||||
buttons: pending.buttons,
|
|
||||||
project_name: ctx.projectName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// File permission: inject multi-choice buttons
|
// File permission: inject multi-choice buttons
|
||||||
const cmdLower = enrichedCmd.toLowerCase();
|
const cmdLower = enrichedCmd.toLowerCase();
|
||||||
if (cmdLower.includes('allow') && !pending.buttons) {
|
if (cmdLower.includes('allow') && !pending.buttons) {
|
||||||
@@ -544,8 +673,10 @@ function _handleChatSnapshot(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(body);
|
const data = JSON.parse(body);
|
||||||
if (data.text && typeof ctx.writeChatSnapshot === 'function') {
|
if (data.text && typeof ctx.writeChatSnapshot === 'function') {
|
||||||
ctx.writeChatSnapshot(`💬 **[DOM 추출] AI 응답**\n\n${data.text}`);
|
const isUser = data.role === 'user';
|
||||||
ctx.logToFile(`[HTTP] chat snapshot written (${data.text.length} chars)`);
|
const prefix = isUser ? '🧑💻 **[DOM 추출] 사용자 요청**' : '💬 **[DOM 추출] AI 응답**';
|
||||||
|
ctx.writeChatSnapshot(`${prefix}\n\n${data.text}`);
|
||||||
|
ctx.logToFile(`[HTTP] chat snapshot written (${data.text.length} chars, role: ${data.role || 'bot'})`);
|
||||||
}
|
}
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ ok: true }));
|
res.end(JSON.stringify({ ok: true }));
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -9,8 +9,14 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
var THROTTLE_MS=500;
|
var THROTTLE_MS=500;
|
||||||
var CLEANUP_MS=300000;
|
var CLEANUP_MS=300000;
|
||||||
|
|
||||||
function log(m){console.log('[GB Observer] '+m);}
|
function log(m){
|
||||||
log('v17 Script loaded — Always Run Auto-Approve + Retry Detection');
|
console.log('[GB Observer] '+m);
|
||||||
|
// v19: Relay important logs to extension via HTTP so they appear in extension.log
|
||||||
|
if (BASE && (m.indexOf('CV-CLASSES')!==-1 || m.indexOf('CV-CHILDREN')!==-1 || m.indexOf('child[')!==-1 || m.indexOf('CV found')!==-1 || m.indexOf('Conversation view')!==-1 || m.indexOf('BEACON')!==-1 || m.indexOf('ERROR')!==-1 || m.indexOf('chat relay')!==-1 || m.indexOf('user-cls')!==-1 || m.indexOf('CONTEXT')!==-1 || m.indexOf('BTN-DOM')!==-1 || m.indexOf('DEFERRED')!==-1 || m.indexOf('DETECTED')!==-1 || m.indexOf('ACCEPT')!==-1)) {
|
||||||
|
try { fetch(BASE+'/log', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({msg:m.substring(0,2000)})}); } catch(e){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log('v17 Script loaded ??Always Run Auto-Approve + Retry Detection');
|
||||||
|
|
||||||
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
|
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
|
||||||
try {
|
try {
|
||||||
@@ -33,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|' +
|
||||||
@@ -108,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;
|
||||||
@@ -123,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)/;
|
||||||
@@ -138,15 +144,31 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
var _bestCodeHeader = '';
|
var _bestCodeHeader = '';
|
||||||
var _sawCodeEls = false;
|
var _sawCodeEls = false;
|
||||||
var _allSkipped = true;
|
var _allSkipped = true;
|
||||||
for (var depth = 0; depth < 5 && node; depth++) {
|
// v22: Increased from 5 to 10 ??AG Native command display (SRi) can be many levels up
|
||||||
|
for (var depth = 0; depth < 10 && node; depth++) {
|
||||||
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
|
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
|
||||||
var codeEls = node.querySelectorAll('pre, code, [class*="terminal"]');
|
// v22: Prioritize pre.font-mono (AG Native command line display from SRi component)
|
||||||
|
var codeEls = node.querySelectorAll('pre.font-mono, pre, code, [class*="terminal"]');
|
||||||
_debugTrail.push('d'+depth+':tag='+((node.tagName||'?').toLowerCase())+',cls='+(((typeof node.className==='string')?node.className:'').substring(0,60))+',codeEls='+codeEls.length);
|
_debugTrail.push('d'+depth+':tag='+((node.tagName||'?').toLowerCase())+',cls='+(((typeof node.className==='string')?node.className:'').substring(0,60))+',codeEls='+codeEls.length);
|
||||||
for (var ci = 0; ci < codeEls.length; ci++) {
|
for (var ci = 0; ci < codeEls.length; ci++) {
|
||||||
var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500));
|
var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500));
|
||||||
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;
|
||||||
@@ -181,6 +203,84 @@ 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
|
||||||
|
// AG Native's command display (pre.font-mono) is a SIBLING of footer, not ancestor
|
||||||
|
if (node && node.parentElement) {
|
||||||
|
var siblings = node.parentElement.children;
|
||||||
|
for (var si = 0; si < siblings.length; si++) {
|
||||||
|
if (siblings[si] === node) continue;
|
||||||
|
if (!siblings[si].querySelector) continue;
|
||||||
|
var sibCodeEls = siblings[si].querySelectorAll('pre.font-mono, pre, code');
|
||||||
|
for (var sci = 0; sci < sibCodeEls.length; sci++) {
|
||||||
|
var sibCode = cleanLines((sibCodeEls[sci].textContent || '').trim().substring(0, 500));
|
||||||
|
if (!sibCode || sibCode.length <= 5) continue;
|
||||||
|
if (JUNK_CODE_RE.test(sibCode) || ICON_GLUE_RE.test(sibCode)) continue;
|
||||||
|
if (PROMPT_ONLY_RE.test(sibCode.trim())) continue;
|
||||||
|
_debugTrail.push('sibling_d'+depth+':tag='+siblings[si].tagName.toLowerCase()+',code='+sibCode.substring(0,40));
|
||||||
|
_bestCodeText = sibCode;
|
||||||
|
_allSkipped = false;
|
||||||
|
// Found in sibling ??return immediately
|
||||||
|
var sibParts = [];
|
||||||
|
var sibHdr = siblings[si].querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]');
|
||||||
|
if (sibHdr) sibParts.push(cleanLines((sibHdr.textContent || '').trim().substring(0, 200)));
|
||||||
|
sibParts.push(sibCode);
|
||||||
|
log('CONTEXT-OK d='+depth+' src=sibling trail='+_debugTrail.join(' > '));
|
||||||
|
_lastContextDebug = _debugTrail.join(' > ');
|
||||||
|
return sibParts.join(' \u2014 ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
node = node.parentElement;
|
node = node.parentElement;
|
||||||
}
|
}
|
||||||
if (_sawCodeEls && _allSkipped) {
|
if (_sawCodeEls && _allSkipped) {
|
||||||
@@ -199,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,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;
|
||||||
}
|
}
|
||||||
@@ -251,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) {
|
||||||
@@ -331,17 +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=5;
|
var MAX_DUMPS=8;
|
||||||
|
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',
|
||||||
@@ -377,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;
|
||||||
@@ -472,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 = '';
|
||||||
@@ -484,15 +585,118 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
var _lastStepTextSent = false;
|
var _lastStepTextSent = false;
|
||||||
var _lastResponseBlockCount = 0; // track number of response blocks for AG Native
|
var _lastResponseBlockCount = 0; // track number of response blocks for AG Native
|
||||||
|
|
||||||
|
function convertNodeToMarkdown(node) {
|
||||||
|
if (!node) return '';
|
||||||
|
if (node.nodeType === 3) return node.textContent; // Text node
|
||||||
|
if (node.nodeType !== 1) return ''; // Skip other node types
|
||||||
|
|
||||||
|
var tag = node.tagName.toLowerCase();
|
||||||
|
|
||||||
|
// Skip hidden or UI elements
|
||||||
|
if (tag === 'style' || tag === 'script' || tag === 'noscript' || tag === 'button' || tag === 'svg') return '';
|
||||||
|
var cls = '';
|
||||||
|
if (typeof node.className === 'string') cls = node.className;
|
||||||
|
else if (node.className && node.className.baseVal) cls = node.className.baseVal;
|
||||||
|
|
||||||
|
if (cls && (cls.indexOf('google-symbols') !== -1 || cls.indexOf('material-icons') !== -1 || cls.indexOf('copy') !== -1 || cls.indexOf('codicon') !== -1)) return '';
|
||||||
|
|
||||||
|
var childrenMd = '';
|
||||||
|
for (var i = 0; i < node.childNodes.length; i++) {
|
||||||
|
childrenMd += convertNodeToMarkdown(node.childNodes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TABLE: Discord doesn't support markdown tables, so convert to fixed-width code block
|
||||||
|
if (tag === 'table') {
|
||||||
|
var rows = node.querySelectorAll('tr');
|
||||||
|
if (!rows || rows.length === 0) return childrenMd;
|
||||||
|
var grid = [];
|
||||||
|
var colWidths = [];
|
||||||
|
for (var ri = 0; ri < rows.length; ri++) {
|
||||||
|
var cells = rows[ri].querySelectorAll('th, td');
|
||||||
|
var row = [];
|
||||||
|
for (var ci = 0; ci < cells.length; ci++) {
|
||||||
|
var cellText = (cells[ci].textContent || '').trim();
|
||||||
|
row.push(cellText);
|
||||||
|
if (!colWidths[ci] || cellText.length > colWidths[ci]) colWidths[ci] = cellText.length;
|
||||||
|
}
|
||||||
|
grid.push(row);
|
||||||
|
}
|
||||||
|
// Build fixed-width text
|
||||||
|
var tbl = '';
|
||||||
|
for (var ri2 = 0; ri2 < grid.length; ri2++) {
|
||||||
|
var line = '';
|
||||||
|
for (var ci2 = 0; ci2 < colWidths.length; ci2++) {
|
||||||
|
var cell = grid[ri2][ci2] || '';
|
||||||
|
var pad = colWidths[ci2] - cell.length;
|
||||||
|
var padding = '';
|
||||||
|
for (var pi = 0; pi < pad; pi++) padding += ' ';
|
||||||
|
line += (ci2 > 0 ? ' | ' : '') + cell + padding;
|
||||||
|
}
|
||||||
|
tbl += line + '\\n';
|
||||||
|
// Add separator after header row (first row)
|
||||||
|
if (ri2 === 0) {
|
||||||
|
var sep = '';
|
||||||
|
for (var si2 = 0; si2 < colWidths.length; si2++) {
|
||||||
|
var dashes = '';
|
||||||
|
for (var di = 0; di < colWidths[si2]; di++) dashes += '-';
|
||||||
|
sep += (si2 > 0 ? '-+-' : '') + dashes;
|
||||||
|
}
|
||||||
|
tbl += sep + '\\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '\\n' + String.fromCharCode(96,96,96) + '\\n' + tbl + String.fromCharCode(96,96,96) + '\\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case 'h1': return '\\n# ' + childrenMd.trim() + '\\n';
|
||||||
|
case 'h2': return '\\n## ' + childrenMd.trim() + '\\n';
|
||||||
|
case 'h3': return '\\n### ' + childrenMd.trim() + '\\n';
|
||||||
|
case 'h4': return '\\n#### ' + childrenMd.trim() + '\\n';
|
||||||
|
case 'p': return '\\n' + childrenMd.trim() + '\\n';
|
||||||
|
case 'div':
|
||||||
|
// Treat specific divs as blocks if they end up behaving like paragraphs
|
||||||
|
if (cls.indexOf('block') !== -1 || cls.indexOf('message') !== -1) return '\\n' + childrenMd.trim() + '\\n';
|
||||||
|
return childrenMd;
|
||||||
|
case 'br': return '\\n';
|
||||||
|
case 'strong':
|
||||||
|
case 'b': return '**' + childrenMd + '**';
|
||||||
|
case 'em':
|
||||||
|
case 'i': return '*' + childrenMd + '*';
|
||||||
|
case 'a':
|
||||||
|
var href = node.getAttribute('href') || '';
|
||||||
|
return '[' + childrenMd + '](' + href + ')';
|
||||||
|
case 'code': return (node.parentNode && node.parentNode.tagName === 'PRE') ? childrenMd : (String.fromCharCode(96) + childrenMd + String.fromCharCode(96));
|
||||||
|
case 'pre': return '\\n' + String.fromCharCode(96,96,96) + '\\n' + childrenMd.trim() + '\\n' + String.fromCharCode(96,96,96) + '\\n';
|
||||||
|
case 'li':
|
||||||
|
var prefix = '- ';
|
||||||
|
if (node.parentNode && node.parentNode.tagName.toLowerCase() === 'ol') {
|
||||||
|
var idx = 1;
|
||||||
|
var curr = node.previousSibling;
|
||||||
|
while(curr) { if (curr.nodeType === 1 && curr.tagName.toLowerCase() === 'li') idx++; curr = curr.previousSibling; }
|
||||||
|
prefix = idx + '. ';
|
||||||
|
}
|
||||||
|
return '\\n' + prefix + childrenMd.trim();
|
||||||
|
case 'ul':
|
||||||
|
case 'ol': return '\\n' + childrenMd + '\\n';
|
||||||
|
case 'blockquote': return '\\n> ' + childrenMd.trim().split('\\n').join('\\n> ') + '\\n';
|
||||||
|
// Table sub-elements: already handled by the table case above via querySelectorAll
|
||||||
|
case 'thead':
|
||||||
|
case 'tbody':
|
||||||
|
case 'tfoot':
|
||||||
|
case 'tr':
|
||||||
|
case 'th':
|
||||||
|
case 'td': return '';
|
||||||
|
default: return childrenMd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function extractCleanStepText(stepEl) {
|
function extractCleanStepText(stepEl) {
|
||||||
if (!stepEl) return '';
|
if (!stepEl) return '';
|
||||||
|
|
||||||
// Clone the step element so we can strip UI elements without affecting the DOM
|
// Clone the step element so we can strip UI elements without affecting the DOM
|
||||||
var clone = stepEl.cloneNode(true);
|
var clone = stepEl.cloneNode(true);
|
||||||
|
|
||||||
// v16: Remove style/script/noscript elements FIRST — AG Native markdown injects <style> blocks
|
// v16: Remove style/script/noscript elements FIRST
|
||||||
// that contain CSS rules (e.g. remark-github-blockquote-alert/alert.css) whose textContent
|
|
||||||
// gets captured as AI response text
|
|
||||||
var styleEls = clone.querySelectorAll('style, script, noscript, link[rel="stylesheet"]');
|
var styleEls = clone.querySelectorAll('style, script, noscript, link[rel="stylesheet"]');
|
||||||
for (var si = 0; si < styleEls.length; si++) {
|
for (var si = 0; si < styleEls.length; si++) {
|
||||||
if (styleEls[si].parentNode) styleEls[si].parentNode.removeChild(styleEls[si]);
|
if (styleEls[si].parentNode) styleEls[si].parentNode.removeChild(styleEls[si]);
|
||||||
@@ -518,34 +722,90 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
|
|
||||||
// Try to get text from markdown rendering area first
|
// Try to get text from markdown rendering area first
|
||||||
// AG Native uses .leading-relaxed.select-text, Cascade uses .markdown-body/.prose
|
// AG Native uses .leading-relaxed.select-text, Cascade uses .markdown-body/.prose
|
||||||
var mdEl = clone.querySelector('.markdown-body, .prose, [class*="markdown"], [class*="rendered"]');
|
var mdEl = clone.querySelector('.markdown-body, .prose, [class*="markdown"], [class*="rendered"]') || clone;
|
||||||
|
|
||||||
// v18 FIX: Temporarily attach to DOM to force layout computation for .innerText
|
// Use our custom DOM-to-Markdown parser instead of innerText
|
||||||
// Without this, .innerText on unattached node behaves exactly like .textContent (loses block newlines)
|
var rawText = convertNodeToMarkdown(mdEl).trim();
|
||||||
var container = document.createElement('div');
|
|
||||||
container.style.position = 'absolute';
|
|
||||||
container.style.left = '-9999px';
|
|
||||||
container.style.top = '-9999px';
|
|
||||||
container.style.opacity = '0';
|
|
||||||
container.style.width = '800px';
|
|
||||||
container.appendChild(clone);
|
|
||||||
document.body.appendChild(container);
|
|
||||||
|
|
||||||
var targetEl = mdEl || clone;
|
// v18 FIX: DO NOT apply cleanLines to full markdown content, it destroys valid code blocks
|
||||||
var rawText = '';
|
// Safely remove "Thought for X" lines only
|
||||||
try {
|
rawText = rawText.replace(/Thought for \\d+s?/gi, '');
|
||||||
if (targetEl.innerText && targetEl.innerText.trim().length > 10) {
|
rawText = rawText.replace(/Thought for a few seconds/gi, '');
|
||||||
rawText = targetEl.innerText.trim();
|
|
||||||
} else {
|
// Cleanup multiple empty lines
|
||||||
// Fallback: get all text but filter aggressively
|
var lines = rawText.split('\\n');
|
||||||
rawText = (targetEl.innerText || targetEl.textContent || '').trim();
|
var finalLines = [];
|
||||||
}
|
var lastEmpty = false;
|
||||||
} finally {
|
for (var i = 0; i < lines.length; i++) {
|
||||||
if (container.parentNode) container.parentNode.removeChild(container);
|
var line = lines[i].replace(/\\s+$/, '');
|
||||||
|
if (line.length === 0) {
|
||||||
|
if (!lastEmpty && finalLines.length > 0) {
|
||||||
|
finalLines.push('');
|
||||||
|
lastEmpty = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalLines.push(line);
|
||||||
|
lastEmpty = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply line-by-line noise filter
|
// v19: Post-process ??wrap markdown table patterns in code blocks for Discord
|
||||||
return cleanLines(rawText).substring(0, 3500);
|
// AG Native renders tables as divs, not <table> HTML, so DOM-level handler can't catch them.
|
||||||
|
// Detect consecutive lines with pipe separators (| col1 | col2 |) and wrap in code block fences
|
||||||
|
var bt = String.fromCharCode(96, 96, 96);
|
||||||
|
var result = [];
|
||||||
|
var tableBlock = [];
|
||||||
|
var inCodeBlock = false;
|
||||||
|
for (var fi = 0; fi < finalLines.length; fi++) {
|
||||||
|
var fl = finalLines[fi];
|
||||||
|
// Track existing code blocks to avoid double-wrapping
|
||||||
|
if (fl.trim().indexOf(bt) === 0) {
|
||||||
|
inCodeBlock = !inCodeBlock;
|
||||||
|
// Flush any pending table block before code block marker
|
||||||
|
if (tableBlock.length > 0) {
|
||||||
|
result.push(bt);
|
||||||
|
for (var ti = 0; ti < tableBlock.length; ti++) result.push(tableBlock[ti]);
|
||||||
|
result.push(bt);
|
||||||
|
tableBlock = [];
|
||||||
|
}
|
||||||
|
result.push(fl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inCodeBlock) {
|
||||||
|
result.push(fl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Detect table row: has at least 2 pipe characters and content between them
|
||||||
|
var pipeCount = 0;
|
||||||
|
for (var pc = 0; pc < fl.length; pc++) { if (fl.charAt(pc) === '|') pipeCount++; }
|
||||||
|
var isTableRow = pipeCount >= 2 && fl.trim().charAt(0) === '|';
|
||||||
|
var isSeparator = isTableRow && /^[\\s|:-]+$/.test(fl.trim());
|
||||||
|
if (isTableRow) {
|
||||||
|
tableBlock.push(fl);
|
||||||
|
} else {
|
||||||
|
// Flush table block if it had enough rows (header + separator + data)
|
||||||
|
if (tableBlock.length >= 2) {
|
||||||
|
result.push(bt);
|
||||||
|
for (var ti2 = 0; ti2 < tableBlock.length; ti2++) result.push(tableBlock[ti2]);
|
||||||
|
result.push(bt);
|
||||||
|
} else {
|
||||||
|
// Not a real table, push lines back normally
|
||||||
|
for (var ti3 = 0; ti3 < tableBlock.length; ti3++) result.push(tableBlock[ti3]);
|
||||||
|
}
|
||||||
|
tableBlock = [];
|
||||||
|
result.push(fl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Flush trailing table block
|
||||||
|
if (tableBlock.length >= 2) {
|
||||||
|
result.push(bt);
|
||||||
|
for (var ti4 = 0; ti4 < tableBlock.length; ti4++) result.push(tableBlock[ti4]);
|
||||||
|
result.push(bt);
|
||||||
|
} else {
|
||||||
|
for (var ti5 = 0; ti5 < tableBlock.length; ti5++) result.push(tableBlock[ti5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.join('\\n').substring(0, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanChatBodies() {
|
function scanChatBodies() {
|
||||||
@@ -554,29 +814,108 @@ 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
|
||||||
|
if (!cv) {
|
||||||
|
var probe = document.querySelector('.leading-relaxed.select-text') || document.querySelector('.text-ide-message-block-bot-color');
|
||||||
|
if (probe) {
|
||||||
|
// Walk up to find a reasonable container (has overflow-y or is big enough)
|
||||||
|
var p = probe.parentElement;
|
||||||
|
for (var pi2 = 0; pi2 < 10 && p && p !== document.body; pi2++) {
|
||||||
|
var pCls = (typeof p.className === 'string') ? p.className : '';
|
||||||
|
if (pCls.indexOf('overflow') !== -1 || p.children.length > 3) {
|
||||||
|
cv = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p = p.parentElement;
|
||||||
|
}
|
||||||
|
if (!cv && probe.parentElement) cv = probe.parentElement.parentElement || probe.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (cv) {
|
if (cv) {
|
||||||
// AG Native path: find AI response blocks by class pattern
|
// v20: Dump CV structure for first 3 scans to ensure we capture it (even with stale HTML cache)
|
||||||
// DOM structure: #conversation > ... > .leading-relaxed.select-text (AI response text)
|
if (_conversationDumpCount < 3) {
|
||||||
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text');
|
_conversationDumpCount++;
|
||||||
|
log('CV found via: ' + (cv.id || (typeof cv.className === 'string' ? cv.className : cv.tagName) || 'unknown').substring(0, 100));
|
||||||
|
// Log all unique class names under #conversation for selector discovery
|
||||||
|
var allCvEls = cv.querySelectorAll('*');
|
||||||
|
var clsSet = {};
|
||||||
|
for (var ci2 = 0; ci2 < allCvEls.length; ci2++) {
|
||||||
|
var cn = allCvEls[ci2].className;
|
||||||
|
if (typeof cn === 'string' && cn.length > 0) {
|
||||||
|
var parts = cn.split(/\\s+/);
|
||||||
|
for (var pi = 0; pi < parts.length; pi++) {
|
||||||
|
if (parts[pi].length > 3 && !clsSet[parts[pi]]) clsSet[parts[pi]] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var clsList = Object.keys(clsSet).sort().join(', ');
|
||||||
|
log('CV-CLASSES (' + Object.keys(clsSet).length + '): ' + clsList.substring(0, 1500));
|
||||||
|
// v19: Log direct children to discover message block structure
|
||||||
|
var cvKids = cv.children;
|
||||||
|
log('CV-CHILDREN (' + cvKids.length + '):');
|
||||||
|
for (var ck = 0; ck < Math.min(cvKids.length, 15); ck++) {
|
||||||
|
var kid = cvKids[ck];
|
||||||
|
var kidCls = (typeof kid.className === 'string') ? kid.className : '';
|
||||||
|
var kidText = (kid.textContent || '').trim().substring(0, 60);
|
||||||
|
log(' child[' + ck + '] tag=' + kid.tagName + ' cls=' + kidCls.substring(0, 120) + ' text=' + kidText);
|
||||||
|
}
|
||||||
|
// v22: Deep-dive into gap-8 container to find individual message blocks
|
||||||
|
var msgContainer = cv.querySelector('.gap-8') || cv.children[0];
|
||||||
|
if (msgContainer) {
|
||||||
|
var msgKids = msgContainer.children;
|
||||||
|
log('MSG-BLOCKS (' + msgKids.length + '):');
|
||||||
|
for (var mk = 0; mk < Math.min(msgKids.length, 30); mk++) {
|
||||||
|
var mb = msgKids[mk];
|
||||||
|
var mbCls = (typeof mb.className === 'string') ? mb.className : '';
|
||||||
|
var mbText = (mb.textContent || '').trim().substring(0, 80);
|
||||||
|
var hasLeadingRelaxed = mb.querySelector('.leading-relaxed.select-text') ? 'Y' : 'N';
|
||||||
|
var firstChildCls = (mb.children[0] && typeof mb.children[0].className === 'string') ? mb.children[0].className : '';
|
||||||
|
log(' msg[' + mk + '] cls=' + mbCls.substring(0, 120) + ' lr=' + hasLeadingRelaxed + ' fc=' + firstChildCls.substring(0, 80) + ' text=' + mbText.substring(0, 60));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Force a dump with conversation context
|
||||||
|
dumpDOMStructure();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, .select-text.rounded-lg');
|
||||||
|
|
||||||
if (responseBlocks.length > 0) {
|
if (responseBlocks.length > 0) {
|
||||||
// Process the LAST (most recent) response block
|
// v22: Filter out thinking/reasoning blocks ??they have ancestor with max-h-[200px]
|
||||||
var lastBlock = responseBlocks[responseBlocks.length - 1];
|
// These are internal AI reasoning and should NOT be relayed to Discord
|
||||||
|
var filteredBlocks = [];
|
||||||
|
for (var fbi = 0; fbi < responseBlocks.length; fbi++) {
|
||||||
|
var isThinking = false;
|
||||||
|
var ancestor = responseBlocks[fbi].parentElement;
|
||||||
|
for (var depth = 0; ancestor && depth < 5; depth++) {
|
||||||
|
var aCls = (typeof ancestor.className === 'string') ? ancestor.className : '';
|
||||||
|
if (aCls.indexOf('max-h-[200px]') !== -1 || aCls.indexOf('max-h-[150px]') !== -1) {
|
||||||
|
isThinking = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ancestor = ancestor.parentElement;
|
||||||
|
}
|
||||||
|
if (!isThinking) filteredBlocks.push(responseBlocks[fbi]);
|
||||||
|
}
|
||||||
|
if (filteredBlocks.length === 0) return;
|
||||||
|
|
||||||
|
// Process the LAST (most recent) non-thinking response block
|
||||||
|
var lastBlock = filteredBlocks[filteredBlocks.length - 1];
|
||||||
|
|
||||||
// Skip if already scraped
|
// Skip if already scraped
|
||||||
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 (responseBlocks.length > _lastResponseBlockCount) {
|
if (filteredBlocks.length > _lastResponseBlockCount) {
|
||||||
// New block appeared — process it
|
// New block appeared ??process it
|
||||||
for (var rbi = responseBlocks.length - 1; rbi >= 0; rbi--) {
|
for (var rbi = filteredBlocks.length - 1; rbi >= 0; rbi--) {
|
||||||
if (responseBlocks[rbi].dataset.agChatScraped !== 'true' && responseBlocks[rbi].dataset.agChatScraped !== 'pending') {
|
if (filteredBlocks[rbi].dataset.agChatScraped !== 'true' && filteredBlocks[rbi].dataset.agChatScraped !== 'pending') {
|
||||||
lastBlock = responseBlocks[rbi];
|
lastBlock = filteredBlocks[rbi];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,13 +926,26 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var blockText = extractCleanStepText(lastBlock);
|
var blockText = extractCleanStepText(lastBlock);
|
||||||
if (blockText && blockText.length > 30) {
|
|
||||||
// QUALITY CHECK: Skip if the text is mostly short lines (UI artifacts)
|
var clsStr = (typeof lastBlock.className === 'string') ? lastBlock.className : '';
|
||||||
var lines = blockText.split('\\n').filter(function(l) { return l.trim().length > 0; });
|
// v19: Log block classes for user message selector discovery
|
||||||
var longLines = lines.filter(function(l) { return l.trim().length > 20; });
|
var parentCls = lastBlock.parentElement ? ((typeof lastBlock.parentElement.className === 'string') ? lastBlock.parentElement.className : '') : '';
|
||||||
if (longLines.length === 0) {
|
var grandCls = (lastBlock.parentElement && lastBlock.parentElement.parentElement) ? ((typeof lastBlock.parentElement.parentElement.className === 'string') ? lastBlock.parentElement.parentElement.className : '') : '';
|
||||||
log('AG-Native: skipped (no long lines, likely UI noise)');
|
log('user-cls-debug block=' + clsStr.substring(0, 150) + ' | parent=' + parentCls.substring(0, 150) + ' | grand=' + grandCls.substring(0, 150) + ' | text=' + (blockText||'').substring(0, 50));
|
||||||
return;
|
// v22: Detect user message: has select-text + rounded-lg but NOT leading-relaxed
|
||||||
|
var isUser = (clsStr.indexOf('rounded-lg') !== -1 && clsStr.indexOf('leading-relaxed') === -1) || clsStr.indexOf('user-color') !== -1;
|
||||||
|
var role = isUser ? 'user' : 'bot';
|
||||||
|
|
||||||
|
// Bot messages often start empty and stream in. User messages are usually immediate.
|
||||||
|
if (blockText && (blockText.length > 30 || isUser && blockText.length > 0)) {
|
||||||
|
// QUALITY CHECK: Skip if the text is mostly short lines (UI artifacts), BUT skip this check for user messages
|
||||||
|
if (!isUser) {
|
||||||
|
var lines = blockText.split('\\n').filter(function(l) { return l.trim().length > 0; });
|
||||||
|
var longLines = lines.filter(function(l) { return l.trim().length > 20; });
|
||||||
|
if (longLines.length === 0) {
|
||||||
|
log('AG-Native: skipped (no long lines, likely UI noise)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for content to stabilize (3s no change)
|
// Wait for content to stabilize (3s no change)
|
||||||
@@ -605,28 +957,32 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_lastStepTextSent) return;
|
if (_lastStepTextSent) return;
|
||||||
if (Date.now() - _lastStepTextTime < 3000) return; // Still waiting
|
// Bot needs 3s to stabilize, User just needs 500ms
|
||||||
|
var waitTime = isUser ? 500 : 3000;
|
||||||
|
if (Date.now() - _lastStepTextTime < waitTime) return; // Still waiting
|
||||||
|
|
||||||
// Content is stable — send it
|
// v21: DOM-based chat relay RE-ENABLED ??GetCascadeTrajectorySteps does NOT
|
||||||
|
// return steps for in-progress cascades, making Step Probe RT-CAPTURE useless.
|
||||||
|
// Observer DOM extraction is the ONLY real-time path for AI response relay.
|
||||||
_lastStepTextSent = true;
|
_lastStepTextSent = true;
|
||||||
_lastResponseBlockCount = responseBlocks.length;
|
_lastResponseBlockCount = filteredBlocks.length;
|
||||||
lastBlock.dataset.agChatScraped = 'pending';
|
lastBlock.dataset.agChatScraped = 'pending';
|
||||||
|
|
||||||
log('AG-Native chat relay: blocks=' + responseBlocks.length + ' text=' + blockText.length + ' chars');
|
log('AG-Native chat relay [' + role + ']: blocks=' + filteredBlocks.length + ' text=' + blockText.length + ' chars');
|
||||||
(function(el, txt, count) {
|
(function(el, txt, count, r) {
|
||||||
fetch(BASE + '/chat', {
|
fetch(BASE + '/chat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: txt, source: 'ag_native_block_' + count, block_index: count })
|
body: JSON.stringify({ text: txt, source: 'ag_native_block_' + count, block_index: count, role: r })
|
||||||
}).then(function() { el.dataset.agChatScraped = 'true'; log('AG-Native chat sent OK'); })
|
}).then(function() { el.dataset.agChatScraped = 'true'; log('AG-Native chat sent OK'); })
|
||||||
.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, responseBlocks.length);
|
})(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
|
||||||
@@ -698,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';
|
||||||
@@ -716,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);
|
||||||
@@ -768,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={
|
||||||
@@ -800,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;
|
||||||
}
|
}
|
||||||
@@ -883,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++){
|
||||||
@@ -895,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){
|
||||||
@@ -918,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();
|
||||||
@@ -973,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.
|
||||||
@@ -453,28 +464,70 @@ function setupMonitor() {
|
|||||||
|
|
||||||
// ── v15: Heartbeat probe — detect step changes when summary API is stale ──
|
// ── v15: Heartbeat probe — detect step changes when summary API is stale ──
|
||||||
// GetAllCascadeTrajectories can return frozen stepCount/lastModifiedTime,
|
// GetAllCascadeTrajectories can return frozen stepCount/lastModifiedTime,
|
||||||
// preventing delta>0 from ever firing. Every 10 polls (~50s), directly
|
// v20: Heartbeat every 3 polls (~15s) — AG API never reports delta for active sessions
|
||||||
// probe GetCascadeTrajectorySteps to get the REAL latest step count.
|
if (delta === 0 && ctx.sdk && ctx.activeSessionId && pollCount % 3 === 0) {
|
||||||
if (delta === 0 && ctx.sdk && ctx.activeSessionId && pollCount % 10 === 0) {
|
|
||||||
try {
|
try {
|
||||||
const hbOffset = Math.max(0, currentCount - 1);
|
const hbOffset = Math.max(0, currentCount - 5);
|
||||||
const hbResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
const hbResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
cascadeId: ctx.activeSessionId,
|
cascadeId: ctx.activeSessionId,
|
||||||
stepOffset: hbOffset,
|
stepOffset: hbOffset,
|
||||||
verbosity: 0, // minimal — just need step count
|
verbosity: 1, // need content for capture
|
||||||
});
|
});
|
||||||
if (hbResp?.steps?.length > 0) {
|
if (hbResp?.steps?.length > 0) {
|
||||||
const realStepCount = hbOffset + hbResp.steps.length;
|
const realStepCount = hbOffset + hbResp.steps.length;
|
||||||
if (realStepCount > lastKnownStepCount) {
|
if (realStepCount > lastKnownStepCount) {
|
||||||
ctx.logToFile(`[HEARTBEAT] summary stale! reported=${lastKnownStepCount} real=${realStepCount} — correcting`);
|
ctx.logToFile(`[HEARTBEAT] stale! reported=${lastKnownStepCount} real=${realStepCount}`);
|
||||||
|
// Process new steps for RT-CAPTURE
|
||||||
|
for (let hi = 0; hi < hbResp.steps.length; hi++) {
|
||||||
|
const hs = hbResp.steps[hi];
|
||||||
|
const hIdx = hbOffset + hi;
|
||||||
|
if (hIdx <= lastResponseCaptureStep) continue;
|
||||||
|
const hType = hs?.type || '';
|
||||||
|
// Capture AI responses
|
||||||
|
if (hType.includes('PLANNER_RESPONSE') && hs?.status?.includes('DONE')) {
|
||||||
|
let text = extractPlannerText(hs) || '';
|
||||||
|
if (text.length > 10) {
|
||||||
|
lastResponseCaptureStep = hIdx;
|
||||||
|
ctx.logToFile(`[HB-CAPTURE] AI step=${hIdx} (${text.length} chars)`);
|
||||||
|
const truncated = text.length > 3500
|
||||||
|
? text.substring(0, 3500) + '\n\n_(이하 생략)_'
|
||||||
|
: text;
|
||||||
|
ctx.writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Capture user messages
|
||||||
|
if (hType.includes('USER_INPUT') && hIdx > lastUserInputStepIdx) {
|
||||||
|
lastUserInputStepIdx = hIdx;
|
||||||
|
const ui = hs?.userInput;
|
||||||
|
const umText = (ui?.userResponse || ui?.text || '').trim();
|
||||||
|
if (umText.length > 2) {
|
||||||
|
const sentAt = ctx.recentDiscordSentTexts.get(umText);
|
||||||
|
if (!sentAt || (Date.now() - sentAt) > 60_000) {
|
||||||
|
const dedupKey = `user_msg:${umText}`;
|
||||||
|
const lastRelayed = lastSnapshotText.get(dedupKey);
|
||||||
|
if (!lastRelayed || (Date.now() - Number(lastRelayed)) > 30_000) {
|
||||||
|
lastSnapshotText.set(dedupKey, String(Date.now()));
|
||||||
|
const clientType = ui?.clientType || '';
|
||||||
|
const source = clientType.includes('IDE') ? 'AG 직접 입력' : 'API';
|
||||||
|
const truncated = umText.length > 800
|
||||||
|
? umText.substring(0, 800) + '\n\n_(이하 생략)_'
|
||||||
|
: umText;
|
||||||
|
ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`);
|
||||||
|
ctx.logToFile(`[HB-CAPTURE] User step=${hIdx} (${umText.length} chars)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
lastKnownStepCount = realStepCount;
|
lastKnownStepCount = realStepCount;
|
||||||
// Trigger RT-CAPTURE by re-entering delta>0 path below
|
} else if (pollCount % 30 === 0) {
|
||||||
// We set currentCount so delta recalculation works
|
ctx.logToFile(`[HEARTBEAT] ok offset=${hbOffset} got=${hbResp.steps.length} real=${realStepCount} known=${lastKnownStepCount}`);
|
||||||
}
|
}
|
||||||
|
} else if (pollCount % 30 === 0) {
|
||||||
|
ctx.logToFile(`[HEARTBEAT] no steps returned for offset=${hbOffset}`);
|
||||||
}
|
}
|
||||||
} catch (hbErr: any) {
|
} catch (hbErr: any) {
|
||||||
// Non-critical — will retry next heartbeat
|
if (pollCount % 10 === 0) ctx.logToFile(`[HEARTBEAT] probe error: ${hbErr.message?.substring(0, 100)}`);
|
||||||
if (pollCount <= 30) ctx.logToFile(`[HEARTBEAT] probe error: ${hbErr.message?.substring(0, 60)}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,12 +581,46 @@ function setupMonitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v20: Capture USER_INPUT steps for user message relay
|
||||||
|
if (sType.includes('USER_INPUT') && actualIdx > lastUserInputStepIdx) {
|
||||||
|
lastUserInputStepIdx = actualIdx;
|
||||||
|
const ui = s?.userInput;
|
||||||
|
const umText = (ui?.userResponse || ui?.text || s?.plannerResponse?.textContent || '').trim();
|
||||||
|
const clientType = ui?.clientType || '';
|
||||||
|
const isFromIDE = clientType.includes('IDE');
|
||||||
|
ctx.logToFile(`[RT-USER-MSG] step=${actualIdx} client=${clientType} text=${umText.substring(0, 100)}`);
|
||||||
|
|
||||||
|
if (umText.length > 2) {
|
||||||
|
// Skip echo: if text was recently sent from Discord
|
||||||
|
const sentAt = ctx.recentDiscordSentTexts.get(umText);
|
||||||
|
if (sentAt && (Date.now() - sentAt) < 60_000) {
|
||||||
|
ctx.recentDiscordSentTexts.delete(umText);
|
||||||
|
ctx.logToFile(`[RT-USER-MSG] skipped echo relay (Discord origin)`);
|
||||||
|
} else {
|
||||||
|
// Content-based dedup
|
||||||
|
const dedupKey = `user_msg:${umText}`;
|
||||||
|
const lastRelayed = lastSnapshotText.get(dedupKey);
|
||||||
|
if (!lastRelayed || (Date.now() - Number(lastRelayed)) > 30_000) {
|
||||||
|
lastSnapshotText.set(dedupKey, String(Date.now()));
|
||||||
|
const truncated = umText.length > 800
|
||||||
|
? umText.substring(0, 800) + '\n\n_(이하 생략)_'
|
||||||
|
: umText;
|
||||||
|
const source = isFromIDE ? 'AG 직접 입력' : 'API';
|
||||||
|
ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`);
|
||||||
|
ctx.logToFile(`[RT-USER-MSG] relayed ${umText.length} chars`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (s?.status === 'CORTEX_STEP_STATUS_WAITING') {
|
if (s?.status === 'CORTEX_STEP_STATUS_WAITING') {
|
||||||
const toolCall = s?.metadata?.toolCall;
|
const toolCall = s?.metadata?.toolCall;
|
||||||
const toolName = toolCall?.name || (sType || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
const toolName = toolCall?.name || (sType || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||||
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;
|
||||||
@@ -721,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
|
||||||
@@ -774,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