Compare commits
1 Commits
main
...
e4dc1b171f
| Author | SHA1 | Date | |
|---|---|---|---|
| e4dc1b171f |
@@ -1,70 +0,0 @@
|
||||
---
|
||||
description: 모든 작업에 자동 적용되는 에이전트 행동 규칙. 새 대화 시작 시 반드시 이 파일을 먼저 읽습니다.
|
||||
---
|
||||
|
||||
# Agent Rules
|
||||
|
||||
## Identity
|
||||
|
||||
당신은 이 프로젝트의 시니어 개발자입니다. 지시를 정확히 따르고, 추측보다 근거를 우선합니다.
|
||||
|
||||
## NEVER (절대 금지)
|
||||
|
||||
1. NEVER start coding without reading relevant reference documents in `.agents/references/`
|
||||
2. NEVER guess when documentation exists — always check `.agents/references/` first
|
||||
3. NEVER repeat a failed approach — check `.agents/references/known-issues.md` first
|
||||
4. NEVER call APIs directly when helper scripts exist in `.agents/workflows/helpers/`
|
||||
5. NEVER skip the pre-task checklist defined in `.agents/workflows/pre-task.md`
|
||||
6. NEVER attempt the same failed approach more than 2 times
|
||||
7. NEVER truncate error messages — always show the full error output
|
||||
8. NEVER say "구현 완료" or "동작 확인" without ACTUAL end-to-end test — import/문법 통과는 검증이 아님
|
||||
9. NEVER confuse "코드가 논리적으로 맞음" with "실제로 동작함" — 실행 로그가 없으면 미검증
|
||||
10. NEVER fix or audit code by looking at only the immediate file:
|
||||
(a) Open the PRODUCER (who creates the data?) and CONSUMER (who reads/deletes?)
|
||||
(b) Search for defense mechanisms (try-catch, dedup, idempotency guards)
|
||||
(c) DISPROVE the bug before reporting — if a defense exists, it may be a false positive
|
||||
(d) Report only bugs with a proven end-to-end triggering path
|
||||
"I traced the flow" without opening actual files = violation.
|
||||
11. NEVER apply changes mechanically across files — every import, variable, function must have at least one callsite in the SAME file
|
||||
|
||||
## ALWAYS (필수)
|
||||
|
||||
1. ALWAYS run `.agents/workflows/pre-task.md` before any implementation task
|
||||
2. ALWAYS check `.agents/references/known-issues.md` before debugging
|
||||
3. ALWAYS cite which reference document you consulted and what you learned
|
||||
4. ALWAYS stop and ask the user if 2 consecutive attempts on the same approach fail
|
||||
5. ALWAYS use existing helper scripts instead of raw API calls
|
||||
6. ALWAYS read related existing code (minimum 3 files) before writing new code
|
||||
7. ALWAYS verify with real execution after implementation — trigger the actual flow, check logs (e.g. extension.log), confirm the expected result appeared
|
||||
8. ALWAYS distinguish "구현했다" vs "검증했다" when reporting to user — 테스트 안 했으면 명시
|
||||
9. ALWAYS cross-reference with project history (devlog, git log -5, Vikunja) when evaluating system state — code absence may mean "intentionally removed" or "deployed externally", not "unimplemented"
|
||||
|
||||
## Failure Protocol
|
||||
|
||||
```
|
||||
1st failure → Re-read reference docs → Try DIFFERENT approach
|
||||
2nd failure (same issue) → STOP → Report diagnosis to user with:
|
||||
- What was tried
|
||||
- What failed
|
||||
- Root cause hypothesis
|
||||
- Suggested next steps
|
||||
3rd attempt on same approach → FORBIDDEN
|
||||
```
|
||||
|
||||
## Reference Loading Order
|
||||
|
||||
1. `.agents/AGENT.md` (this file — behavior rules)
|
||||
2. `.agents/references/known-issues.md` (past failure patterns)
|
||||
3. `.agents/references/` (project-specific knowledge)
|
||||
4. `.agents/workflows/services.md` (service credentials & protocols)
|
||||
5. `.agents/workflows/` (action procedures)
|
||||
|
||||
## Bug Report Protocol
|
||||
|
||||
→ See `.agents/references/bug-report-protocol.md`
|
||||
|
||||
## PowerShell Notes
|
||||
|
||||
- `curl` → PowerShell에서 `Invoke-WebRequest` 별칭. **반드시 `curl.exe`** 사용
|
||||
- `npm` → 실행 정책 문제 시 `cmd /c npm` 사용
|
||||
- JSON 처리 시 `.py` 스크립트 권장 (PowerShell 이스케이핑 이슈 방지)
|
||||
163
.agents/GUIDE.md
163
.agents/GUIDE.md
@@ -1,163 +0,0 @@
|
||||
# AI 에이전트 워크플로우 시스템 가이드
|
||||
|
||||
> 이 가이드는 AI 코딩 에이전트가 더 똑똑하게 동작하도록 설계된 범용 워크플로우 시스템의 사용법을 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## 왜 이 시스템이 필요한가?
|
||||
|
||||
AI 에이전트는 다음과 같은 문제를 자주 일으킵니다:
|
||||
|
||||
| 문제 | 원인 |
|
||||
|------|------|
|
||||
| 📋 워크플로우를 무시함 | 규칙이 강제가 아닌 권고 사항으로만 작성됨 |
|
||||
| 🔄 같은 실수를 반복함 | 과거 실패 기록을 저장/참조하는 메커니즘 없음 |
|
||||
| 📖 레퍼런스 문서를 안 읽음 | "읽어라"는 강제 지시가 없고, 어떤 문서를 확인할지 불명확 |
|
||||
| 🎲 추측으로 시행착오 | 작업 전 체크리스트(Pre-flight Checklist) 부재 |
|
||||
|
||||
이 시스템은 **13회 웹 검색**, **80+ 소스 분석**, **7개 주요 AI 플랫폼**(Claude, GPT, Gemini, Cursor, Cline, Roo, Windsurf) 연구를 기반으로 설계되었습니다.
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조 개요
|
||||
|
||||
```
|
||||
.agents/
|
||||
├── AGENT.md ← 🧠 에이전트 헌법 (NEVER/ALWAYS 규칙)
|
||||
├── GUIDE.md ← 📖 이 가이드
|
||||
├── references/ ← 📚 프로젝트 지식 베이스
|
||||
│ ├── architecture.md ← 아키텍처 설명
|
||||
│ ├── tech-stack.md ← 기술 스택 & 버전
|
||||
│ ├── conventions.md ← 코딩 컨벤션
|
||||
│ └── known-issues.md ← 🔴 과거 실패 기록 (핵심!)
|
||||
└── workflows/ ← ⚙️ 행동 절차
|
||||
├── start.md ← 세션 시작 (룰 로딩 + devlog 복구)
|
||||
├── end.md ← 세션 종료 (devlog + known-issues + Vikunja + Git)
|
||||
├── pre-task.md ← 작업 전 필수 체크리스트
|
||||
├── debug.md ← 디버깅 전용 절차
|
||||
├── services.md ← 서비스 연동 정보 + AI 작업 프로토콜
|
||||
├── check-gitea.md ← Gitea 현황 조회
|
||||
├── check-vikunja.md ← Vikunja 태스크 조회
|
||||
└── helpers/
|
||||
├── vikunja_helper.py ← Vikunja API 안전 래퍼
|
||||
└── wiki_helper.py ← Gitea Wiki 래퍼
|
||||
```
|
||||
|
||||
**프로젝트 루트에 자동 생성되는 디렉토리:**
|
||||
```
|
||||
docs/devlog/ ← 📓 세션별 작업 기록
|
||||
├── YYYY-MM-DD.md ← Index (매일 1줄씩 누적)
|
||||
└── entries/
|
||||
└── YYYYMMDD-NNN.md ← Entry (설계 결정/미완료 시만)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 각 파일의 역할
|
||||
|
||||
### 🧠 `AGENT.md` — 에이전트 헌법
|
||||
|
||||
에이전트가 **모든 대화에서 따라야 하는 글로벌 규칙**입니다.
|
||||
|
||||
**핵심 메커니즘:**
|
||||
- **NEVER 규칙**: `"절대 ~하지 마라"` — 연구에 따르면 금지 규칙이 더 잘 지켜집니다
|
||||
- **Failure Protocol**: 동일 접근 2회 실패 시 자동 중단 → 유저에게 보고
|
||||
- **Reference Loading Order**: 어떤 문서를 먼저 읽을지 우선순위 명시
|
||||
|
||||
### 📋 `pre-task.md` — 사전 점검 체크리스트
|
||||
|
||||
모든 구현 작업 전에 실행하는 **4단계 체크리스트**:
|
||||
1. 요구사항 정리
|
||||
2. 레퍼런스 확인 (추측 금지)
|
||||
3. 계획 수립
|
||||
4. 유저 확인
|
||||
|
||||
### 🔴 `known-issues.md` — 과거 실패 기록
|
||||
|
||||
**가장 중요한 파일.** 에이전트가 같은 실수를 반복하는 근본 원인은 **실패를 기억하지 못하기 때문**입니다. 이 파일은:
|
||||
- 세션 종료 시 에이전트가 자동으로 새 이슈를 추가
|
||||
- 디버깅/구현 전에 에이전트가 반드시 확인
|
||||
- 시간이 지날수록 **축적 학습** 효과
|
||||
|
||||
### 🔧 `debug.md` — 디버깅 전용 워크플로우
|
||||
|
||||
**추측 기반 디버깅을 금지**하는 5단계 절차:
|
||||
1. 정보 수집 (에러 전문 확인)
|
||||
2. known-issues 확인
|
||||
3. 근본 원인 분석 (가설 → 검증)
|
||||
4. 수정 및 검증
|
||||
5. 기록 (known-issues에 추가)
|
||||
|
||||
### 📓 Devlog — 세션별 작업 기록 (start.md / end.md에서 관리)
|
||||
|
||||
known-issues가 **실패만** 기록한다면, devlog는 **전체 세션 이력**을 기록합니다:
|
||||
- **Index** (`docs/devlog/YYYY-MM-DD.md`): 매 작업마다 1줄 (필수)
|
||||
- **Entry** (`docs/devlog/entries/YYYYMMDD-NNN.md`): 설계 결정/미완료/삽질 시만 (선택)
|
||||
- **start.md**에서 자동으로 오늘/어제 devlog를 읽어 맥락 복구
|
||||
|
||||
### ▶️ `start.md` / ⏹️ `end.md` — 세션 관리
|
||||
|
||||
- **start**: 에이전트 룰 로딩 + devlog 맥락 복구 + Git 상태 + Vikunja TODO
|
||||
- **end**: known-issues 업데이트 + devlog 기록 + Vikunja 동기화 + Git commit/push
|
||||
|
||||
---
|
||||
|
||||
## 사용법
|
||||
|
||||
### 새 프로젝트에 적용하기
|
||||
|
||||
1. `.agents/` 디렉토리를 프로젝트에 복사
|
||||
2. `references/` 파일들을 프로젝트에 맞게 채우기:
|
||||
- `architecture.md` — 프로젝트 구조 설명
|
||||
- `tech-stack.md` — 사용 기술 및 버전
|
||||
- `conventions.md` — 코딩 스타일 규칙
|
||||
3. 프로젝트별 워크플로우가 있다면 `workflows/`에 추가
|
||||
|
||||
### 프로젝트별 워크플로우와 함께 사용하기
|
||||
|
||||
이 범용 워크플로우와 프로젝트별 워크플로우(예: Vikunja 동기화, Gitea 연동)는 **함께 사용**합니다:
|
||||
|
||||
```
|
||||
.agents/
|
||||
├── AGENT.md ← 범용 (공통)
|
||||
├── references/ ← 범용 + 프로젝트 특화
|
||||
│ ├── known-issues.md ← 범용 (공통)
|
||||
│ └── ... ← 프로젝트에 맞게 작성
|
||||
└── workflows/
|
||||
├── pre-task.md ← 범용 (공통)
|
||||
├── debug.md ← 범용 (공통)
|
||||
├── start.md ← 범용 기반 + 프로젝트 단계 추가
|
||||
├── end.md ← 범용 기반 + 프로젝트 단계 추가
|
||||
├── services.md ← ⭐ 프로젝트별
|
||||
├── check-vikunja.md ← ⭐ 프로젝트별
|
||||
├── check-gitea.md ← ⭐ 프로젝트별
|
||||
└── helpers/
|
||||
├── vikunja_helper.py ← ⭐ 프로젝트별
|
||||
└── wiki_helper.py ← ⭐ 프로젝트별
|
||||
```
|
||||
|
||||
### 다른 AI IDE에서도 사용하기
|
||||
|
||||
| 대상 플랫폼 | 방법 |
|
||||
|------------|------|
|
||||
| **Cursor** | `AGENT.md` → `.cursor/rules/agent.mdc` (alwaysApply) |
|
||||
| **Claude Code** | `AGENT.md` → `CLAUDE.md`, references를 `@import` |
|
||||
| **Windsurf** | `AGENT.md` → `.windsurfrules` 또는 `.windsurf/rules/agent.md` |
|
||||
| **Cline/Roo** | 루트에 `AGENTS.md`로 복사 |
|
||||
| **Gemini** | `AGENT.md` → `.gemini/GEMINI.md` |
|
||||
|
||||
---
|
||||
|
||||
## 연구 근거 요약
|
||||
|
||||
이 시스템의 각 설계 결정은 학술 연구와 실무 사례에 근거합니다:
|
||||
|
||||
| 설계 결정 | 근거 |
|
||||
|----------|------|
|
||||
| NEVER > ALWAYS (금지 규칙 우선) | Community 검증 — "NEVER use X" ≫ "always prefer Y" |
|
||||
| 2회 실패 시 자동 중단 | Streak Breaker / Sentinel Check 연구 |
|
||||
| 실패 기록 누적 | Reflexion Framework (텍스트 피드백 기반 자기 교정) |
|
||||
| 사전 체크리스트 강제 | Claude Skills 체크리스트 + GPT Chain-of-Thought |
|
||||
| Progressive Disclosure | Anthropic Context Engineering (2025) |
|
||||
| 300줄 이하 규칙 | Claude `CLAUDE.md` 공식 권장 (토큰 효율성) |
|
||||
| 코드 예시 > 설명 | GitHub Copilot Agents, AGENTS.md 공통 Best Practice |
|
||||
@@ -1,322 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
> AI 에이전트는 구현 전 이 문서를 반드시 확인합니다.
|
||||
|
||||
## 1. 프로젝트 개요
|
||||
|
||||
**Gravity Control**은 Antigravity AI 코딩 에이전트와 Discord를 실시간으로 연결하는 브릿지 시스템이다.
|
||||
|
||||
### 핵심 목적
|
||||
- AI 에이전트의 **승인 요청**(코드 실행, 파일 수정 등)을 Discord로 전달하고 사용자 응답을 반환
|
||||
- AI 에이전트의 **작업 스냅샷**(대화 요약, 진행 상황)을 Discord에 실시간 표시
|
||||
- **코드 리뷰**(diff review) accept/reject을 Discord에서 처리
|
||||
- 사용자의 Discord **명령어**(!approve, !reject, !auto 등)를 AG Extension으로 전달
|
||||
- **Auto-approve 모드**로 무인 작업 지원
|
||||
|
||||
### 시스템 구성
|
||||
|
||||
```
|
||||
┌────────────────┐ WebSocket ┌──────────────┐ Discord API ┌─────────┐
|
||||
│ VS Code │◄──────────────────►│ Hub Server │◄───────────────────►│ Discord │
|
||||
│ AG Extension │ type:auth/chat │ (hub.py + │ discord.py bot │ 서버 │
|
||||
│ (TypeScript) │ /pending/resp │ gateway.py)│ │ │
|
||||
└────────────────┘ └──────────────┘ └─────────┘
|
||||
↕ AG SDK (RPC) ↕
|
||||
┌────────────────┐ ┌──────────────┐
|
||||
│ Antigravity │ │ 파일 bridge │ ← 레거시 fallback
|
||||
│ AI Engine │ │ (bridge.py) │ (WS 미사용 시)
|
||||
└────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 디렉토리 구조
|
||||
|
||||
```
|
||||
gravity_control/
|
||||
├── main.py # 진입점: Bot + Hub + Watcher 통합 시작
|
||||
├── config.py # 환경변수 + .env 로드 (66줄)
|
||||
│
|
||||
├── ── 서버 측 (Python) ──
|
||||
├── bot.py # Discord 봇: 승인 UI, 채널 관리, Hub 핸들러 (1,286줄)
|
||||
├── hub.py # WebSocket Hub: 연결 관리, 메시지 라우팅 (580줄)
|
||||
├── auth.py # JWT 토큰 + registration code 인증 (127줄)
|
||||
├── gateway.py # HTTP REST API + /ws endpoint (168줄)
|
||||
├── bridge.py # 파일 기반 IPC (레거시 fallback) (270줄)
|
||||
├── watcher.py # Brain 디렉토리 변경 감시 (290줄)
|
||||
├── parser.py # Markdown → Discord 변환 (245줄)
|
||||
│
|
||||
├── ── Extension 측 (TypeScript) ──
|
||||
├── extension/src/
|
||||
│ ├── extension.ts # 메인: SDK init, activate, 오케스트레이션 (650줄)
|
||||
│ ├── step-probe.ts # 폴링 + 응답 처리 + 승인 전략 (1,479줄)
|
||||
│ │ # setupMonitor(), processResponseFile(),
|
||||
│ │ # writePendingApproval(), tryApprovalStrategies()
|
||||
│ ├── http-bridge.ts # HTTP 서버 (Renderer↔Extension Host 통신) (280줄)
|
||||
│ │ # startHttpBridge(), getDeterministicPort()
|
||||
│ ├── html-patcher.ts # AG HTML 패치 + product.json 체크섬 (280줄)
|
||||
│ │ # setupApprovalObserver(), updateProductChecksums()
|
||||
│ ├── command-handler.ts # Discord→AG 명령어 처리 (175줄)
|
||||
│ │ # watchCommandsDir(), handleWSCommand()
|
||||
│ ├── observer-script.ts # DOM Observer 스크립트 생성 (698줄)
|
||||
│ │ # generateApprovalObserverScript()
|
||||
│ ├── ws-client.ts # WSBridgeClient: Hub 연결, 재연결, 큐 (505줄)
|
||||
│ ├── step-utils.ts # Step 파싱 순수 함수 4개 (114줄)
|
||||
│ │ # extractPlannerText, filterEphemeral,
|
||||
│ │ # extractToolCommand, extractToolDescription
|
||||
│ └── sdk/ # Antigravity SDK 로컬 임베드
|
||||
│ ├── index.js # SDK 런타임 (4,014줄)
|
||||
│ └── index.d.ts # SDK 타입 정의 (2,297줄)
|
||||
│
|
||||
├── ── 테스트 ──
|
||||
├── tests/
|
||||
│ ├── test_ws_hub.py # Hub WS 연결 테스트
|
||||
│ └── test_syntax.py # Python 구문 검증
|
||||
│
|
||||
├── ── 문서 / 설정 ──
|
||||
├── .env # 환경변수 (git 제외)
|
||||
├── .agents/references/ # AI 에이전트 레퍼런스
|
||||
├── docs/devlog/ # 작업 로그
|
||||
└── start_bot.bat # 윈도우용 봇 시작 스크립트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 핵심 모듈 상세
|
||||
|
||||
### 3.1 Hub (hub.py) — WebSocket 메시지 허브
|
||||
|
||||
**역할**: Extension ↔ Bot 간 실시간 양방향 통신 중계
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 연결 관리 | 프로젝트별 다중 인스턴스, 인스턴스 번호 자동 부여 |
|
||||
| JWT 인증 | registration_code → JWT 발급 → 이후 토큰 재인증 |
|
||||
| 메시지 라우팅 | pending, chat, register, auto_resolve, brain_event |
|
||||
| 응답 역라우팅 | request_id → pending_owners → 원본 Extension으로 전달 |
|
||||
| Rate limiting | per-connection 100msg/10s |
|
||||
| Dedup | msg_id 기반 60s TTL 중복 제거 |
|
||||
| Heartbeat | 30s 간격 ping/pong |
|
||||
|
||||
**프로토콜**:
|
||||
```
|
||||
1. Client → Server: {type:"auth", registration_code/token, project, pc}
|
||||
2. Server → Client: {type:"auth_ok", conn_id, instance_number, session_token}
|
||||
3. 양방향 메시지 교환:
|
||||
- Extension→Hub: pending, chat, register, auto_resolve, brain_event
|
||||
- Hub→Extension: response, command, instance_update, error
|
||||
```
|
||||
|
||||
### 3.2 Auth (auth.py) — 인증 관리
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| Registration Code | 사전 공유 코드로 최초 인증 |
|
||||
| JWT 발급 | HMAC-SHA256, 24시간 유효 |
|
||||
| 토큰 검증 | 만료/위조 감지, 프로젝트+PC 메타데이터 포함 |
|
||||
|
||||
### 3.3 Bot (bot.py) — Discord 인터페이스
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 승인 UI | Approve/Reject 버튼, diff_review Accept/Reject |
|
||||
| Auto-approve | `!auto` 토글, 세션 간 초기화 |
|
||||
| 채널 관리 | `#ag-{project}` 자동 채널 매칭 |
|
||||
| 스냅샷 전달 | 2000자 초과 시 파일 첨부 |
|
||||
| 명령어 | !approve, !reject, !auto, !status, !send |
|
||||
| Hub 연동 | on_hub_pending, on_hub_chat, on_hub_register 핸들러 |
|
||||
| IDLE 알림 | AI step 종료 시 Discord 알림 |
|
||||
|
||||
### 3.4 Extension (extension.ts) — VS Code 확장 (오케스트레이터)
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| AG SDK 연동 | getCascadeStatus, getAllTrajectories RPC |
|
||||
| 세션 감지 | activeSessionId 자동 추적 |
|
||||
| 프로젝트 자동 감지 | git remote URL 기반 |
|
||||
| 모듈 초기화 | HTTP bridge, observer, command handler 시작 |
|
||||
| WS bridge | WSBridgeClient 통한 Hub 연결 (우선) |
|
||||
| Status bar | SDK 상태 + 연결 상태 표시 |
|
||||
|
||||
### 3.4a HTTP Bridge (http-bridge.ts)
|
||||
|
||||
`HttpBridgeContext` 인터페이스로 extension.ts의 공유 상태 참조:
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| POST /pending | Renderer가 발견한 승인 버튼 보고 |
|
||||
| GET /response/:rid | Renderer가 Discord 응답 폴링 |
|
||||
| GET /trigger-click | Extension→Renderer 클릭 트리거 |
|
||||
| GET/POST /deep-inspect* | DOM 심층 검사 |
|
||||
| getDeterministicPort | 프로젝트명 기반 결정적 포트 |
|
||||
|
||||
### 3.4b HTML Patcher (html-patcher.ts)
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| setupApprovalObserver | AG Workbench HTML 파일에 observer 스크립트 인라인 삽입 |
|
||||
| updateProductChecksums | product.json SHA256 체크섬 업데이트 (vscode-file:// 프로토콜용) |
|
||||
| CSP 패치 | script-src에 'unsafe-inline' 추가 |
|
||||
| .orig 백업 | 최초 패치 전 원본 백업, 손상 시 자동 복구 |
|
||||
|
||||
### 3.4c Command Handler (command-handler.ts)
|
||||
|
||||
`CommandHandlerContext` 인터페이스로 extension.ts 상태 참조:
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| watchCommandsDir | commands/ 디렉토리 fs.watch + 3s 폴링 |
|
||||
| handleWSCommand | WS Hub 경유 명령어 처리 |
|
||||
| !stop, !auto | AG 에이전트 제어 명령어 |
|
||||
| 텍스트 전달 | Discord → AG `sendPromptToAgentPanel` |
|
||||
|
||||
### 3.5 Step Probe (step-probe.ts) — 상태 폴링
|
||||
|
||||
`BridgeContext` 인터페이스로 extension.ts와 상태 공유:
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| setupMonitor | 3초 간격 SDK 폴링, 세션/step 변화 감지 |
|
||||
| processResponseFile | Discord 응답 → AG RPC 실행 |
|
||||
| writePendingApproval | 승인 요청 파일/WS 전송 |
|
||||
| tryApprovalStrategies | 다단계 승인: DOM click → VS Code command → RPC |
|
||||
| setupResponseWatcher | response/ 디렉토리 파일 감시 |
|
||||
|
||||
**BridgeContext 필드** (14개):
|
||||
`bridgePath`, `projectName`, `sdk`, `wsBridge`, `logToFile`, `autoApproveEnabled`, `activeSessionId`, `setClickTrigger`, `recentDiscordSentTexts`, `writeChatSnapshot`, `writeChatSnapshotWithFiles`, `workspaceUri`, `diffReviewMetadata`, `sessionStalled`, `lastPendingStepIndex`, `stallProbed`, `sawRunningAfterPending`
|
||||
|
||||
### 3.6 WS Client (ws-client.ts) — Hub 클라이언트
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 연결 관리 | WebSocket + 자동 재연결 |
|
||||
| Backoff | 1s→60s 지수 백오프 + ±30% jitter |
|
||||
| 메시지 큐 | 200개 버퍼, 재연결 시 자동 flush |
|
||||
| Heartbeat | 25s 간격 ping |
|
||||
| 인증 | registration_code 또는 session_token |
|
||||
| API | sendPending, sendChat, sendRegister, sendAutoResolve |
|
||||
|
||||
### 3.7 Observer Script (observer-script.ts)
|
||||
|
||||
AG Webview의 DOM을 관찰하여 승인 버튼을 자동 감지/클릭:
|
||||
- MutationObserver로 `.actions-container` 감시
|
||||
- 버튼 텍스트 매칭으로 Approve/Reject 자동 실행
|
||||
- `postMessage`로 Extension과 통신
|
||||
|
||||
### 3.8 Step Utils (step-utils.ts)
|
||||
|
||||
순수 함수 4개:
|
||||
- `extractPlannerText(content)` — AI 응답 텍스트 추출
|
||||
- `filterEphemeral(text)` — 시스템 메시지 필터링
|
||||
- `extractToolCommand(content)` — 도구 명령어 추출
|
||||
- `extractToolDescription(content)` — 도구 설명 추출
|
||||
|
||||
---
|
||||
|
||||
## 4. 데이터 흐름
|
||||
|
||||
### 4.1 승인 요청 플로우
|
||||
|
||||
```
|
||||
AG Engine → SDK RPC → Extension(step-probe.ts)
|
||||
→ setupMonitor: WAITING step 감지
|
||||
→ writePendingApproval: pending 데이터 생성
|
||||
→ [WS] wsBridge.sendPending() → Hub → Bot → Discord (버튼 UI)
|
||||
→ [파일] bridge/pending/{id}.json (fallback)
|
||||
```
|
||||
|
||||
### 4.2 승인 응답 플로우
|
||||
|
||||
```
|
||||
Discord (사용자 버튼 클릭) → Bot
|
||||
→ [Hub connected] Hub.route_response() → WS → Extension
|
||||
→ [File fallback] bridge/response/{id}.json → setupResponseWatcher
|
||||
→ processResponseFile → tryApprovalStrategies
|
||||
1차: DOM observer script (webview inject)
|
||||
2차: VS Code command (cascade.approveCurrentStep)
|
||||
3차: Direct RPC (acknowledgeCodeActionStep)
|
||||
```
|
||||
|
||||
### 4.3 채팅 스냅샷 플로우
|
||||
|
||||
```
|
||||
Extension(step-probe.ts) → 새 step 텍스트 감지
|
||||
→ writeChatSnapshot(text) → truncation + dedup
|
||||
→ [WS] wsBridge.sendChat() → Hub → Bot → Discord (#ag-{project})
|
||||
→ [파일] bridge/pending/ snapshot 파일 (fallback)
|
||||
```
|
||||
|
||||
### 4.4 Diff Review 플로우
|
||||
|
||||
```
|
||||
AG Engine → 파일 수정 → Extension(step-probe.ts)
|
||||
→ edit_step_indices + modified_files 메타데이터 수집
|
||||
→ writePendingApproval (step_type="diff_review", 8초 지연)
|
||||
→ Discord (Accept all / Reject all 버튼)
|
||||
→ 응답 → handleDiffReviewResponse()
|
||||
→ openReviewChanges → 파일별 포커스 → agentAcceptAllInFile VS Code 커맨드
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> diff_review는 **VS Code 커맨드만 유효**합니다. RPC 방식(AcknowledgeCascadeCodeEdit 등)은 모두 실패 확정.
|
||||
> 상세 경위는 known-issues-archive.md의 "Diff Review 관련" 섹션을 참조하세요.
|
||||
|
||||
---
|
||||
|
||||
## 5. 통신 프로토콜
|
||||
|
||||
### 5.1 WebSocket 메시지 타입
|
||||
|
||||
**Extension → Hub (upstream)**:
|
||||
|
||||
| type | data 필드 | 설명 |
|
||||
|------|-----------|------|
|
||||
| `auth` | registration_code/token, project, pc | 최초 인증 |
|
||||
| `pending` | request_id, command, description, buttons | 승인 요청 |
|
||||
| `chat` | content, attached_files, conversation_id | 채팅 스냅샷 |
|
||||
| `register` | conversation_id, project_name | 세션 등록 |
|
||||
| `auto_resolve` | request_id | 자동 해결 알림 |
|
||||
| `brain_event` | (payload) | 브레인 이벤트 |
|
||||
| `heartbeat` | - | 연결 유지 |
|
||||
|
||||
**Hub → Extension (downstream)**:
|
||||
|
||||
| type | data 필드 | 설명 |
|
||||
|------|-----------|------|
|
||||
| `auth_ok` | conn_id, instance_number, session_token | 인증 성공 |
|
||||
| `auth_fail` | reason | 인증 실패 |
|
||||
| `response` | request_id, approved, button_index | 승인 응답 |
|
||||
| `command` | text, action | Discord 명령어 |
|
||||
| `instance_update` | active_count, instances[] | 인스턴스 변경 |
|
||||
| `error` | error | 에러 |
|
||||
|
||||
### 5.2 BOT_MODE 동작 차이
|
||||
|
||||
| 모드 | Watcher | Hub/Gateway | 용도 |
|
||||
|------|---------|-------------|------|
|
||||
| `local` | ✅ (brain 감시) | ❌ | 로컬 개발 (Extension과 같은 PC) |
|
||||
| `gateway` | ❌ | ✅ (port 8585) | 서버 배포 (WS Hub + Gateway) |
|
||||
| `remote` | ✅ | ❌ | ⚠️ **DEPRECATED** — 레거시 Collector (WS Hub 사용 권장) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 보안
|
||||
|
||||
| 항목 | 구현 |
|
||||
|------|------|
|
||||
| WS 인증 | Registration Code → JWT (HMAC-SHA256, 24h) |
|
||||
| Gateway API | API Key 헤더 (`X-API-Key`) |
|
||||
| Rate limit | per-connection 100msg/10s |
|
||||
| 메시지 dedup | msg_id 기반 60s TTL |
|
||||
| Discord | Bot 토큰 + Guild ID 제한 |
|
||||
|
||||
---
|
||||
|
||||
## 7. Extension 설정 (VS Code)
|
||||
|
||||
| 설정 키 | 설명 | 기본값 |
|
||||
|---------|------|--------|
|
||||
| `gravityBridge.bridgePath` | Bridge 디렉토리 경로 | `~/.gemini/antigravity/bridge` |
|
||||
| `gravityBridge.projectName` | 프로젝트 이름 | git remote 자동 감지 |
|
||||
| `gravityBridge.hubUrl` | WebSocket Hub URL | (비어있으면 WS 비활성) |
|
||||
| `gravityBridge.registrationCode` | Hub 등록 코드 | (서버에서 발급) |
|
||||
@@ -1,41 +0,0 @@
|
||||
# Bug Report Protocol
|
||||
|
||||
> 이 프로토콜은 코드 감사 또는 디버깅 시 false positive를 방지하기 위한 6단계 검증 절차입니다.
|
||||
> AGENT.md 규칙 #10의 세부 구현입니다.
|
||||
|
||||
## 절차
|
||||
|
||||
```
|
||||
1. Identify: 로컬 코드에서 잠재적 이슈 식별
|
||||
2. Trace: 해당 데이터의 전체 생명주기 추적
|
||||
- Producer (생성자) 파일을 열어 확인
|
||||
- Transport (전달 경로: file, HTTP, RPC) 확인
|
||||
- Consumer (소비자) 파일을 열어 확인
|
||||
3. Defend: 기존 방어 메커니즘 검색
|
||||
- try-catch, idempotency guard, dedup logic
|
||||
- upstream validation, downstream tolerance
|
||||
4. Disprove: 버그가 아닌 이유를 적극적으로 찾기
|
||||
- "이 코드가 안전한 이유는?"
|
||||
- 방어 메커니즘이 존재하면 → false positive 가능성 높음
|
||||
5. Prove: 여전히 버그라면 트리거 경로를 증명
|
||||
- 구체적 입력 → 구체적 경로 → 구체적 실패
|
||||
- "A가 B를 호출하면 C에서 D가 발생" 형태
|
||||
6. Report: 증명된 버그만 보고
|
||||
- 트리거 경로 + 심각도 + 영향 범위 포함
|
||||
- 증명 못 한 것은 보고하지 않음
|
||||
```
|
||||
|
||||
## 결정 기준
|
||||
|
||||
| 상황 | 판정 |
|
||||
|------|------|
|
||||
| 방어 메커니즘 존재 + 트리거 경로 없음 | ❌ 보고 안 함 |
|
||||
| 방어 메커니즘 없음 + 트리거 경로 증명됨 | ✅ 보고 |
|
||||
| 방어 메커니즘 존재 + 우회 경로 증명됨 | ✅ 보고 (우회 경로 명시) |
|
||||
| 잘 모르겠음 | 🔍 추가 조사 후 판단 (추측으로 보고 금지) |
|
||||
|
||||
## 근거
|
||||
|
||||
- Anthropic Code Review: "verification step attempts to disprove each finding"
|
||||
- LLM Self-Verification: 자기 결과를 검증하지 않으면 noise와 과신 리포트 양산
|
||||
- Systems Thinking: 개별 컴포넌트가 아닌 관계와 상호의존성에 집중
|
||||
@@ -1,125 +0,0 @@
|
||||
# Coding Conventions
|
||||
|
||||
> AI 에이전트는 코드를 작성하기 전 이 컨벤션을 확인합니다.
|
||||
|
||||
## 네이밍
|
||||
|
||||
### Python (서버)
|
||||
|
||||
| 대상 | 규칙 | 예시 |
|
||||
|------|------|------|
|
||||
| 변수/함수 | snake_case | `write_pending_approval()` |
|
||||
| 클래스 | PascalCase | `GravityBot`, `WSHub`, `TokenManager` |
|
||||
| 상수 | UPPER_SNAKE_CASE | `MAX_MSG_SIZE`, `HEARTBEAT_INTERVAL` |
|
||||
| 파일명 | snake_case | `hub.py`, `ws_client.py` |
|
||||
| 로거명 | 모듈명 | `logging.getLogger("hub")` |
|
||||
|
||||
### TypeScript (Extension)
|
||||
|
||||
| 대상 | 규칙 | 예시 |
|
||||
|------|------|------|
|
||||
| 변수/함수 | camelCase | `writePendingApproval()`, `setupMonitor()` |
|
||||
| 클래스 | PascalCase | `WSBridgeClient` |
|
||||
| 인터페이스 | PascalCase | `BridgeContext`, `WSPendingData` |
|
||||
| 상수 | UPPER_SNAKE_CASE | `MAX_QUEUE_SIZE`, `AUTH_TIMEOUT` |
|
||||
| 파일명 | kebab-case | `ws-client.ts`, `step-probe.ts` |
|
||||
| export 함수 | camelCase | `initStepProbe()`, `generateApprovalObserverScript()` |
|
||||
|
||||
## 코드 스타일
|
||||
|
||||
| 항목 | Python | TypeScript |
|
||||
|------|--------|-----------|
|
||||
| 들여쓰기 | 4 spaces | 4 spaces |
|
||||
| 따옴표 | 쌍따옴표 `"` (f-string 포함) | 작은따옴표 `'` |
|
||||
| 세미콜론 | N/A | 사용 |
|
||||
| 줄바꿈 | LF (Unix) | CRLF (Windows, git 자동 변환) |
|
||||
| 최대 줄 길이 | 120자 권장 | 120자 권장 |
|
||||
| 타입 힌트 | 적극 사용 (`-> list[str]`) | strict (`BridgeContext` 인터페이스) |
|
||||
|
||||
## 커밋 메시지
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
type: feat|fix|refactor|test|docs|chore|ci|infra
|
||||
scope: server|extension|hub|bot|gateway|bridge (선택)
|
||||
```
|
||||
|
||||
**예시:**
|
||||
- `feat(hub): WebSocket Hub 구현 + JWT 인증`
|
||||
- `refactor(extension): 모듈 분리 (step-probe, observer-script)`
|
||||
- `fix(bot): auto-approve 세션 간 초기화`
|
||||
- `docs: architecture.md 전면 재작성`
|
||||
|
||||
관련 Vikunja 태스크가 있으면: `feat(hub): WS Hub 구현 #task-395`
|
||||
|
||||
## 주석
|
||||
|
||||
- 한국어/영어 혼용 가능
|
||||
- TODO 주석: `// TODO: 설명` 형식
|
||||
- 섹션 구분: `// ─── Section Name ───` (TypeScript), `# ─── Section ───` (Python)
|
||||
- 복잡한 로직에는 반드시 WHY(왜) 주석 추가
|
||||
- 함수 docstring: Python은 `"""..."""`, TypeScript는 `/** ... */`
|
||||
|
||||
## 모듈 분리 패턴
|
||||
|
||||
Extension 모듈 분리 시 사용하는 패턴:
|
||||
|
||||
| 패턴 | 용도 | 예시 |
|
||||
|------|------|------|
|
||||
| **순수 함수 추출** | 외부 상태 참조 없는 함수 | `step-utils.ts` |
|
||||
| **독립 스크립트** | 문자열 반환 함수 | `observer-script.ts` |
|
||||
| **Context 패턴** | 공유 상태가 많은 함수 그룹 | `step-probe.ts` (BridgeContext) |
|
||||
| **클래스 추출** | 자체 상태 + 메서드 | `ws-client.ts` (WSBridgeClient) |
|
||||
|
||||
## 테스트
|
||||
|
||||
| 항목 | 위치 | 도구 |
|
||||
|------|------|------|
|
||||
| Python 구문 검사 | `tests/test_syntax.py` | `ast.parse` |
|
||||
| WS Hub 연결 | `tests/test_ws_hub.py` | `websockets` |
|
||||
| TypeScript 컴파일 | `npx tsc --noEmit` | TypeScript compiler |
|
||||
| E2E | 수동 (Discord 버튼 클릭) | — |
|
||||
|
||||
## 로깅
|
||||
|
||||
| 측 | 방식 | 포맷 |
|
||||
|----|------|------|
|
||||
| Python | `logging.getLogger(name)` | `YYYY-MM-DD HH:MM:SS [name] LEVEL: message` |
|
||||
| Extension | `logToFile(msg)` → bridge/log/ | `[HH:MM:SS] message` + `[WS]` prefix |
|
||||
| Hub | `[HUB]` prefix | `[HUB] Auth OK: {conn_id} project={project}` |
|
||||
| Gateway | `[GATEWAY]` prefix | `[GATEWAY] HTTP API started on {host}:{port}` |
|
||||
|
||||
## WS / File Bridge 상호 배타 패턴
|
||||
|
||||
> [!IMPORTANT]
|
||||
> WS Hub과 파일 bridge는 **항상 상호 배타적**이어야 합니다.
|
||||
> 양쪽에 동시에 쓰면 이중 전달 버그가 발생합니다. (known-issues-archive 참조)
|
||||
|
||||
**Extension (TypeScript):**
|
||||
```typescript
|
||||
// ✅ 올바른 패턴
|
||||
if (ctx.wsBridge?.isConnected()) {
|
||||
ctx.wsBridge.sendPending(data);
|
||||
return; // ← 반드시 return으로 파일 쓰기 건너뛰기
|
||||
}
|
||||
// File fallback
|
||||
fs.writeFileSync(pendingPath, JSON.stringify(data));
|
||||
```
|
||||
|
||||
**Bot (Python):**
|
||||
```python
|
||||
# ✅ 올바른 패턴
|
||||
if self.hub:
|
||||
await self.hub.send_response(...)
|
||||
else:
|
||||
bridge.write_response(...)
|
||||
```
|
||||
|
||||
**금지 패턴:**
|
||||
```python
|
||||
# ❌ 이중 쓰기 — 절대 금지
|
||||
if self.hub:
|
||||
await self.hub.send_response(...)
|
||||
bridge.write_response(...) # ← Hub 성공해도 파일에도 씀 → 이중 처리
|
||||
```
|
||||
@@ -1,581 +0,0 @@
|
||||
# Known Issues — Archive
|
||||
|
||||
> **해결 완료된 이슈 아카이브입니다.**
|
||||
> 현재 활성 이슈는 [`known-issues.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues.md)를 참조하세요.
|
||||
> 비슷한 문제가 재발할 때 이 문서에서 과거 해결 방법을 검색하세요.
|
||||
|
||||
---
|
||||
|
||||
## 포맷
|
||||
|
||||
```markdown
|
||||
### [날짜] [키워드] — 한줄 요약
|
||||
- **증상**: 무엇이 잘못되었는가
|
||||
- **원인**: 근본 원인
|
||||
- **해결**: 올바른 해결 방법
|
||||
- **주의**: 재발 방지를 위한 교훈
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 승인 전략 결정 체인 (2026-03-08~09 전체 요약)
|
||||
|
||||
> **이 섹션은 2026-03-08~09 전체 세션의 시행착오를 요약합니다.**
|
||||
> **이미 거부된 접근을 다시 시도하지 마세요.**
|
||||
|
||||
### ❌ 시도 후 거부된 접근 (재시도 금지)
|
||||
|
||||
| # | 접근 | 결과 | 거부 사유 |
|
||||
|---|------|------|-----------|
|
||||
| 1 | LS RPC `HandleCascadeUserInteraction` | `socket hang up` | AG 빌드에서 핸들러 미구현 |
|
||||
| 2 | LS RPC `ResolveOutstandingSteps` | CANCEL 동작 (승인이 아님) | 명칭과 달리 step을 취소함 |
|
||||
| 3 | VS Code 7개 승인 명령 (`terminalCommand.run` 등) | `command not found` | AG 런타임에 미등록 |
|
||||
| 4 | 키보드 시뮬레이션 (`type {Enter}`) | 빈 메시지 전송 | Chat input에 캡처됨 |
|
||||
| 5 | `sendChatActionMessage` / `executeCascadeAction` | 미등록 | 119개 명령에 없음 |
|
||||
| 6 | pywinauto (OS 레벨) | 사용자 결정 폐기 | 크로스플랫폼 불가, 창 겹침 문제 |
|
||||
| 7 | CDP (Chrome DevTools Protocol) | **사용자 명시적 거부** | 비표준, `--remote-debugging-port` 필요 |
|
||||
| 8 | Renderer v1 DOM Click (flat scan) | Run 버튼 미발견 | webview iframe 격리 |
|
||||
|
||||
### 🔑 핵심 전제
|
||||
- Run/Accept 버튼은 **`vscode-webview://` origin의 격리된 iframe** 안에 있음
|
||||
- 외부 workbench DOM에서 `querySelector`로 접근 **불가** (cross-origin)
|
||||
- `<webview>.executeJavaScript()`가 **유일한 표준 관통 경로** (Electron API)
|
||||
- AG 패치 후 **반드시 풀 프로세스 재시작** 필요 (Reload Window 불충분)
|
||||
- **양쪽 HTML (workbench.html + workbench-jetski-agent.html) 모두 inline 패치** 필수
|
||||
- HTML 패치 변경 시 **`%APPDATA%\Antigravity\CachedData` 삭제 필수** (V8 바이트코드 캐시 무효화)
|
||||
- **CSP `script-src`에 `'unsafe-inline'` 패치 필수** (없으면 인라인 스크립트 무조건 차단)
|
||||
|
||||
---
|
||||
|
||||
## Renderer / HTML 패치 관련 (2026-03-08~09)
|
||||
|
||||
### [2026-03-08] Antigravity Renderer Injection — Electron 캐시 차단
|
||||
- **증상**: workbench.html, workbench-jetski-agent.html에 `<script>` 태그 추가 후 리로드해도 실행되지 않음
|
||||
- **원인**: Electron의 V8 코드 캐시가 수정된 HTML을 무시하고 캐시된 버전을 서빙
|
||||
- **해결**: 렌더러 인젝션 방식 **포기**. Extension Host에서 RPC 폴링 방식으로 전환
|
||||
- **주의**: Antigravity는 `workbench-jetski-agent.html`을 사용 (Jetski = 내부 코드네임)
|
||||
|
||||
### [2026-03-08] Antigravity 승인 대기 = RUNNING (NOT IDLE)
|
||||
- **증상**: IDLE 기반 승인 감지가 실제 승인 대기를 놓침
|
||||
- **원인**: 승인 대기 시 세션 상태가 `CASCADE_RUN_STATUS_RUNNING` (IDLE 아님), `IDLE`은 대화 대기(notify_user 후)
|
||||
- **해결**: `RUNNING + delta=0` (stall) 기반 감지로 전환. 6 polls (30초) 이상 FROZEN 시 pending 생성
|
||||
- **주의**: Thinking/생성 중에도 `RUNNING + delta=0`이 발생 → `lastModifiedTime`으로 구분 시도했으나 불완전
|
||||
|
||||
### [2026-03-08] ResolveOutstandingSteps RPC — 승인이 아닌 취소!
|
||||
- **증상**: Discord 승인 → `ResolveOutstandingSteps` 호출 → step이 취소됨
|
||||
- **원인**: `ResolveOutstandingSteps`는 blocking steps를 "resolve" = **REJECT/CANCEL**, approve가 아님
|
||||
- **해결**: `ResolveOutstandingSteps` 제거. `HandleCascadeUserInteraction`은 `socket hang up`
|
||||
- **주의**: KI에 "more reliable"로 기록되어 있으나 실제 동작은 cancel임. KI 업데이트 필요
|
||||
|
||||
### [2026-03-08] VS Code Accept Commands — Silent Success 문제
|
||||
- **증상**: 4개 accept command 모두 OK(undefined) 반환하나 실제 승인 안 됨
|
||||
- **원인**: webview에 활성 포커스가 필요. `panel.focus()`로는 충분하지 않음
|
||||
- **해결**: **미해결**. Windows UI Automation 등 OS 레벨 접근 필요
|
||||
- **주의**: reject commands는 동작함. accept만 focus 의존성 있음
|
||||
|
||||
### [2026-03-08] Multi-Window 세션 등록 경쟁 조건
|
||||
- **증상**: 이 창(gravity_control)의 대화가 `#ag-variet_agent` 채널로 메시지 전달
|
||||
- **원인**: `writeRegistration()`이 폴링 루프에서 호출 → 먼저 폴링한 확장이 세션을 자기 프로젝트로 등록
|
||||
- **해결**: `writeRegistration`을 폴링에서 제거, `writeChatSnapshot`/`writePendingApproval`에서만 지연 호출
|
||||
- **주의**: `GetAllCascadeTrajectories`는 모든 창의 세션을 반환하므로 세션→창 매핑은 불가능. 활동 기반 등록만 신뢰 가능
|
||||
|
||||
### [2026-03-08] 공유 렌더러 스크립트 파일 덮어쓰기 문제
|
||||
- **증상**: DOM Observer 렌더러 스크립트가 잘못된 HTTP bridge 포트에 연결
|
||||
- **원인**: 두 확장이 동일한 `ag-sdk-variet-gravity-bridge.js` 파일에 각자 포트를 씀 → 마지막 확장 것만 남음
|
||||
- **해결**: `ag-bridge-ports.json`에 모든 확장의 port를 JSON으로 기록, 렌더러가 all ports를 순회하며 ping
|
||||
- **주의**: 렌더러 스크립트 파일 경로는 SDK patcher namespace에 의해 고정 — 변경 불가
|
||||
|
||||
### [2026-03-08] workbench.html vs workbench-jetski-agent.html
|
||||
- **증상**: 렌더러에서 `[GB Observer]` 로그가 전혀 안 나옴
|
||||
- **원인**: DevTools가 `workbench.html`을 로드 — 스크립트 태그는 `workbench-jetski-agent.html`에만 패치됨
|
||||
- **해결**: `workbench.html`에도 스크립트 태그 필요. Antigravity 재설치 후 SDK patcher가 올바르게 패치하도록 함
|
||||
- **주의**: SDK patcher는 `both` HTML 파일을 패치하지만, 수동 수정은 Antigravity integrity check에 의해 되돌려질 수 있음
|
||||
|
||||
### [2026-03-08] product.json 체크섬 불일치 → 렌더러 스크립트 미로딩
|
||||
- **증상**: `<script>` 태그가 HTML에 존재하고 .js 파일도 디스크에 있으나, 렌더러 콘솔에 스크립트 로그가 전혀 없음
|
||||
- **원인**: Antigravity 재설치 시 `product.json`의 SHA256 체크섬이 원본으로 리셋됨. Extension이 HTML을 패치하지만 `IntegrityManager.suppressCheck()`를 호출하지 않아 체크섬 불일치. `vscode-file://` 프로토콜이 체크섬 불일치 파일을 무시하고 **원본 캐시 HTML**을 서빙
|
||||
- **해결**: `product.json`의 `checksums` 항목에서 수정된 파일(workbench.html, workbench-jetski-agent.html)의 SHA256 해시를 실제 파일 기준으로 업데이트. SDK `IntegrityManager.suppressCheck()` 호출 또는 수동 스크립트로 해결
|
||||
- **주의**: Extension `setupApprovalObserver()`에 `suppressCheck()` 호출을 영구 추가해야 재설치마다 반복 안 됨. 해시 = `base64(sha256(file)).replace(/=+$/, '')`
|
||||
|
||||
### [2026-03-08] vscode-file:// 프로토콜 — 커스텀 .js 파일 서빙 불가
|
||||
- **증상**: `<script src="./ag-sdk-variet-gravity-bridge.js">` 태그가 HTML에 있으나 `net::ERR_FILE_NOT_FOUND` 발생, GB Observer 로그 전혀 없음
|
||||
- **원인**: `vscode-file://` 프로토콜은 원본 배포에 포함된 파일만 서빙. Extension이 디스크에 쓴 커스텀 `.js` 파일은 프로토콜 레벨에서 차단됨
|
||||
- **해결**: 외부 `<script src>` 참조 대신 **인라인 `<script>...코드...</script>`** 방식으로 HTML에 직접 삽입
|
||||
- **주의**: `ag-bridge-ports.json`도 같은 이유로 XHR 로딩 불가. 모든 렌더러 스크립트/데이터는 HTML 인라인으로 전달해야 함
|
||||
|
||||
### [2026-03-08] Renderer 포트 디스커버리 — ag-bridge-ports.json XHR 실패
|
||||
- **증상**: `[GB Observer] Port discovery timeout after 2min` — 렌더러가 bridge 포트를 찾지 못함
|
||||
- **원인**: 렌더러 스크립트가 `./ag-bridge-ports.json`을 동기 XHR로 읽으려 하나, `vscode-file://` 프로토콜이 `.json` 파일 서빙 거부
|
||||
- **해결**: (1) 프로젝트명 해시 기반 **결정론적 포트** 사용 (`gravity_control→34332`), (2) 스크립트 생성 시 포트를 `HARDCODED_PORT=${port}`로 직접 삽입
|
||||
- **주의**: `server.listen(0)` 랜덤 포트 → 매 재시작마다 변경되어 렌더러와 불일치. 결정론적 포트는 `EADDRINUSE` 시 랜덤 폴백 필요
|
||||
|
||||
### [2026-03-08] GetCascadeTrajectorySteps — cascadeId 파라미터 발견
|
||||
- **증상**: `GetCascadeTrajectorySteps`에 `trajectoryId` 파라미터 → 500 "trajectory not found"
|
||||
- **원인**: 파라미터명이 `trajectoryId`가 아니라 **`cascadeId`**. 값은 `GetAllCascadeTrajectories.trajectorySummaries`의 맵 키(세션 ID)
|
||||
- **해결**: `{ cascadeId: sessionId }`로 호출 → 전체 step 배열 반환 성공
|
||||
- **주의**: `latestToolCallStep` 필드는 `GetAllCascadeTrajectories` 응답에 **존재하지 않음** (KI 오류)
|
||||
|
||||
### [2026-03-08] Step 구조 — CORTEX_STEP_STATUS_WAITING 즉시 감지
|
||||
- **증상**: stall-based 감지(100초)가 너무 느림
|
||||
- **원인**: 이제 `GetCascadeTrajectorySteps`로 최신 step의 status를 직접 확인 가능
|
||||
- **해결**: stall 5초 후 step probe → `CORTEX_STEP_STATUS_WAITING` 확인 → 즉시 pending 생성
|
||||
- **Step 구조**: `{type: "CORTEX_STEP_TYPE_RUN_COMMAND", status: "CORTEX_STEP_STATUS_WAITING", metadata: {toolCall: {name, argumentsJson}}, runCommand, requestedInteraction}`
|
||||
- **주의**: 775-step 하드 리밋은 여전히 존재. 긴 세션에서는 fallback(40초) 사용
|
||||
|
||||
### [2026-03-08] Extension 재설치 안전성 — 자동 패치 메커니즘
|
||||
- **증상**: Antigravity 삭제 후 재설치 시 렌더러 스크립트가 동작 안 함
|
||||
- **원인**: 재설치 시 HTML/product.json이 원본으로 리셋됨
|
||||
- **해결**: Extension의 `setupApprovalObserver()`가 **자동으로** 모든 패치를 수행
|
||||
- **주의**: 패치 후 반드시 **Antigravity 풀 재시작** 필요 (Reload Window 불가)
|
||||
|
||||
### [2026-03-08] Response 파일 Race Condition — DOM Observer 승인 실패
|
||||
- **증상**: Discord에서 승인 → `[RESPONSE] renderer-handled approval` 로그 출력 → 실제 버튼 클릭 안 됨
|
||||
- **원인**: `processResponseFile` (파일 감시자)이 response 파일을 즉시 삭제 → renderer의 `pollResponse`가 HTTP `GET /response/:rid`로 조회 시 파일 이미 없음
|
||||
- **해결**: DOM observer 소스일 때는 response 파일을 삭제하지 않도록 수정. HTTP endpoint가 renderer에게 서빙한 후 삭제
|
||||
- **주의**: non-DOM (stall/step_probe relay)는 watcher에서 삭제해도 됨
|
||||
|
||||
### [2026-03-08] Renderer 스크립트 소스 혼동 — 3곳의 코드
|
||||
- **증상**: `extension.ts`에 BTN-DUMP 추가 → Reload 2번 → 콘솔에 안 나옴
|
||||
- **원인**: renderer 코드가 **3곳**에 존재: (1) `extension.ts`의 `generateApprovalObserverScript()` (소스), (2) `ag-sdk-variet-gravity-bridge.js` (배포됨, Reload시 소스에서 재생성), (3) `workbench-jetski-agent.html` inline (HTML, JS파일과 중복로드 방지됨)
|
||||
- **해결**: 항상 `extension.ts`의 `generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
|
||||
- **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트
|
||||
|
||||
---
|
||||
|
||||
## 승인 / RPC 관련 (2026-03-09)
|
||||
|
||||
### [2026-03-09] VS Code Accept — SDK 승인 명령이 AG에 미등록
|
||||
- **증상**: Discord 승인 → `antigravity.terminalCommand.run` 등 7개 명령 → 모두 `command not found`
|
||||
- **원인**: SDK(command-bridge.ts)에 정의된 7개 승인 명령이 현재 AG 빌드에 **등록되어 있지 않음**
|
||||
- **해결**: Renderer DOM Click 구현 → v3 `deepFindButtons()` 업그레이드
|
||||
- **주의**: `agentPanel.focus`도 미등록, `agentSidePanel.focus`만 존재
|
||||
|
||||
### [2026-03-09] Renderer DOM — webview iframe 격리 확인 + v3 deep traversal
|
||||
- **증상**: Renderer trigger-click이 `document.querySelectorAll('button')`으로 버튼 검색 → Run 버튼 미발견
|
||||
- **원인**: Run/Accept 버튼은 AG 채팅 webview iframe (`vscode-webview://` origin) 안에 렌더링
|
||||
- **해결**: Renderer v3 `deepFindButtons()` 구현 (iframe contentDocument + webview.executeJavaScript + shadow DOM)
|
||||
- **주의**: CDP(Chrome DevTools Protocol)는 **사용자 결정에 의해 명시적으로 거부됨**
|
||||
|
||||
### [2026-03-09] Deep Inspect HTTP Endpoint — curl로 DOM 분석 트리거
|
||||
- **증상**: AG DevTools 콘솔에 붙여넣기 불가 (Chromium 보안 정책)
|
||||
- **원인**: Electron 렌더러 DevTools에서 `allow pasting`이 SyntaxError 발생
|
||||
- **해결**: Extension HTTP bridge에 `/deep-inspect` 엔드포인트 추가
|
||||
- **주의**: 시작 3초 후 자동 실행 + curl로 재트리거 가능
|
||||
|
||||
### [2026-03-09] workbench.html inline 스크립트 미삽입 — jetski만 패치한 버그
|
||||
- **증상**: AG 재시작 후 `/deep-inspect` timeout — renderer v3 스크립트 미로딩
|
||||
- **원인**: `setupApprovalObserver()`가 `workbench-jetski-agent.html`에만 inline 삽입
|
||||
- **해결**: HTML 패치 로직을 **양쪽 모두 inline** 삽입으로 변경
|
||||
- **주의**: **항상 양쪽 HTML을 동일하게 패치**
|
||||
|
||||
### [2026-03-09] V8 CachedData — 체크섬 정상이어도 스크립트 미실행
|
||||
- **증상**: HTML 패치 + product.json 체크섬 일치 → 그런데도 renderer 스크립트 미실행
|
||||
- **원인**: V8 바이트코드 캐시가 `vscode-file://` 프로토콜에도 적용
|
||||
- **해결**: `%APPDATA%\Antigravity\CachedData\*` 전체 삭제 후 AG 풀 재시작
|
||||
- **주의**: **HTML 패치 변경 시마다 CachedData 삭제 필수**
|
||||
|
||||
### [2026-03-09] CSP script-src — 인라인 스크립트 무조건 차단
|
||||
- **증상**: HTML 패치 ✅, 체크섬 ✅, CachedData 삭제 ✅ — 그런데도 renderer 미실행
|
||||
- **원인**: CSP `script-src`에 `'unsafe-inline'` 없음
|
||||
- **해결**: CSP `script-src`에 `'unsafe-inline'` 추가
|
||||
- **주의**: `style-src`에는 `'unsafe-inline'`이 있어 스타일은 동작 → 스크립트만 차단되는 것이 함정
|
||||
|
||||
### [2026-03-09] 중복 승인 요청 — DOM scan + Step probe 동시 발동
|
||||
- **증상**: Discord에 같은 명령에 대해 승인 요청이 2개 도착
|
||||
- **원인**: DOM Observer + Step probe가 동일 step에 대해 2개 파일 생성
|
||||
- **해결**: `writePendingApproval()`에 15초 dedup 윈도우 추가
|
||||
- **주의**: DOM scan은 제거 불가 — step probe가 감지 못하는 UI 버튼 존재
|
||||
|
||||
### [2026-03-09] Step probe reject → ResolveOutstandingSteps가 AI 작업 취소
|
||||
- **증상**: Discord에서 거부 클릭 → AI의 현재 step뿐 아니라 진행 중인 작업 전체가 중단
|
||||
- **원인**: step probe 경로의 `tryApprovalStrategies(approved=false)` → `ResolveOutstandingSteps` RPC 호출
|
||||
- **해결**: step probe 경로에서 reject 시 `tryApprovalStrategies` 호출 제거
|
||||
- **주의**: `ResolveOutstandingSteps`는 이름과 달리 "해결"이 아닌 "취소". 승인에 **절대 사용 금지**
|
||||
|
||||
### [2026-03-09] Pending 파일 무한 누적 — write_response 후 미삭제
|
||||
- **증상**: `bridge/pending/` 디렉토리에 79개 이상의 .json 파일 누적
|
||||
- **원인**: `write_response()`가 pending 파일을 삭제하지 않음
|
||||
- **해결**: pending 파일 삭제 + 5분 age filter + 시작 시 cleanup
|
||||
- **주의**: 봇 재시작 시 자동 정리
|
||||
|
||||
### [2026-03-09] Discord 승인 "Run" 표시 — DOM/step_probe 타이밍 불일치
|
||||
- **증상**: Discord에 상세 명령어 대신 "Run"만 표시
|
||||
- **원인**: DOM observer가 "Run" pending 생성 → 봇이 3초 후 전송 → step_probe MERGE가 10초 후 완료
|
||||
- **해결**: step_probe가 기존 DOM pending에 MERGE + 봇에서 짧은 명령어 대기
|
||||
- **주의**: MERGE 타이밍은 최소 10초
|
||||
|
||||
### [2026-03-09] DOM observer false positive — Proceed/Continue/Open 버튼 오감지
|
||||
- **증상**: 작업 전환 시 승인 요청 없는데도 Discord에 승인 요청 도착
|
||||
- **원인**: DOM observer가 AG UI의 PathsToReview 버튼을 승인 버튼으로 오인
|
||||
- **해결**: FALSE_POSITIVE_RE 필터 추가 + sessionStalled 조건
|
||||
- **주의**: 렌더러 인라인 스크립트는 VSIX 빌드 필요
|
||||
|
||||
### [2026-03-09] Discord ApprovalView timeout — 5분 후 버튼 무응답
|
||||
- **증상**: 시간이 지난 후 Discord 승인 버튼 클릭해도 반응 없음
|
||||
- **원인**: Discord.py View의 기본 timeout이 300초
|
||||
- **해결**: timeout을 1800초로 증가
|
||||
- **주의**: Discord View timeout은 서버 재시작 후만 적용
|
||||
|
||||
---
|
||||
|
||||
## 멀티 프로젝트 / Pending 관련 (2026-03-10)
|
||||
|
||||
### [2026-03-10] DOM Observer 승인 ENOENT — response 파일 Race Condition
|
||||
- **증상**: Discord에서 ✅승인 → `ENOENT: response/xxx.json` 에러
|
||||
- **원인**: `processResponseFile()`이 response 파일 즉시 삭제 → renderer 조회 실패
|
||||
- **해결**: DOM observer 경로에서 response 파일을 삭제하지 않고 HTTP handler가 서빙 후 삭제
|
||||
- **주의**: DOM observer와 step_probe 두 경로가 독립적
|
||||
|
||||
### [2026-03-10] Allow Once + Allow This Conversation — 별개 pending으로 분리되는 문제
|
||||
- **증상**: 파일 접근 시 Discord에 2개 별도 메시지로 도착
|
||||
- **원인**: renderer `scan()`이 한 사이클에 한 버튼만 처리
|
||||
- **해결**: `findButtonContainer()` + `collectSiblingButtons()`로 그룹화, `buttons` 배열 전송
|
||||
- **주의**: `buttons` 배열이 없는 legacy pending은 기존 2버튼(✅승인/❌거부)으로 표시
|
||||
|
||||
### [2026-03-10] step_probe verbosity — argumentsJson 미포함
|
||||
- **증상**: Discord 승인 메시지에 파라미터 이름만 표시, 값 없음
|
||||
- **원인**: `GetCascadeTrajectorySteps` 기본 verbosity에서 `argumentsJson` 빈 문자열
|
||||
- **해결**: `verbosity: 1` (DEBUG) 추가
|
||||
- **주의**: verbosity 0=NORMAL (키만), 1=DEBUG (값 포함)
|
||||
|
||||
### [2026-03-10] 파일 권한 응답 — "unexpected user interaction type: not file permission"
|
||||
- **증상**: `.agents` 디렉토리 접근 시 에러
|
||||
- **원인**: 봇이 file_permission pending에 잘못된 interaction type 전송
|
||||
- **해결**: step_type별 올바른 RPC 라우팅
|
||||
- **주의**: file_permission과 run_command가 동시에 대기 시 올바른 RPC 라우팅 필수
|
||||
|
||||
### [2026-03-10] active_project.lock — 멀티 프로젝트 동시 사용 차단
|
||||
- **증상**: 여러 AG 프로젝트 실행 시 첫 번째만 bridge 연결
|
||||
- **원인**: `active_project.lock` 파일이 단일 프로젝트만 허용
|
||||
- **해결**: lock 메커니즘 완전 제거, `project_name` 필터 기반으로 전환
|
||||
- **주의**: bridge 격리는 `project_name` 필드 기반 filtering으로 충분
|
||||
|
||||
### [2026-03-10] step_probe file_permission — 3-button 미주입
|
||||
- **증상**: Discord에 3개 선택지 대신 2개만 표시
|
||||
- **원인**: `writePendingApproval()`이 buttons 배열 미주입
|
||||
- **해결**: `step_type === 'file_permission'`일 때 자동 3-button 배열 주입
|
||||
- **주의**: DOM observer 경로는 기존 command 텍스트 기반 감지 유지
|
||||
|
||||
### [2026-03-10] GetAllCascadeTrajectories — 크로스 윈도우 세션 가로채기
|
||||
- **증상**: Deriva AG에서 대화 시작 → gravity_control 채널에 Deriva 내용이 릴레이
|
||||
- **원인**: `GetAllCascadeTrajectories`가 모든 인스턴스의 세션 반환
|
||||
- **해결**: `workspaces[0].workspaceFolderAbsoluteUri` 비교하여 자기 workspace 세션만 처리
|
||||
- **주의**: workspace URI normalize 필수 (protocol strip, %3A decode, 슬래시 통일, lowercase)
|
||||
|
||||
### [2026-03-10] 크로스 프로젝트 Response Watcher 우회
|
||||
- **증상**: Deriva 세션 승인 시도가 gravity_control에서 실패
|
||||
- **원인**: pending 파일 삭제 후 response watcher가 project_name 체크 건너뜀
|
||||
- **해결**: response JSON의 project_name으로 fallback 필터
|
||||
- **주의**: response 데이터 자체에 project_name 필수
|
||||
|
||||
### [2026-03-10] file_permission — write 도구 3-button 미주입
|
||||
- **증상**: `replace_file_content` 등 파일 수정 시 Discord에 2개만 표시
|
||||
- **원인**: step_probe의 file_permission 도구 리스트에 write 도구 누락
|
||||
- **해결**: write 도구를 file_permission 리스트에 추가
|
||||
- **주의**: AG가 파일 접근 권한을 요청하는 모든 도구는 이 리스트에 포함필요
|
||||
|
||||
### [2026-03-10] bestSession IDLE 고착 — RUNNING 세션 못 잡는 버그
|
||||
- **증상**: 새 대화 시작 → bridge가 구 IDLE 세션만 추적
|
||||
- **원인**: `bestSession` 선택이 `lastModifiedTime`만 비교
|
||||
- **해결**: RUNNING 세션이 IDLE보다 항상 우선
|
||||
- **주의**: Reload Window로도 해결되지만 근본적으로는 RUNNING 우선 로직 필요
|
||||
|
||||
### [2026-03-10] Bot IDLE 채널 자동 생성 — 불필요한 Discord 채널 증식
|
||||
- **증상**: 봇 시작 시 모든 등록된 프로젝트의 채널을 자동 생성
|
||||
- **원인**: `pending_approval_scanner`가 매 사이클마다 채널 생성
|
||||
- **해결**: 자동 채널 생성 루프 제거, on-demand 생성
|
||||
- **주의**: `_get_channel()`은 이미 on-demand 생성 로직 포함
|
||||
|
||||
### [2026-03-10] Reload Window 후 세션 stale
|
||||
- **증상**: Reload Window 후 세션이 IDLE/구 stepCount로 고정
|
||||
- **원인**: LS 프로세스는 유지되어 trajectory tracker 캐시 미갱신
|
||||
- **해결**: AG 완전 종료 → 재실행 (Full restart)
|
||||
- **주의**: Extension 코드 변경 후 배포 시 Full restart 권장
|
||||
|
||||
### [2026-03-10] start_bot.bat — Windows Store Python 스텁 우선 실행
|
||||
- **증상**: `start_bot.bat` 실행 시 스텁이 먼저 실행
|
||||
- **원인**: `where python`이 Windows Store의 Python 스텁을 먼저 찾음
|
||||
- **해결**: conda 경로를 우선 확인
|
||||
- **주의**: Windows 10/11에서 App Aliases의 python.exe가 PATH에 기본 포함
|
||||
|
||||
### [2026-03-10] VSIX 빌드 — SDK JS 파일 미포함 (require 실패)
|
||||
- **증상**: Extension 활성화 후 `SDK not initialized`
|
||||
- **원인**: TypeScript 컴파일러가 `.js` 파일을 `out/`에 복사하지 않음
|
||||
- **해결**: `compile` 스크립트에 복사 단계 추가 (`src/sdk/` → `out/sdk/`)
|
||||
- **주의**: VSIX 패키징은 `out/sdk/`를 포함함. 문제는 빌드 단계 복사 누락
|
||||
|
||||
### [2026-03-10] SDK _findLSProcess — 대소문자 구분 workspace hint 매칭 실패
|
||||
- **증상**: variet-agent AG에서 Discord에 신호 미도달
|
||||
- **원인**: SDK가 workspace hint를 대소문자 구분으로 비교
|
||||
- **해결**: `fixLSConnection()` 함수로 대소문자 무시 비교 + 재연결
|
||||
- **주의**: 각 AG 창마다 별도 LS 프로세스 존재 (workspace_id로 구분)
|
||||
|
||||
---
|
||||
|
||||
## 환경변수 / 설정 관련 (2026-03-11)
|
||||
|
||||
### [2026-03-11] config.py BRAIN_PATH — `.env` 빈 문자열 → CWD 해석 버그
|
||||
- **증상**: 봇이 Extension의 snapshot/pending을 전혀 읽지 못함
|
||||
- **원인**: `.env`에 `BRAIN_PATH=` (빈 값)이면 빈 문자열 반환
|
||||
- **해결**: `os.getenv("BRAIN_PATH") or default` 패턴
|
||||
- **주의**: `os.getenv(key, default)`는 빈 값이라도 default 미사용
|
||||
|
||||
### [2026-03-11] Extension DEDUP MERGE — 크로스 프로젝트 pending 오염
|
||||
- **증상**: `#ag-lifetimepd` 채널에 variet_agent의 승인 요청 표시
|
||||
- **원인**: DEDUP 로직이 `project_name`을 체크하지 않음
|
||||
- **해결**: 3곳 dedup 조건에 `project_name` 가드 추가
|
||||
- **주의**: 모든 Extension 인스턴스가 동일한 `bridge/pending/` 디렉토리 공유
|
||||
|
||||
### [2026-03-11] Collector 동기 HTTP — aiohttp 전환
|
||||
- **증상**: Collector가 이벤트 루프 전체 블로킹
|
||||
- **원인**: `urllib.request.urlopen()` 사용 (blocking I/O)
|
||||
- **해결**: `aiohttp.ClientSession` 기반 비동기 전환
|
||||
- **주의**: `import aiohttp`는 lazy
|
||||
|
||||
---
|
||||
|
||||
## Rate Limit / 무한 루프 관련 (2026-03-12)
|
||||
|
||||
### [2026-03-12] RemoteTransport 429 무한 루프 — Extension 크래시 + AG 먹통
|
||||
- **증상**: `429 Rate limited` 로그가 초당 수십 건 무한 반복
|
||||
- **원인**: 3가지 복합 (백오프 없음 + 개별 HTTP 요청 + 공격적 rate limit)
|
||||
- **해결**: 지수 백오프 + `Retry-After` 지원 + rate limit 완화
|
||||
- **주의**: AG 먹통은 봇 자체가 유발한 문제
|
||||
|
||||
### [2026-03-12] workbench.html 0-byte 파괴 — AG 새 창 먹통
|
||||
- **증상**: AG 새 창 열면 화면 먹통
|
||||
- **원인**: 3개 Extension 인스턴스가 동시에 workbench.html 읽기/쓰기 → 0 bytes로 덮어쓰기
|
||||
- **해결**: pre-patch backup(.orig) + 구조 검증 + 자동 복원
|
||||
- **주의**: 멀티 윈도우 환경에서 HTML 패치 race condition은 파일 잠금 없이 완전 해결 불가
|
||||
|
||||
### [2026-03-12] workbench.html 크로스 복원 — CSS 미로딩으로 레이아웃 깨짐
|
||||
- **증상**: 아이콘은 보이지만 레이아웃 완전 깨짐
|
||||
- **원인**: workbench.html을 jetski HTML에서 복원할 때 CSS 교체 누락
|
||||
- **해결**: 파일별 `requiredMarker` 검증 + `.orig` 백업 + 자동 복원
|
||||
- **주의**: **workbench.html과 workbench-jetski-agent.html은 교환 불가능**
|
||||
|
||||
### [2026-03-12] Collector 단일 프로젝트 폴링 — 멀티 프로젝트 command 전달 불가
|
||||
- **증상**: Deriva AG IDE에 명령 전달되지 않음
|
||||
- **원인**: Collector가 단일 프로젝트만 폴링
|
||||
- **해결**: `_discover_local_projects()`로 모든 프로젝트 폴링
|
||||
- **주의**: `/api/commands/all` 엔드포인트는 크로스 PC 명령 오염을 유발
|
||||
|
||||
### [2026-03-12] RemoteTransport backing off 무한 반복
|
||||
- **증상**: IDLE 시 `backing off 1s` 경고가 영구 반복
|
||||
- **원인**: 3가지 구조적 결함 (즉시 리셋 + 불필요 요청 + asyncio burst)
|
||||
- **해결**: 연속 5회 성공 후 절반 감소 + adaptive 간격 + 루프 stagger
|
||||
- **주의**: `_reset_backoff()` 즉시 리셋 패턴은 다중 소비자 환경에서 **절대 사용 금지**
|
||||
|
||||
---
|
||||
|
||||
## DEDUP / 크로스 세션 관련 (2026-03-15)
|
||||
|
||||
### [2026-03-15] DEDUP step_index 크로스 세션 충돌 — 승인 신호 누락
|
||||
- **증상**: WAITING step 감지 → pending 미생성 → 10분+ 대기
|
||||
- **원인**: DEDUP 로직이 `conversation_id`를 비교하지 않음
|
||||
- **해결**: DEDUP 조건에 `conversation_id` 가드 추가
|
||||
- **주의**: `project_name` 가드만으로는 불충분 — 같은 Extension이 여러 세션을 볼 수 있음
|
||||
|
||||
### [2026-03-15] Discord Gateway MESSAGE_CREATE 중복 — embed 이중 전송
|
||||
- **증상**: Discord 명령 시 동일 embed가 2개 전송
|
||||
- **원인**: Discord Gateway가 WebSocket 불안정 시 이벤트 중복 전달
|
||||
- **해결**: `on_message`에 `_processed_message_ids` dedup 추가
|
||||
- **주의**: Gateway reconnection, RESUME 실패 시 발생 빈도 증가
|
||||
|
||||
### [2026-03-15] HTML 패치 멀티 인스턴스 race condition — 화면 파괴
|
||||
- **증상**: Extension 패치 후 AG 재시작 시 전체 화면 날아감
|
||||
- **원인**: 2+ Extension 인스턴스가 동시에 같은 HTML에 readFileSync/writeFileSync
|
||||
- **해결**: `.patch-lock` 파일 기반 cross-instance lock 추가
|
||||
- **주의**: Lock은 "방지", .orig 백업은 "복구". 둘 다 유지
|
||||
|
||||
### [2026-03-15] 로컬 승인 ↔ Discord 승인 교차 race condition
|
||||
- **증상**: AG에서 직접 Run 클릭 후 Discord 승인 요청이 "완료됨" 표시 안 됨
|
||||
- **원인**: auto_resolve가 Discord에 알림 없음 + processResponseFile 상태 미체크
|
||||
- **해결**: writeChatSnapshot 추가 + 상태 확인 후 skip + _approval_messages dict
|
||||
- **주의**: processResponseFile L2534의 리셋이 핵심 gate
|
||||
|
||||
### [2026-03-15] 크로스 프로젝트 DEDUP MERGE — Deriva→gravity_control 오염
|
||||
- **증상**: Deriva의 데이터가 gravity_control pending에 MERGE됨
|
||||
- **원인**: MERGE 조건에 `project_name` 가드 없음
|
||||
- **해결**: MERGE 조건에 `project_name` 추가
|
||||
- **주의**: `bridge/pending/` 디렉토리는 모든 Extension 인스턴스가 공유
|
||||
|
||||
### [2026-03-15] Double-Fire Auto-Approve — AI 세션 중단
|
||||
- **증상**: auto-approve ON 시 AI 세션이 간헐적으로 중단
|
||||
- **원인**: Extension auto-approve 경로 + Bot auto-approve 경로 동시 실행 → 2번 RPC
|
||||
- **해결**: Extension auto-approve 경로 제거. Bot만 담당
|
||||
- **주의**: 단일 경로 원칙 유지
|
||||
|
||||
### [2026-03-15] DOM Observer "Deny" False Positive — Auto-approve 세션 크래시
|
||||
- **증상**: auto-approve ON 시 "Deny" command가 자동 승인됨 → 세션 크래시
|
||||
- **원인**: DOM observer가 Deny를 독립 pending으로 생성. default 분기로 잘못된 RPC 전송
|
||||
- **해결**: FALSE_POSITIVE_RE에 Deny/Allow Once 등 추가 + reject-word 차단 가드
|
||||
- **주의**: VSIX 빌드 → AG 풀 재시작 필요
|
||||
|
||||
### [2026-03-15] PATS 배열 Deny 트리거 — 근본 수정
|
||||
- **증상**: Deny가 주 트리거로 사용됨
|
||||
- **원인**: PATS 배열에 Deny 패턴 포함
|
||||
- **해결**: PATS에서 거절/보조 버튼 제거. 긍정 버튼만 그룹 트리거
|
||||
- **주의**: PATS = "그룹 생성 트리거", ALL_ACTION_RE = "형제 수집 패턴"
|
||||
|
||||
### [2026-03-15] Auto-Resolved 채팅 폭주 — 루프 내 writeChatSnapshot
|
||||
- **증상**: "✅ AG에서 직접 승인됨" 메시지가 반복 전송
|
||||
- **원인**: 루프 내부에서 매 파일마다 writeChatSnapshot 호출
|
||||
- **해결**: 루프 바깥에서 1회 + conversation_id 조건 추가
|
||||
- **주의**: 외부 시스템에 메시지 보낼 때는 반드시 루프 바깥에서 집계 후 1회 발송
|
||||
|
||||
### [2026-03-15] projectName=default 승인 오발
|
||||
- **증상**: workspace 없는 AG 창이 다른 프로젝트의 WAITING을 감지
|
||||
- **원인**: `detectProjectName()`이 workspace 없으면 "default" 반환
|
||||
- **해결**: `projectName === 'default'`이면 pending 생성/auto-approve 억제
|
||||
- **주의**: Empty Window에서는 bridge 기능을 최소화
|
||||
|
||||
### [2026-03-15] 이전 분석 오판(False Positive) — 교훈
|
||||
- **증상**: P0/P1으로 보고한 문제들이 이미 방어되고 있었음
|
||||
- **원인**: 로컬 코드 스니펫만 보고 판단
|
||||
- **해결**: 전체 Flow 추적으로 교차 검증
|
||||
- **주의**: **코드 감사 시 반드시 producer→transport→consumer→side effects 전체 경로를 추적**
|
||||
|
||||
---
|
||||
|
||||
## processResponseFile 상태 관리 (2026-03-16)
|
||||
|
||||
### [2026-03-16] processResponseFile 상태 리셋 — 무한 루프 vs auto_resolve 회귀
|
||||
- **증상**: Discord 승인 후 같은 step에 대해 pending이 반복 생성 → 무한 auto-approve 루프
|
||||
- **원인**: processResponseFile이 무조건 리셋 → step_probe가 같은 WAITING step을 새 step으로 착각
|
||||
- **해결**: `sawRunningAfterPending = true`만 설정. lastPendingStepIndex와 stallProbed 유지
|
||||
- **주의**: **processResponseFile의 상태 리셋은 sawRunningAfterPending = true만 설정**. `docs/approval-flow.md` 참조
|
||||
|
||||
### [2026-03-16] recentPendingSteps 메모리 dedup
|
||||
- **증상**: pending 파일 삭제 → 같은 step_index로 새 pending 생성
|
||||
- **원인**: writePendingApproval()의 dedup이 파일 존재 여부에만 의존
|
||||
- **해결**: `recentPendingSteps` Map (TTL 60초) 추가
|
||||
- **주의**: DOM observer HTTP 경로는 이 메모리 dedup 미적용
|
||||
|
||||
### [2026-03-16] 멀티 프로젝트 동시 신호 정지 — Scanner O(N) Discord API 병목
|
||||
- **증상**: 여러 프로젝트 동시 pending → 모든 프로젝트 신호 전달 정지
|
||||
- **원인**: scanner가 1 tick에 모든 pending 순차 처리 → Discord 429 rate limit
|
||||
- **해결**: `discord.utils.get(guild.channels)` 캐시 + per-tick cap (5건)
|
||||
- **주의**: `guild.channels`는 discord.py 내부 캐시
|
||||
|
||||
---
|
||||
|
||||
## Diff Review 관련 (2026-03-16)
|
||||
|
||||
### [2026-03-16] step_type 매핑 버그 — write_to_file이 file_permission으로 잘못 매핑
|
||||
- **증상**: 코드 편집 승인 시 잘못된 RPC 전송
|
||||
- **원인**: 쓰기 도구가 읽기 도구와 함께 `file_permission`으로 매핑
|
||||
- **해결**: 읽기/쓰기 도구 분리, 쓰기는 `code_edit` step_type 사용
|
||||
- **주의**: AG는 대부분 파일 쓰기에 WAITING 안 만듦
|
||||
|
||||
### [2026-03-16] diff_review isDirty 실패 — AG diff는 VS Code dirty 아님
|
||||
- **증상**: Accept 클릭 → `isDirty` 문서 0개 → 효과 없음
|
||||
- **원인**: AG stacked code review는 VS Code `isDirty`와 무관
|
||||
- **해결**: `AcknowledgeCascadeCodeEdit` RPC → fallback으로 VS Code 커맨드
|
||||
- **주의**: diff_review pending에 `modified_files`와 `edit_step_indices` 필수
|
||||
|
||||
### [2026-03-16] diff_review pending 순서 — AI 응답보다 먼저 Discord 도착
|
||||
- **증상**: diff_review 버튼이 먼저, AI 응답 텍스트가 나중
|
||||
- **원인**: pending_approval_scanner가 chat_snapshot_scanner보다 먼저 fire
|
||||
- **해결**: diff_review pending 생성을 `setTimeout(8000)`으로 지연
|
||||
- **주의**: 8초는 전체 전파 경로 고려
|
||||
|
||||
### [2026-03-16] diff_review AcknowledgeCascadeCodeEdit steps=[] — Collector pending 삭제 race
|
||||
- **증상**: Accept all 클릭 → RPC SUCCESS → diff review 바 안 사라짐
|
||||
- **원인**: Collector가 pending 파일 즉시 삭제 → Extension이 메타데이터 못 읽음
|
||||
- **해결**: `diffReviewMetadata` 인메모리 Map 추가
|
||||
- **주의**: Extension Reload 시 소실되지만 새 diff_review는 정상 동작
|
||||
|
||||
### [2026-03-16] AcknowledgeCascadeCodeEdit SUCCESS → diff review bar 미해제 — 잘못된 RPC 메서드명
|
||||
- **증상**: RPC SUCCESS 반환 → diff review bar 여전히 표시
|
||||
- **원인**: RPC 메서드명 자체가 틀렸음 (`AcknowledgeCascadeCodeEdit` → 실제는 `acknowledgeCodeActionStep`)
|
||||
- **해결**: `agentAcceptAllInFile` / `agentRejectAllInFile` VS Code 커맨드 사용
|
||||
- **주의**: AG의 RPC는 잘못된 메서드명도 에러 없이 `{}` 반환. **RPC `{}`는 실패로 간주해야 함**
|
||||
|
||||
### [2026-03-16] diff_review RPC 3개 전략 모두 dead-end
|
||||
- **증상**: 3개 RPC 전략 모두 실패
|
||||
- **원인**: (1) submitCodeAcknowledgement 미등록, (2) acknowledgeCodeActionStep 404, (3) AcknowledgeCascadeCodeEdit no-op
|
||||
- **해결**: VS Code 커맨드 기반 (`agentAcceptAllInFile` / `agentRejectAllInFile`)
|
||||
- **주의**: **diff_review를 RPC로 해결하려는 시도 모두 실패 확정. VS Code 커맨드 기반만 유효**
|
||||
|
||||
### [2026-03-16] AG 소스 역분석 — diff review 내부 동작 체인
|
||||
- **증상**: Accept all / Reject all 내부 동작을 재현해야 함
|
||||
- **원인**: AG 공식 API/문서 없음
|
||||
- **해결**: AG 설치 경로의 JS 소스에서 역분석
|
||||
- **주의**: minified JS에서 변수명은 버전마다 변경됨
|
||||
|
||||
### [2026-03-16] diff_review 이중 승인 요청 — DOM observer가 Accept/Reject 버튼 캡처
|
||||
- **증상**: diff_review 외에 별도 "Accept"/"Reject" pending 도착
|
||||
- **원인**: `openReviewChanges` 커맨드가 diff UI 패널 → DOM observer 감지
|
||||
- **해결**: FALSE_POSITIVE_RE에 Accept/Reject all 추가
|
||||
- **주의**: diff review bar 버튼은 전용 시스템에서 처리
|
||||
|
||||
### [2026-03-16] diff_review가 brain/ artifact에도 트리거
|
||||
- **증상**: task.md만 수정해도 "코드 리뷰" pending 생성
|
||||
- **원인**: diff_review 감지 로직이 모든 수정 파일 추적
|
||||
- **해결**: `.gemini/antigravity/brain/` 경로 파일 필터링하여 제외
|
||||
- **주의**: 코드 파일 + brain artifact 혼합 시 코드 파일만 diff_review
|
||||
|
||||
---
|
||||
|
||||
## WS Hub 전환 관련 (2026-03-16~17)
|
||||
|
||||
### [2026-03-16] !auto 이중 메시지 — Extension echo + Bot embed
|
||||
- **증상**: `!auto` 토글 시 메시지 2개 표시
|
||||
- **원인**: Bot embed + Extension writeChatSnapshot echo
|
||||
- **해결**: Extension의 `!auto` handler에서 writeChatSnapshot echo 제거
|
||||
- **주의**: Bot 재시작 시 auto_approve_projects 초기화 → 수동 모드 복귀
|
||||
|
||||
### [2026-03-16] 병렬 WAITING step 누락 — step_probe break문
|
||||
- **증상**: 병렬 tool call 시 1개만 승인 요청 도착
|
||||
- **원인**: step_probe 루프에서 `break`문이 첫 번째 WAITING 후 종료
|
||||
- **해결**: `break` 제거, 모든 WAITING step에 pending 생성
|
||||
- **주의**: 중복 방지는 `recentPendingSteps` Map이 처리
|
||||
|
||||
### [2026-03-16] Bot chat_snapshot 전송 로깅 부재
|
||||
- **증상**: 전송 성공/실패를 Bot 로그에서 확인 불가
|
||||
- **원인**: `channel.send()` 성공 후 INFO 로그 없음
|
||||
- **해결**: 전송 성공/실패 로그 추가
|
||||
- **주의**: `_get_channel()` 실패 시 WARNING은 이전에도 있었음
|
||||
|
||||
### [2026-03-16] 크로스 프로젝트 이벤트 폭주 — Watcher/Collector 무필터
|
||||
- **증상**: /start 시 타 프로젝트 알림 유입
|
||||
- **원인**: watcher.py가 brain/ 전체를 감시
|
||||
- **해결**: `_is_my_session()` 필터 + 이벤트 전달 필터 추가
|
||||
- **주의**: 미등록 세션은 allow-through 방식
|
||||
|
||||
### [2026-03-16] pending 파일 139개 누적 — 정리 로직 부재
|
||||
- **증상**: bridge/pending/에 139개 파일 누적
|
||||
- **원인**: auto_resolved/expired 파일을 아무도 삭제 안 함
|
||||
- **해결**: Collector에서 auto_resolved/expired 전달 후 삭제 + 10분 자동 삭제
|
||||
- **주의**: startup_pending은 정리 대상에서 제외
|
||||
|
||||
### [2026-03-17] NPM WebSocket 프록시 — Upgrade 헤더 미전달
|
||||
- **증상**: `wss://ag.variet.net/ws` 연결 시 HTTP 400
|
||||
- **원인**: Nginx Proxy Manager에 WebSocket Support 미활성화
|
||||
- **해결**: NPM 대시보드에서 Websockets Support 체크
|
||||
- **주의**: 새 프록시 호스트 생성 시 반드시 확인
|
||||
|
||||
### [2026-03-17] WS auth_fail 무한 재연결 — _cleanup() close 이벤트
|
||||
- **증상**: Auth failed 후 60초마다 재연결 반복
|
||||
- **원인**: `_cleanup()`이 ws.close() → close 이벤트 → _scheduleReconnect() 체인
|
||||
- **해결**: `this.shouldReconnect = false`를 `_cleanup()` 이전에 설정
|
||||
- **주의**: `_cleanup()`은 이벤트 핸들러를 트리거하므로 상태 변경은 반드시 호출 전에
|
||||
|
||||
### [2026-03-17] initStepProbe workspaceUri 누락 — 세션 감지 완전 불능
|
||||
- **증상**: POLL alive만 출력, SESSION-FILTER/SNAPSHOT 없음
|
||||
- **원인**: `initStepProbe()` 호출 시 필수 필드 미전달 + `as` 캐스트가 런타임 검증 없음
|
||||
- **해결**: `workspaceUri`, `diffReviewMetadata: new Map()` 추가
|
||||
- **주의**: TypeScript `as` 캐스트는 런타임 검증 없음
|
||||
|
||||
### [2026-03-17] WS 명령어 에코 릴레이 — Discord 메시지 2번 표시
|
||||
- **증상**: Discord 메시지 입력 → 같은 메시지가 다시 표시
|
||||
- **원인**: handleWSCommand에서 `recentDiscordSentTexts`에 마킹 안 함
|
||||
- **해결**: `recentDiscordSentTexts.set()` 추가
|
||||
- **주의**: 파일 기반 경로에는 이미 마킹 있었음
|
||||
|
||||
### [2026-03-17] writeRegistration 이중 쓰기 — WS 전송 후 파일도 작성
|
||||
- **증상**: WS 상태에서도 register/ 파일 생성
|
||||
- **원인**: WS 전송 후 `return` 없이 파일 쓰기 실행
|
||||
- **해결**: WS 전송 후 `return` 추가
|
||||
- **주의**: 새 WS 전송 함수 추가 시 file fallback과 상호 배타적 `return` 확인
|
||||
@@ -1,629 +0,0 @@
|
||||
# Known Issues & Lessons Learned
|
||||
|
||||
|
||||
|
||||
> **<2A>씠 <20>뙆<EFBFBD>씪<EFBFBD><EC94AA><EFBFBD> SSOT(Single Source of Truth)<29>엯<EFBFBD>땲<EFBFBD>떎.**
|
||||
|
||||
> <EFBFBD>뵒踰꾧퉭<EFBFBD>씠<EFBFBD>굹 援ы쁽 <20>쟾<EFBFBD>뿉 **諛섎뱶<EC848E>떆** <20>씠 <20>뙆<EFBFBD>씪<EFBFBD>쓣 <20>솗<EFBFBD>씤<EFBFBD>븯<EFBFBD>꽭<EFBFBD>슂.
|
||||
|
||||
> <EFBFBD>꽭<EFBFBD>뀡 醫낅즺 <20>떆 <20>깉濡<EAB989> 諛쒓껄<EC9293>맂 <20>씠<EFBFBD>뒋瑜<EB928B> <20>씠 <20>뙆<EFBFBD>씪<EFBFBD>뿉 異붽<E795B0><EBB6BD><EFBFBD>빀<EFBFBD>땲<EFBFBD>떎.
|
||||
|
||||
|
||||
|
||||
# Known Issues & Lessons Learned
|
||||
|
||||
> **씠 뙆씪 SSOT(Single Source of Truth)엯땲떎.**
|
||||
> 뵒踰꾧퉭씠굹 援ы쁽 쟾뿉 **諛섎뱶떆** 씠 뙆뙆씪쓣 솗씤븯꽭슂.
|
||||
> 꽭뀡 醫낅즺 떆 깉濡 諛쒓껄맂 씠뒋瑜 씠 뙆뙆씪뿉 異붽빀땲떎.
|
||||
|
||||
> [!TIP]
|
||||
> 빐寃 셿猷뚮맂 怨쇨굅 씠뒋뒗 [`known-issues-archive.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues-archive.md)뿉 蹂닿릺뼱 엳뒿땲떎.
|
||||
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 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)
|
||||
- **증상**: 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 응답 텍스트로 추출됨.
|
||||
- **해결 (v0.5.52)**: `extractCleanStepText()` 최상단에 `clone.querySelectorAll('style, script, noscript, link[rel="stylesheet"]')` 제거 로직 추가. CSS/JS가 텍스트로 포함되는 것을 원천 차단.
|
||||
- **배포**: v0.5.52 VSIX 설치 + v0.5.50/out/ JS 직접 복사 + V8 CachedData 삭제. AG File→Quit 재시작 필요.
|
||||
- **이전 블로커 해결 이력**:
|
||||
- SDK 경로: AG Native는 Cascade trajectory API 미등록 → step-probe RT-CAPTURE 불가 (구조적 한계)
|
||||
- DOM 경로: v15에서 `#conversation` + `.leading-relaxed.select-text` 셀렉터 추가로 해결
|
||||
- BEACON=0: AG 프로세스 완전 재시작으로 해결 (Reload Window로는 렌더러 HTML 캐시 유지)
|
||||
- **주의**: AG Native 마크다운 렌더링은 `<style>` 블록을 응답 DOM 내부에 인라인으로 삽입함. DOM 텍스트 추출 시 반드시 style/script 태그를 먼저 제거해야 함.
|
||||
- **Vikunja**: #632
|
||||
|
||||
### [2026-04-16] [Extension] ★ AG Native Observer innerText로 인한 마크다운 포맷 유실 및 사용자 요청 누락
|
||||
- **증상**: Discord로 릴레이되는 AI 응답에서 리스트 번호(`1. 2.`)나 불릿(`-`) 등 마크다운 포맷이 완전히 유실되고 텍스트만 이어져서 나옴. 그리고 사용자가 입력한 메시지(요청)는 아예 릴레이되지 않음.
|
||||
- **원인 1**: `extractCleanStepText()`에서 `innerText`를 사용하여 텍스트를 추출할 때, 렌더링되지 않은 DOM이나 CSS 카운터로 생성된 list-item 마커가 무시됨.
|
||||
- **원인 2**: `scanChatBodies()` 로직이 `.leading-relaxed.select-text` (AI 응답 블록)만을 타겟팅하여 사용자의 메시지 박스(예: `.text-ide-message-block-user-color`)는 추출 대상에서 제외됨.
|
||||
- **해결 (계획 중)**: `innerText` 대신 HTML DOM 노드를 순회하며 `convertNodeToMarkdown` 변환 함수를 통해 마크다운 문법을 보존하도록 개선. User 블록도 함께 감지하여 브릿지에 `role: 'user'` 플래그를 추가 전송하도록 수정 예정.
|
||||
- **주의**: Webview 내에서 텍스트 노드를 파싱할 때 `innerText`는 브라우저 레이아웃 엔진에 의존하므로 완전한 마크다운 보존을 위해서는 Node Type 순회를 활용한 구조 복원이 보장되어야 함.
|
||||
|
||||
### [2026-04-16] [Extension] 터미널 출력(stdout) 텍스트가 명령어로 Discord에 전송 (v0.5.50)
|
||||
- **증상**: Discord에 `cmd="No extension.log found"`, `cmd="AG CLI not found..."`, `cmd="Log found: C:\..."` 등 터미널 **출력** 텍스트가 명령어로 전송됨
|
||||
- **원인**: Observer가 code 블록 2개를 감지: (1) 프롬프트+명령어 → JUNK_CODE_RE로 스킵, (2) 터미널 출력 → 유효한 code로 판단 → description에 포함. http-bridge enrichment에서 description에 prompt marker(`>`)가 없으면 rawDesc 전체를 enrichedCmd로 채택
|
||||
- **해결 (v0.5.50)**: `promptMatch` 실패 시(description에 `>` 없음) → 터미널 OUTPUT으로 판단하여 `terminal_output` 사유로 즉시 필터. 실제 명령어는 항상 `…\project > command` 프롬프트를 포함
|
||||
- **주의**: enrichment은 반드시 prompt marker가 있는 경우에만 수행. description에 `>` 없으면 code 블록의 출력 텍스트
|
||||
|
||||
### [2026-04-15] [Extension] Observer fallback 컨텍스트가 채팅/UI 텍스트를 명령어로 추출 (v0.5.46)
|
||||
- **증상**: Discord에 `cmd="실 동작검증을 해봐야하는데"`, `cmd="variet.gravity-bridge"` 등 사용자 채팅/AI 응답 텍스트가 명령어로 전송됨
|
||||
- **원인**: v0.5.45에서 `PROMPT_ONLY_RE`가 `code/pre` 요소 스킵 성공했으나, `extractContextFromNearby()`의 fallback(`span/div/p` 수집)이 여전히 작동하여 DOM 트리의 채팅 본문/UI 라벨을 명령어로 추출
|
||||
- **해결 (v0.5.46)**: Observer v13에 `_promptOnlySkipped` 플래그 — code 요소가 모두 prompt-only이면 fallback 비활성화. http-bridge에 generic button + no-context일 때 무조건 필터
|
||||
- **주의**: 프롬프트 스킵과 fallback 비활성화는 항상 연동해야 함. VSIX 설치 누락 방지를 위해 빌드 후 즉시 `code --install-extension` 확인 필수
|
||||
|
||||
### [2026-04-15] [Extension] PROMPT_ONLY_RE regex fixes — prompt-only terminal text leaking as enriched cmd (v0.5.45)
|
||||
- **증상**: Discord에 `cmd="…\gravity_control >"` (실제 명령어 없는 빈 터미널 프롬프트)가 전송됨. 명령어가 포함된 경우는 정상 작동
|
||||
- **원인 1 (Observer)**: `PROMPT_ONLY_RE` 의 `\\\\s`(4중 backslash)가 template literal 안의 regex 리터럴에서 "literal backslash+s"가 되어 whitespace class가 아닌 문자열 매칭
|
||||
- **원인 2 (http-bridge)**: `PROMPT_ONLY_RE = /^[\s\\\/]*[\w_.-]+\s*[>»$#]\s*$/` — AG 터미널 프롬프트가 `…`(U+2026 ellipsis)로 시작하는데 첫 문자 클래스에 포함 안 됨
|
||||
- **해결 (v0.5.45, `d2c44fe`)**: Observer `\\s`→`\s`, http-bridge 패턴을 `/^.*[>»$#]\s*$/`로 단순화
|
||||
- **검증**: 11개 테스트 케이스 전체 통과
|
||||
|
||||
---
|
||||
|
||||
### [2026-04-14] [Extension] Observer template literal 정규식 이스케이핑 오류 — "Running N commands" 스킵 미작동
|
||||
- **증상**: v9에서 `Running N commands` 그룹 헤더를 스킵하는 정규식 `/^Running\s*\d+\s*commands?$/i`를 추가했으나, 실제로 "Running2 commands" 텍스트를 전혀 매칭하지 못하여 여전히 Discord에 `cmd="Running2 commands"`가 전송됨
|
||||
- **원인**: `observer-script.ts`의 전체 코드가 TypeScript template literal (`` ` `` ... `` ` ``) 안에 있으므로, 정규식 리터럴의 백슬래시가 2중 해석됨:
|
||||
- TS 소스 `\\\\s` (4중) → template literal 출력 `\\s` → **브라우저에서 regex 리터럴 `\\s` = 리터럴 백슬래시+s** ❌
|
||||
- TS 소스 `\\s` (2중) → template literal 출력 `\s` → **브라우저에서 regex 리터럴 `\s` = whitespace class** ✅
|
||||
- TS 소스 `\s` (1중) → template literal에서 이스케이프 소멸 → **출력 `s`** ❌
|
||||
- **영향 범위**: `NOISE_CODE_RE`, `cleanLines()` (word boundary `\b`, newline `\n`), `cleanButtonText()` (whitespace `\s`), port 탐색 regex `\d`, "Running N commands" 스킵 regex 전부 미작동이었음
|
||||
- 단, `NOISE_RE` (new RegExp() 사용)는 문자열 이스케이핑이므로 4중 백슬래시가 올바름 (string → RegExp는 별도 이스케이핑 레이어)
|
||||
- **해결**: 모든 정규식 리터럴 (`/pattern/flags`) 안의 `\\\\s` → `\\s`, `\\\\d` → `\\d`, `\\\\b` → `\\b`, `\\\\n` → `\\n` 으로 수정 (v0.5.41, `8e89209`)
|
||||
- **주의**: **template literal 안에서 정규식 특수문자를 쓸 때 반드시 구분:**
|
||||
- `new RegExp('pattern')` 문자열: `\\\\s` (4중) — string 이스케이핑(1번)+regex 이스케이핑(1번) = 총 2번
|
||||
- `/pattern/` 정규식 리터럴: `\\s` (2중) — template literal 이스케이핑(1번)만 = 총 1번
|
||||
- **검증법**: `node -e "var f = require('./extension/out/observer-script.js').generateApprovalObserverScript; require('fs').writeFileSync('tmp.js', f(0)); console.log(require('fs').readFileSync('tmp.js','utf8').match(/Running.{10}/g));"` 으로 생성된 코드 확인
|
||||
|
||||
---
|
||||
|
||||
### [2026-04-13] [Extension] Observer v8 "Running N commands" 그룹 헤더를 승인 버튼으로 오인 — Discord 빈 내용+잘못된 버튼
|
||||
- **증상**: Discord 승인 요청에 command="Running2 commands", description 비어있음, 버튼도 "Running2 commands / Always run" 형태. 실제 코드/명령어 내용이 전혀 표시되지 않음
|
||||
- **원인 1**: `observer-script.ts`의 `isActionBtn()`에 `/Running\\s*\\d*\\s*command/i` 패턴이 있어 AG UI의 그룹 헤더 버튼("Running 3 commands")을 승인 버튼으로 분류. `scan()`이 이 버튼을 먼저 만나고 `break`로 나가 실제 "Always run"/"Cancel" 버튼은 처리 안 됨
|
||||
- **원인 2**: `extractStepContext()`가 `data-step-index` 속성 없으면 `cleanButtonText(btn)` = "Running2 commands"를 그대로 반환. AG Native에는 `data-step-index`/`data-testid` 속성이 없음 (DOM 덤프로 확인)
|
||||
- **원인 3**: `http-bridge.ts`의 "Run/Always run" 필터가 step-probe 미활성(activeSessionId 비어있음) 시에도 DOM observer 신호를 차단
|
||||
- **해결**: observer v9 (v0.5.40):
|
||||
1. `isActionBtn()`에서 "Running N commands" 패턴 제거
|
||||
2. `scan()`에서 `^Running\\s*\\d+\\s*commands?$` 명시적 스킵
|
||||
3. `extractContextFromNearby()` 추가: `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출
|
||||
4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent로 확대, 그룹 헤더 스킵
|
||||
5. `http-bridge.ts`의 "Run" 필터에 `ctx.activeSessionId` 체크 추가 — step-probe 미활성 시 DOM observer 허용
|
||||
- **주의**: AG Native UI의 "Running N commands"는 아코디언/그룹 헤더이며, 실제 승인 버튼은 하위 레벨의 "Run"/"Always run"/"Cancel". DOM 구조상 버튼 탐색 시 그룹 헤더를 반드시 스킵해야 함
|
||||
|
||||
### [2026-04-13] [Extension] HTTP Bridge UTF-8 인코딩 깨짐 — 한글 description 손실
|
||||
- **증상**: pending/ 파일의 description 필드에서 한글이 `[AI ]`처럼 깨져서 저장됨. Discord로 전달되는 승인 요청 본문도 깨짐
|
||||
- **원인**: Node.js HTTP 서버의 `req.on('data', chunk)` 콜백에서 chunk가 Buffer 타입으로 전달되는데, `body += chunk`로 string 결합 시 Buffer의 기본 인코딩(latin1)이 사용되어 multi-byte UTF-8 문자가 손실됨
|
||||
- **해결**: 모든 POST 핸들러(`/pending`, `/dump-html`, `/chat`, `/deep-inspect-result`, `/test-rpc`)에 `req.setEncoding('utf8')` 추가 (v0.5.39)
|
||||
- **주의**: Node.js HTTP 서버에서 POST body를 문자열로 수집할 때는 반드시 `req.setEncoding('utf8')`을 호출하거나, Buffer를 배열로 모은 후 `Buffer.concat().toString('utf8')`로 변환해야 함
|
||||
|
||||
### [2026-04-13] [Extension] Observer noise 필터 미작동 — textContent가 아이콘 텍스트를 줄바꿈 없이 합침
|
||||
- **증상**: pending description에 `Thought for 1s`, `chevron_right` 등 Material 아이콘명과 UI 노이즈가 그대로 남아있음
|
||||
- **원인**: DOM `textContent`는 block 요소 사이에 newline을 삽입하지 않아 `[AI 본문 요약]Thought for 1schevron_right[결행 명령]`처럼 한 줄로 합쳐짐. `cleanLines()`의 줄 단위 noise 필터(`^pattern$`)가 매칭 실패. 또한 `codeText` 추출에는 `cleanLines()`가 아예 미적용
|
||||
- **해결**: `cleanLines()`에 인라인 pre-strip 추가 — icon명 18종을 regex로 먼저 `\n`으로 치환 후 줄 단위 필터 적용. `codeText`에도 `cleanLines()` 적용 (v0.5.39)
|
||||
- **주의**: DOM에서 텍스트 추출 시 `textContent`는 레이아웃 무시, `innerText`는 detached 노드에서 미작동. 노이즈 필터링은 줄 단위뿐 아니라 인라인 패턴 제거도 병행해야 함
|
||||
|
||||
### [2026-04-13] [Extension] html-patcher String.replace() `$'` 특수 패턴으로 인라인 스크립트 SyntaxError
|
||||
- **증상**: Observer v8 인라인 스크립트가 workbench.html에 삽입되었으나 렌더러에서 전혀 실행되지 않음 (BEACON 핑 0건). V8 캐시 삭제 + AG 재시작 후에도 동일
|
||||
- **원인**: `html-patcher.ts`에서 `html.replace('</body>', '\n' + inlineBlock + '\n</body>')`를 사용. 인라인 스크립트의 NOISE_RE 정규식에 `')$', 'i'`가 있는데, `$'`는 JS `String.replace()`의 특수 대체 패턴(match 뒤의 텍스트)으로 해석됨. 이로 인해 `</body>` 뒤의 원본 HTML 구조(`<!-- Startup -->`, `<script src="workbench.js">`, `</html>`)가 JS 코드 중간(정규식 리터럴 안)에 삽입되어 **치명적 SyntaxError** 발생
|
||||
- **해결**: `inlineBlock.replace(/\$/g, '$$$$')`로 모든 `$`를 이스케이프한 후 replacement 문자열로 사용 (v0.5.38, `d6ed876`)
|
||||
- **주의**: `String.prototype.replace()`의 replacement 문자열에서 `$&`, `$'`, `` $` ``, `$1` 등은 특수 패턴. 동적 콘텐츠를 replacement로 사용할 때 반드시 `$` → `$$` 이스케이프 필요
|
||||
|
||||
### [2026-04-12] [Extension] V8 CachedData가 Observer 스크립트 로딩을 차단
|
||||
- **증상**: html-patcher가 workbench-jetski-agent.html에 observer v7 인라인 스크립트를 성공적으로 삽입했지만, deep-inspect가 렌더러에서 응답 없음 (10s timeout). AG 재시작 후에도 observer가 로드되지 않음
|
||||
- **원인**: `%APPDATA%\Antigravity\CachedData\` (50MB)에 V8 캐시가 남아있어, AG가 패치된 HTML 대신 캐시된 이전 버전을 로드. extension.log에 `patcher.install() called (needs reload)` 메시지가 표시되지만 실제 적용이 안 됨
|
||||
- **해결**: `Remove-Item "$env:APPDATA\Antigravity\CachedData\*" -Recurse -Force` 실행 후 AG 재시작. known-issues-archive #6에도 동일 케이스 있음
|
||||
- **주의**: HTML 패치 변경 시 **반드시** V8 CachedData 삭제 + AG 재시작 필요. 단순 AG 재시작만으로는 부족
|
||||
|
||||
### [2026-04-12] [DOM] text-ide-message-block-bot-color는 AI 응답 컨테이너가 아닌 NUX tooltip 전용
|
||||
- **증상**: observer-script가 `.text-ide-message-block-bot-color`를 AI 응답 컨테이너로 사용하지만, 실제 AI 텍스트를 추출하지 못함
|
||||
- **원인**: 번들 분석(jetskiAgent/main.js 10.8MB)으로 확인 결과, 이 클래스는 `hsn` 컴포넌트(NUX Tooltip 텍스트 색상)에서만 사용. AI 응답 텍스트는 `plannerResponse` step의 `Whi` 렌더러 → `div.px-2.py-1` → `MarkdownRenderer` 내부에 렌더링됨
|
||||
- **해결**: observer-script에서 `.text-ide-message-block-bot-color` 의존성 제거 필요. `markdown-body` 클래스도 AG Native에 존재하지 않음
|
||||
- **주의**: AI 응답 마크다운은 `prose` 관련 클래스나 MarkdownRenderer 내부 구조로 타겟팅해야 함. 실제 DOM 덤프로 정확한 셀렉터 확인 필요
|
||||
|
||||
### [2026-04-12] [SDK/DOM] AG Native 세션은 Cascade SDK API에 등록되지 않음 — DOM이 유일한 데이터 소스
|
||||
- **증상**: AG Native 세션에서 Discord 릴레이로 AI 응답이 전혀 전달되지 않고, 대신 UI 노이즈(`content_copy`, `Always run`, `keyboard_arrow_up`, `Cancel`)가 전송됨
|
||||
- **원인 1 (SDK)**: `GetCascadeTrajectorySteps(cascadeId=세션ID)` → `500 trajectory not found`. `GetDiagnostics` → `404`. AG Native 세션은 Cascade trajectory API에 전혀 등록되지 않는 별도 시스템
|
||||
- **원인 2 (DOM)**: `observer-script.ts` v6의 `scanChatBodies()`가 `.text-ide-message-block-bot-color` 컨테이너의 `textContent`를 통째로 가져오면서 내부 버튼/아이콘 텍스트까지 포함
|
||||
- **해결**: `observer-script.ts` v7로 전면 재설계:
|
||||
1. `[data-testid="conversation-view"]` + `[data-step-index]` 기반 step-aware 파싱
|
||||
2. `extractCleanStepText()`: 클론 후 button/svg/icon 엘리먼트 제거 → 마크다운 텍스트만 추출
|
||||
3. `extractStepContext()`: `getStepContainer()` → step 헤더 + code 블록만 추출
|
||||
4. `NOISE_RE`: Material icon 이름, 버튼 레이블, UI 텍스트 전면 차단
|
||||
5. 최초 `conversation-view` 감지 시 DOM 구조 자동 덤프 (`/dump-html`)
|
||||
- **주의**: SDK 경로(step-probe RT-CAPTURE)는 AG Native에서 사용 불가. DOM이 유일한 콘텐츠 소스이므로 AG UI 업데이트 시 `data-testid`/`data-step-index` 속성 존재 여부 반드시 확인 필요
|
||||
|
||||
|
||||
### [2026-04-09] [Bridge] Discord Body Content Missing Due to Step Probe Dummy Payload
|
||||
- **利앹긽**: <20><><EFBFBD>洹쒕え UI 留덉씠洹몃젅<EBAA83>씠<EFBFBD>뀡 <20>썑, <20>뵒<EFBFBD>뒪肄붾뱶 <20>듅<EFBFBD>씤 硫붿떆吏<EB9686> 蹂몃Ц<EBAA83>뿉 <20>떎<EFBFBD>뻾<EFBFBD>븷 肄붾뱶/紐낅졊<EB8285>뼱媛<EBBCB1> <20>셿<EFBFBD>쟾<EFBFBD>엳 <20>늻<EFBFBD>씫<EFBFBD>릺怨<EBA6BA> "Step #15"<22><><EFBFBD> 媛숈<E5AA9B><EC8888> <20>뵒<EFBFBD>뤃<EFBFBD>듃 <20>뀓<EFBFBD>뒪<EFBFBD>듃留<EB9383> <20>쟾<EFBFBD>넚<EFBFBD>맖.
|
||||
- **<2A>썝<EFBFBD>씤**: Native UI 蹂<>寃쎌쑝濡<EC919D> <20>씤<EFBFBD>빐 DOM observer媛<72> 異붿텧<EBB6BF>븳 踰꾪듉 <20>뀓<EFBFBD>뒪<EFBFBD>듃("Always run")媛<> `http-bridge.ts` <20>븘<EFBFBD>꽣 <20>슦<EFBFBD>쉶 諛<> bot.py<70>뿉<EFBFBD>꽌 吏<><EFA79E>뿰(defer) 泥섎━<EC848E>맖. 諛섎㈃ `step-probe.ts`媛<EFBFBD> `GetAllCascadeTrajectories` <20>뤃留곸쓣 <20>넻<EFBFBD>빐 <20>룞<EFBFBD>떆<EFBFBD>뿉 諛쒖깮<EC9296>떆<EFBFBD>궓 dummy pending payload (紐낅졊<EB8285>뼱 <20>긽<EFBFBD>꽭 <20>궡<EFBFBD>슜<EFBFBD>씠 <20>뾾<EFBFBD>씠 `Step #XX` <20>씪<EFBFBD>뒗 <20>뀓<EFBFBD>뒪<EFBFBD>듃留<EB9383> <20>룷<EFBFBD>븿)媛<> 遊뉗뿉 <20>쓽<EFBFBD>빐 癒쇱<E79992><EC87B1> <20>옄<EFBFBD>룞 <20>듅<EFBFBD>씤<EFBFBD>릺硫댁꽌 <20>젙<EFBFBD>옉 <20>떎<EFBFBD>젣 肄붾뱶 <20>쁺<EFBFBD>뿭 <20>젙蹂닿<E8B982><EB8BBF> 利앸컻<EC95B8>븿.
|
||||
- **<2A>빐寃<EBB990>**: `step-probe.ts` <20>궡<EFBFBD>뿉 `formatStepProbeCommand` <20>뿬<EFBFBD>띁 <20>븿<EFBFBD>닔瑜<EB8B94> 異붽<E795B0><EBB6BD><EFBFBD>븯<EFBFBD>뿬, WAITING <20>긽<EFBFBD>깭 <20>뒪<EFBFBD>뀦<EFBFBD>쓽 `argumentsJson` <20>뜲<EFBFBD>씠<EFBFBD>꽣瑜<EABDA3> 吏곸젒 <20>뙆<EFBFBD>떛<EFBFBD>븯怨<EBB8AF> `CommandLine`, `TargetFile` <20>벑 <20>떎<EFBFBD>젣 紐낅졊<EB8285>뼱<EFBFBD><EBBCB1><EFBFBD> <20>긽<EFBFBD>꽭 <20>씤<EFBFBD>옄/肄붾뱶瑜<EBB1B6> `command`<EFBFBD><EFBFBD><EFBFBD> `description`<EFBFBD>쑝濡<EFBFBD> <20>븷<EFBFBD>떦<EFBFBD>븯<EFBFBD>뿬 釉뚮┸吏<E294B8>濡<EFBFBD> <20>꽆湲곕룄濡<EBA384> <20>뙣移섑븿. DOM <20>샃<EFBFBD><EC8383><EFBFBD>踰꾩쓽 遺덉븞<EB8D89>젙<EFBFBD>꽦怨<EABDA6> 愿<>怨꾩뾾<EABEA9>씠 <20>씪愿<EC94AA><E684BF>맂 蹂몃Ц <20>쟾<EFBFBD>떖 蹂댁옣.
|
||||
- **二쇱쓽**: UI <20>뒪<EFBFBD>겕<EFBFBD>옒<EFBFBD>븨<EFBFBD>뿉 <20>쓽議댄븯<EB8C84>뒗 DOM Observer 諛⑹떇<E291B9><EB9687><EFBFBD> UI <20>젅<EFBFBD>씠<EFBFBD>븘<EFBFBD>썐, <20>븘<EFBFBD>씠肄<EC94A0> <20>궫<EFBFBD>엯 <20>벑<EFBFBD>뿉 痍⑥빟<E291A5>븯誘<EBB8AF>濡<EFBFBD>, <20>긽<EFBFBD>꽭 <20>럹<EFBFBD>씠濡쒕뱶 異붿텧<EBB6BF><ED85A7><EFBFBD> <20>빆<EFBFBD>긽 100% <20>떊猶<EB968A> 媛<><E5AA9B>뒫<EFBFBD>븳 SDK RPC(`step-probe.ts`) <20>뜲<EFBFBD>씠<EFBFBD>꽣瑜<EABDA3> <20>슦<EFBFBD>꽑 <20>궗<EFBFBD>슜<EFBFBD>븯<EFBFBD>룄濡<EBA384> 援ъ꽦<D18A>빐<EFBFBD>빞 <20>븿.
|
||||
|
||||
|
||||
|
||||
## <20>룷留<EBA3B7>
|
||||
|
||||
|
||||
|
||||
### [2026-03-23] [Extension] Cross-Project DOM Observer Leakage
|
||||
|
||||
- **利앹긽**: <20>떎以<EB968E> <20>썝寃<EC8D9D> 而댄벂<EB8C84>꽣<EFBFBD>뿉<EFBFBD>꽌 <20>룞<EFBFBD>씪<EFBFBD>븳 <20>봽濡쒖젥<EC9296>듃紐낆쑝濡<EC919D> <20>떎<EFBFBD>뻾<EFBFBD>맂 VS Code<64>뱾<EFBFBD>씠 <20>꽌濡쒖쓽 `execute JavaScript` (Allow) <20>듅<EFBFBD>씤 <20>떊<EFBFBD>샇瑜<EC8387> 媛<>濡쒖콈嫄곕굹 <20>뿁<EFBFBD>슧<EFBFBD>븳 <20>꽌踰꾨줈 蹂대깂.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: Extension<6F>씠 `workbench.html`<EFBFBD>뿉 <20>뒪<EFBFBD>겕由쏀듃瑜<EB9383> 二쇱엯<EC87B1>븷 <20>븣 寃곗젙濡좎쟻 <20>룷<EFBFBD>듃瑜<EB9383> <20>븯<EFBFBD>뱶肄붾뵫<EBB6BE>뻽<EFBFBD>뒗<EFBFBD>뜲, <20>쟾<EFBFBD>뿭 罹먯떆<EBA8AF>맂 HTML <20>뙆<EFBFBD>씪<EFBFBD>쓣 紐⑤뱺 濡쒖뺄/<2F>썝寃<EC8D9D> <20>뿰寃곗씠 怨듭쑀<EB93AD>븯硫댁꽌 留덉<EFA78D><EB8D89>留됱뿉 <20>뿴由<EBBFB4> <20>봽濡쒖젥<EC9296>듃<EFBFBD>쓽 <20>룷<EFBFBD>듃 踰덊샇濡<EC8387> <20>뜮<EFBFBD>뼱<EFBFBD>뵆<EFBFBD>썙吏<EC8D99>.
|
||||
|
||||
- **<2A>빐寃<EBB990>**: `extension.ts`<EFBFBD>뿉<EFBFBD>꽌 <20>긽<EFBFBD>깭 <20>몴<EFBFBD>떆以<EB9686>(Status Bar) `tooltip`<EFBFBD>뿉 <20>룷<EFBFBD>듃瑜<EB9383> 二쇱엯<EC87B1>븯怨<EBB8AF>, `observer-script.ts`<EFBFBD>뿉<EFBFBD>꽌 DOM 荑쇰━瑜<E29481> <20>넻<EFBFBD>빐 <20>룞<EFBFBD>쟻<EFBFBD>쑝濡<EC919D> <20>옄<EFBFBD>떊<EFBFBD>쓽 李<>(Window)<29>뿉 <20>븷<EFBFBD>떦<EFBFBD>맂 <20>룷<EFBFBD>듃瑜<EB9383> 李얠븘<EC96A0>궡<EFBFBD>룄濡<EBA384> <20>닔<EFBFBD>젙. `vscode.env.asExternalUri`瑜<EFBFBD> <20>궗<EFBFBD>슜<EFBFBD>븯<EFBFBD>뿬 <20>룷<EFBFBD>듃 異⑸룎 <20>떆 <20>슦<EFBFBD>쉶<EFBFBD>맂 二쇱냼源뚯<E6BA90><EB9AAF> 濡쒖뺄 <20>룷<EFBFBD>썙<EFBFBD>뵫<EFBFBD>뿉 留ㅽ븨<E385BD>릺<EFBFBD>룄濡<EBA384> 吏<><EFA79E>썝.
|
||||
|
||||
- **二쇱쓽**: VS Code UI 肄붿뼱(HTML) <20>뙣移<EB99A3> <20>떆, <20>뿬<EFBFBD>윭 李<>(Window)<29>씠<EFBFBD>굹 <20>떎以<EB968E> <20>썝寃<EC8D9D> <20>젒<EFBFBD>냽 <20>떆 <20>솚寃<EC869A>(Scope) 遺꾨━<EABEA8>뿉 媛곷퀎<EAB3B7>븳 二쇱쓽媛<EC93BD> <20>븘<EFBFBD>슂<EFBFBD>븿. <20>쟾<EFBFBD>뿭 <20>옄<EFBFBD>썝<EFBFBD>뿉 <20>쓽議댄븯<EB8C84>뒗 <20>븯<EFBFBD>뱶肄붾뵫 吏<><EFA79E>뼇.
|
||||
|
||||
|
||||
|
||||
### [<5B>궇吏<EAB687>] [<5B>궎<EFBFBD>썙<EFBFBD>뱶] <20><><EFBFBD> <20>븳以<EBB8B3> <20>슂<EFBFBD>빟
|
||||
|
||||
- **利앹긽**: 臾댁뾿<EB8C81>씠 <20>옒紐삳릺<EC82B3>뿀<EFBFBD>뒗媛<EB9297>
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: 洹쇰낯 <20>썝<EFBFBD>씤
|
||||
|
||||
- **<2A>빐寃<EBB990>**: <20>삱諛붾Ⅸ <20>빐寃<EBB990> 諛⑸쾿
|
||||
|
||||
- **二쇱쓽**: <20>옱諛<EC98B1> 諛⑹<E8AB9B><E291B9>瑜<EFBFBD> <20>쐞<EFBFBD>븳 援먰썕
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
### [2026-04-08] [Discord Bot] Channel Deletion Cache Desync
|
||||
|
||||
- **利앹긽**: 遊뉗씠 耳쒖졇 <20>엳<EFBFBD>뒗 <20>긽<EFBFBD>깭<EFBFBD>뿉<EFBFBD>꽌 Discord 梨꾨꼸(g-project-name)<29>쓣 <20>궘<EFBFBD>젣<EFBFBD>븯硫<EBB8AF>, 遊뉗씠 <20>궘<EFBFBD>젣瑜<ECA0A3> <20>씤吏<EC94A4><EFA79E>븯吏<EBB8AF> 紐삵븯怨<EBB8AF> <20>깉 梨꾨꼸<EABEA8>쓣 <20>깮<EFBFBD>꽦<EFBFBD>븯吏<EBB8AF> <20>븡<EFBFBD>쑝硫<EC919D> 硫붿떆吏<EB9686><EFA79E>룄 利앸컻<EC95B8>븿.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: ot.py<70>쓽 self.project_channels <20>뵓<EFBFBD>뀛<EFBFBD>꼫由ъ뿉 梨꾨꼸 媛앹껜媛<EABB9C> 罹먯떆<EBA8AF>릺<EFBFBD>뼱 <20>엳<EFBFBD>뼱, API <20>샇異<EC8387> <20>뾾<EFBFBD>씠 罹먯떆<EBA8AF>맂(<28>궘<EFBFBD>젣<EFBFBD>맂) 梨꾨꼸濡<EABCB8> 硫붿떆吏<EB9686>瑜<EFBFBD> 蹂대궡<EB8C80>젮 <20>떆<EFBFBD>룄<EFBFBD>븯<EFBFBD>떎 404 <20>뿉<EFBFBD>윭 諛쒖깮 <20>썑 <20>떎<EFBFBD>뙣<EFBFBD>븿.
|
||||
|
||||
- **<2A>빐寃<EBB990>**: 梨꾨꼸 留듯븨<EB93AF>씠 瑗ъ<E79197><D18A><EFBFBD>쓣 <20>븣<EFBFBD>뒗 **Python 遊<>(Docker 而⑦뀒<E291A6>씠<EFBFBD>꼫)<29>쓣 <20>옱<EFBFBD>떆<EFBFBD>옉**<2A>븯<EFBFBD>뿬 罹먯떆瑜<EB9686> 珥덇린<EB8D87>솕<EFBFBD>븯怨<EBB8AF> 梨꾨꼸 紐⑸줉<E291B8>쓣 <20>깉濡<EAB989> 媛깆떊<EAB986>븯寃<EBB8AF> <20>븿.
|
||||
|
||||
- **二쇱쓽**: 梨꾨꼸 愿<>由щ뒗 罹먯떆<EBA8AF>뿉 <20>쓽議댄븯湲<EBB8AF> <20>븣臾몄뿉 媛뺤젣濡<ECA0A3> Discord UI<55>뿉<EFBFBD>꽌 梨꾨꼸<EABEA8>쓣 吏<><EFA79E>썱<EFBFBD>쓣 <20>븣<EFBFBD>뒗 諛섎뱶<EC848E>떆 遊뉗쓣 <20>옱援щ룞<D189>빐<EFBFBD>빞 <20>븿.
|
||||
|
||||
|
||||
|
||||
### [2026-04-08] [Extension] Multiple Workspace LS Cross-Connection
|
||||
|
||||
- **利앹긽**: ariet-llm 李쎌뿉<EC8E8C>꽌 耳곗쑝<EAB397>굹 gravity_control<6F>쓽 諛깃렇<EAB983>씪<EFBFBD>슫<EFBFBD>뱶 援щ룞 以묒씤 LS<4C>뿉 <20>뿰寃곕릺<EAB395>뼱 <20>옄湲<EC9884> <20>옄<EFBFBD>떊 李쎌쓽 <20>떊<EFBFBD>샇瑜<EC8387> <20>옟吏<EC989F> 紐삵븿.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: <20>뿬<EFBFBD>윭 VS Code 李쎌쓣 <20>쓣<EFBFBD>썱<EFBFBD>쓣 <20>븣 <20>뼱<EFBFBD>뼡 李쎌뿉<EC8E8C>꽌<EFBFBD>뒗 Antigravity <20>뙣<EFBFBD>꼸<EFBFBD>쓣 <20>늻瑜댁<E7919C><EB8C81> <20>븡<EFBFBD>븘 <20>쟾<EFBFBD>슜 LS媛<53> <20>떆<EFBFBD>옉<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬. ixLSConnection()<29>씠 <20>옄湲<EC9884> 紐レ쓽 LS瑜<53> 李얠<EFA7A1><EC96A0> 紐삵븯怨<EBB8AF> fallback<63>쑝濡<EC919D> 湲곗〈<EAB397>뿉 <20>뼚 <20>엳<EFBFBD>뜕 <20>떎瑜<EB968E> 李쎌쓽 LS<4C>뿉 <20>뿰寃곕맖.
|
||||
|
||||
- **<2A>빐寃<EBB990>**: <20><><EFBFBD><EFBFBD>긽 李쎌뿉<EC8E8C>꽌 Developer: Reload Window <20>떎<EFBFBD>뻾 <20>썑 **<2A>궗<EFBFBD>씠<EFBFBD>뱶諛붿쓽 濡쒖뺄 Antigravity 梨쀫큸 <20>뙣<EFBFBD>꼸<EFBFBD>쓣 <20>븳 踰<> <20>뿴<EFBFBD>뼱** <20>옄<EFBFBD>떊<EFBFBD>쓽 LS <20>봽濡쒖꽭<EC9296>뒪瑜<EB92AA> <20>쓣<EFBFBD>슫 <20>뮘<EFBFBD>뿉 Gravity Bridge瑜<65> Start<72>븿.
|
||||
|
||||
- **二쇱쓽**: LS<4C>뒗 <20>옄<EFBFBD>룞<EFBFBD>쑝濡<EC919D> <20>떆<EFBFBD>옉<EFBFBD>릺吏<EBA6BA> <20>븡怨<EBB8A1> <20>궗<EFBFBD>슜<EFBFBD>옄媛<EC9884> 梨꾪똿 <20>뙣<EFBFBD>꼸<EFBFBD>쓣 <20>븳 踰<> <20>겢由<EAB2A2>/<2F>솢<EFBFBD>꽦<EFBFBD>솕<EFBFBD>빐<EFBFBD>빞留<EBB99E> Spawn <20>맖.
|
||||
|
||||
|
||||
|
||||
## <20>윍<EFBFBD> Active/Recent Issues
|
||||
|
||||
|
||||
|
||||
### [2026-04-09] [Extension] Agent UI Native Migration & Icon Text Gluing
|
||||
|
||||
- **利앹긽**: UI Tailwind/Native 留덉씠洹몃젅<EBAA83>씠<EFBFBD>뀡 諛<> <20>븘<EFBFBD>씠肄<EC94A0> <20>쟻<EFBFBD>슜 <20>썑, Discord 釉뚮┸吏<E294B8>濡<EFBFBD> <20>떊<EFBFBD>샇媛<EC8387> <20>쟾<EFBFBD>넚<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: <20>꽕<EFBFBD>씠<EFBFBD>떚釉<EB969A> UI 踰꾪듉<EABEAA>쓽 `textContent` 異붿텧 <20>떆, Codicons <20>벑 <20>븘<EFBFBD>씠肄<EC94A0> <20>룿<EFBFBD>듃 臾몄옄<EBAA84>뿴(e.g., `渽<EFBFBD> Accept`)<29>씠 <20>븵遺<EBB8B5>遺꾩뿉 蹂묓빀(Gluing)<29>릺硫댁꽌, 湲곗〈<EAB397>쓽 `^` <20>빑而ㅺ<E8808C><E385BA> <20>룷<EFBFBD>븿<EFBFBD>맂 <20>젙洹쒖떇 留ㅼ묶(`/^(?:Always\s*)?Run/i`)<29>씠 <20>떎<EFBFBD>뙣<EFBFBD>븿.
|
||||
|
||||
- **<2A>빐寃<EBB990>**: `observer-script.ts`<60>쓽 <20>뒪罹<EB92AA>, Sibling 踰꾪듉 <20>닔吏<EB8B94>, Webview Trigger-click <20>벑 `textContent`瑜<> 異붿텧<EBB6BF>븯<EFBFBD>뒗 紐⑤뱺 DOM <20>씫湲<EC94AB> 援ш컙<D188>뿉 `txt.replace(/^[^a-zA-Z0-9]+/, '')` <20>쟾泥섎━瑜<E29481> <20>쟻<EFBFBD>슜<EFBFBD>븯<EFBFBD>뿬 <20>꽑<EFBFBD>뻾 湲고샇/<2F>븘<EFBFBD>씠肄섏쓣 <20>븞<EFBFBD>쟾<EFBFBD>븯寃<EBB8AF> <20>젣嫄<ECA0A3>.
|
||||
|
||||
- **二쇱쓽**: Native UI 而댄룷<EB8C84>꼳<EFBFBD>듃 <20>솚寃쎌뿉<EC8E8C>꽌<EFBFBD>뒗 <20>뀓<EFBFBD>뒪<EFBFBD>듃 <20>끂<EFBFBD>뱶肉먮쭔 <20>븘<EFBFBD>땲<EFBFBD>씪 <20>븘<EFBFBD>씠肄<EC94A0>/SVG 而댄룷<EB8C84>꼳<EFBFBD>듃<EFBFBD>쓽 <20>뀓<EFBFBD>뒪<EFBFBD>듃 湲<>猷⑥엵 <20>쁽<EFBFBD>긽<EFBFBD>쑝濡<EC919D> <20>씤<EFBFBD>빐 <20>뾼寃⑺븳 <20>떆<EFBFBD>옉<EFBFBD>젏(`^`) <20>젙洹쒖떇<EC9296>씠 源⑥쭏 <20>닔 <20>엳<EFBFBD>쑝誘<EC919D>濡<EFBFBD>, <20>빆<EFBFBD>긽 遺덊븘<EB8D8A>슂<EFBFBD>븳 <20>듅<EFBFBD>닔臾몄옄 <20>쟾泥섎━瑜<E29481> <20>꽑<EFBFBD>뻾<EFBFBD>빐<EFBFBD>빞 <20>븿.
|
||||
|
||||
|
||||
|
||||
### [2026-04-09] [Extension] Agent UI Native Migration & CodeLens False Positive Filter
|
||||
|
||||
- **利앹긽**: UI Tailwind/Native 留덉씠洹몃젅<EBAA83>씠<EFBFBD>뀡 <20>쟻<EFBFBD>슜 <20>썑, Discord 釉뚮┸吏<E294B8>濡<EFBFBD> <20>떊<EFBFBD>샇媛<EC8387> <20>쟾<EFBFBD><EC9FBE><EFBFBD> <20>쟾<EFBFBD>넚<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: Agent <20>뙣<EFBFBD>꼸<EFBFBD>씠 <20>꺆/<2F>뿉<EFBFBD>뵒<EFBFBD>꽣 蹂몃Ц<EBAA83>뿉 吏곸젒 <20>젋<EFBFBD>뜑留곷릺硫댁꽌, 湲곗〈 <20>삤<EFBFBD>옉<EFBFBD>룞 諛⑹<E8AB9B><E291B9> 濡쒖쭅(`if (b.closest('.monaco-editor'))`)<29>뿉 <20>뙣<EFBFBD>꼸 <20>쟾泥<EC9FBE> 踰꾪듉<EABEAA>씠 <20>룷李⑸릺<E291B8>뼱 臾댁떆<EB8C81>맖
|
||||
|
||||
- **<2A>빐寃<EBB990>**: <20>꼫臾<EABCAB> 愿묐쾾<EBAC90>쐞<EFBFBD>븳 `.monaco-editor` 諛⑹뼱瑜<EBBCB1> <20>빐<EFBFBD>젣<EFBFBD>븯怨<EBB8AF>, 肄붾뱶 <20>젋利<ECA08B> 怨좎쑀 而⑦뀒<E291A6>씠<EFBFBD>꼫<EFBFBD>씤 `.codelens-decoration` <20>궡遺<EAB6A1><E981BA>씪 寃쎌슦<EC8E8C>뿉留<EBBF89> 臾댁떆<EB8C81>븯<EFBFBD>룄濡<EBA384> <20><><EFBFBD><EFBFBD>룷<EFBFBD>씤<EFBFBD>듃 <20>닔<EFBFBD>젙
|
||||
|
||||
- **二쇱쓽**: DOM <20>샃<EFBFBD><EC8383><EFBFBD>踰<EFBFBD> <20>븘<EFBFBD>꽣 議곌굔 <20>옉<EFBFBD>꽦 <20>떆 <20>옒<EFBFBD>띁 <20>겢<EFBFBD>옒<EFBFBD>뒪<EFBFBD>뒗 UI <20>뵒<EFBFBD>옄<EFBFBD>씤 媛쒗렪(Native, Editor Tab <20>벑 <20>쐞移<EC909E> 蹂<>寃<EFBFBD>)<29>뿉 留ㅼ슦 痍⑥빟<E291A5>븿. 媛<><E5AA9B>옣 援ъ껜<D18A>쟻<EFBFBD>씤 <20>궡遺<EAB6A1> <20>끂<EFBFBD>뱶 <20>겢<EFBFBD>옒<EFBFBD>뒪<EFBFBD>굹 <20><><EFBFBD>寃<EFBFBD> 怨좎쑀 <20>냽<EFBFBD>꽦<EFBFBD>쓣 <20>넻<EFBFBD>빐 <20>븘<EFBFBD>꽣留곹븷 寃<>
|
||||
|
||||
|
||||
|
||||
### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop)
|
||||
|
||||
- **利앹긽**: `guitar_score` <20>벑<EFBFBD>뿉<EFBFBD>꽌 <20>솢<EFBFBD>꽦<EFBFBD>솕<EFBFBD>맂 <20>꽭<EFBFBD>뀡<EFBFBD>쓽 <20>뵒<EFBFBD>뒪肄붾뱶 <20>듅<EFBFBD>씤 <20>떊<EFBFBD>샇瑜<EC8387> "怨꾩냽<EABEA9>빐<EFBFBD>꽌" <20>옟吏<EC989F> 紐삵븿. (WS 60珥<30> <20><><EFBFBD><EFBFBD>엫<EFBFBD>븘<EFBFBD>썐蹂대떎 <20>뜑 移섎챸<EC848E>쟻<EFBFBD>쑝濡<EC919D> <20>떊<EFBFBD>샇媛<EC8387> <20>븘<EFBFBD>삁 媛<>吏<EFBFBD> <20>븡<EFBFBD>쓬)
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: Extension<6F>씠 <20>솢<EFBFBD>꽦 <20>꽭<EFBFBD>뀡<EFBFBD>쓣 李얘린 <20>쐞<EFBFBD>빐 <20>샇異쒗븯<EC9297>뒗 `GetAllCascadeTrajectories` LS API媛<49> `{}`(鍮<> <20>씤<EFBFBD>옄)濡<> <20>샇異쒕맆 <20>븣, 湲곕낯<EAB395>쟻<EFBFBD>쑝濡<EC919D> **10媛쒖쓽 <20>꽭<EFBFBD>뀡留<EB80A1> 諛섑솚<EC8491>븯<EFBFBD>뒗 <20>븯<EFBFBD>뱶 由щ컠(Pagination Limit)**<2A>씠 嫄몃젮<EBAA83>엳<EFBFBD>쓬. <20>씠濡<EC94A0> <20>씤<EFBFBD>빐 <20>옉<EFBFBD>뾽 <20>궡<EFBFBD>뿭<EFBFBD>씠 <20>늻<EFBFBD>쟻<EFBFBD>릺硫<EBA6BA> <20>닔留롮<EFA78D><EBA1AE> 理쒖떊/吏꾪뻾 以<> <20>꽭<EFBFBD>뀡<EFBFBD>뱾<EFBFBD>씠 10媛<30> 紐⑸줉<E291B8>뿉<EFBFBD>꽌 諛<><E8AB9B>젮<EFBFBD>굹 <20>늻<EFBFBD>씫<EFBFBD>맖. <20>씡<EFBFBD>뒪<EFBFBD>뀗<EFBFBD>뀡<EFBFBD><EB80A1><EFBFBD> <20>꽭<EFBFBD>뀡<EFBFBD>씠 <20>뾾<EFBFBD>떎怨<EB968E> <20>뙋<EFBFBD>떒<EFBFBD>빐 媛뺤젣濡<ECA0A3> `IDLE` 紐⑤뱶<E291A4>뿉 吏꾩엯<EABEA9>븯硫<EBB8AF>, <20>듅<EFBFBD>씤 <20><><EFBFBD>湲곗뿴(WAITING) <20>옄泥대<EFA7A3><EB8C80> 寃<><E5AF83>궗<EFBFBD>븯吏<EBB8AF> <20>븡寃<EBB8A1> <20>맖.
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.14): `v0.5.13`<60>뿉<EFBFBD>꽌 <20>룄<EFBFBD>엯<EFBFBD>뻽<EFBFBD>뜕 `{ limit: 100 }`<60>씠 LS <20>떒<EFBFBD>쓽 荑쇰━ 怨쇰<E680A8><EC87B0><EFBFBD>븯濡<EBB8AF> <20>씤<EFBFBD>븳 VS Code UI <20>봽由ъ쭠(DoS)<29>쓣 <20>쑀諛쒗븯<EC9297>뿬 濡ㅻ갚<E385BB>븯<EFBFBD>뒗 以<> <20>븘<EFBFBD>닔 <20>젙<EFBFBD>젹 <20>뙆<EFBFBD>씪誘명꽣(`descending: true`)源뚯<E6BA90><EB9AAF> <20>냼<EFBFBD>떎<EFBFBD>릺<EFBFBD>뿀<EFBFBD>뜕 <20>떎<EFBFBD>닔瑜<EB8B94> 援먯젙<EBA8AF>븿. 理쒖쥌<EC9296>쟻<EFBFBD>쑝濡<EC919D> `{ limit: 30, descending: true }`瑜<> <20>쟻<EFBFBD>슜<EFBFBD>븯<EFBFBD>뿬 <20>뙆<EFBFBD>떛 遺<><E981BA>븯 理쒖냼<EC9296>솕 諛<> 理쒖떊 <20>꽭<EFBFBD>뀡 理쒖긽<EC9296>떒(Index 0) 議고쉶瑜<EC89B6> <20>븞<EFBFBD>쟾<EFBFBD>븯寃<EBB8AF> 援ы쁽<D18B>븿.
|
||||
|
||||
- **二쇱쓽**: LS<4C>쓽 湲곕낯 SQLite/DB <20>쓳<EFBFBD>떟 Limit 洹쒖튃<EC9296>뿉 <20>쓽議댄븯<EB8C84>뿬 <20>쟾泥<EC9FBE> <20>뜲<EFBFBD>씠<EFBFBD>꽣 <20>뒪罹붿쓣 <20>닔<EFBFBD>뻾<EFBFBD>븯<EFBFBD>뒗 濡쒖쭅<EC9296><ECAD85><EFBFBD> <20>뼵<EFBFBD>젣<EFBFBD>뱺 Truncation <20>씠<EFBFBD>뒋(Data Loss)瑜<> <20>쑀諛쒗븷 <20>닔 <20>엳<EFBFBD>쓬.
|
||||
|
||||
|
||||
|
||||
### [2026-03-31] [WS] Browser API Fallback 60s Timeout (Zombie Connection)
|
||||
|
||||
- **利앹긽**: `guitar_score` <20>벑 紐⑤뱺 <20>옉<EFBFBD>뾽 <20>솚寃쎌뿉<EC8E8C>꽌 <20>빟 60珥덈쭏<EB8D88>떎 WebSocket <20>뿰寃곗씠 <20>걡湲곌퀬 <20>옱<EFBFBD>뿰寃곕릺<EAB395>뒗 <20>쁽<EFBFBD>긽<EFBFBD>씠 諛섎났<EC848E>릺硫<EBA6BA>(extension.log<6F>뿉 `Heartbeat timeout` 怨꾩냽 異쒕젰), 洹<> <20>궗<EFBFBD>씠 <20>뵒<EFBFBD>뒪肄붾뱶 <20>듅<EFBFBD>씤 <20>떊<EFBFBD>샇瑜<EC8387> <20>넃移<EB8483>.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: Extension<6F>씠 `ws` 紐⑤뱢 濡쒕뱶 <20>떎<EFBFBD>뙣(VS Code <20>솚寃<EC869A> <20>벑)濡<> <20>씤<EFBFBD>빐 釉뚮씪<EB9AAE>슦<EFBFBD><EC8AA6><EFBFBD> <20>궡<EFBFBD>옣 `WebSocket` 媛앹껜濡<EABB9C> Fallback <20>맖. 釉뚮씪<EB9AAE>슦<EFBFBD><EC8AA6><EFBFBD> WS<57>뒗 <20>꽌踰꾩쓽 <20>꽕<EFBFBD>씠<EFBFBD>떚釉<EB969A> ping<6E>쓣 諛쏆븘 pong<6E>쓣 <20>옄<EFBFBD>룞 <20>쓳<EFBFBD>떟<EFBFBD>븯吏<EBB8AF>留<EFBFBD> JS<4A>뿉 <20>씠踰ㅽ듃瑜<EB9383> <20>끂異쒗븯吏<EBB8AF> <20>븡<EFBFBD>쓬. <20>씠濡<EC94A0> <20>씤<EFBFBD>빐 `lastPongTime` 媛깆떊<EAB986>씠 遺덇<E981BA><EB8D87><EFBFBD>뒫<EFBFBD>빐<EFBFBD>졇, `Date.now() - lastPongTime > 60000` 議곌굔<EAB38C>씠 臾댁“嫄<E2809C> <20>넻怨쇰릺<EC87B0>뼱 硫<>姨≫븳 <20>뿰寃곗쓣 媛뺤젣 醫낅즺<EB8285>븿 (False Positive).
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.12):
|
||||
|
||||
1. `hub.py`: `{"type": "heartbeat"}` JSON 硫붿떆吏<EB9686> <20>닔<EFBFBD>떊 <20>떆 紐낆떆<EB8286>쟻<EFBFBD>쑝濡<EC919D> `{"type": "pong"}` JSON<4F>쓣 <20>쓳<EFBFBD>떟<EFBFBD>븯<EFBFBD>룄濡<EBA384> <20>닔<EFBFBD>젙.
|
||||
|
||||
2. `ws-client.ts`: 紐낆떆<EB8286>쟻 `pong` <20>빖<EFBFBD>뱾<EFBFBD>윭 異붽<E795B0><EBB6BD>. JSON pong 吏<><EFA79E>썝 <20>꽌踰꾧굅<EABEA7>굹 Node.js ws瑜<73> <20>궗<EFBFBD>슜<EFBFBD>븷 <20>븣留<EBB8A3> 60珥<30> <20><><EFBFBD><EFBFBD>엫<EFBFBD>븘<EFBFBD>썐 寃<>利앹쓣 嫄곗튂<EAB397>룄濡<EBA384> 議곌굔 蹂닿컯 (`forceHeartbeatTimeoutIfNoPong`).
|
||||
|
||||
- **二쇱쓽**: 釉뚮씪<EB9AAE>슦<EFBFBD><EC8AA6><EFBFBD> <20>몴以<EBAAB4> WebSockets(W3C)<29>뒗 ping/pong <20>젣<EFBFBD>뼱 <20>봽<EFBFBD>젅<EFBFBD>엫<EFBFBD>쓣 JS濡<53> <20>끂異쒗븯吏<EBB8AF> <20>븡<EFBFBD>쓬. <20>뤃由ы븘/<2F>겕濡쒖뒪<EC9296>뵆<EFBFBD>옯<EFBFBD>뤌 WS <20>옒<EFBFBD>띁 <20>궗<EFBFBD>슜 <20>떆 <20>븯<EFBFBD>듃鍮꾪듃<EABEAA>뒗 諛섎뱶<EC848E>떆 JSON 硫붿꽭吏<EABDAD> <20>삎<EFBFBD>깭<EFBFBD>쓽 Application Layer Ping/Pong<6E>쑝濡<EC919D> <20><><EFBFBD><EFBFBD>뼱<EFBFBD>궡嫄곕굹, Native WS API <20>뿬遺<EBBFAC>瑜<EFBFBD> <20>솗<EFBFBD>떎<EFBFBD>엳 泥댄겕<EB8C84>빐<EFBFBD>빞 <20>븿.
|
||||
|
||||
|
||||
|
||||
### [2026-03-28] [step-probe] GetCascadeTrajectorySteps UTF-8 <20>뿉<EFBFBD>윭 臾댄븳 猷⑦봽
|
||||
|
||||
- **利앹긽**: `guitar_score` <20>봽濡쒖젥<EC9296>듃<EFBFBD>뿉<EFBFBD>꽌 `[STEP-PROBE] error: ...invalid UTF-8` <20>뿉<EFBFBD>윭媛<EC9CAD> 5珥덈쭏<EB8D88>떎 諛섎났<EC848E>릺硫<EBA6BA> Discord <20>듅<EFBFBD>씤 <20>떊<EFBFBD>샇媛<EC8387> <20>쟾<EFBFBD>떖<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: AG LS <20>꽌踰꾩뿉<EABEA9>꽌 <20>듅<EFBFBD>젙 step<65>쓽 `CortexStepEphemeralMessage.content`<60>뿉 諛붿씠<EBB6BF>꼫由<EABCAB> <20>뜲<EFBFBD>씠<EFBFBD>꽣(<28>씠誘몄<E8AA98><EBAA84> <20>벑) <20>룷<EFBFBD>븿 <20>넂 proto UTF-8 吏곷젹<EAB3B7>솕 500 <20>뿉<EFBFBD>윭. `catch(e)` 釉붾줉<EBB6BE>뿉<EFBFBD>꽌 `stallProbed=true`瑜<> <20>꽕<EFBFBD>젙<EFBFBD>븯吏<EBB8AF> <20>븡<EFBFBD>븘 `!ctx.stallProbed` 議곌굔<EAB38C>씠 <20>빆<EFBFBD>긽 true <20>넂 5珥덈쭏<EB8D88>떎 <20>룞<EFBFBD>씪 <20>슂泥<EC8A82> 臾댄븳 <20>옱<EFBFBD>떆<EFBFBD>룄.
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.11): `catch` 釉붾줉<EBB6BE>뿉<EFBFBD>꽌 UTF-8 <20>뿉<EFBFBD>윭 媛먯<E5AA9B><EBA8AF> <20>떆 `stepOffset=currentCount-20`<60>쑝濡<EC919D> fallback <20>슂泥<EC8A82>. offset<65>룄 <20>떎<EFBFBD>뙣 <20>떆 `stallProbed=true` <20>꽕<EFBFBD>젙<EFBFBD>븯<EFBFBD>뿬 猷⑦봽 李⑤떒. `delta>0` <20>씠踰ㅽ듃 諛쒖깮 <20>떆 L433<33>뿉<EFBFBD>꽌 <20>옄<EFBFBD>룞 由ъ뀑.
|
||||
|
||||
- **二쇱쓽**: `stallProbed=true`<60>뒗 <20>쁺援<EC81BA> Lock<63>씠 <20>븘<EFBFBD>떂 <20><><EFBFBD> `delta>0` <20>떆 <20>옄<EFBFBD>룞 由ъ뀑. UTF-8 <20>뿉<EFBFBD>윭<EFBFBD>뒗 AG <20>꽌踰<EABD8C> 痢<> 臾몄젣(<28>씠誘몄<E8AA98><EBAA84>/諛붿씠<EBB6BF>꼫由<EABCAB> <20>뜲<EFBFBD>씠<EFBFBD>꽣媛<EABDA3> ephemeral message<67>뿉 <20>룷<EFBFBD>븿)<29>씠誘<EC94A0>濡<EFBFBD> Extension<6F>뿉<EFBFBD>꽌 graceful fallback留<6B> 泥섎━.
|
||||
|
||||
|
||||
|
||||
### [2026-03-28] [approval-handler] stepIndex 誘명솗<EBAA85>젙 <20>떆 wrong-stepIndex RPC <20>궘鍮<EAB698>
|
||||
|
||||
- **利앹긽**: DOM observer 寃쎈줈濡<ECA488> `terminal_command` pending <20>깮<EFBFBD>꽦 <20>썑 Discord <20>듅<EFBFBD>씤 <20>떆 `HandleCascadeUserInteraction(stepIndex=0)` <20>넂 `"input not registered for step 0"` <20>넂 LS reconnect <20>넂 <20>옱<EFBFBD>떆<EFBFBD>룄 <20>넂 DOM click fallback<63>쑝濡<EC919D> <20><><EFBFBD><EFBFBD>븯. (wrong-LS<4C><53><EFBFBD> <20>룞<EFBFBD>씪<EFBFBD>븳 利앹긽<EC95B9>씠<EFBFBD>굹 <20>떎瑜<EB968E> <20>썝<EFBFBD>씤)
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: `ctx.lastPendingStepIndex=-1` (step-probe媛<65> UTF-8 <20>뿉<EFBFBD>윭濡<EC9CAD> WAITING 誘멸컧吏<ECBBA7>)<29>엫<EFBFBD>뿉<EFBFBD>룄 `Math.max(0, -1)=0`<60>쑝濡<EC919D> clamp<6D>릺<EFBFBD>뼱 議댁옱<EB8C81>븯吏<EBB8AF> <20>븡<EFBFBD>뒗 step 0<>뿉 RPC <20>쟾<EFBFBD>넚.
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.11): `effectiveStepIndex = stepIndex >= 0 ? stepIndex : (lastPendingStepIndex >= 0 ? lastPendingStepIndex : -1)`. `effectiveStepIndex < 0`<60>씠硫<EC94A0> RPC 釉붾줉 <20>쟾泥<EC9FBE> skip <20>넂 DOM click 吏곹뻾 (湲곗〈怨<E38088> <20>룞<EFBFBD>옉 <20>룞<EFBFBD>씪, LS reconnect <20>궘鍮<EAB698> <20>젣嫄<ECA0A3>).
|
||||
|
||||
- **二쇱쓽**: 湲곗〈 洹쒖튃 #14(`uint32`<60>뿉 <20>쓬<EFBFBD>닔 湲덉<E6B9B2><EB8D89>)<29><><EFBFBD> 異⑸룎泥섎읆 蹂댁씠<EB8C81>굹, `effectiveStepIndex=-1`<60>씪 <20>븣 RPC <20>옄泥대<EFA7A3><EB8C80> **<2A>쟾<EFBFBD>넚<EFBFBD>븯吏<EBB8AF> <20>븡<EFBFBD>쑝誘<EC919D>濡<EFBFBD>** <20>쐞諛<EC909E> <20>븘<EFBFBD>떂. RPC <20>쟾<EFBFBD>넚 <20>떆<EFBFBD>뿉<EFBFBD>뒗 <20>뿬<EFBFBD>쟾<EFBFBD>엳 <20>쑀<EFBFBD>슚<EFBFBD>븳 stepIndex留<78> <20>궗<EFBFBD>슜.
|
||||
|
||||
|
||||
|
||||
### [2026-03-25] [Architecture] Discord Signal Drop & Extension Freezes
|
||||
|
||||
- **利앹긽**: <20>옣<EFBFBD>떆媛<EB9686> <20>옄由щ퉬<D189><ED89AC><EFBFBD> <20>썑 蹂듦<E8B982><EB93A6> <20>떆 Discord濡<64> <20>듅<EFBFBD>씤 <20>떊<EFBFBD>샇媛<EC8387> <20>삤吏<EC82A4> <20>븡嫄곕굹 VS Code UI媛<49> 媛꾪뿉<EABEAA>쟻/吏<><EFA79E>냽<EFBFBD>쟻<EFBFBD>쑝濡<EC919D> 硫덉땄(Freeze).
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**:
|
||||
|
||||
1. `ws.onerror` 諛쒖깮 <20>썑 `onclose` <20>늻<EFBFBD>씫 <20>떆 <20>옱<EFBFBD>뿰寃<EBBFB0> 肄쒕갚 <20>샇異쒖씠 <20>씠猷⑥뼱吏<EBBCB1>吏<EFBFBD> <20>븡<EFBFBD>븘 臾댄븳 <20><><EFBFBD>湲<EFBFBD> (<28>옣<EFBFBD>떆媛<EB9686> 留덈퉬)
|
||||
|
||||
2. `ws-client` <20>옱<EFBFBD>뿰寃<EBBFB0> <20>떆 <20>늻<EFBFBD>쟻<EFBFBD>맂 200媛<30> <20>걧瑜<EAB1A7> <20>룞湲곗떇 burst <20>쟾<EFBFBD>넚<EFBFBD>븯<EFBFBD>뿬 Hub<75>쓽 <20>냽<EFBFBD>룄 <20>젣<EFBFBD>븳(60媛<30>/10珥<30>)<29>뿉 嫄몃젮 <20>솗<EFBFBD>젙 <20>쁺援<EC81BA> <20>궘<EFBFBD>젣<EFBFBD>맖
|
||||
|
||||
3. 濡쒖뺄 釉뚮┸吏<E294B8> `http-bridge.ts`<60>쓽 怨쇨굅 <20>쑀<EFBFBD>궛<EFBFBD>씤 `FALSE_POSITIVE_RE` <20>젙洹쒖떇<EC9296>씠 AI 怨좎쑀 踰꾪듉(Allow, Deny, Accept) 留덉<EFA78D><EB8D89> <20>븘<EFBFBD>꽣留곹븯<EAB3B9>뿬 Discord <20>쟾<EFBFBD>넚 <20>썝泥<EC8D9D> 李⑤떒
|
||||
|
||||
4. `step-probe.ts` <20>뤃留<EBA483> 猷⑦봽 <20>궡 <20>룞湲곗떇 <20>뙆<EFBFBD>씪 I/O <20>궗<EFBFBD>슜<EFBFBD>쑝濡<EC919D> <20>씤<EFBFBD>븳 <20>봽由ъ쫰
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.10): ws-client<6E>뿉 <20>븯<EFBFBD>뱶 <20><><EFBFBD><EFBFBD>엫<EFBFBD>븘<EFBFBD>썐 諛<> 50ms Paced-flush <20>쟻<EFBFBD>슜, http-bridge<67>쓽 <20>젙洹쒖떇 湲곕뒫 <20>셿<EFBFBD>솕, step-probe 鍮꾨룞湲<EBA39E> I/O <20>쟾<EFBFBD>솚 泥댁젣 <20>쟻<EFBFBD>슜, observer-script<70>쓽 <20>븘<EFBFBD>꽣<EFBFBD>맂 <20>떊<EFBFBD>샇 臾댄븳 HTTP <20>뤃留<EBA483> 諛⑹뼱 肄붾뱶 諛섏쁺.
|
||||
|
||||
- **二쇱쓽**: Extension <20>궡遺<EAB6A1> 濡쒖쭅 踰꾧렇<EABEA7><EBA087><EFBFBD><EFBFBD>쑝誘<EC919D>濡<EFBFBD> Hub(Python) 肄붾뱶<EBB6BE>뒗 嫄대뱶由ъ<E794B1><D18A> <20>븡<EFBFBD>쓬. Hub <20>냽<EFBFBD>룄 <20>젣<EFBFBD>븳<EFBFBD><EBB8B3><EFBFBD> <20>젙<EFBFBD>긽 諛⑹뼱 湲곗젣<EAB397>씠誘<EC94A0>濡<EFBFBD> <20>겢<EFBFBD>씪<EFBFBD>씠<EFBFBD>뼵<EFBFBD>듃 <20>떒<EFBFBD>쓽 Pacing<6E>씠 <20>삱諛붾Ⅸ 諛⑺뼢<E291BA>엫.
|
||||
|
||||
### [2026-03-24] DOM Observer /trigger-click <20>젋<EFBFBD>뜑留<EB9C91> <20>닚<EFBFBD>꽌 <20>삤<EFBFBD>옉<EFBFBD>룞 諛<> False Positive <20>봽由ъ쭠
|
||||
|
||||
- **利앹긽**: v0.5.9 <20>뙣移<EB99A3> <20>씠<EFBFBD>썑 肄붾뵫 <20>떆 Agent <20>솕硫댁씠 <20>걡<EFBFBD>엫<EFBFBD>뾾<EFBFBD>씠 <20>꽌紐<EABD8C> <20><><EFBFBD>湲<EFBFBD>(Pending) <20>긽<EFBFBD>깭濡<EAB9AD> 硫덉땄. <20>삉<EFBFBD>뒗 <20>뵒<EFBFBD>뒪肄붾뱶<EBB6BE>뿉<EFBFBD>꽌 `Approve` <20>떆 <20>뿉<EFBFBD>뵒<EFBFBD>꽣 <20>궡<EFBFBD>쓽 <20>뿁<EFBFBD>슧<EFBFBD>븳 `Run Test`(肄붾뱶 <20>젋利<ECA08B>)瑜<> <20>겢由<EAB2A2><E794B1>븿.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: <20>뀓<EFBFBD>뒪<EFBFBD>듃<EFBFBD><EB9383><EFBFBD> <20>젙洹쒖떇(`/^Run/i` <20>벑)<29>뿉留<EBBF89> <20>쓽議댄븯<EB8C84>뿬 `querySelectorAll`<60>쓣 <20>닔<EFBFBD>뻾<EFBFBD>븷 寃쎌슦, DOM <20>듃由ъ뿉 <20>젋<EFBFBD>뜑留곷맂 <20>닔留롮<EFA78D><EBA1AE> VS Code <20>꽕<EFBFBD>씠<EFBFBD>떚釉<EB969A> 肄붾뱶 <20>젋利<ECA08B> 踰꾪듉<EABEAA>쓣 Agent 踰꾪듉蹂대떎 癒쇱<E79992><EC87B1> 李얠븘踰꾨━<EABEA8>뒗 諛쒖깮 <20>쐞移<EC909E>(Context)<29>쓽 <20>븳怨꾩젏.
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.10):
|
||||
|
||||
1. 媛먯<E5AA9B><EBA8AF>(Scan): `isVSCodeMainWindow` 諛<> <20>깘<EFBFBD>깋 <20>끂<EFBFBD>뱶 `isBodyRoot` <20>솗<EFBFBD>씤<EFBFBD>쓣 <20>넻<EFBFBD>빐, <20>뿉<EFBFBD>뵒<EFBFBD>꽣 蹂몃Ц <20>쁺<EFBFBD>뿭<EFBFBD>뿉<EFBFBD>꽌<EFBFBD>뒗 "Run", "Approve" 媛먯<E5AA9B><EBA8AF>瑜<EFBFBD> <20>썝泥<EC8D9D> <20>젣嫄<ECA0A3> (<28>삤吏<EC82A4> <20>뙣<EFBFBD>꼸 <20>궡濡<EAB6A1> <20>븳<EFBFBD>젙).
|
||||
|
||||
2. <20>겢由<EAB2A2>(Trigger-click): `deepFindButtons()` <20>궡<EFBFBD>뿉<EFBFBD>꽌 `findPanel()`(<28>뿉<EFBFBD>씠<EFBFBD>쟾<EFBFBD>듃 <20>뙣<EFBFBD>꼸) -> <20>븣由<EBB8A3> Toasts -> Document 蹂몃Ц <20>닚<EFBFBD>쑝濡<EC919D> <20>깘<EFBFBD>깋 **<2A>슦<EFBFBD>꽑<EFBFBD>닚<EFBFBD>쐞(Priority)**瑜<> 媛뺤젣 <20>쟻<EFBFBD>슜.
|
||||
|
||||
- **二쇱쓽**: 踰꾪듉 <20>씠踰ㅽ듃 <20>썑<EFBFBD>궧 <20>떆 <20>뀓<EFBFBD>뒪<EFBFBD>듃 留ㅼ묶<E385BC>뿉留<EBBF89> <20>쓽議댄븯吏<EBB8AF> 留먭퀬, 諛섎뱶<EC848E>떆 DOM <20>깘<EFBFBD>깋 <20>슦<EFBFBD>꽑<EFBFBD>닚<EFBFBD>쐞<EFBFBD><EC909E><EFBFBD> 而⑦뀓<E291A6>뒪<EFBFBD>듃 踰붿쐞瑜<EC909E> <20>븿猿<EBB8BF> <20>븘<EFBFBD>꽣留곹븯<EAB3B9>뿬 False Positive瑜<65> 李⑤떒<E291A4>븷 寃<>.
|
||||
|
||||
|
||||
|
||||
### [2026-03-24] DOM Observer <20><><EFBFBD> VS Code Native UI Blind Spot
|
||||
|
||||
- **利앹긽**: "Always Allow" 諛<> <20>씪諛<EC94AA> "Allow Alt+<2B>넻" 沅뚰븳 <20>븣由<EBB8A3> 踰꾪듉<EABEAA>씠 <20>뵒<EFBFBD>뒪肄붾뱶 沅뚰븳 <20>꽱<EFBFBD>떛<EFBFBD>뿉<EFBFBD>꽌 <20>셿<EFBFBD>쟾<EFBFBD>엳 <20>늻<EFBFBD>씫<EFBFBD>맖.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: VS Code <20>꽕<EFBFBD>씠<EFBFBD>떚釉<EB969A> <20>븣由<EBB8A3> 諛<> 梨꾪똿 <20>뙣<EFBFBD>꼸 <20>궡<EFBFBD>쓽 踰꾪듉<EABEAA><EB9389><EFBFBD> `<button>` <20>깭洹<EAB9AD> <20><><EFBFBD><EFBFBD>떊 `<a role="button">`, `<vscode-button>` <20>벑<EFBFBD>쓣 <20>궗<EFBFBD>슜<EFBFBD>븯<EFBFBD>뒗<EFBFBD>뜲, 湲곗〈 DOM scan 濡쒖쭅<EC9296>씠 `querySelectorAll('button')`<60>쑝濡<EC919D> <20>븯<EFBFBD>뱶肄붾뵫<EBB6BE>릺<EFBFBD>뼱 <20>끂<EFBFBD>뱶瑜<EBB1B6> <20>븘<EFBFBD>삁 李얠<EFA7A1><EC96A0> 紐삵븿. (異붽<E795B0><EBB6BD>濡<EFBFBD> Always Allow <20>젙洹쒖떇 <20>늻<EFBFBD>씫)
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.9): DOM scan, 由ъ뒯 <20>썒 <20>벑 紐⑤뱺 <20>깘<EFBFBD>깋 濡쒖쭅 <20><><EFBFBD><EFBFBD>젆<EFBFBD>꽣瑜<EABDA3> `button, [role="button"], vscode-button, .monaco-text-button` <20>쑝濡<EC919D> <20>쟾硫<EC9FBE> 媛쒗렪. <20>젙洹쒖떇<EC9296>쓣 `/^(?:Always )?Allow/i`濡<> <20>닔<EFBFBD>젙.
|
||||
|
||||
|
||||
|
||||
### [2026-03-24] Python Hub <20><><EFBFBD> 醫<>鍮<EFBFBD> 而ㅻ꽖<E385BB>뀡 諛<> UI <20>봽由ъ쭠
|
||||
|
||||
- **利앹긽**: `npm run` 紐낅졊<EB8285>씠 `<EFBFBD>떎<EFBFBD>뻾 <20>젙梨<ECA099>` 愿<><E684BF>젴 <20>삤瑜섎줈 <20>떎<EFBFBD>뙣
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: PowerShell <20>뒪<EFBFBD>겕由쏀듃 <20>떎<EFBFBD>뻾 <20>젙梨낆씠 <20>젣<EFBFBD>븳<EFBFBD>쟻
|
||||
|
||||
- **<2A>빐寃<EBB990>**: `cmd /c npm run dev` <20>삎<EFBFBD>떇<EFBFBD>쑝濡<EC919D> cmd瑜<64> <20>넻<EFBFBD>빐 <20>떎<EFBFBD>뻾
|
||||
|
||||
- **二쇱쓽**: npm 愿<><E684BF>젴 紐낅졊<EB8285><ECA18A><EFBFBD> <20>빆<EFBFBD>긽 `cmd /c` <20>젒<EFBFBD>몢<EFBFBD>뼱 <20>궗<EFBFBD>슜 沅뚯옣
|
||||
|
||||
|
||||
|
||||
### [2026-03-08] PowerShell curl <20><><EFBFBD> Invoke-WebRequest 異⑸룎
|
||||
|
||||
- **利앹긽**: `curl` 紐낅졊<EB8285>씠 <20>삁<EFBFBD>긽怨<EAB8BD> <20>떎瑜<EB968E> <20>쓳<EFBFBD>떟 <20>삎<EFBFBD>떇<EFBFBD>쓣 諛섑솚
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: PowerShell<6C>뿉<EFBFBD>꽌 `curl`<60><><EFBFBD> `Invoke-WebRequest`<60>쓽 蹂꾩묶
|
||||
|
||||
- **<2A>빐寃<EBB990>**: **`curl.exe`**瑜<> 紐낆떆<EB8286>쟻<EFBFBD>쑝濡<EC919D> <20>궗<EFBFBD>슜
|
||||
|
||||
- **二쇱쓽**: HTTP 愿<><E684BF>젴 紐⑤뱺 紐낅졊<EB8285>뿉<EFBFBD>꽌 `curl.exe` <20>궗<EFBFBD>슜 <20>븘<EFBFBD>닔
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 誘명빐寃<EBB990> <20>씠<EFBFBD>뒋
|
||||
|
||||
|
||||
|
||||
### [2026-03-23/24] <20>룊<EFBFBD>깮 吏<><EFA79E>냽<EFBFBD>릺<EFBFBD>뒗 WebSocket 醫<>鍮<EFBFBD> 而ㅻ꽖<E385BB>뀡 諛<> False Positive 媛뺤젣 <20>뿰寃<EBBFB0> <20>걡源<EAB1A1> (v0.5.5 <20>넂 0.5.8)
|
||||
|
||||
- **利앹긽**:
|
||||
|
||||
1. (v0.5.5) <20>젅<EFBFBD>쟾 紐⑤뱶 蹂듦뎄 <20>떆 <20>떎<EFBFBD>뿰寃곗씠 <20>걡<EFBFBD>뼱議뚯쓬<EB9AAF>뿉<EFBFBD>룄 <20>솗<EFBFBD>옣<EFBFBD>씠 <20>씠瑜<EC94A0> <20>씤吏<EC94A4><EFA79E>븯吏<EBB8AF> 紐삵븯<EC82B5>뒗 醫<>鍮<EFBFBD>(Half-open) <20>냼耳<EB83BC> 諛쒖깮.
|
||||
|
||||
2. (v0.5.6) 醫<>鍮<EFBFBD> <20>냼耳볦쓣 <20>옟湲<EC989F> <20>쐞<EFBFBD>빐 10珥<30> <20><><EFBFBD><EFBFBD>씠癒<EC94A0>(`pongTimeoutTimer`)瑜<> <20>꽔<EFBFBD>뿀<EFBFBD>쑝<EFBFBD>굹, VS Code<64>쓽 臾닿굅<EB8BBF>슫 <20>뙆<EFBFBD>씪 寃<><E5AF83>깋 <20>떆 Event Loop媛<70> 釉붾줈<EBB6BE>궧<EFBFBD>릺硫<EBA6BA> 硫<>姨≫븳 <20>뿰寃곗씤<EAB397>뜲<EFBFBD>룄 <20>뿀<EFBFBD>쐞 <20><><EFBFBD><EFBFBD>엫<EFBFBD>븘<EFBFBD>썐(False Positive) <20>뙋<EFBFBD>젙<EFBFBD>쑝濡<EC919D> <20>뿰寃곗쓣 媛뺤젣 醫낅즺<EB8285>븿. <20>씠濡<EC94A0> <20>씤<EFBFBD>빐 <20>늻<EFBFBD>쟻<EFBFBD>맂 <20>옱<EFBFBD>뿰寃<EBBFB0> <20>뵜<EFBFBD>젅<EFBFBD>씠(Exponential Backoff)媛<> 60珥덇퉴吏<ED89B4> <20>뒛<EFBFBD>뼱<EFBFBD>굹硫댁꽌 <20>솗<EFBFBD>옣<EFBFBD>씠 <20>떖媛곹븯寃<EBB8AF> 硫덉땄(Freeze).
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: Node.js `ws` <20>씪<EFBFBD>씠釉뚮윭由ъ쓽 `ws.ping()`<60><><EFBFBD> 鍮꾨룞湲<EBA39E> I/O <20>꽕<EFBFBD>듃<EFBFBD>썙<EFBFBD>겕 <20>걧瑜<EAB1A7> <20><><EFBFBD>吏<EFBFBD>留<EFBFBD>, `setTimeout(..., 10000)` <20><><EFBFBD><EFBFBD>엫<EFBFBD>븘<EFBFBD>썐<EFBFBD><EC8D90><EFBFBD> Event Loop 釉붾줈<EBB6BE>궧 <20>빐<EFBFBD>젣 吏곹썑 怨㏓컮濡<ECBBAE> 留뚮즺<EB9AAE>릺<EFBFBD>뼱 踰꾨┝. <20>뵲<EFBFBD>씪<EFBFBD>꽌 <20>꽕<EFBFBD>듃<EFBFBD>썙<EFBFBD>겕 I/O <20>쓳<EFBFBD>떟(pong)蹂대떎 濡쒖뺄 <20><><EFBFBD><EFBFBD>씠癒멸<E79992><EBA9B8> 癒쇱<E79992><EC87B1> <20>꽣<EFBFBD>졇<EFBFBD>꽌 <20>젙<EFBFBD>긽<EFBFBD>쟻<EFBFBD>씤 <20>냼耳볦쓣 二쎌엫.
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.8 <20>셿<EFBFBD>꽦):
|
||||
|
||||
- <20>쐞<EFBFBD>뿕<EFBFBD>븳 `setTimeout` 諛⑹떇 <20>룓湲<EBA393>.
|
||||
|
||||
- 湲곗〈<EAB397>쓽 25珥<35> 二쇨린 `setInterval` <20>븯<EFBFBD>듃鍮꾪듃 猷⑦봽 <20>궡遺<EAB6A1><E981BA>뿉 `Date.now() - lastPongTime > 60000` (60珥<30> 珥덇낵 <20>떆 <20><><EFBFBD><EFBFBD>엫<EFBFBD>븘<EFBFBD>썐) 寃<>利<EFBFBD> 濡쒖쭅<EC9296>쓣 <20>룄<EFBFBD>엯.
|
||||
|
||||
- 留뚯빟 Event Loop媛<70> <20>닔<EFBFBD>떗 珥<> 諛<>由щ뜑<D189>씪<EFBFBD>룄, 釉붾줈<EBB6BE>궧 <20>빐<EFBFBD>젣 <20>썑 <20>걧<EFBFBD>맂 I/O <20>씠踰ㅽ듃(`pong`)媛<> `setInterval` <20><><EFBFBD><EFBFBD>씠癒<EC94A0> 肄쒕갚 <20>씠<EFBFBD>쟾<EFBFBD>뿉 癒쇱<E79992><EC87B1> 泥섎━<EC848E>릺嫄곕굹(Node.js Phase 洹쒖튃), <20>쟻<EFBFBD>뼱<EFBFBD>룄 60珥덈씪<EB8D88>뒗 踰꾪띁 <20>뜒遺꾩뿉 **False Positive 媛<><E5AA9B>뒫<EFBFBD>꽦<EFBFBD>쓣 <20>썝泥<EC8D9D> 李⑤떒**<2A>븿怨<EBB8BF> <20>룞<EFBFBD>떆<EFBFBD>뿉 醫<>鍮<EFBFBD> <20>냼耳볦쓣 <20>븞<EFBFBD>젙<EFBFBD>쟻<EFBFBD>쑝濡<EC919D> <20>젣嫄고븿.
|
||||
|
||||
- **二쇱쓽**: Node.js<6A>쓽 <20>떒<EFBFBD>씪 <20>뒪<EFBFBD>젅<EFBFBD>뱶 Event Loop <20>솚寃<EC869A>(<28>듅<EFBFBD>엳 臾닿굅<EB8BBF>슫 <20>룞湲<EBA39E> <20>옉<EFBFBD>뾽<EFBFBD>씠 <20>옦<EFBFBD><EC98A6><EFBFBD> VS Code Extension)<29>뿉<EFBFBD>꽌 <20>꽕<EFBFBD>듃<EFBFBD>썙<EFBFBD>겕 I/O瑜<4F> 濡쒖뺄 `setTimeout`怨<> 寃쎌<(Race)<29>떆<EFBFBD>궎<EFBFBD>뒗 <20>꽕怨꾨뒗 <20>븘<EFBFBD>뿰<EFBFBD>쟻<EFBFBD>쑝濡<EC919D> False Positive瑜<65> <20>궠<EFBFBD>쓬. Timestamp(`Date.now()`) 湲곕컲 媛꾧꺽 寃<>利<EFBFBD>(Interval check)<29>씠 <20>썾<EFBFBD>뵮 <20>븞<EFBFBD>쟾<EFBFBD>븿.
|
||||
|
||||
|
||||
|
||||
### [2026-03-11] rejectAgentStep / !stop <20><><EFBFBD> AG 誘몃벑濡<EBB291> 而ㅻ㎤<E385BB>뱶 + <20>젋<EFBFBD>뜑<EFBFBD>윭 <20>쟾<EFBFBD>슜 <20>븿<EFBFBD>닔 + <20>뒪<EFBFBD>뀒<EFBFBD>씪 <20>봽由щ<E794B1>명떚釉<EB969A>
|
||||
|
||||
- **利앹긽**: `!stop` 紐낅졊<EB8285>씠 AI瑜<49> 硫덉텛吏<ED859B> 紐삵븿. 濡쒓렇: "No active cascade" / "no session tracked yet"
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: (1) `antigravity.agent.rejectAgentStep`<60><><EFBFBD> AG 誘몃벑濡<EBB291> 而ㅻ㎤<E385BB>뱶. (2) <20><><EFBFBD>泥댄븳 `getActiveCascadeId()`<60>뒗 **<2A>젋<EFBFBD>뜑<EFBFBD>윭(DOM) <20>쟾<EFBFBD>슜 <20>븿<EFBFBD>닔** <20><><EFBFBD> Extension host<73>뿉<EFBFBD>꽌 <20>빆<EFBFBD>긽 `undefined` 諛섑솚. (3) **v0.4.5 <20>닔<EFBFBD>젙<EFBFBD>룄 <20>떎<EFBFBD>뙣**: `extension.ts`<60>쓽 `getActiveSessionId: () => activeSessionId`媛<> module-level <20>뒪<EFBFBD>듃留<EB9383> <20>봽由щ<E794B1>명떚釉뚮<E98789><EB9AAE> 李몄“ <20><><EFBFBD> step-probe媛<65> `ctx.activeSessionId`瑜<> <20>뾽<EFBFBD>뜲<EFBFBD>씠<EFBFBD>듃<EFBFBD>빐<EFBFBD>룄 extension.ts<74>쓽 蹂<><E8B982>닔<EFBFBD>뒗 遺덈<E981BA><EB8D88> (<28>봽由щ<E794B1>명떚釉<EB969A> 蹂듭궗)
|
||||
|
||||
- **<2A>빐寃<EBB990>** (2026-03-18 v0.4.6): `step-probe.ts`<60>뿉<EFBFBD>꽌 `getActiveSessionId()` getter <20>븿<EFBFBD>닔 export <20>넂 extension.ts closures<65>뿉<EFBFBD>꽌 `getStepProbeSessionId()` <20>샇異<EC8387>. <20>씠<EFBFBD>젣 step-probe<62>쓽 live `ctx.activeSessionId`瑜<> 吏곸젒 <20>씫<EFBFBD>쓬 (`ab0c116`)
|
||||
|
||||
- **二쇱쓽**: JS<4A>뿉<EFBFBD>꽌 **string/number<65>뒗 <20>봽由щ<E794B1>명떚釉뚮씪 李몄“ <20>쟾<EFBFBD>떖 遺덇<E981BA><EB8D87>** <20><><EFBFBD> 媛앹껜 <20>냽<EFBFBD>꽦<EFBFBD>쓣 怨듭쑀<EB93AD>븯<EFBFBD>젮硫<ECA0AE> getter <20>븿<EFBFBD>닔<EFBFBD>굹 媛앹껜 <20>옒<EFBFBD>띁 <20>궗<EFBFBD>슜 <20>븘<EFBFBD>닔
|
||||
|
||||
- **Vikunja**: #411, #410
|
||||
|
||||
|
||||
|
||||
### [2026-03-19] browser_subagent Allow <20><><EFBFBD> <20>옒紐삳맂 RPC payload
|
||||
|
||||
- **利앹긽**: <20>꽌釉<EABD8C> <20>뿉<EFBFBD>씠<EFBFBD>쟾<EFBFBD>듃 "execute JavaScript on localhost" Allow 踰꾪듉<EABEAA>씠 <20>옄<EFBFBD>룞 <20>듅<EFBFBD>씤<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: `step-probe.ts`<60>뿉<EFBFBD>꽌 `browser_subagent` toolName<6D>씠 step_type 遺꾨쪟 <20>뾾<EFBFBD>씠 raw toolName<6D>쑝濡<EC919D> <20>쟾<EFBFBD>떖 <20>넂 `approval-handler.ts`<60>뿉<EFBFBD>꽌 `runExtensionCode` 留ㅽ븨<E385BD>뿉 <20>룷<EFBFBD>븿<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>븘 default `runCommand` RPC payload <20>궗<EFBFBD>슜 <20>넂 AG媛<47> <20>옒紐삳맂 interaction type<70>쑝濡<EC919D> 臾댁떆
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.1): `approval-handler.ts` L384<38>뿉 `browser_subagent` 異붽<E795B0><EBB6BD>, `step-probe.ts` L481/L549<34>뿉 `browser_subagent`/`open_browser_url` step_type 遺꾨쪟 異붽<E795B0><EBB6BD> (`549af6d`)
|
||||
|
||||
- **二쇱쓽**: <20>깉濡쒖슫 AG <20>룄援<EBA384> 異붽<E795B0><EBB6BD> <20>떆 諛섎뱶<EC848E>떆 (1) step-probe step_type 留ㅽ븨 (2) approval-handler RPC payload 留ㅽ븨 <20>뼇履<EBBC87> 紐⑤몢 <20>뾽<EFBFBD>뜲<EFBFBD>씠<EFBFBD>듃
|
||||
|
||||
|
||||
|
||||
### [2026-03-21] Idle<6C>넂Resume <20>떊<EFBFBD>샇 <20>냼<EFBFBD>떎 <20><><EFBFBD> 3以<33> 踰꾧렇
|
||||
|
||||
- **利앹긽**: AG <20>옣<EFBFBD>떆媛<EB9686> idle <20>썑 <20>옉<EFBFBD>뾽 <20>옱媛<EC98B1> <20>떆 Discord <20>듅<EFBFBD>씤 <20>떊<EFBFBD>샇媛<EC8387> <20>쟾<EFBFBD>떖<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: (1) `ws-client.ts` `auth_fail` <20>떆 `shouldReconnect=false` <20><><EFBFBD> JWT 24h 留뚮즺 <20>떆 WS <20>쁺援<EC81BA> 醫낅즺. (2) `hub.py` `_disconnect`<60>뿉<EFBFBD>꽌 <20>쑀<EFBFBD>씪 <20>뿰寃<EBBFB0> <20>떆 `pending_owners` <20>궘<EFBFBD>젣 <20><><EFBFBD> <20>옱<EFBFBD>뿰寃<EBBFB0> <20>썑 Discord 踰꾪듉 臾댄슚. (3) `step-probe.ts` `stallProbed=true` + `lastPendingStepIndex=N`<60>씠 WS <20>옱<EFBFBD>뿰寃<EBBFB0> <20>떆 由ъ뀑 <20>븞 <20>맖 <20><><EFBFBD> WAITING step <20>옱<EFBFBD>쟾<EFBFBD>넚 <20>쁺援<EC81BA> 李⑤떒
|
||||
|
||||
- **<2A>빐寃<EBB990>** (v0.5.2): (1) `auth_fail` <20>넂 `registrationCode` <20>옱<EFBFBD>떆<EFBFBD>룄. (2) `pending_owners` orphan 留덉빱濡<EBB9B1> 蹂댁〈+<2B>옱<EFBFBD>븷<EFBFBD>떦. (3) `resetPendingStateForReconnect()` + `onConnected`<60>뿉<EFBFBD>꽌 <20>샇異<EC8387>
|
||||
|
||||
- **二쇱쓽**: WS `onConnected`<60>뿉<EFBFBD>꽌 諛섎뱶<EC848E>떆 step-probe <20>긽<EFBFBD>깭 由ъ뀑 <20>븘<EFBFBD>닔. `stallProbed`/`lastPendingStepIndex`<60>뒗 TTL <20>뾾<EFBFBD>뒗 <20>쁺援<EC81BA> 媛<>
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
> v0.4.5 <20>닔<EFBFBD>젙 <20>궗<EFBFBD>빆(Hub pending_owners, diff_review WS, auto_approve <20>씠以묒벐湲<EBB290>, WS dual-write, ApprovalView fallback)<29><><EFBFBD>
|
||||
|
||||
> 肄붾뱶 <20>닔<EFBFBD>젙 <20>셿猷뚮맖. E2E <20>넻<EFBFBD>빀 寃<>利앹<EFA79D><EC95B9> Vikunja #410<31>뿉<EFBFBD>꽌 異붿쟻 以<>.
|
||||
|
||||
|
||||
|
||||
### [2026-03-21] stepIndex=-1 <20><><EFBFBD> AG proto uint32 <20>뿉<EFBFBD>윭
|
||||
|
||||
- **利앹긽**: DOM observer媛<72> Allow 踰꾪듉 媛먯<E5AA9B><EBA8AF> <20>넂 Discord <20>듅<EFBFBD>씤 <20>넂 RPC `HandleCascadeUserInteraction` 400 <20>뿉<EFBFBD>윭
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: DOM observer 寃쎈줈<EC8E88>뒗 step index瑜<78> 紐⑤쫫 <20>넂 `stepIndex=-1` <20>쟾<EFBFBD>떖 <20>넂 AG proto `uint32` <20>븘<EFBFBD>뱶<EFBFBD>뿉 <20>쓬<EFBFBD>닔 遺덇<E981BA><EB8D87>
|
||||
|
||||
- **<2A>빐寃<EBB990>**: `Math.max(0, ...)` 濡<> clamp. `permission` type <20>넂 `runExtensionCode.confirm` 留ㅽ븨 異붽<E795B0><EBB6BD> (v0.5.4)
|
||||
|
||||
- **二쇱쓽**: DOM observer 寃쎈줈<EC8E88>쓽 step_type<70><65><EFBFBD> <20>빆<EFBFBD>긽 `stepIndex=-1`<60>씪 <20>닔 <20>엳<EFBFBD>쑝誘<EC919D>濡<EFBFBD> proto <20>쟾<EFBFBD>떖 <20>쟾 <20>뼇<EFBFBD>닔 蹂댁옣 <20>븘<EFBFBD>닔
|
||||
|
||||
|
||||
|
||||
### [2026-03-21] reviewAbsoluteUris <20><><EFBFBD> latestNotifyUserStep <20>븘<EFBFBD>뱶紐<EBB1B6> 遺덉씪移<EC94AA>
|
||||
|
||||
- **利앹긽**: `notify_user`<60>쓽 PathsToReview <20>뙆<EFBFBD>씪 由대젅<EB8C80>씠媛<EC94A0> <20>븳 踰덈룄 <20>옉<EFBFBD>룞<EFBFBD>븯吏<EBB8AF> <20>븡<EFBFBD>쓬
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: AG <20>떎<EFBFBD>젣 <20>븘<EFBFBD>뱶紐<EBB1B6> `reviewAbsoluteUris` vs 肄붾뱶 `pathsToReview`/`paths_to_review`/`filePaths`
|
||||
|
||||
- **<2A>빐寃<EBB990>**: `reviewAbsoluteUris` 瑜<> 泥<> 踰덉㎏ <20>썑蹂대줈 異붽<E795B0><EBB6BD> (v0.5.3)
|
||||
|
||||
- **二쇱쓽**: AG RPC <20>븘<EFBFBD>뱶紐낆<EFA78F><EB8286> extension.log `[NOTIFY-STEP] keys=` 濡<> <20>솗<EFBFBD>씤 媛<><E5AA9B>뒫. 異붿륫 湲덉<E6B9B2><EB8D89>
|
||||
|
||||
|
||||
|
||||
### [2026-03-21] <20>꽭<EFBFBD>뀡 <20>쟾<EFBFBD>솚 <20><><EFBFBD> 泥<> WAITING 媛먯<E5AA9B><EBA8AF> 20-25s 吏<><EFA79E>뿰
|
||||
|
||||
- **利앹긽**: <20>깉 <20><><EFBFBD><EFBFBD>솕 <20>떆<EFBFBD>옉 <20>썑 泥<> run_command <20>듅<EFBFBD>씤<EFBFBD>씠 Discord<72>뿉 <20>븞 <20>삤怨<EC82A4> AG<41>뿉<EFBFBD>꽌 吏곸젒 <20>듅<EFBFBD>씤<EFBFBD>빐<EFBFBD>빞 <20>븿
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: `lastModTime=''` 由ъ뀑 <20>넂 `modTimeChanged=true` <20>넂 THINKING 遺꾧린 諛섎났 <20>넂 probe 15-25s 吏<><EFA79E>뿰
|
||||
|
||||
- **<2A>빐寃<EBB990>**: `lastModTime=currentModTime` + `return` <20>젣嫄<ECA0A3> + 利됱떆 probe 媛뺤젣 + <20>쉶洹<EC89B6> 媛<><E5AA9B>뱶 異붽<E795B0><EBB6BD> (v0.5.3)
|
||||
|
||||
- **二쇱쓽**: <20>꽭<EFBFBD>뀡 <20>쟾<EFBFBD>솚 <20>떆 `wasRunning`/`pendingModifiedFiles` 由ъ뀑 <20>븘<EFBFBD>닔 (<28>씠<EFBFBD>쟾 <20>꽭<EFBFBD>뀡 <20>옍<EFBFBD>뿬臾쇰줈 false diff_review 諛⑹<E8AB9B><E291B9>)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## <20>빑<EFBFBD>떖 <20>옉<EFBFBD>뾽 洹쒖튃 (怨쇨굅 <20>씠<EFBFBD>뒋<EFBFBD>뿉<EFBFBD>꽌 諛섎났<EC848E>맂 <20>뙣<EFBFBD>꽩)
|
||||
|
||||
|
||||
|
||||
> <20>븘<EFBFBD>옒<EFBFBD>뒗 怨쇨굅 <20>씠<EFBFBD>뒋<EFBFBD>뿉<EFBFBD>꽌 諛섎났<EC848E>쟻<EFBFBD>쑝濡<EC919D> <20>굹<EFBFBD><EAB5B9><EFBFBD><EFBFBD>궃 <20>뙣<EFBFBD>꽩<EFBFBD>쓣 洹쒖튃<EC9296>쑝濡<EC919D> <20>젙由ы븳 寃껋엯<EABB8B>땲<EFBFBD>떎.
|
||||
|
||||
|
||||
|
||||
| # | 洹쒖튃 | 愿<><E684BF>젴 <20>씠<EFBFBD>뒋 (archive 李몄“) |
|
||||
|
||||
|---|------|--------------------------|
|
||||
|
||||
| 1 | **Hub WS<57><53><EFBFBD> file bridge<67>뒗 <20>긽<EFBFBD>샇 諛고<E8AB9B><EAB3A0><EFBFBD>쟻** <20><><EFBFBD> `if hub: ws + return` / `else: file` | WS dual-write, _auto_approve <20>씠以<EC94A0> <20>벐湲<EBB290> |
|
||||
|
||||
| 2 | **WS 寃쎈줈 異붽<E795B0><EBB6BD> <20>떆 file-bridge<67>쓽 紐⑤뱺 遺꾧린瑜<EBA6B0> <20>룷<EFBFBD>똿** | diff_review WS regression |
|
||||
|
||||
| 3 | **AG RPC `{}` <20>쓳<EFBFBD>떟<EFBFBD><EB969F><EFBFBD> <20>떎<EFBFBD>뙣濡<EB99A3> 媛꾩<** <20><><EFBFBD> 硫붿꽌<EBB6BF>뱶紐<EBB1B6> <20><><EFBFBD><EFBFBD>젮<EFBFBD>룄 <20>뿉<EFBFBD>윭 <20>뾾<EFBFBD>씠 `{}` 諛섑솚 | AcknowledgeCascadeCodeEdit |
|
||||
|
||||
| 4 | **ResolveOutstandingSteps<70>뒗 CANCEL <20>룞<EFBFBD>옉** <20><><EFBFBD> <20>듅<EFBFBD>씤<EFBFBD>뿉 <20>젅<EFBFBD><ECA085><EFBFBD> <20>궗<EFBFBD>슜 湲덉<E6B9B2><EB8D89> | Step probe reject |
|
||||
|
||||
| 5 | **Extension 肄붾뱶 <20>닔<EFBFBD>젙 <20>썑 諛섎뱶<EC848E>떆 VSIX 鍮뚮뱶 + AG <20><><EFBFBD> <20>옱<EFBFBD>떆<EFBFBD>옉** | Extension 踰꾩쟾 誘몃같<EBAA83>룷 |
|
||||
|
||||
| 6 | **HTML <20>뙣移<EB99A3> 蹂<>寃<EFBFBD> <20>떆 V8 CachedData <20>궘<EFBFBD>젣 <20>븘<EFBFBD>닔** | V8 CachedData, CSP |
|
||||
|
||||
| 7 | **`bridge/pending/` 議곗옉 <20>떆 諛섎뱶<EC848E>떆 `project_name` + `conversation_id` <20>븘<EFBFBD>꽣** | <20>겕濡쒖뒪 <20>봽濡쒖젥<EC9296>듃 DEDUP MERGE |
|
||||
|
||||
| 8 | **`processResponseFile` <20>긽<EFBFBD>깭 由ъ뀑<D18A><EB8091><EFBFBD> `sawRunningAfterPending=true`留<>** | processResponseFile 臾댄븳 猷⑦봽 |
|
||||
|
||||
| 9 | **fs.watch Windows 遺덉븞<EB8D89>젙 <20><><EFBFBD> 諛섎뱶<EC848E>떆 polling fallback 蹂묓뻾** | fs.watch silent fail |
|
||||
|
||||
| 10 | **diff_review<65>뒗 VS Code 而ㅻ㎤<E385BB>뱶留<EBB1B6> <20>쑀<EFBFBD>슚** <20><><EFBFBD> RPC 3媛<33> <20>쟾<EFBFBD>왂 紐⑤몢 <20>떎<EFBFBD>뙣 <20>솗<EFBFBD>젙 | diff_review RPC dead-end |
|
||||
|
||||
| 11 | **HttpBridgeContext<78>뿉 <20>봽由щ<E794B1>명떚釉<EB969A> by-value 蹂듭궗 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> 蹂꾨룄 媛앹껜 <20>깮<EFBFBD>꽦 <20>떆 getter <20>궗<EFBFBD>슜 | HttpBridgeContext stale primitive |
|
||||
|
||||
| 12 | **<2A>깉 AG <20>룄援<EBA384> 異붽<E795B0><EBB6BD> <20>떆 step-probe step_type 留ㅽ븨 + approval-handler RPC payload 留ㅽ븨 <20>뼇履<EBBC87> <20>븘<EFBFBD>닔** | browser_subagent Allow |
|
||||
|
||||
| 13 | **WS `onConnected`<60>뿉<EFBFBD>꽌 step-probe <20>긽<EFBFBD>깭 由ъ뀑 <20>븘<EFBFBD>닔** <20><><EFBFBD> `stallProbed`/`lastPendingStepIndex`<60>뒗 TTL <20>뾾<EFBFBD>뒗 <20>쁺援<EC81BA> 媛<> | Idle<6C>넂Resume <20>떊<EFBFBD>샇 <20>냼<EFBFBD>떎 |
|
||||
|
||||
| 14 | **AG proto `uint32` <20>븘<EFBFBD>뱶<EFBFBD>뿉 <20>쓬<EFBFBD>닔 <20>쟾<EFBFBD>떖 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> `stepIndex` <20>벑<EFBFBD><EBB291><EFBFBD> `Math.max(0, ...)` <20>븘<EFBFBD>닔 | stepIndex=-1 RPC 400 |
|
||||
|
||||
| 15 | **RPC "input not registered" = wrong-LS <20>뿰寃<EBBFB0>** <20><><EFBFBD> `fixLSConnection()` <20>옄<EFBFBD>룞 <20>옱<EFBFBD>떆<EFBFBD>룄 <20>븘<EFBFBD>닔, `lines.length<=1` 議곌린醫낅즺 湲덉<E6B9B2><EB8D89> | Deriva wrong-LS (v0.5.5) |
|
||||
|
||||
| 16 | **<2A>씡<EFBFBD>뒪<EFBFBD>뀗<EFBFBD>뀡(Bridge)<29><><EFBFBD> <20>옄<EFBFBD>쓽<EFBFBD>쟻 鍮꾩쫰<EABEA9>땲<EFBFBD>뒪 <20>뙋<EFBFBD>떒 <20>젅<EFBFBD><ECA085><EFBFBD> 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> `SafeToAutoRun` <20>벑<EFBFBD>쓽 議곌굔 釉뚮옖移<EC9896> 遺꾧린<EABEA7>뒗 紐⑤몢 遊뉗쑝濡<EC919D> <20>쐞<EFBFBD>엫 (Agnostic Bridge) | SafeToAutoRun Deadlock (v0.5.15) |
|
||||
|
||||
| 17 | **package.json 鍮뚮뱶 <20>뒪<EFBFBD>겕由쏀듃 媛뺤젣** <20><><EFBFBD> `vscode:prepublish` 異붽<E795B0><EBB6BD>濡<EFBFBD> <20>궊<EFBFBD><EAB68A><EFBFBD> <20>냼<EFBFBD>뒪 諛고룷 <20>썝泥<EC8D9D> 李⑤떒 | VSIX v0.5.15 鍮뚮뱶 <20>늻<EFBFBD>씫 |
|
||||
|
||||
| 18 | **<2A>룞湲곗떇 `cp.execSync` <20>궗<EFBFBD>슜 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> Windows <20>솚寃쎌뿉<EC8E8C>꽌 硫붿씤 <20>씠踰ㅽ듃猷⑦봽 <20>봽由ъ쭠 諛<> WS heartbeat <20>떒<EFBFBD>젅 <20>쑀諛<EC9180> | detectProjectName <20>봽由ъ쭠 |
|
||||
|
||||
|
||||
### [2026-04-09] [Bot/Extension] Discord Signal Relay Failure & Empty Body
|
||||
- **利앹긽**: <20>뵒<EFBFBD>뒪肄붾뱶 遊뉗<E9818A><EB8997> '<27>옄<EFBFBD>룞 <20>듅<EFBFBD>씤<EFBFBD>맖'<27>쓣 <20>쓣<EFBFBD>슦吏<EC8AA6>留<EFBFBD> <20>떎<EFBFBD>젣 肄붾뱶 蹂몃Ц<EBAA83>씠 <20>몴<EFBFBD>떆<EFBFBD>릺吏<EBA6BA> <20>븡怨<EBB8A1>, 梨꾨꼸<EABEA8>뿉 吏꾩쭨 梨꾪똿 硫붿떆吏<EB9686><EFA79E>굹 <20>븣由쇱씠 <20>뒪<EFBFBD>뙵 <20>걧 <20>뮘<EFBFBD>뿉 諛<><E8AB9B>젮 <20>쟾<EFBFBD>넚<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬.
|
||||
- **<2A>썝<EFBFBD>씤**: 1) observer-script.ts<74>뿉<EFBFBD>꽌 踰꾪듉 <20>뀓<EFBFBD>뒪<EFBFBD>듃 留ㅼ묶 <20>떆 Run <20>떒<EFBFBD>뼱<EFBFBD>쓽 寃쎄퀎(\b) 泥섎━瑜<E29481> <20>븯吏<EBB8AF> <20>븡<EFBFBD>븘 VS Code <20>븯<EFBFBD>떒<EFBFBD>쓽 'Running 1 command'瑜<> 媛<>濡쒖콈<EC9296>뼱 PENDING <20>뒪<EFBFBD>뙵 臾댄븳 <20>깮<EFBFBD>꽦. 2) bot.py<70>뿉<EFBFBD>꽌 <20>옄<EFBFBD>룞 <20>듅<EFBFBD>씤 Embed <20>깮<EFBFBD>꽦 <20>떆 req.description<6F>쓣 洹몃━吏<E29481> <20>븡怨<EBB8A1> 踰꾪듉 <20>뀓<EFBFBD>뒪<EFBFBD>듃(req.command)留<> <20>몴<EFBFBD>떆. 3) step-probe.ts<74>뿉<EFBFBD>꽌 <20>꽭<EFBFBD>뀡 援먯껜 <20>떆 理쒓렐 <20>븣由<EBB8A3> <20>씤<EFBFBD>뜳<EFBFBD>뒪 珥덇린<EB8D87>솕瑜<EC8695> <20>옒紐삵븯<EC82B5>뿬 <20>꽭<EFBFBD>뀡<EFBFBD>쓽 泥<> 硫붿떆吏<EB9686>瑜<EFBFBD> 臾댁“嫄<E2809C> <20>뱶濡<EBB1B6>.
|
||||
- **<2A>빐寃<EBB990>**: DOM 媛먯<E5AA9B><EBA8AF> <20>젙洹쒖떇<EC9296>뿉 \b 媛뺤젣 遺<><E981BA>뿬 (/Run\b/), bot.py<70>쓽 Auto-Approve 履<> Embed 蹂몃Ц<EBAA83>뿉 req.description <20>젋<EFBFBD>뜑留<EB9C91> 異붽<E795B0><EBB6BD>, step-probe.ts<74>뿉<EFBFBD>꽌 session init <20>떆 index瑜<78> -1濡<31> 由ъ뀑.
|
||||
- **二쇱쓽**: Native UI <20>뀓<EFBFBD>뒪<EFBFBD>듃 媛먯<E5AA9B><EBA8AF> <20>떆 <20>떒<EFBFBD>뼱 寃쎄퀎(\b)源뚯<E6BA90><EB9AAF> 寃<>利앺빐<EC95BA>빞 False Positive瑜<65> 留됱쓣 <20>닔 <20>엳<EFBFBD>쑝硫<EC919D>, Auto-Approve<76>뒗 諛섎뱶<EC848E>떆 蹂몃Ц<EBAA83>쓣 <20>끂異쒗빐<EC9297>빞 <20>븿.
|
||||
|
||||
### [2026-04-10] [Extension] AI Response Content Missing (Nested PlannerResponse)
|
||||
- **利앹긽**: <20>뵒<EFBFBD>뒪肄붾뱶 梨꾪똿諛⑹뿉 Agent<6E>쓽 <20>뀓<EFBFBD>뒪<EFBFBD>듃 <20>쓳<EFBFBD>떟(AI <20>쓳<EFBFBD>떟)<29>씠 <20>븘<EFBFBD>삁 <20>늻<EFBFBD>씫<EFBFBD>릺<EFBFBD>뼱 <20>쟾<EFBFBD>넚<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬.
|
||||
- **<2A>썝<EFBFBD>씤**: GetCascadeTrajectorySteps媛<73> 諛섑솚<EC8491>븯<EFBFBD>뒗 plannerResponse媛<65> <20>봽濡쒗넗肄<EB8497> 諛⑹떇<E291B9>뿉 <20>뵲<EFBFBD>씪 理쒖긽<EC9296>떒(s.plannerResponse)<29>씠 <20>븘<EFBFBD>땶 s.step.plannerResponse<73>뿉 以묒꺽<EBAC92>릺<EFBFBD>뼱 <20>뱾<EFBFBD>뼱<EFBFBD>삱 <20>닔 <20>엳<EFBFBD>쓬. 湲곗〈 <20>뙆<EFBFBD>꽌<EFBFBD>뒗 <20>븯<EFBFBD>뱶肄붾뵫<EBB6BE>맂 <20>븘<EFBFBD>뱶 諛<> <20>뵆<EFBFBD>옯 援ъ“留<E2809C> 議고쉶<EAB3A0>븯<EFBFBD>뿬 <20>쓳<EFBFBD>떟<EFBFBD>쓣 踰꾨┝.
|
||||
- **二쇱쓽**: AG RPC <20>븘<EFBFBD>뱶紐<EBB1B6> 援ъ“ 異붿륫 湲덉<E6B9B2><EB8D89>. <20>븘<EFBFBD>슂 <20>떆 <20>깒<EFBFBD>뱶諛뺤뒪濡<EB92AA> <20>몢 媛<>吏<EFBFBD> 援ъ“(Flat, Nested) 紐⑤몢 紐⑦궧<E291A6>븯<EFBFBD>뿬 吏곸젒 <20>뙆<EFBFBD>떛 <20>솗<EFBFBD>씤.
|
||||
|
||||
### [2026-04-10] [Extension] Fast Execution `<5s` Response Capture Missed (IDLE-to-IDLE)
|
||||
- **利앹긽**: <20>뵒<EFBFBD>뒪肄붾뱶濡<EBB1B6> <20>궡<EFBFBD>슜<EFBFBD>씠 <20>븘<EFBFBD>삁 <20>쟾<EFBFBD>떖<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>쓬. `[RT-CAPTURE]`, `[RESPONSE-CAPTURE]` 濡쒓렇 紐⑤몢 <20>쟾<EFBFBD><EC9FBE><EFBFBD> <20>궓吏<EAB693> <20>븡<EFBFBD>쓬.
|
||||
- **<2A>썝<EFBFBD>씤**: AI <20>쓳<EFBFBD>떟<EFBFBD>씠<EFBFBD>굹 肄붾뵫 <20>옉<EFBFBD>뾽<EFBFBD>씠 5珥<35>(<28>뤃留<EBA483> 二쇨린) 誘몃쭔<EBAA83>쑝濡<EC919D> 留ㅼ슦 鍮좊Ⅴ寃<E285A4> <20>걹<EFBFBD>굹硫<EAB5B9>, <20>솗<EFBFBD>옣<EFBFBD>씠 `IDLE -> IDLE` <20>긽<EFBFBD>깭留<EAB9AD> 愿<>李고븯硫<EBB8AF> `wasRunning` <20>뵆<EFBFBD>옒洹멸<E6B4B9><EBA9B8> `false`濡<> <20>쑀吏<EC9180><EFA79E>맖. 湲곗〈 `[RESPONSE-CAPTURE]` 議곌굔<EAB38C>떇(`wasRunning && !isRunning && currentCount > ...`)<29>씠 `wasRunning=false`濡<> <20>씤<EFBFBD>빐 釉붾줉<EBB6BE>릺<EFBFBD>뼱 罹≪쿂 <20>옄泥대<EFA7A3><EB8C80> <20>셿<EFBFBD>쟾<EFBFBD>엳 嫄대꼫<EB8C80>쎇寃<EC8E87> <20>맖.
|
||||
- **<2A>빐寃<EBB990>**: `wasRunning` 寃<>利앹쓣 <20>궘<EFBFBD>젣<EFBFBD>븯怨<EBB8AF> `!isRunning && currentCount > lastResponseCaptureStep` 議곌굔<EAB38C>쑝濡<EC919D> <20>셿<EFBFBD>솕<EFBFBD>븯<EFBFBD>뿬 <20>늻<EFBFBD>씫<EFBFBD>맂 step<65>씠 <20>엳<EFBFBD>쓣 <20>븣 臾댁“嫄<E2809C> 罹≪쿂<E289AA>븯<EFBFBD>룄濡<EBA384> 蹂<>寃<EFBFBD>. 異붽<E795B0><EBB6BD>濡<EFBFBD> <20>삤<EFBFBD>옒<EFBFBD>맂 `[RESPONSE-CAPTURE]` <20>궡 <20>븯<EFBFBD>뱶肄붾뵫 <20>뙆<EFBFBD>꽌瑜<EABD8C> `extractPlannerText`濡<> <20>씪<EFBFBD>썝<EFBFBD>솕 <20>쟻<EFBFBD>슜.
|
||||
- **二쇱쓽**: <20>뤃留<EBA483> 諛⑹떇<E291B9>뿉<EFBFBD>꽌<EFBFBD>뒗 <20>긽<EFBFBD>깭(RUNNING->IDLE) <20>쟾<EFBFBD>씠瑜<EC94A0> <20>솗<EFBFBD>떊<EFBFBD>븷 <20>닔 <20>뾾<EFBFBD>쑝誘<EC919D>濡<EFBFBD>, Step Count(<28>씤<EFBFBD>뜳<EFBFBD>뒪 <20>쟾吏<EC9FBE>)<29>씪<EFBFBD>뒗 100% <20>떊猶<EB968A> 媛<><E5AA9B>뒫<EFBFBD>븳 留덉빱瑜<EBB9B1> <20>넻<EFBFBD>빐 <20>깉 <20>쓳<EFBFBD>떟 <20>뿬遺<EBBFAC>瑜<EFBFBD> 媛먯<E5AA9B><EBA8AF><EFBFBD>빐<EFBFBD>빞 <20>븿.
|
||||
|
||||
### [2026-04-10] [Bot] chat_snapshot_scanner 臾댄븳 Abort 諛<> <20>뙆<EFBFBD>씪 <20>쟻泥<EC9FBB> (Exception <20>늻<EFBFBD>씫)
|
||||
- **利앹긽**: 遊뉗씠 <20>뵒<EFBFBD>뒪肄붾뱶濡<EBB1B6> AI <20>떟蹂<EB969F>(梨꾪똿 <20>뒪<EFBFBD>깄<EFBFBD>꺑)<29>쓣 <20>쟾<EFBFBD><EC9FBE><EFBFBD> <20>쟾<EFBFBD>넚<EFBFBD>븯吏<EBB8AF> 紐삵븯怨<EBB8AF> <20>젆<EFBFBD>씠 嫄몃┝. ridge/chat_snapshots/<2F>뿉 泥섎━<EC848E>릺吏<EBA6BA> <20>븡<EFBFBD><EBB8A1><EFBFBD> JSON <20>뙆<EFBFBD>씪<EFBFBD>씠 <20>닔<EFBFBD>떗 媛<> <20>쟻泥대맖.
|
||||
- **<2A>썝<EFBFBD>씤**: ot.py<70>쓽 chat_snapshot_scanner<65>뿉<EFBFBD>꽌 <20>뙆<EFBFBD>씪<EFBFBD>쓣 <20>닚<EFBFBD>쉶 <20>뙆<EFBFBD>떛<EFBFBD>븷 <20>븣 <20>궡遺<EAB6A1><E981BA>쓽 .unlink() 怨쇱젙<EC87B1>뿉<EFBFBD>꽌 諛쒖깮<EC9296>븯<EFBFBD>뒗 <20>삁<EFBFBD>쇅<EFBFBD>굹 discord.Embed <20>깮<EFBFBD>꽦 <20>삁<EFBFBD>쇅 <20>벑<EFBFBD>쓣 猷⑦봽 <20>븞<EFBFBD>뿉<EFBFBD>꽌 <20>옟<EFBFBD>븘二쇱<E4BA8C><EC87B1> 紐삵븿. 泥<> <20>뿉<EFBFBD>윭 <20>뙆<EFBFBD>씪(poison pill)<29>쓣 留뚮굹<EB9AAE>뒗 <20>닚媛<EB8B9A> 猷⑦봽 <20>쟾泥닿<EFA7A3><EB8BBF> <20>룺<EFBFBD>뙆<EFBFBD>릺<EFBFBD>뼱 <20>뮘履쎌쓽 <20>젙<EFBFBD>긽 <20>뙆<EFBFBD>씪<EFBFBD>뱾<EFBFBD>룄 <20>쁺<EFBFBD>썝<EFBFBD>엳 泥섎━<EC848E>릺吏<EBA6BA> <20>븡怨<EBB8A1> <20>떎<EFBFBD>쓬 <20>뤃 <20>뒪耳<EB92AA>以꾩뿉<EABEA9>꽌 <20>떎<EFBFBD>떆 泥<> <20>뙆<EFBFBD>씪<EFBFBD>뿉 留됲옒.
|
||||
- **<2A>빐寃<EBB990>**: 猷⑦봽 <20>궡遺<EAB6A1><E981BA>뿉 except Exception<6F>쓣 異붽<E795B0><EBB6BD><EFBFBD>븯<EFBFBD>뿬 <20>쟾<EFBFBD>뿭 <20>삁<EFBFBD>쇅瑜<EC8785> <20>옟<EFBFBD>븘 諛⑹뼱. <20>떎<EFBFBD>뙣<EFBFBD>븳 <20>뙆<EFBFBD>씪<EFBFBD><EC94AA><EFBFBD> glob<6F>뿉<EFBFBD>꽌 諛섎났 <20>떆<EFBFBD>룄<EFBFBD>릺吏<EBA6BA> <20>븡寃<EBB8A1> .json.failed濡<64> <20>슦<EFBFBD>쉶(rename)<29>떆耳<EB9686> <20>걧瑜<EAB1A7> 鍮꾩썙以<EC8D99>.
|
||||
- **二쇱쓽**: <20>뤃留<EBA483>/<2F>뒪罹먮꼫 or 猷⑦봽 <20>궡遺<EAB6A1><E981BA>뿉<EFBFBD>꽌<EFBFBD>뒗 媛쒕퀎 <20>븘<EFBFBD>씠<EFBFBD>뀥 <20>뙆<EFBFBD>떛 <20>떒怨꾩뿉<EABEA9>꽌 諛쒖깮 媛<><E5AA9B>뒫<EFBFBD>븳 紐⑤뱺 <20>삁<EFBFBD>쇅 <20>긽<EFBFBD>깭<EFBFBD>뿉 <20><><EFBFBD><EFBFBD>븳 Defensive Catch 諛<> Continue(<28>슦<EFBFBD>쉶) 濡쒖쭅<EC9296>씠 <20>븘<EFBFBD>닔<EFBFBD>엫.
|
||||
|
||||
### [2026-04-10] [Extension] GetAllCascadeTrajectories 10-Item Hard Limit Bypass (Signal Drop)
|
||||
- **증상**: 기존에 작성했던 { limit: 30 } 파라미터가 LS 백엔드에서 무시되어 최신 세션이 10개 제한에 걸려 잘려나감. (Discord로 메시지 단 한 글자도 안 넘어옴).
|
||||
- **원인**: GetAllCascadeTrajectories는 구조적으로 pagination 옵션을 무시하거나 강제 10 제한이 걸림.
|
||||
- **해결**: step-probe.ts에서 기본 GetAllCascadeTrajectories와 더불어 모든 트래젝토리를 덤프하는 GetDiagnostics API를 병행 호출하고 머지하여 최신 Session ID를 놓치지 않고 추출하게 함.
|
||||
- **주의**: LS Backend에서 정의한 RPC의 한계상 Argument 조작으로 제한을 회피할 수 없으므로, 향후 GetDiagnostics 등 백도어 데이터를 활용할 것.
|
||||
|
||||
|
||||
|
||||
### [2026-04-10] [Probe Logging] <20><><EFBFBD> AI<41>쓳<EFBFBD>떟 <20>뀓<EFBFBD>뒪<EFBFBD>듃 & WAITING <20>뒪<EFBFBD>뀦 <20>룞<EFBFBD>떆 <20>늻<EFBFBD>씫 踰꾧렇
|
||||
|
||||
- **利앹긽**: 援됱옣<EB90B1>엳 鍮좊Ⅸ AI <20>쓳<EFBFBD>떟(<28>삉<EFBFBD>뒗 利됯컖<EB90AF>쟻<EFBFBD>씤 <20>댋 <20>샇異<EC8387>) <20>떆 `step-probe.ts`媛<> 硫붿떆吏<EB9686><EFA79E><EFBFBD><EFBFBD> <20>듅<EFBFBD>씤 <20>떎<EFBFBD>씠<EFBFBD>뼹濡쒓렇瑜<EBA087> 紐⑤몢 Discord濡<64> 由대젅<EB8C80>씠<EFBFBD>븯吏<EBB8AF> 紐삵븿.
|
||||
|
||||
- **<2A>썝<EFBFBD>씤**: <20>떎<EFBFBD>떆媛<EB9686> <20>뀓<EFBFBD>뒪<EFBFBD>듃 罹≪쿂(`delta > 0`) 議곌굔<EAB38C>뿉 `isRunning &&`<60>씠 嫄몃젮<EBAA83>엳<EFBFBD>뼱, <20>긽<EFBFBD>깭媛<EAB9AD> `WAITING`<60>씠<EFBFBD>굹 `IDLE`濡<> 利됱떆 <20>꽆<EFBFBD>뼱媛<EBBCB1>硫<EFBFBD> <20>뀓<EFBFBD>뒪<EFBFBD>듃瑜<EB9383> 罹≪쿂<E289AA>븯<EFBFBD>뒗 猷⑦떞<E291A6>씠 <20>쟾遺<EC9FBE> <20>뒪<EFBFBD>궢<EFBFBD>맖. <20>삉<EFBFBD>븳 <20>씠 <20>닚媛<EB8B9A> `isStall` 議곌굔<EAB38C>룄 <20><><EFBFBD>吏<EFBFBD> <20>븡<EFBFBD>븘 `WAITING` <20>뵒<EFBFBD>뀓<EFBFBD>뀡<EFBFBD>룄 利앸컻<EC95B8>븿.
|
||||
|
||||
- **<2A>빐寃<EBB990>**: <20>떎<EFBFBD>떆媛<EB9686> 罹≪쿂 濡쒖쭅<EC9296>뿉<EFBFBD>꽌 `isRunning &&` 議곌굔<EAB38C>쓣 <20>젣嫄고븯怨<EBB8AF>, `delta > 0`<60>씪 <20>븣 異붽<E795B0><EBB6BD><EFBFBD>맂 理쒖떊 <20>뒪<EFBFBD>뀦<EFBFBD>쓣 <20>뒪罹뷀븯硫댁꽌 `PLANNER_RESPONSE`<60><><EFBFBD> `WAITING` <20>뒪<EFBFBD>뀦<EFBFBD>쓣 紐⑤몢 泥섎━<EC848E>븯<EFBFBD>룄濡<EBA384> <20>닔<EFBFBD>젙<EFBFBD>븿.
|
||||
|
||||
- **二쇱쓽**: LS Backend 10媛<30> Session <20>젣<EFBFBD>븳 踰꾧렇媛<EBA087> <20>엳<EFBFBD>뼱, <20>떎瑜<EB968E> 李쎌뿉<EC8E8C>꽌 <20>닔<EFBFBD>룞 梨꾪똿(`1fbca84c`)<29>씠 IDLE濡<45> <20>궓<EFBFBD>븘<EFBFBD>엳<EFBFBD>쑝硫<EC919D> <20>옄<EFBFBD>룞<EFBFBD>솕 <20>뿉<EFBFBD>씠<EFBFBD>쟾<EFBFBD>듃<EFBFBD>쓽 <20>썙<EFBFBD>겕<EFBFBD>뒪<EFBFBD>럹<EFBFBD>씠<EFBFBD>뒪 <20>꽭<EFBFBD>뀡怨<EB80A1> <20>뿷媛덈┫ <20>닔 <20>엳<EFBFBD>쑝<EFBFBD>굹, <20>씠 踰꾧렇<EABEA7>뒗 polling <20><><EFBFBD><EFBFBD>씠諛<EC94A0> 臾몄젣<EBAA84><ECA0A3><EFBFBD><EFBFBD>쓬.
|
||||
|
||||
|
||||
### [2026-04-10] [Extension] AI Response Missing for New Sessions (Session Tracking Failure)
|
||||
- **利앹긽**: <20>깉濡쒖슫 <20><><EFBFBD><EFBFBD>솕(Session) <20>떆<EFBFBD>옉 <20>떆 泥<> AI <20>쓳<EFBFBD>떟 <20>뀓<EFBFBD>뒪<EFBFBD>듃媛<EB9383> <20>뵒<EFBFBD>뒪肄붾뱶<EBB6BE>뿉 <20>쟾<EFBFBD><EC9FBE><EFBFBD> <20>쟾<EFBFBD>넚<EFBFBD>릺吏<EBA6BA> <20>븡<EFBFBD>뒗 <20>쁽<EFBFBD>긽.
|
||||
- **<2A>썝<EFBFBD>씤**: 諛깆뿏<EAB986>뱶<EFBFBD>쓽 `GetAllCascadeTrajectories`媛<> 10媛<30> <20>꽭<EFBFBD>뀡留<EB80A1> 諛섑솚<EC8491>븯<EFBFBD>뿬 <20>깉 <20>꽭<EFBFBD>뀡<EFBFBD>씠 <20>늻<EFBFBD>씫<EFBFBD>맖. <20>씠瑜<EC94A0> 蹂댁셿<EB8C81>븯湲<EBB8AF> <20>쐞<EFBFBD>빐 `brain/` <20>뵒<EFBFBD>젆<EFBFBD>넗由щ<E794B1><D189> <20>뒪罹뷀븯<EBB780>뒗 Fallback 濡쒖쭅<EC9296>씠 <20>룞<EFBFBD>옉<EFBFBD>뻽<EFBFBD>쑝<EFBFBD>굹, <20>떊洹<EB968A> <20>꽭<EFBFBD>뀡<EFBFBD>쓽 泥<> <20>떒怨꾩뿉<EABEA9>꽌 `GetCascadeTrajectorySteps`(stepOffset: 0) <20>샇異<EC8387> <20>떆 <20>궡遺<EAB6A1> <20>쓳<EFBFBD>떟(UTF-8 <20>뙆<EFBFBD>떛 <20>벑) <20>뿉<EFBFBD>윭濡<EC9CAD> <20>씤<EFBFBD>빐 Exception<6F>씠 諛쒖깮, `trajectorySummaries`<60>뿉 <20>꽭<EFBFBD>뀡<EFBFBD>씠 <20>븘<EFBFBD>삁 <20>벑濡앸릺吏<EBA6BA> <20>븡<EFBFBD>쓬. <20>꽭<EFBFBD>뀡<EFBFBD>씠 異붿쟻<EBB6BF>릺吏<EBA6BA> <20>븡<EFBFBD>쑝<EFBFBD>땲 `delta > 0` 湲곕컲<EAB395>쓽 <20>쓳<EFBFBD>떟 罹≪쿂媛<ECBF82> 諛쒖깮<EC9296>븯吏<EBB8AF> <20>븡<EFBFBD>쓬.
|
||||
- **<2A>빐寃<EBB990>**: `step-probe.ts`<60>쓽 Fallback 2 `catch` 釉붾줉<EBB6BE>뿉<EFBFBD>꽌 <20>뿉<EFBFBD>윭媛<EC9CAD> 諛쒖깮<EC9296>븯<EFBFBD>뜑<EFBFBD>씪<EFBFBD>룄 媛뺤젣濡<ECA0A3> `stepCount: 1`濡<> <20>꽭<EFBFBD>뀡<EFBFBD>쓣 <20>벑濡앺븯<EC95BA>룄濡<EBA384> <20>뙣移섑븯<EC8491>뿬 <20>꽭<EFBFBD>뀡 <20>씤<EFBFBD>떇 <20>쑀<EFBFBD>떎 諛⑹<E8AB9B><E291B9>.
|
||||
- **二쇱쓽**: API <20>샇異<EC8387> <20>떎<EFBFBD>뙣瑜<EB99A3> 議곗슜<EAB397>엳 `catch`濡<> <20>꽆湲곕㈃ <20>쟾泥<EC9FBE> <20>뙆<EFBFBD>씠<EFBFBD>봽<EFBFBD>씪<EFBFBD>씤(<28>뿬湲곗꽌<EAB397>뒗 <20>긽<EFBFBD>깭 <20>뤃留<EBA483>)<29>씠 <20>빐<EFBFBD>떦 <20>뜲<EFBFBD>씠<EFBFBD>꽣瑜<EABDA3> <20>쁺<EFBFBD>썝<EFBFBD>엳 臾댁떆<EB8C81>븯寃<EBB8AF> <20>릺<EFBFBD>뒗 移섎챸<EC848E>쟻 踰꾧렇媛<EBA087> 諛쒖깮<EC9296>븿. <20>옣<EFBFBD>븷 <20>뿀<EFBFBD>슜 <20>꽕怨<EABD95> <20>떆 湲곕낯媛<EB82AF> 蹂듭썝(Fallback State) <20>꽕<EFBFBD>젙 <20>븘<EFBFBD>닔.
|
||||
|
||||
|
||||
|
||||
### [2026-04-10] [Extension] Trigger-Click False Positives & Button Matching Failure
|
||||
- **利앹긽**: <20>뵒<EFBFBD>뒪肄붾뱶<EBB6BE>뿉<EFBFBD>꽌 <20>듅<EFBFBD>씤(Approve)<29>쓣 <20>늻瑜대㈃, <20>뿉<EFBFBD>씠<EFBFBD>쟾<EFBFBD>듃 <20>솗<EFBFBD>옣 <20>봽濡쒓렇<EC9293>옩<EFBFBD>씠 <20>븣留욎<EFA78D><EC9A8E> 踰꾪듉(<28>삁: `Always run`)<29>쓣 <20>늻瑜댁<E7919C><EB8C81> 紐삵븯嫄곕굹, <20>뿁<EFBFBD>슧<EFBFBD>븳 踰꾪듉(<28>삁: <20>긽<EFBFBD>떒<EFBFBD>쓽 `Running1 command`)<29>쓣 <20>닃<EFBFBD>윭踰꾨젮 <20>떎<EFBFBD>젣 <20>듅<EFBFBD>씤 泥섎━媛<E29481> <20>늻<EFBFBD>씫<EFBFBD>릺<EFBFBD>뒗 <20>쁽<EFBFBD>긽.
|
||||
- **<2A>썝<EFBFBD>씤**: 1) UI 踰꾪듉 <20>뀓<EFBFBD>뒪<EFBFBD>듃<EFBFBD>뿉 `keyboard_arrow_up` <20>벑 癒명떚由ъ뼹 <20>븘<EFBFBD>씠肄<EC94A0> <20>뀓<EFBFBD>뒪<EFBFBD>듃媛<EB9383> <20>젒李<ECA092>(`Always runkeyboard_arrow_up`)<29>릺<EFBFBD>뼱 <20>젙洹쒖떇<EC9296>씠 <20>떎<EFBFBD>뙣<EFBFBD>븷 寃껋쓣 <20>슦<EFBFBD>젮<EFBFBD>빐 <20>떒<EFBFBD>뼱 寃쎄퀎(`\b`)瑜<> <20>젣嫄고븳 <20>뙣移섍<E7A7BB><EC848D> <20>썝<EFBFBD>씤. <20>떒<EFBFBD>뼱 寃쎄퀎媛<ED808E> <20>궗<EFBFBD>씪吏<EC94AA>硫댁꽌 `/Run/i` <20>뙣<EFBFBD>꽩<EFBFBD>씠 `Running1 command` 媛숈<E5AA9B><EC8888> <20>떎瑜<EB968E> <20>긽<EFBFBD>깭 <20>뀓<EFBFBD>뒪<EFBFBD>듃 踰꾪듉<EABEAA>뿉 <20>삤<EFBFBD>깘(False Positive)<29>맖. 2) DOM <20>닚<EFBFBD>꽌<EFBFBD>긽 <20>긽<EFBFBD>깭 <20>뀓<EFBFBD>뒪<EFBFBD>듃 踰꾪듉<EABEAA>씠 <20>븵<EFBFBD>꽌 <20>엳<EFBFBD>쑝誘<EC919D>濡<EFBFBD> <20>삤<EFBFBD>깘<EFBFBD>맂 踰꾪듉<EABEAA>씠 <20>슦<EFBFBD>꽑 <20>겢由<EAB2A2><E794B1>맖.
|
||||
- **<2A>빐寃<EBB990>**: `trigger-click` 濡쒖쭅 <20>떎<EFBFBD>뻾 <20>쟾 踰꾪듉<EABEAA>쓽 `textContent`<60>뿉<EFBFBD>꽌 `keyboard_arrow_up` <20>벑 <20>븣<EFBFBD>젮吏<ECA0AE> 瑗щ━ <20>븘<EFBFBD>씠肄<EC94A0> 臾몄옄<EBAA84>뿴<EFBFBD>쓣 紐낆떆<EB8286>쟻<EFBFBD>쑝濡<EC919D> <20>젣嫄<ECA0A3>(strip)<29>븯怨<EBB8AF>, 紐⑤뱺 <20>듃由ш굅 <20>젙洹쒖떇<EC9296>뿉 <20>떎<EFBFBD>떆 <20>떒<EFBFBD>뼱 寃쎄퀎(`\b`)瑜<> 媛뺤젣 <20>궫<EFBFBD>엯<EFBFBD>븯<EFBFBD>뿬 <20>삤<EFBFBD>깘<EFBFBD>쓣 <20>썝泥<EC8D9D> 李⑤떒<E291A4>븿.
|
||||
- **二쇱쓽**: UI <20>슂<EFBFBD>냼瑜<EB83BC> DOM<4F>뿉<EFBFBD>꽌 湲곸뼱<EAB3B8>삱 <20>븣<EFBFBD>뒗 <20>뀓<EFBFBD>뒪<EFBFBD>듃<EFBFBD>뿉 <20>닲寃⑥쭊 <20>븘<EFBFBD>씠肄<EC94A0>/<2F>쎒<EFBFBD>룿<EFBFBD>듃 由ш굅爾<EAB585>(ligatures)媛<> <20>뾾<EFBFBD>뒗吏<EB9297> 寃<><E5AF83>넗<EFBFBD>빐<EFBFBD>빞 <20>븿. <20>뙣<EFBFBD>꽩 留ㅼ묶 <20>떆 瑗щ━<D189>몴瑜<EBAAB4> 癒쇱<E79992><EC87B1> <20>젣嫄고븯怨<EBB8AF> 紐낇솗<EB8287>븳 寃쎄퀎瑜<ED808E> 遺<><E981BA>뿬<EFBFBD>븷 寃<>.
|
||||
|
||||
|
||||
|
||||
### [2026-04-10] [Extension] Ghost Session Hijack & Infinite Polling Loop (trajectory not found)
|
||||
|
||||
- **利앹긽**: <20>떊洹<EB968A> <20>옉<EFBFBD>뾽 <20>떆 '<27>떊<EFBFBD>샇<EFBFBD>븞<EFBFBD>뱾<EFBFBD>뼱<EFBFBD><EBBCB1><EFBFBD>' (Discord濡<64> 由대젅<EB8C80>씠 <20>븞 <20>맖). 濡쒓렇<EC9293>뿉 500 error trajectory not found 臾댄븳 諛섎났.\n- **<EFBFBD>썝<EFBFBD>씤**: Antigravity媛<79> <20>옉<EFBFBD>뾽<EFBFBD>븯硫댁꽌 brain/<2F>뿉 36湲<36><E6B9B2>옄 <20>뤃<EFBFBD>뜑瑜<EB9C91> <20>깮<EFBFBD>꽦<EFBFBD>븯<EFBFBD>뒗<EFBFBD>뜲, Cascade媛<65> <20>븘<EFBFBD>땲誘<EB95B2>濡<EFBFBD> GetCascadeTrajectorySteps<70>뿉<EFBFBD>꽌 500 <20>뿉<EFBFBD>윭瑜<EC9CAD> <20>깄<EFBFBD>땲<EFBFBD>떎. <20>븯吏<EBB8AF>留<EFBFBD> <20>씠<EFBFBD>쟾 <20>떊洹<EB968A> <20>꽭<EFBFBD>뀡 <20>쑀<EFBFBD>떎 諛⑹<E8AB9B><E291B9> <20>뙣移섍<E7A7BB><EC848D> <20>씠 Ghost <20>꽭<EFBFBD>뀡<EFBFBD>쓣 RUNNING<4E>쑝濡<EC919D> 媛뺤젣 <20>벑濡앺븯硫댁꽌, <20>솢<EFBFBD>꽦 <20>꽭<EFBFBD>뀡(activeSessionId)<29>쓣 <20>깉痍⑦븯怨<EBB8AF> 臾댄븳 <20>뿉<EFBFBD>윭 猷⑦봽<E291A6>뿉 鍮좎<E98DAE><ECA28E>寃<EFBFBD> 留뚮뱾<EB9AAE>뿀<EFBFBD>뒿<EFBFBD>땲<EFBFBD>떎.\n- **<EFBFBD>빐寃<EFBFBD>**: step-probe.ts<74>뿉<EFBFBD>꽌 <20>뤃諛<EBA483> <20>벑濡<EBB291> <20>떆 error message<67>뿉 'trajectory not found'媛<> <20>룷<EFBFBD>븿<EFBFBD>릺硫<EBA6BA> Ghost <20>꽭<EFBFBD>뀡<EFBFBD>쑝濡<EC919D> 媛꾩<<EABEA9>빐 媛뺤젣 <20>벑濡<EBB291>(continue)<29>쓣 嫄대꼫<EB8C80>쎇寃<EC8E87> <20>븯怨<EBB8AF>, Stall Probe <20>뿉<EFBFBD>윭 catch<63>뿉<EFBFBD>꽌<EFBFBD>룄 UTF-8 <20>뿉<EFBFBD>윭媛<EC9CAD> <20>븘<EFBFBD>땲硫<EB95B2> stallProbed=true瑜<65> 二쇱뼱 <20>옱<EFBFBD>떆<EFBFBD>룄 臾댄븳 猷⑦봽瑜<EBB4BD> <20>셿<EFBFBD>쟾<EFBFBD>엳 <20>걡<EFBFBD>뼱<EFBFBD>깉<EFBFBD>뒿<EFBFBD>땲<EFBFBD>떎.\n- **二쇱쓽**: uuid 湲몄씠(36<33>옄)留뚯쑝濡<EC919D> <20>뵒<EFBFBD>젆<EFBFBD>넗由щ<E794B1><D189> <20>떇蹂꾪븷 <20>븣 Antigravity<74><79><EFBFBD> Google Agent媛<74> 紐⑦샇<E291A6>빐吏<EBB990> <20>닔 <20>엳<EFBFBD>쑝誘<EC919D>濡<EFBFBD>, 諛섎뱶<EC848E>떆 Backend <20>쓳<EFBFBD>떟<EFBFBD>쓽 <20>솗<EFBFBD>떎<EFBFBD>븳 <20>뿉<EFBFBD>윭(trajectory not found) 硫붿떆吏<EB9686>濡<EFBFBD> <20>삁<EFBFBD>쇅 <20>뙋蹂꾩쓣 <20>빐<EFBFBD>빞 <20>빀<EFBFBD>땲<EFBFBD>떎.\n
|
||||
@@ -1,205 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,216 +0,0 @@
|
||||
# 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 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)
|
||||
@@ -1,99 +0,0 @@
|
||||
# Tech Stack
|
||||
|
||||
> AI 에이전트는 구현 전 이 문서를 확인하여 올바른 기술/버전을 사용합니다.
|
||||
|
||||
## 언어 & 런타임
|
||||
|
||||
| 항목 | 버전 | 경로/비고 |
|
||||
|------|------|-----------|
|
||||
| Python | 3.12 (miniforge3) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` |
|
||||
| Node.js | 시스템 설치 | `node`, `npm` (PowerShell에서 `cmd /c npm` 권장) |
|
||||
| TypeScript | 5.3+ | `extension/src/*.ts` → `tsc` → `extension/out/*.js` |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Python은 **반드시** 위 miniforge3 경로를 사용. WindowsApps의 python stub은 동작하지 않음.
|
||||
|
||||
## 프레임워크 & 라이브러리
|
||||
|
||||
### Python (서버)
|
||||
|
||||
| 패키지 | 버전 | 용도 |
|
||||
|--------|------|------|
|
||||
| discord.py | 2.x | Discord 봇 (슬래시 명령, 버튼 UI, 이벤트) |
|
||||
| aiohttp | 3.x | Gateway HTTP 서버 + WebSocket endpoint |
|
||||
| watchdog | - | Brain 디렉토리 파일시스템 감시 |
|
||||
| python-dotenv | - | .env 파일 로드 |
|
||||
| PyJWT | - | ❌ 미사용 (자체 HMAC-SHA256 구현) |
|
||||
|
||||
### TypeScript (Extension)
|
||||
|
||||
| 패키지 | 용도 |
|
||||
|--------|------|
|
||||
| @types/vscode | VS Code Extension API 타입 |
|
||||
| @types/node | Node.js 타입 |
|
||||
| typescript | 컴파일러 |
|
||||
| ws | WebSocket Hub 연결 (`.vscodeignore`에 `!node_modules/ws/**` 필수) |
|
||||
| antigravity-sdk | AG RPC 호출 (로컬 임베드 `sdk/`) |
|
||||
|
||||
## 패키지 관리
|
||||
|
||||
| 측 | 도구 | 파일 |
|
||||
|----|------|------|
|
||||
| Python | pip | `requirements.txt` |
|
||||
| Extension | npm | `extension/package.json` |
|
||||
|
||||
## 개발 도구 & 명령어
|
||||
|
||||
| 작업 | 명령어 |
|
||||
|------|--------|
|
||||
| **봇 실행** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe main.py` |
|
||||
| **봇 실행 (gateway)** | `.env`에서 `BOT_MODE=gateway` 설정 후 위 명령 |
|
||||
| **Extension 구문 검사** | `cd extension && npx tsc --noEmit` |
|
||||
| **Extension 컴파일** | `cd extension && cmd /c npm run compile` |
|
||||
| **Extension VSIX** | `cd extension && npx @vscode/vsce package --no-dependencies` |
|
||||
| **Python 구문 검사** | `python -c "import ast; [ast.parse(open(f).read()) for f in ['bot.py','hub.py',...]]"` |
|
||||
| **Hub WS 테스트** | `python tests/test_ws_hub.py` (서버 기동 상태에서) |
|
||||
|
||||
## 환경 변수 (.env)
|
||||
|
||||
### 필수
|
||||
|
||||
| 변수명 | 용도 | 기본값 |
|
||||
|--------|------|--------|
|
||||
| DISCORD_TOKEN | Discord 봇 토큰 | (필수) |
|
||||
| DISCORD_GUILD_ID | Discord 서버 ID | (필수) |
|
||||
|
||||
### 선택
|
||||
|
||||
| 변수명 | 용도 | 기본값 |
|
||||
|--------|------|--------|
|
||||
| BRAIN_PATH | AG 브레인 경로 | `~/.gemini/antigravity/brain` |
|
||||
| BOT_MODE | `local` / `gateway` | `local` |
|
||||
| DEBOUNCE_SECONDS | Watcher 디바운스 간격 | `5` |
|
||||
| PROJECT_NAME | 프로젝트 이름 | `gravity_control` |
|
||||
|
||||
### Gateway 모드 전용
|
||||
|
||||
| 변수명 | 용도 | 기본값 |
|
||||
|--------|------|--------|
|
||||
| GATEWAY_PORT | Gateway HTTP/WS 포트 | `8585` |
|
||||
| GATEWAY_API_KEY | REST API 인증 키 | (미설정 시 인증 미사용) |
|
||||
| GRAVITY_HUB_SECRET | WS Hub JWT 서명 시크릿 (64char hex) | (미설정 시 인증 생략) |
|
||||
| GRAVITY_REGISTRATION_CODE | Extension 등록 코드 (32char hex) | (미설정 시 인증 생략) |
|
||||
|
||||
## Extension VS Code 설정
|
||||
|
||||
| 설정 키 | 용도 |
|
||||
|---------|------|
|
||||
| `gravityBridge.bridgePath` | Bridge 디렉토리 경로 |
|
||||
| `gravityBridge.projectName` | 프로젝트 이름 (기본: git remote) |
|
||||
| `gravityBridge.hubUrl` | Hub WS URL (예: `ws://localhost:8585/ws`) |
|
||||
| `gravityBridge.registrationCode` | Hub 등록 코드 |
|
||||
|
||||
## 빌드 산출물
|
||||
|
||||
| 항목 | 경로 | 설명 |
|
||||
|------|------|------|
|
||||
| VSIX | `extension/gravity-bridge-{ver}.vsix` | VS Code 확장 패키지 |
|
||||
| JS 출력 | `extension/out/*.js` | TypeScript 컴파일 결과물 |
|
||||
| SDK 복사 | `extension/out/sdk/` | compile 시 자동 복사 |
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
description: Gitea API로 저장소 커밋/이슈/PR 현황을 조회하는 워크플로우
|
||||
---
|
||||
|
||||
# Gitea 저장소 현황 조회
|
||||
|
||||
서비스 정보는 `.agents/workflows/services.md` 참조.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## 절차
|
||||
|
||||
1. 최근 커밋 조회 (최신 10개):
|
||||
```powershell
|
||||
$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"}
|
||||
$commits = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/gravity_control/commits?limit=10&sha=main" -Headers $h
|
||||
$commits | ForEach-Object { Write-Host "$($_.sha.Substring(0,7)) $($_.commit.message.Split("`n")[0])" }
|
||||
```
|
||||
|
||||
2. 열린 이슈 조회:
|
||||
```powershell
|
||||
$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"}
|
||||
$issues = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/gravity_control/issues?state=open&type=issues" -Headers $h
|
||||
$issues | ForEach-Object { Write-Host "#$($_.number) $($_.title)" }
|
||||
```
|
||||
|
||||
3. Wiki 페이지 목록:
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py list
|
||||
```
|
||||
|
||||
4. Wiki 페이지 읽기:
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py read "Architecture"
|
||||
```
|
||||
|
||||
5. Wiki 페이지 업데이트:
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py update "페이지-제목" /tmp/wiki_content.md
|
||||
```
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
description: Vikunja API로 프로젝트 태스크 현황을 조회하는 워크플로우
|
||||
---
|
||||
|
||||
# Vikunja 태스크 현황 조회
|
||||
|
||||
서비스 정보는 `.agents/workflows/services.md` 참조.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## 절차
|
||||
|
||||
1. 전체 목록:
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list
|
||||
```
|
||||
|
||||
2. TODO만:
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list todo
|
||||
```
|
||||
|
||||
3. DONE만:
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list done
|
||||
```
|
||||
|
||||
4. 태스크 완료 처리 (**⚠️ 반드시 이 방법 사용 — 직접 API 호출 금지**):
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {TASK_ID}
|
||||
```
|
||||
|
||||
5. 새 태스크 생성:
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> **절대로** `Invoke-RestMethod -Method Post -Body '{"done": true}'` 같은 직접 API 호출을 사용하지 마세요.
|
||||
> Vikunja API는 POST 시 body에 포함되지 않은 필드를 빈값으로 덮어씁니다.
|
||||
> `vikunja_helper.py`는 항상 GET → 기존 필드 보존 → POST 패턴을 사용합니다.
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
description: 에러/버그 발생 시 체계적 디버깅 워크플로우 (에러, 안돼요, 왜 안돼, 버그, 디버그, 수정)
|
||||
---
|
||||
|
||||
# Debug Workflow
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 추측으로 코드를 수정하지 마세요. 반드시 이 순서를 따릅니다.
|
||||
|
||||
## 1단계: 정보 수집 (추측 금지)
|
||||
|
||||
- [ ] 에러 메시지 **전문** 확인 (절대 잘라내지 않기)
|
||||
- [ ] 관련 로그 파일 확인
|
||||
- [ ] 환경 정보 확인 (OS, Node/Python 버전, 의존성 버전 등)
|
||||
- [ ] 에러가 발생하는 **정확한 입력/조건** 파악
|
||||
|
||||
## 2단계: Known Issues 확인
|
||||
|
||||
`.agents/references/known-issues.md`를 읽고 동일하거나 유사한 문제가 있는지 확인합니다.
|
||||
|
||||
> [!CAUTION]
|
||||
> **known-issues 확인 없이 해결 시도를 시작하지 마세요.**
|
||||
> 이미 해결된 문제를 다시 삽질하는 것은 시간 낭비입니다.
|
||||
|
||||
## 3단계: 근본 원인 분석
|
||||
|
||||
- [ ] 에러가 발생하는 **정확한 코드 위치** 확인
|
||||
- [ ] 가설을 세우고, 가설을 검증할 수 있는 **최소한의 테스트** 수행
|
||||
- [ ] 가설이 틀렸다면 **즉시 다른 가설로 전환**
|
||||
|
||||
> [!WARNING]
|
||||
> **동일한 접근을 2회 초과 시도하지 마세요.**
|
||||
> 2회 실패 시 유저에게 보고하고 판단을 요청합니다.
|
||||
> 보고 내용: 시도한 것 / 실패한 것 / 원인 가설 / 다음 제안
|
||||
|
||||
## 4단계: 수정 및 검증
|
||||
|
||||
- [ ] 수정 적용
|
||||
- [ ] 동일 에러가 재현되지 않는지 확인
|
||||
- [ ] 사이드 이펙트(다른 기능에 영향) 없는지 확인
|
||||
|
||||
## 5단계: 기록
|
||||
|
||||
- [ ] `known-issues.md`에 새 항목 추가 (아래 포맷 사용)
|
||||
|
||||
```markdown
|
||||
### [날짜] [키워드] — 한줄 요약
|
||||
- **증상**: 무엇이 잘못되었는가
|
||||
- **원인**: 근본 원인
|
||||
- **해결**: 올바른 해결 방법
|
||||
- **주의**: 재발 방지를 위한 교훈
|
||||
```
|
||||
@@ -1,165 +0,0 @@
|
||||
---
|
||||
description: 세션 종료 시 devlog 기록 + git commit + Vikunja 동기화 (끝, 마무리, 커밋해, 완료)
|
||||
---
|
||||
|
||||
# 세션 종료 프로토콜
|
||||
|
||||
작업 완료, "끝", "마무리", "커밋해" 등 요청 시 이 워크플로우를 실행합니다.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## 0. 학습 기록 (실패/시행착오 저장)
|
||||
|
||||
이번 세션에서 발생한 실패, 시행착오, 새로 알게 된 사실을 정리합니다:
|
||||
|
||||
- [ ] `.agents/references/known-issues.md`에 추가할 항목이 있는지 확인
|
||||
- [ ] 있다면 아래 포맷으로 추가:
|
||||
|
||||
```markdown
|
||||
### [날짜] [키워드] — 한줄 요약
|
||||
- **증상**: ...
|
||||
- **원인**: ...
|
||||
- **해결**: ...
|
||||
- **주의**: ...
|
||||
```
|
||||
|
||||
## 1. Devlog 기록
|
||||
|
||||
### Index 업데이트 (필수 — 매 작업)
|
||||
|
||||
오늘 날짜의 index 파일에 완료된 작업 1줄을 추가합니다.
|
||||
|
||||
- **파일**: `docs/devlog/YYYY-MM-DD.md`
|
||||
- **형식**:
|
||||
```markdown
|
||||
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> - ✅ = 완료, 🔧 = 미완료 (다음 세션에서 이어받기)
|
||||
> - 파일이 없으면 새로 생성 (테이블 헤더 포함)
|
||||
|
||||
### Entry 작성 (선택적 — 필요할 때만)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Entry는 **git/Vikunja/wiki에 없는 정보**가 있을 때만 작성합니다.
|
||||
|
||||
**Entry 작성 기준:**
|
||||
- ✅ 설계 결정이 있었을 때 (왜 A가 아닌 B를 선택했는지)
|
||||
- ✅ 미완료 사항이 있을 때 (다음 세션이 이어받아야 할 맥락)
|
||||
- ✅ 삽질/트러블슈팅이 있었을 때 (같은 실수 방지)
|
||||
|
||||
**Entry 불필요:**
|
||||
- ❌ 단순 버그 픽스 (커밋 메시지로 충분)
|
||||
- ❌ 문서 업데이트 (git diff로 충분)
|
||||
- ❌ 이미 Vikunja 태스크에 상세 설명이 있는 경우
|
||||
|
||||
**Entry 파일**: `docs/devlog/entries/YYYYMMDD-NNN.md`
|
||||
```markdown
|
||||
# 작업 제목
|
||||
|
||||
- **시간**: YYYY-MM-DD HH:MM~HH:MM
|
||||
- **Commit**: `해시`
|
||||
- **Vikunja**: #태스크번호 → done/진행중
|
||||
|
||||
## 결정 사항
|
||||
- 왜 이 방식을 선택했는지
|
||||
|
||||
## 미완료
|
||||
- 남은 작업 (있을 경우)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Vikunja 동기화
|
||||
|
||||
> [!CAUTION]
|
||||
> **반드시 `vikunja_helper.py` 사용.** 직접 API 호출 금지.
|
||||
> Vikunja API는 POST 시 body에 없는 필드를 빈값으로 덮어씁니다.
|
||||
|
||||
### 2-1. 커밋 전수 검사
|
||||
|
||||
이번 세션의 **모든 커밋을 하나씩 검사**하고 Vikunja에 매핑합니다.
|
||||
|
||||
```powershell
|
||||
git log --oneline -20
|
||||
```
|
||||
|
||||
| 커밋 유형 | Vikunja 액션 |
|
||||
|-----------|-------------|
|
||||
| 기존 태스크 해당 작업 **완료** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {ID}` |
|
||||
| 신규 작업 완료 (기존 태스크 없음) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --done --labels Backend,Priority:High` |
|
||||
| 작업 중 발견된 **미완료 TODO** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:Mid` |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 모든 커밋이 기존 또는 신규 태스크에 매핑되었는지 확인.
|
||||
|
||||
### 2-2. 완료 처리
|
||||
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {TASK_ID}
|
||||
```
|
||||
|
||||
### 2-3. 신규 태스크 생성
|
||||
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High
|
||||
```
|
||||
|
||||
### 라벨 규칙
|
||||
|
||||
**영역 (필수 1개 이상):** `Backend` / `Frontend` / `Engine` / `Infra` / `Test`
|
||||
**우선순위 (필수 1개):** `Priority:High` / `Priority:Mid` / `Priority:Low`
|
||||
|
||||
---
|
||||
|
||||
## 3. Wiki 동기화 (해당 시에만)
|
||||
|
||||
| 코드 변경 | 대상 Wiki |
|
||||
|-----------|----------|
|
||||
| 서버 변경 | Architecture |
|
||||
| 프론트엔드 변경 | Architecture |
|
||||
| 인프라 변경 | Architecture |
|
||||
| 새 모듈/패키지 추가 | Architecture |
|
||||
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py update "Architecture" /tmp/wiki_content.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Git Commit & Push
|
||||
|
||||
```powershell
|
||||
git add -A
|
||||
git status --short
|
||||
```
|
||||
```powershell
|
||||
git commit -m "커밋 메시지"
|
||||
```
|
||||
```powershell
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**커밋 메시지 컨벤션:**
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
type: feat|fix|refactor|test|docs|chore|ci|infra
|
||||
scope: (선택)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 최종 체크리스트
|
||||
|
||||
> [!WARNING]
|
||||
> 아래 항목 중 하나라도 누락되면 세션 종료를 완료할 수 없습니다.
|
||||
|
||||
- [ ] known-issues 업데이트됨 (새 이슈가 있었다면)
|
||||
- [ ] devlog index 업데이트됨
|
||||
- [ ] devlog entry 작성됨 (필요한 경우만)
|
||||
- [ ] Vikunja 태스크 생성/완료 처리됨 (커밋 전수 검사 기반)
|
||||
- [ ] Wiki 동기화됨 (아키텍처 변경이 있었다면)
|
||||
- [ ] git push 완료
|
||||
- [ ] 사용자에게 완료 보고
|
||||
@@ -1,160 +0,0 @@
|
||||
"""Analyze AG Native DOM structure to find AI response containers."""
|
||||
import json, os, sys
|
||||
|
||||
def load_dump():
|
||||
bridge = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge')
|
||||
# Try deep-inspect result first, then dump_html
|
||||
for fname in ['deep-inspect-result.json', 'dump_html.json']:
|
||||
fpath = os.path.join(bridge, fname)
|
||||
if os.path.exists(fpath):
|
||||
print(f"Loading: {fname} ({os.path.getsize(fpath)} bytes)")
|
||||
with open(fpath, 'r', encoding='utf-8-sig') as f:
|
||||
return json.load(f), fname
|
||||
return None, None
|
||||
|
||||
def find_text_containers(node, path="", depth=0, results=None):
|
||||
"""Recursively find nodes with substantial text content (potential AI response containers)."""
|
||||
if results is None:
|
||||
results = []
|
||||
if not isinstance(node, dict):
|
||||
return results
|
||||
|
||||
tag = node.get('tag', '')
|
||||
cls = node.get('cls', '')
|
||||
text = node.get('text', '')
|
||||
attrs = node.get('attrs', {})
|
||||
children = node.get('children', [])
|
||||
|
||||
cur_path = f"{path}/{tag}"
|
||||
if cls:
|
||||
short_cls = cls[:60]
|
||||
cur_path += f".{short_cls}"
|
||||
|
||||
# Look for nodes with long text (potential AI responses)
|
||||
if text and len(text) > 50:
|
||||
results.append({
|
||||
'path': cur_path,
|
||||
'depth': depth,
|
||||
'tag': tag,
|
||||
'cls': cls[:100],
|
||||
'text_len': len(text),
|
||||
'text_preview': text[:120],
|
||||
'attrs': {k:v for k,v in attrs.items() if k not in ('style',)}
|
||||
})
|
||||
|
||||
for child in children:
|
||||
find_text_containers(child, cur_path, depth+1, results)
|
||||
|
||||
return results
|
||||
|
||||
def find_by_class_pattern(node, patterns, path="", depth=0, results=None):
|
||||
"""Find nodes matching class patterns."""
|
||||
if results is None:
|
||||
results = []
|
||||
if not isinstance(node, dict):
|
||||
return results
|
||||
|
||||
tag = node.get('tag', '')
|
||||
cls = node.get('cls', '')
|
||||
attrs = node.get('attrs', {})
|
||||
children = node.get('children', [])
|
||||
text = node.get('text', '')
|
||||
|
||||
cur_path = f"{path}/{tag}"
|
||||
|
||||
for pattern in patterns:
|
||||
if pattern.lower() in cls.lower() or pattern.lower() in str(attrs).lower():
|
||||
child_count = len(children)
|
||||
results.append({
|
||||
'path': cur_path,
|
||||
'depth': depth,
|
||||
'tag': tag,
|
||||
'cls': cls[:150],
|
||||
'pattern': pattern,
|
||||
'text_preview': text[:80] if text else '',
|
||||
'child_count': child_count,
|
||||
'attrs': {k:v[:50] for k,v in attrs.items() if k != 'style'}
|
||||
})
|
||||
|
||||
for child in children:
|
||||
find_by_class_pattern(child, patterns, cur_path, depth+1, results)
|
||||
|
||||
return results
|
||||
|
||||
def analyze_chat_structure(node, path="", depth=0):
|
||||
"""Find the chat/conversation area by looking at the main layout."""
|
||||
if not isinstance(node, dict):
|
||||
return
|
||||
tag = node.get('tag', '')
|
||||
cls = node.get('cls', '')
|
||||
children = node.get('children', [])
|
||||
text = node.get('text', '')
|
||||
attrs = node.get('attrs', {})
|
||||
|
||||
# Print interesting structural nodes at shallow depths
|
||||
if depth <= 6:
|
||||
child_count = len(children)
|
||||
has_text = bool(text and len(text) > 10)
|
||||
info = f"{' '*depth}{tag}"
|
||||
if cls:
|
||||
info += f" .{cls[:80]}"
|
||||
if attrs:
|
||||
attr_str = ' '.join(f'{k}={v[:30]}' for k,v in attrs.items() if k not in ('style','class'))
|
||||
if attr_str:
|
||||
info += f" [{attr_str}]"
|
||||
info += f" children={child_count}"
|
||||
if has_text:
|
||||
info += f" text=\"{text[:50]}...\""
|
||||
print(info)
|
||||
|
||||
for child in children:
|
||||
analyze_chat_structure(child, f"{path}/{tag}", depth+1)
|
||||
|
||||
data, fname = load_dump()
|
||||
if not data:
|
||||
print("No dump file found!")
|
||||
sys.exit(1)
|
||||
|
||||
# Handle both dump formats
|
||||
body = data.get('body', data)
|
||||
qi = data.get('quickInfo', {})
|
||||
|
||||
print("=" * 60)
|
||||
print("QUICK INFO")
|
||||
print("=" * 60)
|
||||
if qi:
|
||||
for k, v in qi.items():
|
||||
if k == 'buttons':
|
||||
print(f"buttons ({len(v)}):")
|
||||
for b in v[:15]:
|
||||
print(f" [{b.get('tag')}] \"{b.get('text','')[:50]}\" visible={b.get('visible')} cls={b.get('cls','')[:60]}")
|
||||
elif k == 'dataAttrs':
|
||||
print(f"dataAttrs: {v[:30]}")
|
||||
else:
|
||||
print(f"{k}: {v}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("CHAT-RELATED CLASS PATTERNS")
|
||||
print("=" * 60)
|
||||
patterns = ['chat', 'message', 'conversation', 'response', 'answer', 'reply',
|
||||
'markdown', 'prose', 'content', 'panel', 'agent', 'assistant',
|
||||
'planner', 'step', 'trajectory', 'bot', 'ai-', 'turn']
|
||||
matches = find_by_class_pattern(body, patterns)
|
||||
for m in matches:
|
||||
print(f" [{m['tag']}] cls=\"{m['cls']}\" pattern={m['pattern']} children={m['child_count']} {m.get('attrs',{})}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("LONG TEXT NODES (potential AI responses)")
|
||||
print("=" * 60)
|
||||
texts = find_text_containers(body)
|
||||
texts.sort(key=lambda x: x['text_len'], reverse=True)
|
||||
for t in texts[:20]:
|
||||
print(f" [{t['tag']}] depth={t['depth']} len={t['text_len']} cls=\"{t['cls'][:60]}\"")
|
||||
print(f" text: \"{t['text_preview']}\"")
|
||||
if t['attrs']:
|
||||
print(f" attrs: {t['attrs']}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("DOM TREE (depth<=6)")
|
||||
print("=" * 60)
|
||||
analyze_chat_structure(body)
|
||||
@@ -1,19 +0,0 @@
|
||||
import json, os, sys
|
||||
|
||||
dump_path = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html.json')
|
||||
with open(dump_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
qi = data.get('quickInfo', {})
|
||||
print('=== Quick Info ===')
|
||||
print('hasConversationView:', qi.get('hasConversationView'))
|
||||
print('hasStepIndex:', qi.get('hasStepIndex'))
|
||||
print('hasBotColor:', qi.get('hasBotColor'))
|
||||
print('hasMarkdownBody:', qi.get('hasMarkdownBody'))
|
||||
print('hasProse:', qi.get('hasProse'))
|
||||
print('totalElements:', qi.get('totalElements'))
|
||||
print('dataTestIds:', qi.get('dataTestIds'))
|
||||
print('dataAttrs (first 20):', qi.get('dataAttrs', [])[:20])
|
||||
print('buttons (first 10):')
|
||||
for b in qi.get('buttons', [])[:10]:
|
||||
print(f" [{b.get('tag')}] {b.get('text', '')[:60]} visible={b.get('visible')}")
|
||||
@@ -1,83 +0,0 @@
|
||||
"""Search AG Native DOM dump for chat content and buttons."""
|
||||
import json, os
|
||||
|
||||
fpath = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html_5.json')
|
||||
with open(fpath, 'r', encoding='utf-8-sig') as f:
|
||||
data = json.load(f)
|
||||
|
||||
body = data.get('body', data.get('bodyTree', {}))
|
||||
qi = data.get('quickInfo', {})
|
||||
|
||||
# Show all buttons
|
||||
print('=== BUTTONS ===')
|
||||
for b in qi.get('buttons', []):
|
||||
print(f' [{b["tag"]}] "{b["text"][:60]}" visible={b["visible"]} cls={b.get("cls","")[:80]}')
|
||||
|
||||
# Data attrs
|
||||
print('\n=== DATA ATTRS ===')
|
||||
for attr in qi.get('dataAttrs', []):
|
||||
print(f' {attr}')
|
||||
|
||||
# Recursive search for nodes by text
|
||||
def find_nodes_by_text(node, target, path='', results=None, depth=0):
|
||||
if results is None: results = []
|
||||
if not isinstance(node, dict): return results
|
||||
tag = node.get('tag','')
|
||||
cls = node.get('cls','')
|
||||
text = node.get('text','')
|
||||
children = node.get('children', [])
|
||||
cur = f'{path}/{tag}'
|
||||
if target.lower() in text.lower():
|
||||
results.append({'path': cur, 'depth': depth, 'cls': cls[:80], 'text': text[:80], 'children': len(children)})
|
||||
for c in children:
|
||||
find_nodes_by_text(c, target, cur, results, depth+1)
|
||||
return results
|
||||
|
||||
print('\n=== NODES containing "Always run" ===')
|
||||
matches = find_nodes_by_text(body, 'Always run')
|
||||
for m in matches:
|
||||
print(f' depth={m["depth"]} cls="{m["cls"]}" text="{m["text"]}" children={m["children"]}')
|
||||
|
||||
print('\n=== NODES containing "Always" ===')
|
||||
matches = find_nodes_by_text(body, 'Always')
|
||||
for m in matches:
|
||||
print(f' depth={m["depth"]} cls="{m["cls"]}" text="{m["text"]}" children={m["children"]}')
|
||||
|
||||
# Find ALL text nodes with > 30 chars
|
||||
def find_all_text(node, results=None, depth=0, path=''):
|
||||
if results is None: results = []
|
||||
if not isinstance(node, dict): return results
|
||||
tag = node.get('tag','')
|
||||
cls = node.get('cls','')
|
||||
text = node.get('text','')
|
||||
children = node.get('children', [])
|
||||
if text and len(text) > 30:
|
||||
results.append({'depth': depth, 'tag': tag, 'cls': cls[:80], 'text': text[:100], 'path': f'{path}/{tag}'})
|
||||
for c in children:
|
||||
find_all_text(c, results, depth+1, f'{path}/{tag}')
|
||||
return results
|
||||
|
||||
print('\n=== LONG TEXT NODES (>30 chars) ===')
|
||||
texts = find_all_text(body)
|
||||
texts.sort(key=lambda x: len(x['text']), reverse=True)
|
||||
for t in texts[:25]:
|
||||
print(f' d={t["depth"]} [{t["tag"]}] cls="{t["cls"][:50]}" len={len(t["text"])} "{t["text"][:80]}"')
|
||||
|
||||
# Find nodes with many children (structural containers)
|
||||
def find_containers(node, results=None, depth=0, path=''):
|
||||
if results is None: results = []
|
||||
if not isinstance(node, dict): return results
|
||||
tag = node.get('tag','')
|
||||
cls = node.get('cls','')
|
||||
children = node.get('children', [])
|
||||
if len(children) > 5:
|
||||
results.append({'depth': depth, 'tag': tag, 'cls': cls[:100], 'children': len(children), 'path': f'{path}/{tag}'})
|
||||
for c in children:
|
||||
find_containers(c, results, depth+1, f'{path}/{tag}')
|
||||
return results
|
||||
|
||||
print('\n=== CONTAINERS (>5 children) ===')
|
||||
conts = find_containers(body)
|
||||
conts.sort(key=lambda x: x['children'], reverse=True)
|
||||
for c in conts[:20]:
|
||||
print(f' d={c["depth"]} [{c["tag"]}] children={c["children"]} cls="{c["cls"][:70]}"')
|
||||
@@ -1,109 +0,0 @@
|
||||
"""Trace the DOM path from body to AI response container."""
|
||||
import json, os
|
||||
|
||||
fpath = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html_5.json')
|
||||
with open(fpath, 'r', encoding='utf-8-sig') as f:
|
||||
data = json.load(f)
|
||||
|
||||
body = data.get('body', data.get('bodyTree', {}))
|
||||
|
||||
def find_path_to_class(node, target_cls, path=None, depth=0):
|
||||
"""Find the DOM path down to a node with a matching class."""
|
||||
if path is None: path = []
|
||||
if not isinstance(node, dict): return []
|
||||
|
||||
tag = node.get('tag', '')
|
||||
cls = node.get('cls', '')
|
||||
children = node.get('children', [])
|
||||
text = node.get('text', '')
|
||||
attrs = node.get('attrs', {})
|
||||
|
||||
entry = {
|
||||
'depth': depth,
|
||||
'tag': tag,
|
||||
'cls': cls[:120],
|
||||
'children': len(children),
|
||||
'text': text[:60] if text else '',
|
||||
'attrs': {k:v[:40] for k,v in attrs.items() if k not in ('style',)}
|
||||
}
|
||||
|
||||
if target_cls.lower() in cls.lower():
|
||||
return path + [entry]
|
||||
|
||||
for i, child in enumerate(children):
|
||||
result = find_path_to_class(child, target_cls, path + [entry], depth+1)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return []
|
||||
|
||||
# Find path to the AI response container
|
||||
print("=== PATH TO 'leading-relaxed select-text' ===")
|
||||
path = find_path_to_class(body, 'leading-relaxed select-text')
|
||||
for p in path:
|
||||
indent = ' ' * p['depth']
|
||||
print(f'{indent}[{p["tag"]}] cls="{p["cls"]}" children={p["children"]} {p["attrs"]}')
|
||||
if p['text']:
|
||||
print(f'{indent} text: "{p["text"]}"')
|
||||
|
||||
# Now get the full subtree of the AI response container
|
||||
def get_subtree(node, target_cls, depth=0):
|
||||
if not isinstance(node, dict): return None
|
||||
cls = node.get('cls', '')
|
||||
if target_cls.lower() in cls.lower():
|
||||
return node
|
||||
for child in node.get('children', []):
|
||||
result = get_subtree(child, target_cls, depth+1)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
|
||||
print("\n=== AI RESPONSE CONTAINER SUBTREE ===")
|
||||
container = get_subtree(body, 'leading-relaxed select-text')
|
||||
if container:
|
||||
def print_tree(node, depth=0, max_depth=4):
|
||||
if not isinstance(node, dict) or depth > max_depth: return
|
||||
tag = node.get('tag','')
|
||||
cls = node.get('cls','')[:80]
|
||||
text = node.get('text','')
|
||||
children = node.get('children', [])
|
||||
indent = ' ' * depth
|
||||
line = f'{indent}[{tag}]'
|
||||
if cls: line += f' cls="{cls}"'
|
||||
line += f' children={len(children)}'
|
||||
if text: line += f' text="{text[:60]}"'
|
||||
print(line)
|
||||
for c in children:
|
||||
print_tree(c, depth+1, max_depth)
|
||||
|
||||
print_tree(container, 0, 3)
|
||||
|
||||
# Also search for the chat panel container - what wraps the entire conversation
|
||||
print("\n=== SEARCH FOR CHAT PANEL WRAPPERS ===")
|
||||
chat_patterns = ['chat', 'antigravity', 'gemini', 'panel', 'agentview', 'sidebar', 'conversation']
|
||||
for pat in chat_patterns:
|
||||
path = find_path_to_class(body, pat)
|
||||
if path:
|
||||
last = path[-1]
|
||||
print(f' Pattern "{pat}" found at depth={last["depth"]} [{last["tag"]}] cls="{last["cls"]}" children={last["children"]}')
|
||||
|
||||
# Find the parent chain from body to the container - look by scanning ALL class names
|
||||
print("\n=== ALL UNIQUE CLASS NAMES (depth <= 12) ===")
|
||||
all_classes = set()
|
||||
def collect_classes(node, depth=0, max_depth=12):
|
||||
if not isinstance(node, dict) or depth > max_depth: return
|
||||
cls = node.get('cls', '')
|
||||
if cls:
|
||||
for c in cls.split():
|
||||
if len(c) > 3 and not c.startswith('{') and 'mtk' not in c:
|
||||
all_classes.add(c)
|
||||
for child in node.get('children', []):
|
||||
collect_classes(child, depth+1, max_depth)
|
||||
|
||||
collect_classes(body)
|
||||
# Print classes sorted, grouped by potential relevance
|
||||
relevant = sorted([c for c in all_classes if any(k in c.lower() for k in
|
||||
['chat', 'message', 'response', 'agent', 'gemini', 'turn', 'model', 'user', 'bot', 'conversation', 'markdown', 'prose', 'text-', 'content'])])
|
||||
print("Relevant classes:")
|
||||
for c in relevant:
|
||||
print(f' {c}')
|
||||
@@ -1,217 +0,0 @@
|
||||
"""Vikunja safe task updater — preserves existing fields when updating tasks.
|
||||
|
||||
Usage:
|
||||
python vikunja_helper.py done 75 # Mark task #75 as done
|
||||
python vikunja_helper.py done 71 77 78 # Mark multiple tasks done
|
||||
python vikunja_helper.py undone 75 # Mark task #75 as not done
|
||||
python vikunja_helper.py comment 75 "text" # Add comment to task #75
|
||||
python vikunja_helper.py desc 75 "text" # Set description (appends if exists)
|
||||
python vikunja_helper.py create "title" "desc" --labels Backend,Priority:High
|
||||
python vikunja_helper.py create "title" "desc" --done --labels Frontend,Priority:Mid
|
||||
python vikunja_helper.py label 75 Backend Priority:High # Add labels to task
|
||||
python vikunja_helper.py list # List all tasks
|
||||
python vikunja_helper.py list todo # List TODO only
|
||||
python vikunja_helper.py list done # List DONE only
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import io
|
||||
|
||||
# Fix Windows console encoding (cp949 → utf-8)
|
||||
if sys.stdout.encoding != "utf-8":
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
# ============================================================
|
||||
# ⚙️ CONFIGURATION — PROJECT_ID만 프로젝트별로 변경하세요
|
||||
# ============================================================
|
||||
API_BASE = "https://plan.variet.net/api/v1"
|
||||
TOKEN = "tk_070f8e0b715e818bb7178c3815ed5389040eddca"
|
||||
PROJECT_ID = 8 # gravity_control project
|
||||
# ============================================================
|
||||
|
||||
HEADERS = {
|
||||
"Authorization": f"Bearer {TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Label name → Vikunja label ID mapping
|
||||
# Customize for your project's labels
|
||||
LABEL_MAP = {
|
||||
"Backend": 1, "Frontend": 2, "Engine": 3, "Infra": 4, "Test": 5,
|
||||
"Priority:High": 6, "Priority:Mid": 7, "Priority:Low": 8,
|
||||
"Agent": 17, "Tool": 18, "AI/LLM": 19,
|
||||
}
|
||||
|
||||
|
||||
def api_get(path: str):
|
||||
req = urllib.request.Request(f"{API_BASE}{path}", headers=HEADERS)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def api_post(path: str, data: dict):
|
||||
body = json.dumps(data).encode("utf-8")
|
||||
req = urllib.request.Request(f"{API_BASE}{path}", data=body, headers=HEADERS, method="POST")
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def api_put(path: str, data: dict):
|
||||
body = json.dumps(data).encode("utf-8")
|
||||
req = urllib.request.Request(f"{API_BASE}{path}", data=body, headers=HEADERS, method="PUT")
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def get_task(task_id: int) -> dict:
|
||||
return api_get(f"/tasks/{task_id}")
|
||||
|
||||
|
||||
def safe_update_task(task_id: int, updates: dict) -> dict:
|
||||
task = get_task(task_id)
|
||||
safe_body = {
|
||||
"title": task.get("title", ""),
|
||||
"description": task.get("description", ""),
|
||||
"priority": task.get("priority", 0),
|
||||
"done": task.get("done", False),
|
||||
}
|
||||
safe_body.update(updates)
|
||||
return api_post(f"/tasks/{task_id}", safe_body)
|
||||
|
||||
|
||||
def mark_done(task_ids: list):
|
||||
for tid in task_ids:
|
||||
result = safe_update_task(tid, {"done": True})
|
||||
title = result.get("title", "?")
|
||||
print(f" ✅ #{tid} → done=True [{title}]")
|
||||
|
||||
|
||||
def mark_undone(task_ids: list):
|
||||
for tid in task_ids:
|
||||
result = safe_update_task(tid, {"done": False})
|
||||
title = result.get("title", "?")
|
||||
print(f" ⬜ #{tid} → done=False [{title}]")
|
||||
|
||||
|
||||
def add_comment(task_id: int, comment: str):
|
||||
result = api_put(f"/tasks/{task_id}/comments", {"comment": comment})
|
||||
print(f" 💬 #{task_id} comment added (id={result.get('id', '?')})")
|
||||
|
||||
|
||||
def set_description(task_id: int, desc: str, append: bool = True):
|
||||
task = get_task(task_id)
|
||||
existing = task.get("description", "") or ""
|
||||
if append and existing:
|
||||
new_desc = existing.rstrip() + "\n\n" + desc
|
||||
else:
|
||||
new_desc = desc
|
||||
result = safe_update_task(task_id, {"description": new_desc})
|
||||
print(f" 📝 #{task_id} description updated [{result.get('title', '?')}]")
|
||||
|
||||
|
||||
def list_tasks(filter_: str = "all"):
|
||||
all_tasks = []
|
||||
page = 1
|
||||
while True:
|
||||
batch = api_get(f"/projects/{PROJECT_ID}/tasks?per_page=50&page={page}")
|
||||
if not batch:
|
||||
break
|
||||
all_tasks.extend(batch)
|
||||
if len(batch) < 50:
|
||||
break
|
||||
page += 1
|
||||
|
||||
if filter_ == "todo":
|
||||
all_tasks = [t for t in all_tasks if not t["done"]]
|
||||
elif filter_ == "done":
|
||||
all_tasks = [t for t in all_tasks if t["done"]]
|
||||
|
||||
all_tasks.sort(key=lambda t: t["id"])
|
||||
for t in all_tasks:
|
||||
status = "✅" if t["done"] else "⬜"
|
||||
desc = (t.get("description") or "")[:50].replace("\n", " ")
|
||||
labels = ", ".join(l["title"] for l in (t.get("labels") or []))
|
||||
print(f" {status} #{t['id']:3d} {t['title'][:40]:<40} [{labels}] {desc}")
|
||||
print(f"\n Total: {len(all_tasks)} tasks")
|
||||
|
||||
|
||||
def add_labels(task_id: int, label_names: list):
|
||||
for name in label_names:
|
||||
label_id = LABEL_MAP.get(name)
|
||||
if not label_id:
|
||||
print(f" ⚠️ Unknown label '{name}'. Valid: {', '.join(LABEL_MAP.keys())}")
|
||||
continue
|
||||
try:
|
||||
api_put(f"/tasks/{task_id}/labels", {"label_id": label_id})
|
||||
print(f" 🏷️ #{task_id} + {name} (id={label_id})")
|
||||
except Exception as e:
|
||||
if "already" in str(e).lower() or "409" in str(e):
|
||||
print(f" 🏷️ #{task_id} already has {name}")
|
||||
else:
|
||||
print(f" ⚠️ #{task_id} label {name} failed: {e}")
|
||||
|
||||
|
||||
def create_task(title: str, description: str = "", done: bool = False, labels: list = None):
|
||||
payload = {"title": title, "description": description}
|
||||
result = api_put(f"/projects/{PROJECT_ID}/tasks", payload)
|
||||
task_id = result["id"]
|
||||
print(f" ✨ #{task_id} created: {result.get('title', '?')}")
|
||||
|
||||
if labels:
|
||||
add_labels(task_id, labels)
|
||||
|
||||
if done:
|
||||
result = safe_update_task(task_id, {"done": True})
|
||||
print(f" ✅ #{task_id} → done=True")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
return
|
||||
|
||||
cmd = sys.argv[1].lower()
|
||||
|
||||
if cmd == "done":
|
||||
ids = [int(x) for x in sys.argv[2:]]
|
||||
mark_done(ids)
|
||||
elif cmd == "undone":
|
||||
ids = [int(x) for x in sys.argv[2:]]
|
||||
mark_undone(ids)
|
||||
elif cmd == "comment":
|
||||
add_comment(int(sys.argv[2]), sys.argv[3])
|
||||
elif cmd == "desc":
|
||||
set_description(int(sys.argv[2]), sys.argv[3])
|
||||
elif cmd == "list":
|
||||
f = sys.argv[2] if len(sys.argv) > 2 else "all"
|
||||
list_tasks(f)
|
||||
elif cmd == "label":
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: vikunja_helper.py label TASK_ID Label1 Label2 ...")
|
||||
return
|
||||
add_labels(int(sys.argv[2]), sys.argv[3:])
|
||||
elif cmd == "create":
|
||||
title = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
desc = sys.argv[3] if len(sys.argv) > 3 and not sys.argv[3].startswith("--") else ""
|
||||
is_done = "--done" in sys.argv
|
||||
labels = None
|
||||
for i, arg in enumerate(sys.argv):
|
||||
if arg == "--labels" and i + 1 < len(sys.argv):
|
||||
labels = sys.argv[i + 1].split(",")
|
||||
break
|
||||
if not title:
|
||||
print("Error: title is required")
|
||||
return
|
||||
create_task(title, desc, done=is_done, labels=labels)
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
print(__doc__)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,100 +0,0 @@
|
||||
"""Gitea Wiki helper: list, read, create, update wiki pages.
|
||||
|
||||
Usage:
|
||||
wiki_helper.py list — list all pages
|
||||
wiki_helper.py read <title> — read a page
|
||||
wiki_helper.py create <title> <file> — create a page from file
|
||||
wiki_helper.py update <title> <file> — update a page from file
|
||||
"""
|
||||
import sys, io, json, base64, urllib.request, urllib.error
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
# ============================================================
|
||||
# ⚙️ CONFIGURATION — GITEA_REPO만 프로젝트별로 변경하세요
|
||||
# ============================================================
|
||||
GITEA_BASE_URL = "https://git.variet.net"
|
||||
GITEA_OWNER = "Variet"
|
||||
GITEA_REPO = "gravity_control" # ← 프로젝트별 변경 필요
|
||||
GITEA_TOKEN = "3a01b4b15a39921572e64c413353e870d4d2161b"
|
||||
# ============================================================
|
||||
|
||||
BASE = f"{GITEA_BASE_URL}/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}/wiki"
|
||||
HEADERS = {"Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json"}
|
||||
|
||||
def _req(method, path, data=None):
|
||||
url = f"{BASE}{path}"
|
||||
body = json.dumps(data).encode() if data else None
|
||||
req = urllib.request.Request(url, data=body, headers=HEADERS, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
err = e.read().decode()
|
||||
print(f" ⚠️ HTTP {e.code}: {err}")
|
||||
return None
|
||||
|
||||
def _find_sub_url(title):
|
||||
pages = _req("GET", "/pages")
|
||||
if pages:
|
||||
for p in pages:
|
||||
if p.get("title", "").lower() == title.lower():
|
||||
return p.get("sub_url", title)
|
||||
return title
|
||||
|
||||
def list_pages():
|
||||
pages = _req("GET", "/pages")
|
||||
if pages:
|
||||
print(f"=== {len(pages)} Wiki Pages ===")
|
||||
for p in pages:
|
||||
print(f" {p.get('title', '?')}")
|
||||
return pages
|
||||
|
||||
def read_page(title):
|
||||
sub = _find_sub_url(title)
|
||||
page = _req("GET", f"/page/{sub}")
|
||||
if page and page.get("content_base64"):
|
||||
content = base64.b64decode(page["content_base64"]).decode("utf-8")
|
||||
return content
|
||||
return None
|
||||
|
||||
def create_page(title, content):
|
||||
data = {
|
||||
"title": title,
|
||||
"content_base64": base64.b64encode(content.encode()).decode(),
|
||||
}
|
||||
result = _req("POST", "/new", data)
|
||||
if result:
|
||||
print(f" ✅ Created wiki page: {title}")
|
||||
return result
|
||||
|
||||
def update_page(title, content):
|
||||
sub = _find_sub_url(title)
|
||||
data = {
|
||||
"title": title,
|
||||
"content_base64": base64.b64encode(content.encode()).decode(),
|
||||
}
|
||||
result = _req("PATCH", f"/page/{sub}", data)
|
||||
if result:
|
||||
print(f" ✅ Updated wiki page: {title}")
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
|
||||
|
||||
if cmd == "list":
|
||||
list_pages()
|
||||
elif cmd == "read" and len(sys.argv) > 2:
|
||||
content = read_page(sys.argv[2])
|
||||
if content:
|
||||
print(content[:5000])
|
||||
else:
|
||||
print(f" Page '{sys.argv[2]}' not found")
|
||||
elif cmd == "create" and len(sys.argv) > 3:
|
||||
with open(sys.argv[3], "r", encoding="utf-8") as f:
|
||||
create_page(sys.argv[2], f.read())
|
||||
elif cmd == "update" and len(sys.argv) > 3:
|
||||
with open(sys.argv[3], "r", encoding="utf-8") as f:
|
||||
update_page(sys.argv[2], f.read())
|
||||
else:
|
||||
print("Usage: wiki_helper.py list|read <title>|create <title> <file>|update <title> <file>")
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
description: 모든 구현 작업 전 실행하는 사전 점검 체크리스트 (pre-task, 준비, 시작 전, 계획, 구현)
|
||||
---
|
||||
|
||||
# Pre-Task Checklist
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 코딩을 시작하기 전에 반드시 이 체크리스트를 순서대로 완료하세요.
|
||||
> 체크리스트를 건너뛸 경우 불필요한 시행착오가 발생합니다.
|
||||
|
||||
## 1단계: 요구사항 정리
|
||||
|
||||
- [ ] 유저 요청을 구체적 작업 항목으로 분해
|
||||
- [ ] 변경 범위(scope)를 명확히 정의 (영향받는 파일/모듈)
|
||||
- [ ] 성공 기준(acceptance criteria) 확인
|
||||
|
||||
## 2단계: 레퍼런스 확인 (추측 금지)
|
||||
|
||||
- [ ] `.agents/references/architecture.md` — 현재 아키텍처 확인
|
||||
- [ ] `.agents/references/tech-stack.md` — 기술 스택 및 버전 확인
|
||||
- [ ] `.agents/references/conventions.md` — 코딩 컨벤션 확인
|
||||
- [ ] `.agents/references/known-issues.md` — 과거 실패 패턴 확인
|
||||
- [ ] 관련 기존 코드 최소 3개 파일 읽기
|
||||
|
||||
> [!CAUTION]
|
||||
> 레퍼런스 문서가 존재하는 주제에 대해 추측하지 마세요.
|
||||
> 문서가 없으면 유저에게 확인을 요청하세요.
|
||||
|
||||
## 3단계: 계획 수립
|
||||
|
||||
- [ ] 변경할 파일 목록 작성
|
||||
- [ ] 의존성 순서 파악 (어떤 파일부터 수정해야 하는가?)
|
||||
- [ ] 리스크 식별 (어디서 실패할 가능성이 높은가?)
|
||||
- [ ] 테스트 방법 결정 (어떻게 검증할 것인가?)
|
||||
|
||||
## 4단계: 유저 확인
|
||||
|
||||
- [ ] 계획을 유저에게 보고하고 승인받기 (변경 파일 3개 이상인 경우)
|
||||
- [ ] 작은 변경은 바로 실행하되, 변경 내용을 명확히 설명
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
description: 프로젝트 연동 서비스 URL, API 키, 프로젝트 정보 참조
|
||||
---
|
||||
|
||||
# 서비스 연동 정보
|
||||
|
||||
> [!CAUTION]
|
||||
> 이 파일에는 API 토큰이 포함되어 있습니다. 프라이빗 레포에서만 사용하세요.
|
||||
|
||||
## 로컬 환경
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| **Node.js** | 시스템 설치 (`node`, `npm`) |
|
||||
| **Python** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` (**항상 이 경로 사용**) |
|
||||
| **Shell** | PowerShell (`curl` = `Invoke-WebRequest` 별칭이므로 반드시 **`curl.exe`** 사용) |
|
||||
|
||||
## Gitea (Git Repository)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| **Base URL** | `https://git.variet.net` |
|
||||
| **API Base** | `https://git.variet.net/api/v1` |
|
||||
| **Repo** | `Variet/gravity_control` |
|
||||
| **Token** | `3a01b4b15a39921572e64c413353e870d4d2161b` |
|
||||
| **Auth Header** | `-H "Authorization: token 3a01b4b15a39921572e64c413353e870d4d2161b"` |
|
||||
|
||||
## Vikunja (Task Management)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| **Base URL** | `https://plan.variet.net` |
|
||||
| **API Base** | `https://plan.variet.net/api/v1` |
|
||||
| **Project ID** | `8` |
|
||||
| **Token** | `tk_070f8e0b715e818bb7178c3815ed5389040eddca` |
|
||||
| **Auth Header** | `-H "Authorization: Bearer tk_070f8e0b715e818bb7178c3815ed5389040eddca"` |
|
||||
|
||||
## Vikunja 태스크 조회
|
||||
|
||||
> [!TIP]
|
||||
> 태스크 목록은 항상 라이브 조회를 사용합니다. 하드코딩된 매핑은 유지하지 않습니다.
|
||||
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list todo
|
||||
```
|
||||
|
||||
## 기타 서비스
|
||||
|
||||
| 서비스 | URL | 용도 |
|
||||
|--------|-----|------|
|
||||
| Uptime Kuma | `https://status.variet.net` | 서비스 모니터링 |
|
||||
| Authentik | `https://auth.variet.net` | SSO 인증 |
|
||||
|
||||
## AI 작업 프로토콜
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 아래 규칙은 모든 작업에 자동 적용됩니다. 유저가 별도 지시하지 않아도 따릅니다.
|
||||
|
||||
### Vikunja = Single Source of Truth (SSOT)
|
||||
|
||||
- **Vikunja가 유일한 작업 현황 관리 도구**입니다.
|
||||
- 로컬 `task.md`는 현재 대화 내 세부 체크리스트용으로만 사용합니다.
|
||||
- 새 TODO 발견 시 → Vikunja에 태스크 생성 (로컬 파일에만 적는 것은 금지)
|
||||
- 작업 완료 시 → Vikunja 태스크 완료 처리 (로컬 체크만 하는 것은 금지)
|
||||
|
||||
### Vikunja 태깅 규칙
|
||||
|
||||
태스크 생성 시 반드시 아래 라벨을 적절히 부여합니다:
|
||||
|
||||
**영역 라벨 (필수, 1개 이상):**
|
||||
|
||||
| ID | 라벨 | 적용 대상 |
|
||||
|:--:|-------|-----------:|
|
||||
| 1 | `Backend` | 서버, DB, API |
|
||||
| 2 | `Frontend` | UI, 웹 프론트엔드 |
|
||||
| 3 | `Engine` | 핵심 엔진/로직 |
|
||||
| 4 | `Infra` | Docker, CI/CD, 모니터링 |
|
||||
| 5 | `Test` | 테스트, E2E |
|
||||
|
||||
**우선순위 라벨 (필수, 1개):**
|
||||
|
||||
| ID | 라벨 | 기준 |
|
||||
|:--:|-------|------:|
|
||||
| 6 | `Priority:High` | 핵심 기능 미완성, 블로커 |
|
||||
| 7 | `Priority:Mid` | 기능 개선, UX 향상, 리팩터링 |
|
||||
| 8 | `Priority:Low` | nice-to-have, 문서, 코드 정리 |
|
||||
|
||||
**태스크 제목 규칙:**
|
||||
- 한글 + 핵심 키워드 (예: `WebSocket 재연결 로직 구현`)
|
||||
- 50자 이내
|
||||
|
||||
### 작업 시작 시
|
||||
1. `git pull` 으로 최신 코드 동기화
|
||||
2. Vikunja 태스크 조회 (`/check-vikunja`) → 관련 태스크 ID 확인
|
||||
3. 관련 태스크가 있으면 Vikunja에서 진행중 표시
|
||||
4. 관련 태스크가 없으면 Vikunja에 새 태스크 생성 (태깅 규칙 준수)
|
||||
|
||||
### 작업 중
|
||||
5. 의미 있는 단위마다 자주 커밋 (대규모 변경을 한번에 커밋하지 않음)
|
||||
6. 커밋 메시지 규칙:
|
||||
- `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, `ci:` 접두사 사용
|
||||
- 관련 Vikunja 태스크가 있으면 `#task-{ID}` 참조 포함
|
||||
- 예: `feat(server): WebSocket 재연결 로직 #task-21`
|
||||
|
||||
### 작업 완료 시
|
||||
7. 모든 변경사항 커밋 + `git push`
|
||||
8. Vikunja 태스크 완료 처리 (**반드시 `vikunja_helper.py` 사용**):
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {TASK_ID}
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> **직접 `Invoke-RestMethod -Body '{"done": true}'` 사용 금지!**
|
||||
> Vikunja API는 POST 시 body에 없는 필드를 빈값으로 덮어씁니다.
|
||||
|
||||
9. 작업 중 발견된 새 TODO → Vikunja에 태스크 생성
|
||||
|
||||
### 멀티 AI 협업 시 추가 규칙
|
||||
- 작업 전 `git pull` 필수 (다른 AI가 push한 변경 반영)
|
||||
- 같은 파일을 동시에 수정하지 않음
|
||||
- 공유 인터페이스 수정 시 즉시 commit + push
|
||||
- 충돌 발생 시 유저에게 확인 요청
|
||||
|
||||
## PowerShell 주의사항
|
||||
|
||||
- `curl` → PowerShell에서 `Invoke-WebRequest`의 별칭. **반드시 `curl.exe`** 사용
|
||||
- `npm` → PowerShell에서 실행 정책 문제 시 `cmd /c npm` 사용
|
||||
- JSON 파이프 파싱 시 PowerShell 이스케이핑 문제 → `.py` 스크립트 파일로 만들어 실행 권장
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
description: 세션 시작 시 프로젝트 맥락을 빠르게 복구하는 워크플로우 (시작, continue, 이어서, 작업 시작)
|
||||
---
|
||||
|
||||
# 세션 시작 프로토콜
|
||||
|
||||
새 대화 시작, "continue", "이어서", "작업 시작" 등 요청 시 이 워크플로우를 실행합니다.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## 절차
|
||||
|
||||
### 0. 에이전트 룰 & 맥락 로딩 (자동)
|
||||
|
||||
`.agents/AGENT.md`를 읽고 에이전트 행동 규칙을 로딩합니다.
|
||||
`.agents/references/known-issues.md`를 읽어 최근 이슈를 파악합니다.
|
||||
`.agents/workflows/services.md`의 **로컬 환경** 섹션을 읽고 Python 경로 등 환경 설정을 확인합니다.
|
||||
|
||||
### 1. Devlog 맥락 복구
|
||||
|
||||
오늘 + 어제 devlog index를 읽고 최근 작업 흐름을 파악합니다.
|
||||
|
||||
```powershell
|
||||
$today = Get-Date -Format "yyyy-MM-dd"
|
||||
$yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd")
|
||||
if (Test-Path "docs\devlog\$today.md") {
|
||||
Write-Host "=== Devlog: $today ==="
|
||||
Get-Content "docs\devlog\$today.md"
|
||||
} elseif (Test-Path "docs\devlog\$yesterday.md") {
|
||||
Write-Host "=== Devlog: $yesterday (no entry for today yet) ==="
|
||||
Get-Content "docs\devlog\$yesterday.md"
|
||||
} else {
|
||||
Write-Host "=== No recent devlog found ==="
|
||||
}
|
||||
```
|
||||
|
||||
미완료(🔧) 항목이 있으면 해당 entry 파일을 읽어 이어받기 맥락을 확보합니다:
|
||||
- Entry 경로: `docs/devlog/entries/YYYYMMDD-NNN.md`
|
||||
|
||||
### 2. Git 상태 확인
|
||||
|
||||
```powershell
|
||||
git status --short
|
||||
```
|
||||
```powershell
|
||||
git log --oneline -5
|
||||
```
|
||||
|
||||
### 3. Vikunja TODO 태스크
|
||||
|
||||
```powershell
|
||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list todo
|
||||
```
|
||||
|
||||
### 4. 종합 보고
|
||||
|
||||
결과를 종합하여 사용자에게 보고:
|
||||
- 마지막 작업 맥락 + 미완료 항목 (devlog 🔧 기반)
|
||||
- TODO 태스크 목록 (라벨 + 우선순위)
|
||||
- 다음 작업 제안
|
||||
|
||||
**우선순위 판단 기준** (라벨만으로 판단 금지):
|
||||
- P0: 최근 커밋에서 스키마/모델/인터페이스 변경 → 연쇄 영향 점검
|
||||
- P1: 서버 기동/API 응답 장애
|
||||
- P2: 기능 미완성/UX 개선
|
||||
- P3: 정확도 향상, 신규 기능, CI/CD, 문서 정리
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
*.vsix
|
||||
extension/node_modules/
|
||||
extension/out/
|
||||
.deps_installed
|
||||
gravity_control.log
|
||||
*.tar.gz
|
||||
28
.env.example
28
.env.example
@@ -1,31 +1,17 @@
|
||||
# Discord Bot Token (필수)
|
||||
# Discord Bot Token
|
||||
DISCORD_TOKEN=your_discord_bot_token_here
|
||||
|
||||
# Discord Guild (서버) ID (필수) — 봇이 채널을 생성할 서버
|
||||
# Discord Guild (서버) ID — 봇이 채널을 생성할 서버
|
||||
DISCORD_GUILD_ID=
|
||||
|
||||
# Bridge 디렉토리 (기본값: ~/.gemini/antigravity/bridge)
|
||||
# 보통 수정 불필요 — Extension과 동일 경로 사용
|
||||
BRIDGE_PATH=
|
||||
|
||||
# Antigravity Brain Path (Watcher용)
|
||||
BRAIN_PATH=
|
||||
# Antigravity Brain Path
|
||||
BRAIN_PATH=C:\Users\Certes\.gemini\antigravity\brain
|
||||
|
||||
# 세션 활성 판단: 마지막 파일 변경으로부터 이 시간(초) 이내면 활성
|
||||
ACTIVE_TIMEOUT_SECONDS=300
|
||||
|
||||
# Project name (used for Discord channel: AG-{PROJECT_NAME})
|
||||
PROJECT_NAME=gravity_control
|
||||
|
||||
# Watcher Settings
|
||||
DEBOUNCE_SECONDS=2
|
||||
|
||||
# Bot mode: 'local' (default, file-based) or 'gateway' (서버 Docker + WS Hub)
|
||||
BOT_MODE=local
|
||||
|
||||
# Gateway API Key (보안)
|
||||
# 서버와 Collector에 동일한 키를 설정하세요
|
||||
# 생성: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
GATEWAY_API_KEY=
|
||||
|
||||
# Hub WebSocket 인증 (선택 — 미설정 시 인증 생략)
|
||||
# 생성: python -c "import secrets; print(secrets.token_hex(32))"
|
||||
GRAVITY_HUB_SECRET=
|
||||
GRAVITY_REGISTRATION_CODE=
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,6 +11,8 @@ build/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Agents (contains tokens)
|
||||
.agents/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
@@ -26,4 +28,3 @@ Thumbs.db
|
||||
# Node
|
||||
node_modules/
|
||||
extension/out/
|
||||
*.vsix
|
||||
|
||||
BIN
.gitlog.txt
BIN
.gitlog.txt
Binary file not shown.
18
Dockerfile
18
Dockerfile
@@ -1,18 +0,0 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt aiohttp>=3.9.0
|
||||
|
||||
# Copy application code (all Python modules)
|
||||
COPY *.py ./
|
||||
|
||||
# Default environment (can be overridden via docker-compose)
|
||||
ENV BOT_MODE=gateway
|
||||
ENV GATEWAY_PORT=8585
|
||||
|
||||
EXPOSE 8585
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
2
antigravity-sdk-main/.github/FUNDING.yml
vendored
2
antigravity-sdk-main/.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
custom:
|
||||
- https://github.com/Kanezal/antigravity-sdk#support
|
||||
49
antigravity-sdk-main/.github/workflows/docs.yml
vendored
49
antigravity-sdk-main/.github/workflows/docs.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: Deploy TypeDoc to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'package.json'
|
||||
- 'tsconfig.json'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- run: npx typedoc --out docs-site src/index.ts --tsconfig tsconfig.json
|
||||
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs-site
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
15
antigravity-sdk-main/.gitignore
vendored
15
antigravity-sdk-main/.gitignore
vendored
@@ -1,15 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
docs-site/
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
|
||||
# Internal reference — not for public repo
|
||||
GEMINI.md
|
||||
docs/implementation-plan.md
|
||||
docs/internals.md
|
||||
scripts/
|
||||
|
||||
# Extensions (separate repo)
|
||||
example-extension/
|
||||
xray-extension/
|
||||
@@ -1,68 +0,0 @@
|
||||
# Legal Notice
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This project is an unofficial, community-maintained SDK for building extensions
|
||||
for [Antigravity IDE](https://antigravity.dev). It is **not affiliated with,
|
||||
endorsed by, or sponsored by Google LLC or any of its subsidiaries.**
|
||||
|
||||
## Nature of the Project
|
||||
|
||||
Antigravity SDK provides a **TypeScript library** for VS Code extension
|
||||
developers who want to build tools that work within Antigravity IDE.
|
||||
|
||||
The SDK interacts with Antigravity exclusively through:
|
||||
|
||||
- **VS Code Extension API** — the standard, documented `vscode.*` namespace
|
||||
that all extensions use
|
||||
- **Registered commands** — commands exposed by Antigravity through the
|
||||
standard `vscode.commands` interface
|
||||
- **Local state files** — reading (not writing) locally stored settings
|
||||
|
||||
## Compliance
|
||||
|
||||
- This SDK **does not access** Google's backend servers, gRPC endpoints,
|
||||
or authentication systems directly.
|
||||
- This SDK **does not extract** AI models, training data, weights, or
|
||||
proprietary algorithms.
|
||||
- This SDK **does not bypass** security features, licensing, rate limits,
|
||||
or usage restrictions.
|
||||
- This SDK **does not proxy** or relay requests to Google's infrastructure.
|
||||
- All communication goes through Antigravity's own extension host — the same
|
||||
mechanism used by any VS Code extension.
|
||||
|
||||
## Interoperability
|
||||
|
||||
This SDK is developed to enable interoperability between Antigravity IDE
|
||||
and third-party extensions, as provided by:
|
||||
|
||||
- **EU Software Directive** (Directive 2009/24/EC), Article 6 — permits
|
||||
analysis of software for the purpose of achieving interoperability
|
||||
- **UK Copyright, Designs and Patents Act 1988**, Section 50B
|
||||
- Similar provisions in other jurisdictions
|
||||
|
||||
The API interfaces documented in this project were derived through observation
|
||||
of Antigravity's public extension API surface — the same surface available to
|
||||
any VS Code extension running inside Antigravity.
|
||||
|
||||
## User Responsibility
|
||||
|
||||
Users and extension developers are responsible for ensuring their use of
|
||||
this SDK and any extensions built with it comply with applicable terms of
|
||||
service and local laws.
|
||||
|
||||
Extension developers should:
|
||||
|
||||
1. Not use the SDK to access Google's backend directly
|
||||
2. Not use the SDK to extract or replicate AI model behavior
|
||||
3. Not use the SDK to bypass security or licensing restrictions
|
||||
4. Follow Antigravity's extension guidelines where applicable
|
||||
|
||||
## Takedown
|
||||
|
||||
If Google or the Antigravity team requests removal of this project, we will
|
||||
comply promptly. Contact: [open a GitHub issue](https://github.com/Kanezal/antigravity-sdk/issues).
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the [GNU Affero General Public License v3.0](LICENSE).
|
||||
@@ -1,644 +0,0 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they receive
|
||||
widespread use, become available for other developers to incorporate.
|
||||
Many developers of free software are heartened and encouraged by the
|
||||
resulting cooperation. However, in the case of software used on network
|
||||
servers, this result may fail to come about. The GNU General Public
|
||||
License permits making a modified version and letting the public access
|
||||
it on a server without ever releasing its source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding Source
|
||||
of the work are being offered to the general public at no charge under
|
||||
subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied by
|
||||
the Installation Information. But this requirement does not apply if
|
||||
neither you nor any third party retains the ability to install modified
|
||||
object code on the User Product (for example, the work has been
|
||||
installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in source
|
||||
code form), and must require no special password or key for unpacking,
|
||||
reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions; the
|
||||
above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
|
||||
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR
|
||||
DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR
|
||||
A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
|
||||
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Antigravity SDK
|
||||
Copyright (C) 2026 Kanezal
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@@ -1,348 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
# Antigravity SDK
|
||||
|
||||
**Community SDK for building extensions for [Antigravity IDE](https://antigravity.dev)**
|
||||
|
||||
[](https://www.npmjs.com/package/antigravity-sdk)
|
||||
[](LICENSE)
|
||||
[](https://github.com/Kanezal/antigravity-sdk#support)
|
||||
|
||||
*Build powerful extensions that work alongside Antigravity's AI agent.*
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## What is this?
|
||||
|
||||
A TypeScript SDK for building **VS Code extensions** that extend Antigravity IDE. It gives you programmatic access to the agent's conversations, preferences, step control, real-time activity monitoring, and a declarative API for integrating custom UI directly into the Agent View — all through Antigravity's own extension protocols.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This SDK is designed **exclusively** for building Antigravity extensions. It is **not** a tool for integrating Antigravity with third-party applications, extracting data, or proxying requests. See [Compliance](#compliance).
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install antigravity-sdk
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { AntigravitySDK } from 'antigravity-sdk';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const sdk = new AntigravitySDK(context);
|
||||
await sdk.initialize();
|
||||
|
||||
// List conversations with real titles
|
||||
const sessions = await sdk.cascade.getSessions();
|
||||
console.log(`${sessions.length} conversations`);
|
||||
|
||||
// Read all 16 agent preferences
|
||||
const prefs = await sdk.cascade.getPreferences();
|
||||
console.log('Terminal policy:', prefs.terminalExecutionPolicy);
|
||||
|
||||
// Monitor agent activity in real time
|
||||
sdk.monitor.onStepCountChanged((e) => {
|
||||
console.log(`${e.title}: +${e.delta} steps`);
|
||||
});
|
||||
sdk.monitor.onActiveSessionChanged((e) => {
|
||||
console.log(`Switched to: ${e.title}`);
|
||||
});
|
||||
sdk.monitor.start();
|
||||
|
||||
// Accept/reject agent steps programmatically
|
||||
await sdk.cascade.acceptStep();
|
||||
await sdk.cascade.acceptTerminalCommand();
|
||||
|
||||
context.subscriptions.push(sdk);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Agent View UI Integration
|
||||
|
||||
The SDK provides **9 integration points** in the Agent View panel — add buttons, metadata, badges, menu items, and interactive elements with a fluent, declarative API. Everything is theme-aware and survives Antigravity updates via auto-repair.
|
||||
|
||||
```typescript
|
||||
import { IntegrationManager, IntegrationPoint } from 'antigravity-sdk';
|
||||
|
||||
const ui = new IntegrationManager();
|
||||
|
||||
// Fluent API — chain calls
|
||||
ui.addTopBarButton('stats', '📊', 'Show Stats', {
|
||||
title: 'Session Stats',
|
||||
rows: [{ key: 'Steps:', value: '42' }],
|
||||
})
|
||||
.addInputButton('tokens', '🔢', 'Token Counter')
|
||||
.addTurnMetadata('meta', ['turnNumber', 'userCharCount', 'aiCharCount', 'codeBlocks'])
|
||||
.addUserBadges('badges', 'charCount')
|
||||
.addBotAction('inspect', '🔍', 'Inspect Response')
|
||||
.addDropdownItem('export', 'Export Chat', '📋')
|
||||
.addTitleInteraction('title', 'dblclick', 'Double-click to bookmark');
|
||||
|
||||
await ui.install();
|
||||
ui.enableAutoRepair(); // Survives Antigravity updates
|
||||
```
|
||||
|
||||
| Integration Point | Location | Use Cases |
|
||||
|-------------------|----------|-----------|
|
||||
| `TOP_BAR` | Header icon bar | Session overview, navigation |
|
||||
| `TOP_RIGHT` | Before close button | Status indicators, quick toggle |
|
||||
| `INPUT_AREA` | Next to send button | Token counter, prompt templates |
|
||||
| `BOTTOM_ICONS` | Bottom icon row | Mode switches, quick actions |
|
||||
| `TURN_METADATA` | Inside each turn | Character count, code block stats, turn numbers |
|
||||
| `USER_BADGE` | User message bubble | Message length indicator |
|
||||
| `BOT_ACTION` | Next to Good/Bad | Response analysis, copy actions |
|
||||
| `DROPDOWN_MENU` | 3-dot overflow menu | Export, settings, debug tools |
|
||||
| `CHAT_TITLE` | Conversation title | Rename, bookmark on interaction |
|
||||
|
||||
> [!NOTE]
|
||||
> The integration script runs in the renderer process, independent of the extension. The SDK uses a **heartbeat mechanism** to prevent orphaned integrations: `sdk.initialize()` refreshes a timestamp marker, and the script silently exits if the marker is stale (48h). Disabling your extension will automatically stop the integration on the next IDE restart after the grace period.
|
||||
|
||||
### Conversation Management
|
||||
|
||||
Full control over Cascade conversations — list, create, switch, send messages, and manage agent steps.
|
||||
|
||||
```typescript
|
||||
// List sessions with titles, step counts, timestamps
|
||||
const sessions = await sdk.cascade.getSessions();
|
||||
|
||||
// Switch to a conversation
|
||||
await sdk.cascade.focusSession(sessions[0].id);
|
||||
|
||||
// Send a message to the active chat
|
||||
await sdk.cascade.sendPrompt('Analyze this file');
|
||||
|
||||
// Create a background conversation
|
||||
const id = await sdk.cascade.createBackgroundSession('Run tests quietly');
|
||||
```
|
||||
|
||||
### Real-Time Event Monitoring
|
||||
|
||||
Watch for state changes as they happen — new conversations, step progress, session switches, preference updates.
|
||||
|
||||
```typescript
|
||||
// Agent made progress (added steps)
|
||||
sdk.monitor.onStepCountChanged((e) => {
|
||||
statusBar.text = `${e.title}: step ${e.newCount}`;
|
||||
});
|
||||
|
||||
// User switched to a different conversation
|
||||
sdk.monitor.onActiveSessionChanged((e) => {
|
||||
console.log(`Now viewing: ${e.title}`);
|
||||
});
|
||||
|
||||
// New conversation created
|
||||
sdk.monitor.onNewConversation(() => {
|
||||
console.log('New conversation detected');
|
||||
});
|
||||
|
||||
// Any USS state changed (preferences, settings, etc.)
|
||||
sdk.monitor.onStateChanged((e) => {
|
||||
console.log(`${e.key}: ${e.previousSize} → ${e.newSize} bytes`);
|
||||
});
|
||||
|
||||
sdk.monitor.start(3000, 5000); // USS poll: 3s, trajectory poll: 5s
|
||||
```
|
||||
|
||||
### Agent Step Control
|
||||
|
||||
Programmatically accept, reject, or run agent actions — build approval workflows, auto-accept policies, or custom review UIs.
|
||||
|
||||
```typescript
|
||||
await sdk.cascade.acceptStep(); // Accept code edit
|
||||
await sdk.cascade.rejectStep(); // Reject code edit
|
||||
await sdk.cascade.acceptTerminalCommand(); // Accept terminal command
|
||||
await sdk.cascade.rejectTerminalCommand(); // Reject terminal command
|
||||
await sdk.cascade.runTerminalCommand(); // Run pending command
|
||||
await sdk.cascade.acceptCommand(); // Accept non-terminal action
|
||||
```
|
||||
|
||||
### State & Preferences
|
||||
|
||||
Read the agent's current settings — terminal policies, secure mode, sandbox config, and more. Decoded directly from protobuf sentinel values.
|
||||
|
||||
```typescript
|
||||
const prefs = await sdk.cascade.getPreferences();
|
||||
|
||||
prefs.terminalExecutionPolicy // OFF | AUTO | EAGER
|
||||
prefs.artifactReviewPolicy // ALWAYS | TURBO | AUTO
|
||||
prefs.secureModeEnabled // boolean
|
||||
prefs.terminalSandboxEnabled // boolean
|
||||
prefs.shellIntegrationEnabled // boolean
|
||||
prefs.allowNonWorkspaceFiles // boolean
|
||||
// ... 16 preferences total
|
||||
```
|
||||
|
||||
### IDE Diagnostics
|
||||
|
||||
Access system information, extension logs, and recent conversation metadata.
|
||||
|
||||
```typescript
|
||||
const diag = await sdk.cascade.getDiagnostics();
|
||||
|
||||
console.log(diag.systemInfo.operatingSystem);
|
||||
console.log(diag.systemInfo.userName);
|
||||
console.log(diag.isRemote); // SSH?
|
||||
|
||||
// MCP URL, browser port, git status
|
||||
const mcpUrl = await sdk.cascade.getMcpUrl();
|
||||
const browserPort = await sdk.cascade.getBrowserPort();
|
||||
const ignored = await sdk.cascade.isFileGitIgnored('secret.env');
|
||||
```
|
||||
|
||||
### Headless Cascade (LSBridge)
|
||||
|
||||
Create and manage conversations programmatically through the Language Server — no UI flicker, no panel switching.
|
||||
|
||||
```typescript
|
||||
import { Models } from 'antigravity-sdk';
|
||||
|
||||
// Create a headless cascade with model selection
|
||||
const cascadeId = await sdk.ls.createCascade({
|
||||
text: 'Analyze test coverage in this project',
|
||||
model: Models.GEMINI_FLASH,
|
||||
});
|
||||
|
||||
// Send follow-up messages
|
||||
await sdk.ls.sendMessage({
|
||||
cascadeId,
|
||||
text: 'Now fix the failing tests',
|
||||
model: Models.GEMINI_PRO_HIGH,
|
||||
});
|
||||
|
||||
// Focus in UI when ready
|
||||
await sdk.ls.focusCascade(cascadeId);
|
||||
|
||||
// Or make raw RPC calls to any of the 68 verified LS methods
|
||||
const status = await sdk.ls.getUserStatus();
|
||||
const cascades = await sdk.ls.listCascades();
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> LSBridge auto-discovers the Language Server port and CSRF token from the running LS process. If auto-discovery fails (sandboxed environments), use `sdk.ls.setConnection(port, csrfToken)` manually.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Your Extension
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ antigravity-sdk │
|
||||
│ │
|
||||
│ sdk.cascade ← CascadeManager │
|
||||
│ Sessions, preferences, step control │
|
||||
│ │
|
||||
│ sdk.monitor ← EventMonitor │
|
||||
│ USS polling, trajectory tracking │
|
||||
│ │
|
||||
│ sdk.integration ← IntegrationManager │
|
||||
│ Declarative UI for Agent View │
|
||||
│ │
|
||||
│ sdk.commands ← CommandBridge │
|
||||
│ 60+ verified Antigravity commands │
|
||||
│ │
|
||||
│ sdk.state ← StateBridge │
|
||||
│ Read-only access to USS preferences │
|
||||
│ │
|
||||
│ sdk.ls ← LSBridge │
|
||||
│ Local LS communication (advanced) │
|
||||
│ │
|
||||
└────────────────────────────────────────-─┘
|
||||
│
|
||||
vscode.commands.executeCommand()
|
||||
+ read-only state.vscdb (sql.js)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The SDK uses `sql.js` (pure JS/WASM SQLite) instead of `better-sqlite3` because Antigravity's Electron ABI (v140 / Node v22.21.1) is incompatible with native modules. This was verified in runtime.
|
||||
|
||||
---
|
||||
|
||||
## Compliance
|
||||
|
||||
> [!CAUTION]
|
||||
> **Token extraction is a violation of Google's Terms of Service.**
|
||||
>
|
||||
> The SDK **actively blocks** access to authentication tokens (`oauthToken`, `agentManagerInitState`, and other sensitive keys). Any attempt to read these keys will throw an error.
|
||||
>
|
||||
> Extracting, storing, forwarding, or reusing Antigravity OAuth tokens — directly or through third-party tools — violates Google's TOS and may result in account termination.
|
||||
|
||||
### What this SDK is for
|
||||
|
||||
- Building **VS Code extensions** that run inside Antigravity IDE
|
||||
- Extending Antigravity's functionality for your own workflows
|
||||
- Adding custom UI elements to the Agent View
|
||||
- Monitoring and automating agent step approval
|
||||
- Reading preferences and conversation metadata
|
||||
|
||||
### What this SDK is NOT for
|
||||
|
||||
- Integrating Antigravity with external applications or services
|
||||
- Proxying or relaying requests to Google's infrastructure
|
||||
- Extracting AI model outputs for training other models
|
||||
- Accessing Google's backend servers, gRPC endpoints, or auth systems
|
||||
- Building alternative clients or wrappers around Antigravity
|
||||
|
||||
### How it works
|
||||
|
||||
All SDK communication goes through three safe, local channels:
|
||||
|
||||
1. **`vscode.commands.executeCommand()`** — the standard VS Code Extension API that all extensions use. Antigravity decides what to execute.
|
||||
2. **Read-only local state** — the SDK reads `state.vscdb` for preferences and metadata, never writes.
|
||||
3. **Local Language Server** — the SDK communicates with the LS process on `127.0.0.1` using the same ConnectRPC protocol that Antigravity itself uses. Authentication is via an ephemeral per-session CSRF token (not the user's OAuth token). No data leaves the local machine through this channel.
|
||||
|
||||
The SDK includes a `SENSITIVE_KEYS` blocklist that prevents extension developers from accidentally (or intentionally) accessing authentication data.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[GEMINI.md](GEMINI.md)** — Full internal architecture docs, verified DOM selectors, protobuf schemas
|
||||
- **[LEGAL.md](LEGAL.md)** — Legal notice, interoperability rights, compliance details
|
||||
- **[API Reference](https://kanezal.github.io/antigravity-sdk)** — TypeDoc (coming soon)
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a community project. PRs welcome!
|
||||
|
||||
1. Fork the repo
|
||||
2. Create a feature branch
|
||||
3. Follow the existing code style
|
||||
4. Add JSDoc comments for all public methods
|
||||
5. Submit a PR
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> [!WARNING]
|
||||
> This project is not affiliated with Google or the Antigravity team. The SDK interacts with Antigravity through its existing extension API and local state files. Use at your own risk and in compliance with applicable terms of service.
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Support
|
||||
|
||||
If you find this project useful and want to support its development, you can send **USDT** to:
|
||||
|
||||
| Network | Address |
|
||||
|---------|---------|
|
||||
| **TON** | `UQCjVh3C3mZc44GjT2IDsS4pmeOoUgRNxWMcb85NS5Bz_v1d` |
|
||||
| **TRON (TRC20)** | `TH3JKGjNrSDCsjkkSuneaSMZoJYF7CNTXD` |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](LICENSE)
|
||||
3106
antigravity-sdk-main/package-lock.json
generated
3106
antigravity-sdk-main/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "antigravity-sdk",
|
||||
"version": "1.6.0",
|
||||
"description": "Community SDK for building extensions for Antigravity IDE",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"lint": "eslint src/",
|
||||
"docs": "typedoc --out docs-site src/index.ts",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"antigravity",
|
||||
"antigravity-ide",
|
||||
"google-antigravity",
|
||||
"sdk",
|
||||
"cascade",
|
||||
"ai-agent",
|
||||
"vscode-extension"
|
||||
],
|
||||
"author": "Kanezal",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Kanezal/antigravity-sdk.git"
|
||||
},
|
||||
"homepage": "https://kanezal.github.io/antigravity-sdk",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/vscode": "^1.85.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typedoc": "^0.27.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"sql.js": "^1.14.0"
|
||||
}
|
||||
}
|
||||
@@ -1,527 +0,0 @@
|
||||
/**
|
||||
* Cascade Manager — Session listing, creation, and monitoring.
|
||||
*
|
||||
* Provides high-level API to interact with Cascade conversations
|
||||
* using verified transport layer (CommandBridge + StateBridge).
|
||||
*
|
||||
* VERIFIED 2026-02-28: getDiagnostics.recentTrajectories returns clean JSON
|
||||
* with { googleAgentId, trajectoryId, summary, lastStepIndex, lastModifiedTime }.
|
||||
*
|
||||
* @module cascade/cascade-manager
|
||||
*/
|
||||
|
||||
import { IDisposable, DisposableStore } from '../core/disposable';
|
||||
import { EventEmitter, Event } from '../core/events';
|
||||
import { Logger } from '../core/logger';
|
||||
import type {
|
||||
ITrajectoryEntry,
|
||||
IAgentPreferences,
|
||||
IDiagnosticsInfo,
|
||||
ICreateSessionOptions,
|
||||
} from '../core/types';
|
||||
import { CommandBridge, AntigravityCommands } from '../transport/command-bridge';
|
||||
import { StateBridge } from '../transport/state-bridge';
|
||||
|
||||
const log = new Logger('CascadeManager');
|
||||
|
||||
/**
|
||||
* Manages Cascade conversations.
|
||||
*
|
||||
* Primary data source: `antigravity.getDiagnostics` → `recentTrajectories`
|
||||
* Fallback: `antigravityUnifiedStateSync.trajectorySummaries` protobuf parsing
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const manager = new CascadeManager(commands, state);
|
||||
* await manager.initialize();
|
||||
*
|
||||
* // List sessions (real titles from getDiagnostics)
|
||||
* const sessions = await manager.getSessions();
|
||||
* sessions.forEach(s => console.log(`${s.title} (step ${s.stepCount})`));
|
||||
*
|
||||
* // Read preferences (all 16 sentinel values)
|
||||
* const prefs = await manager.getPreferences();
|
||||
*
|
||||
* // Create & send
|
||||
* await manager.createSession({ task: 'Analyze coverage', background: true });
|
||||
* ```
|
||||
*/
|
||||
export class CascadeManager implements IDisposable {
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private _sessions: ITrajectoryEntry[] = [];
|
||||
private _initialized = false;
|
||||
|
||||
// Events
|
||||
private readonly _onSessionsChanged = this._disposables.add(new EventEmitter<ITrajectoryEntry[]>());
|
||||
/** Fires when the session list changes */
|
||||
public readonly onSessionsChanged: Event<ITrajectoryEntry[]> = this._onSessionsChanged.event;
|
||||
|
||||
constructor(
|
||||
private readonly _commands: CommandBridge,
|
||||
private readonly _state: StateBridge,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Initialize the cascade manager.
|
||||
* Loads the initial session list from getDiagnostics.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
await this._loadSessions();
|
||||
this._initialized = true;
|
||||
log.info(`Initialized with ${this._sessions.length} sessions`);
|
||||
}
|
||||
|
||||
// ─── Read API ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get all known Cascade sessions.
|
||||
*
|
||||
* Uses `getDiagnostics.recentTrajectories` (clean JSON with titles).
|
||||
*
|
||||
* @returns List of trajectory entries sorted by recency
|
||||
*/
|
||||
async getSessions(): Promise<ITrajectoryEntry[]> {
|
||||
if (!this._initialized) {
|
||||
await this._loadSessions();
|
||||
}
|
||||
return [...this._sessions];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the session list.
|
||||
*
|
||||
* @returns Updated session list
|
||||
*/
|
||||
async refreshSessions(): Promise<ITrajectoryEntry[]> {
|
||||
await this._loadSessions();
|
||||
this._onSessionsChanged.fire(this._sessions);
|
||||
return [...this._sessions];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent preferences (all 16 sentinel values).
|
||||
*/
|
||||
async getPreferences(): Promise<IAgentPreferences> {
|
||||
return this._state.getAgentPreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IDE diagnostics (176KB JSON with system info, logs, trajectories).
|
||||
*
|
||||
* Structure (verified):
|
||||
* - isRemote, systemInfo (OS, user, email)
|
||||
* - extensionLogs (Array[375])
|
||||
* - rendererLogs, mainThreadLogs, agentWindowConsoleLogs
|
||||
* - languageServerLogs
|
||||
* - recentTrajectories (Array[10])
|
||||
*
|
||||
* @returns Parsed diagnostics information
|
||||
*/
|
||||
async getDiagnostics(): Promise<IDiagnosticsInfo> {
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
|
||||
if (!raw || typeof raw !== 'string') {
|
||||
throw new Error('getDiagnostics returned unexpected type');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
return {
|
||||
isRemote: parsed.isRemote ?? false,
|
||||
systemInfo: {
|
||||
operatingSystem: parsed.systemInfo?.operatingSystem ?? 'unknown',
|
||||
timestamp: parsed.systemInfo?.timestamp ?? '',
|
||||
userEmail: parsed.systemInfo?.userEmail ?? '',
|
||||
userName: parsed.systemInfo?.userName ?? '',
|
||||
},
|
||||
raw: parsed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Chrome DevTools MCP URL.
|
||||
*
|
||||
* Verified: returns `http://127.0.0.1:{port}/mcp`
|
||||
*
|
||||
* @returns MCP URL string
|
||||
*/
|
||||
async getMcpUrl(): Promise<string> {
|
||||
const result = await this._commands.execute<string>('antigravity.getChromeDevtoolsMcpUrl');
|
||||
return result ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is gitignored.
|
||||
*
|
||||
* @param filePath - Relative or absolute file path
|
||||
* @returns true if gitignored, false/null otherwise
|
||||
*/
|
||||
async isFileGitIgnored(filePath: string): Promise<boolean> {
|
||||
const result = await this._commands.execute<boolean | null>('antigravity.isFileGitIgnored', filePath);
|
||||
return result === true;
|
||||
}
|
||||
|
||||
// ─── Write API ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Two-layer architecture (VERIFIED 2026-02-28):
|
||||
//
|
||||
// Layer 1 -- HEADLESS LS API (RECOMMENDED):
|
||||
// Access: sdk.ls (LSBridge from antigravity-sdk)
|
||||
// Method: Preact VNode tree -> component.props.lsClient -> 148 LS methods
|
||||
// Creates cascade WITHOUT opening panel or switching UI.
|
||||
// Usage: await sdk.ls.createCascade({ text: 'prompt' })
|
||||
//
|
||||
// Layer 2 — COMMAND API (FALLBACK, this file):
|
||||
// Access: vscode.commands.executeCommand (extension host)
|
||||
// Method: startNewConversation → sendPromptToAgentPanel → restore
|
||||
// PROBLEM: Always switches UI, causes flickering, race conditions.
|
||||
// Use only when renderer integration is not available.
|
||||
//
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a new Cascade conversation via VS Code commands.
|
||||
*
|
||||
* ⚠️ **FALLBACK APPROACH** — causes UI flickering.
|
||||
* For true headless creation, use `sdk.ls.createCascade()`
|
||||
* from the SDK's LS bridge (see LSBridge module).
|
||||
*
|
||||
* VERIFIED 2026-02-28:
|
||||
* - `startNewConversation` ✅ creates new chat (but switches UI)
|
||||
* - `prioritized.chat.openNewConversation` ❌ does NOT create new
|
||||
* - `sendPromptToAgentPanel` ✅ sends to currently visible chat (always opens panel)
|
||||
* - `sendTextToChat` ❌ does not visibly work
|
||||
*
|
||||
* @param options - Session creation options
|
||||
* @returns Session ID (googleAgentId) or empty string if not detected
|
||||
*/
|
||||
async createSession(options: ICreateSessionOptions): Promise<string> {
|
||||
log.info(`Creating session (command fallback): "${options.task.substring(0, 50)}..."`);
|
||||
|
||||
// Snapshot current sessions to detect the new one
|
||||
const beforeIds = new Set(this._sessions.map(s => s.id));
|
||||
|
||||
// Remember current active session (for background restore)
|
||||
let previousActiveId = '';
|
||||
if (options.background) {
|
||||
try {
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
if (raw && typeof raw === 'string') {
|
||||
const diag = JSON.parse(raw);
|
||||
if (Array.isArray(diag.recentTrajectories) && diag.recentTrajectories.length > 0) {
|
||||
previousActiveId = diag.recentTrajectories[0].googleAgentId ?? '';
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// Create new conversation (VERIFIED: startNewConversation works)
|
||||
await this._commands.execute(AntigravityCommands.START_NEW_CONVERSATION);
|
||||
await this._delay(1500); // Wait for UI to initialize
|
||||
|
||||
// Send initial prompt
|
||||
if (options.task) {
|
||||
await this._commands.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, options.task);
|
||||
}
|
||||
|
||||
// Mark as background if requested
|
||||
if (options.background) {
|
||||
await this._commands.execute(AntigravityCommands.TRACK_BACKGROUND_CONVERSATION);
|
||||
}
|
||||
|
||||
// Wait for new session to appear in getDiagnostics
|
||||
const newId = await this._waitForNewSession(beforeIds, 8000);
|
||||
|
||||
// If background: switch back to original conversation
|
||||
if (options.background && previousActiveId) {
|
||||
await this._delay(500);
|
||||
await this._commands.execute(AntigravityCommands.SET_VISIBLE_CONVERSATION, previousActiveId);
|
||||
log.info(`Background session created, restored to ${previousActiveId}`);
|
||||
}
|
||||
|
||||
if (newId) {
|
||||
log.info(`Session created: ${newId}`);
|
||||
} else {
|
||||
log.warn('Session created but ID not detected within timeout');
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a background Cascade conversation via commands.
|
||||
*
|
||||
* ⚠️ **FALLBACK** — Uses quick-switch approach (UI flickers briefly).
|
||||
* For true headless background sessions, use the SDK's LS bridge:
|
||||
* ```typescript
|
||||
* // Using LSBridge:
|
||||
* const cascadeId = await sdk.ls.createCascade({ text: 'task', modelId: 1018 });
|
||||
* ```
|
||||
*
|
||||
* @param task - Initial task/prompt to send
|
||||
* @returns Session ID or empty string
|
||||
*/
|
||||
async createBackgroundSession(task: string): Promise<string> {
|
||||
return this.createSession({ task, background: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the active Cascade conversation.
|
||||
*
|
||||
* Uses `antigravity.sendTextToChat` — the primary text sending command.
|
||||
*/
|
||||
async sendMessage(text: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SEND_TEXT_TO_CHAT, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prompt directly to the agent panel.
|
||||
*
|
||||
* Uses `antigravity.sendPromptToAgentPanel` — focuses the agent panel.
|
||||
*/
|
||||
async sendPrompt(text: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat action message (e.g., typing indicator, feedback).
|
||||
*
|
||||
* Uses `antigravity.sendChatActionMessage`.
|
||||
*/
|
||||
async sendChatAction(action: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SEND_CHAT_ACTION, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific conversation.
|
||||
*
|
||||
* @param sessionId - Conversation UUID (googleAgentId)
|
||||
*/
|
||||
async focusSession(sessionId: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SET_VISIBLE_CONVERSATION, sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new conversation in the agent panel (prioritized command).
|
||||
*
|
||||
* Uses `antigravity.prioritized.chat.openNewConversation` which both
|
||||
* opens the panel AND creates a fresh conversation.
|
||||
*/
|
||||
async openNewConversation(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.OPEN_NEW_CONVERSATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Cascade action.
|
||||
*
|
||||
* Uses `antigravity.executeCascadeAction`.
|
||||
*
|
||||
* @param action - Action data to execute
|
||||
*/
|
||||
async executeCascadeAction(action: unknown): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.EXECUTE_CASCADE_ACTION, action);
|
||||
}
|
||||
|
||||
// ─── Step Control ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Accept the current agent step (code edit, file write, etc.).
|
||||
*
|
||||
* Uses `antigravity.agent.acceptAgentStep`.
|
||||
*/
|
||||
async acceptStep(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.ACCEPT_AGENT_STEP);
|
||||
}
|
||||
|
||||
/** Reject the current agent step. */
|
||||
async rejectStep(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.REJECT_AGENT_STEP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a pending command (non-terminal, e.g. file edit confirmation).
|
||||
*
|
||||
* Uses `antigravity.command.accept`.
|
||||
* This is DIFFERENT from terminalCommand.accept.
|
||||
*/
|
||||
async acceptCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.COMMAND_ACCEPT);
|
||||
}
|
||||
|
||||
/** Reject a pending command (non-terminal). */
|
||||
async rejectCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.COMMAND_REJECT);
|
||||
}
|
||||
|
||||
// ─── Terminal Control ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Accept a pending terminal command.
|
||||
*
|
||||
* Uses `antigravity.terminalCommand.accept`.
|
||||
*/
|
||||
async acceptTerminalCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.TERMINAL_ACCEPT);
|
||||
}
|
||||
|
||||
/** Reject a pending terminal command. */
|
||||
async rejectTerminalCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.TERMINAL_REJECT);
|
||||
}
|
||||
|
||||
/** Run a pending terminal command. */
|
||||
async runTerminalCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.TERMINAL_RUN);
|
||||
}
|
||||
|
||||
// ─── Panel Control ──────────────────────────────────────────────────────
|
||||
|
||||
/** Open the Cascade agent panel */
|
||||
async openPanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.OPEN_AGENT_PANEL);
|
||||
}
|
||||
|
||||
/** Focus the Cascade agent panel */
|
||||
async focusPanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.FOCUS_AGENT_PANEL);
|
||||
}
|
||||
|
||||
/** Open the agent side panel */
|
||||
async openSidePanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.OPEN_AGENT_SIDE_PANEL);
|
||||
}
|
||||
|
||||
/** Focus the agent side panel */
|
||||
async focusSidePanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.FOCUS_AGENT_SIDE_PANEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the browser integration port (e.g., 57401).
|
||||
*/
|
||||
async getBrowserPort(): Promise<number> {
|
||||
return this._commands.execute<number>(AntigravityCommands.GET_BROWSER_PORT);
|
||||
}
|
||||
|
||||
// ─── Private ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load sessions from getDiagnostics.recentTrajectories (clean JSON).
|
||||
*
|
||||
* VERIFIED structure per entry:
|
||||
* {
|
||||
* googleAgentId: "uuid", ← conversation ID
|
||||
* trajectoryId: "uuid", ← internal trajectory ID
|
||||
* summary: "title", ← human-readable title
|
||||
* lastStepIndex: 992, ← step count
|
||||
* lastModifiedTime: "ISO" ← last activity
|
||||
* }
|
||||
*/
|
||||
private async _loadSessions(): Promise<void> {
|
||||
try {
|
||||
// Primary: getDiagnostics.recentTrajectories (10 most recent, with titles)
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
if (raw && typeof raw === 'string') {
|
||||
const diag = JSON.parse(raw);
|
||||
if (Array.isArray(diag.recentTrajectories)) {
|
||||
this._sessions = diag.recentTrajectories.map((entry: any) => ({
|
||||
id: entry.googleAgentId ?? '',
|
||||
title: entry.summary ?? 'Untitled',
|
||||
stepCount: entry.lastStepIndex ?? 0,
|
||||
workspaceUri: '',
|
||||
lastModifiedTime: entry.lastModifiedTime ?? '',
|
||||
trajectoryId: entry.trajectoryId ?? '',
|
||||
}));
|
||||
log.debug(`Loaded ${this._sessions.length} sessions from getDiagnostics`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('getDiagnostics failed, falling back to USS', error);
|
||||
}
|
||||
|
||||
// Fallback: parse trajectory summaries protobuf
|
||||
try {
|
||||
await this._loadSessionsFromUSS();
|
||||
} catch (error) {
|
||||
log.error('Failed to load sessions from USS', error);
|
||||
this._sessions = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: extract sessions from USS trajectory summaries protobuf.
|
||||
*/
|
||||
private async _loadSessionsFromUSS(): Promise<void> {
|
||||
const raw = await this._state.getRawValue('antigravityUnifiedStateSync.trajectorySummaries');
|
||||
if (!raw) {
|
||||
this._sessions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(raw, 'base64');
|
||||
const text = buffer.toString('utf8');
|
||||
|
||||
// Extract UUIDs
|
||||
const uuids = [...new Set(text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g) || [])];
|
||||
|
||||
this._sessions = uuids.map((id, i) => ({
|
||||
id,
|
||||
title: `Conversation ${i + 1}`,
|
||||
stepCount: 0,
|
||||
workspaceUri: '',
|
||||
}));
|
||||
|
||||
log.debug(`Loaded ${this._sessions.length} sessions from USS (fallback)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a new session to appear in getDiagnostics.
|
||||
* Polls every 500ms up to timeoutMs.
|
||||
*
|
||||
* @returns New session ID or empty string if timeout
|
||||
*/
|
||||
private async _waitForNewSession(beforeIds: Set<string>, timeoutMs: number): Promise<string> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const pollInterval = 500;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
await this._delay(pollInterval);
|
||||
|
||||
try {
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
if (!raw || typeof raw !== 'string') continue;
|
||||
|
||||
const diag = JSON.parse(raw);
|
||||
if (!Array.isArray(diag.recentTrajectories)) continue;
|
||||
|
||||
for (const entry of diag.recentTrajectories) {
|
||||
const id = entry.googleAgentId;
|
||||
if (id && !beforeIds.has(id)) {
|
||||
// Update local session list
|
||||
await this._loadSessions();
|
||||
return id;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore, retry
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple delay utility.
|
||||
*/
|
||||
private _delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Cascade module re-exports.
|
||||
* @module cascade
|
||||
*/
|
||||
|
||||
export { CascadeManager } from './cascade-manager';
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Disposable pattern for resource cleanup.
|
||||
*
|
||||
* @module disposable
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object that can release resources when no longer needed.
|
||||
*/
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects multiple disposables and disposes them all at once.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const store = new DisposableStore();
|
||||
* store.add(someEventSub);
|
||||
* store.add(anotherSub);
|
||||
* // Later:
|
||||
* store.dispose(); // cleans up everything
|
||||
* ```
|
||||
*/
|
||||
export class DisposableStore implements IDisposable {
|
||||
private readonly _disposables: IDisposable[] = [];
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* Add a disposable to the store.
|
||||
*
|
||||
* @param disposable - The disposable to track
|
||||
* @returns The same disposable (for chaining)
|
||||
*/
|
||||
add<T extends IDisposable>(disposable: T): T {
|
||||
if (this._disposed) {
|
||||
disposable.dispose();
|
||||
console.warn('[AntigravitySDK] Adding disposable to already disposed store');
|
||||
} else {
|
||||
this._disposables.push(disposable);
|
||||
}
|
||||
return disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all tracked disposables.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
this._disposed = true;
|
||||
|
||||
for (const d of this._disposables) {
|
||||
try {
|
||||
d.dispose();
|
||||
} catch (error) {
|
||||
console.error('[AntigravitySDK] Dispose error:', error);
|
||||
}
|
||||
}
|
||||
this._disposables.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a disposable from a cleanup function.
|
||||
*
|
||||
* @param fn - Cleanup function to call on dispose
|
||||
*/
|
||||
export function toDisposable(fn: () => void): IDisposable {
|
||||
return { dispose: fn };
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* SDK-specific error classes.
|
||||
*
|
||||
* @module errors
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base error for all Antigravity SDK errors.
|
||||
*/
|
||||
export class AntigravitySDKError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`[AntigravitySDK] ${message}`);
|
||||
this.name = 'AntigravitySDKError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when Antigravity IDE is not detected or not running.
|
||||
*/
|
||||
export class AntigravityNotFoundError extends AntigravitySDKError {
|
||||
constructor() {
|
||||
super('Antigravity IDE not detected. Make sure this extension is running inside Antigravity.');
|
||||
this.name = 'AntigravityNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a command fails to execute.
|
||||
*/
|
||||
export class CommandExecutionError extends AntigravitySDKError {
|
||||
constructor(
|
||||
public readonly command: string,
|
||||
public readonly reason: string,
|
||||
) {
|
||||
super(`Command "${command}" failed: ${reason}`);
|
||||
this.name = 'CommandExecutionError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the state database cannot be read.
|
||||
*/
|
||||
export class StateReadError extends AntigravitySDKError {
|
||||
constructor(
|
||||
public readonly key: string,
|
||||
public readonly reason: string,
|
||||
) {
|
||||
super(`Failed to read state key "${key}": ${reason}`);
|
||||
this.name = 'StateReadError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a session/conversation is not found.
|
||||
*/
|
||||
export class SessionNotFoundError extends AntigravitySDKError {
|
||||
constructor(public readonly sessionId: string) {
|
||||
super(`Session "${sessionId}" not found`);
|
||||
this.name = 'SessionNotFoundError';
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Lightweight event system for SDK.
|
||||
*
|
||||
* Follows VS Code's `Event<T>` / `EventEmitter<T>` pattern.
|
||||
* Supports subscription, disposal, and one-shot listeners.
|
||||
*
|
||||
* @module events
|
||||
*/
|
||||
|
||||
import type { IDisposable } from './disposable';
|
||||
|
||||
/**
|
||||
* A function that represents a subscription to an event.
|
||||
* Call the returned disposable to unsubscribe.
|
||||
*/
|
||||
export type Event<T> = (listener: (e: T) => void) => IDisposable;
|
||||
|
||||
/**
|
||||
* Emits events to registered listeners.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const emitter = new EventEmitter<string>();
|
||||
*
|
||||
* const sub = emitter.event((msg) => console.log(msg));
|
||||
* emitter.fire('hello'); // logs: hello
|
||||
* sub.dispose();
|
||||
* emitter.fire('world'); // nothing happens
|
||||
* ```
|
||||
*/
|
||||
export class EventEmitter<T> implements IDisposable {
|
||||
private _listeners: Set<(e: T) => void> = new Set();
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* The event that listeners can subscribe to.
|
||||
*/
|
||||
readonly event: Event<T> = (listener: (e: T) => void): IDisposable => {
|
||||
if (this._disposed) {
|
||||
throw new Error('EventEmitter has been disposed');
|
||||
}
|
||||
|
||||
this._listeners.add(listener);
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
this._listeners.delete(listener);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fire the event, notifying all listeners.
|
||||
*
|
||||
* @param data - The event data to send to listeners
|
||||
*/
|
||||
fire(data: T): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const listener of this._listeners) {
|
||||
try {
|
||||
listener(data);
|
||||
} catch (error) {
|
||||
console.error('[AntigravitySDK] Event listener error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the event, but only fire once.
|
||||
*
|
||||
* @param listener - Callback to invoke once
|
||||
* @returns Disposable to cancel before the event fires
|
||||
*/
|
||||
once(listener: (e: T) => void): IDisposable {
|
||||
const sub = this.event((data) => {
|
||||
sub.dispose();
|
||||
listener(data);
|
||||
});
|
||||
return sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current number of listeners.
|
||||
*/
|
||||
get listenerCount(): number {
|
||||
return this._listeners.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the emitter and all listeners.
|
||||
*/
|
||||
dispose(): void {
|
||||
this._disposed = true;
|
||||
this._listeners.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Core module — types, events, disposables, errors, logging.
|
||||
*
|
||||
* @module core
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './events';
|
||||
export * from './disposable';
|
||||
export * from './errors';
|
||||
export { Logger, LogLevel } from './logger';
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Debug logger for SDK internals.
|
||||
*
|
||||
* Respects the `antigravitySDK.debug` setting.
|
||||
*
|
||||
* @module logger
|
||||
*/
|
||||
|
||||
/**
|
||||
* Log levels for SDK logging.
|
||||
*/
|
||||
export enum LogLevel {
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Error = 3,
|
||||
Off = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK logger with level-based filtering.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const log = new Logger('CascadeManager');
|
||||
* log.debug('Loading sessions...');
|
||||
* log.info('Found 5 sessions');
|
||||
* log.error('Failed to load', err);
|
||||
* ```
|
||||
*/
|
||||
export class Logger {
|
||||
private static _globalLevel: LogLevel = LogLevel.Warn;
|
||||
|
||||
/**
|
||||
* Set the global log level for all SDK loggers.
|
||||
*
|
||||
* @param level - Minimum level to output
|
||||
*/
|
||||
static setLevel(level: LogLevel): void {
|
||||
Logger._globalLevel = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a logger for a specific module.
|
||||
*
|
||||
* @param module - Module name (shown in log prefix)
|
||||
*/
|
||||
constructor(private readonly module: string) { }
|
||||
|
||||
/** Log a debug message. */
|
||||
debug(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Debug, message, args);
|
||||
}
|
||||
|
||||
/** Log an informational message. */
|
||||
info(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Info, message, args);
|
||||
}
|
||||
|
||||
/** Log a warning. */
|
||||
warn(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Warn, message, args);
|
||||
}
|
||||
|
||||
/** Log an error. */
|
||||
error(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Error, message, args);
|
||||
}
|
||||
|
||||
private _log(level: LogLevel, message: string, args: unknown[]): void {
|
||||
if (level < Logger._globalLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = `[AntigravitySDK:${this.module}]`;
|
||||
const fn =
|
||||
level === LogLevel.Error ? console.error
|
||||
: level === LogLevel.Warn ? console.warn
|
||||
: level === LogLevel.Info ? console.info
|
||||
: console.debug;
|
||||
|
||||
fn(prefix, message, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
/**
|
||||
* Core type definitions for Antigravity SDK.
|
||||
*
|
||||
* These types mirror the internal protobuf schemas used by Antigravity's
|
||||
* Language Server, extracted via reverse engineering of the minified source.
|
||||
*
|
||||
* @module types
|
||||
*/
|
||||
|
||||
// ─── Enums ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Terminal command auto-execution policy.
|
||||
*
|
||||
* Controls how terminal commands are handled when the agent requests execution.
|
||||
*/
|
||||
export enum TerminalExecutionPolicy {
|
||||
/** Always ask user before running */
|
||||
OFF = 1,
|
||||
/** Auto-run safe commands, ask for potentially dangerous ones */
|
||||
AUTO = 2,
|
||||
/** Always auto-run without asking */
|
||||
EAGER = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Artifact review policy for code changes.
|
||||
*/
|
||||
export enum ArtifactReviewPolicy {
|
||||
/** Always show diff review */
|
||||
ALWAYS = 1,
|
||||
/** Skip review for simple changes */
|
||||
TURBO = 2,
|
||||
/** Automatically decide based on change complexity */
|
||||
AUTO = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of a Cortex step (tool call) in a trajectory.
|
||||
*/
|
||||
export enum CortexStepType {
|
||||
RunCommand = 'RunCommand',
|
||||
WriteToFile = 'WriteToFile',
|
||||
ViewFile = 'ViewFile',
|
||||
ViewFileOutline = 'ViewFileOutline',
|
||||
ViewCodeItem = 'ViewCodeItem',
|
||||
SearchWeb = 'SearchWeb',
|
||||
ReadUrlContent = 'ReadUrlContent',
|
||||
OpenBrowserUrl = 'OpenBrowserUrl',
|
||||
ReadBrowserPage = 'ReadBrowserPage',
|
||||
ListBrowserPages = 'ListBrowserPages',
|
||||
ListDirectory = 'ListDirectory',
|
||||
FindByName = 'FindByName',
|
||||
CodebaseSearch = 'CodebaseSearch',
|
||||
GrepSearch = 'GrepSearch',
|
||||
SendCommandInput = 'SendCommandInput',
|
||||
ReadTerminal = 'ReadTerminal',
|
||||
ShellExec = 'ShellExec',
|
||||
McpTool = 'McpTool',
|
||||
InvokeSubagent = 'InvokeSubagent',
|
||||
Memory = 'Memory',
|
||||
KnowledgeGeneration = 'KnowledgeGeneration',
|
||||
UserInput = 'UserInput',
|
||||
SystemMessage = 'SystemMessage',
|
||||
PlannerResponse = 'PlannerResponse',
|
||||
Wait = 'Wait',
|
||||
ProposeCode = 'ProposeCode',
|
||||
WriteCascadeEdit = 'WriteCascadeEdit',
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of a Cortex step.
|
||||
*/
|
||||
export enum StepStatus {
|
||||
/** Step is being processed */
|
||||
Running = 'running',
|
||||
/** Step completed successfully */
|
||||
Completed = 'completed',
|
||||
/** Step failed */
|
||||
Failed = 'failed',
|
||||
/** Step is waiting for user interaction */
|
||||
WaitingForUser = 'waiting_for_user',
|
||||
/** Step was cancelled */
|
||||
Cancelled = 'cancelled',
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of trajectory (conversation).
|
||||
*/
|
||||
export enum TrajectoryType {
|
||||
/** Standard chat conversation */
|
||||
Chat = 'chat',
|
||||
/** Agent mode (Cascade) */
|
||||
Cascade = 'cascade',
|
||||
}
|
||||
|
||||
// ─── Interfaces ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A single step (tool call) in a Cascade trajectory.
|
||||
*/
|
||||
export interface ICortexStep {
|
||||
/** Unique step identifier */
|
||||
readonly id: string;
|
||||
|
||||
/** Step index within the trajectory */
|
||||
readonly index: number;
|
||||
|
||||
/** Type of tool call */
|
||||
readonly type: CortexStepType;
|
||||
|
||||
/** Current status */
|
||||
readonly status: StepStatus;
|
||||
|
||||
/** Human-readable summary of what this step does */
|
||||
readonly summary: string;
|
||||
|
||||
/** Step-specific data (command line, file path, etc.) */
|
||||
readonly data: Record<string, unknown>;
|
||||
|
||||
/** Internal metadata not shown in UI */
|
||||
readonly metadata: IStepMetadata;
|
||||
|
||||
/** Timestamp when step was created */
|
||||
readonly createdAt: Date;
|
||||
|
||||
/** Timestamp when step completed (if completed) */
|
||||
readonly completedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal metadata attached to each step.
|
||||
*/
|
||||
export interface IStepMetadata {
|
||||
/** Raw protobuf fields from the server response */
|
||||
readonly rawFields: Record<string, unknown>;
|
||||
|
||||
/** Token count for this step's input */
|
||||
readonly inputTokens?: number;
|
||||
|
||||
/** Token count for this step's output */
|
||||
readonly outputTokens?: number;
|
||||
|
||||
/** Model used for this step */
|
||||
readonly model?: string;
|
||||
|
||||
/** Whether this step was auto-approved */
|
||||
readonly autoApproved?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A chat message in a conversation.
|
||||
*/
|
||||
export interface IChatMessage {
|
||||
/** Message role */
|
||||
readonly role: 'user' | 'assistant' | 'system';
|
||||
|
||||
/** Message content */
|
||||
readonly content: string;
|
||||
|
||||
/** Message ID */
|
||||
readonly id: string;
|
||||
|
||||
/** Timestamp */
|
||||
readonly createdAt: Date;
|
||||
|
||||
/** Hidden metadata */
|
||||
readonly metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the current context window usage.
|
||||
*/
|
||||
export interface IContextInfo {
|
||||
/** Total tokens currently in context */
|
||||
readonly totalTokens: number;
|
||||
|
||||
/** Maximum context window size */
|
||||
readonly maxTokens: number;
|
||||
|
||||
/** Usage as percentage (0-100) */
|
||||
readonly usagePercent: number;
|
||||
|
||||
/** Token breakdown by category */
|
||||
readonly breakdown: ITokenBreakdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token usage breakdown.
|
||||
*/
|
||||
export interface ITokenBreakdown {
|
||||
/** System prompt tokens */
|
||||
readonly system: number;
|
||||
/** User message tokens */
|
||||
readonly userMessages: number;
|
||||
/** Assistant response tokens */
|
||||
readonly assistantMessages: number;
|
||||
/** Tool call input tokens */
|
||||
readonly toolCalls: number;
|
||||
/** Tool result tokens */
|
||||
readonly toolResults: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Cascade session (conversation/trajectory).
|
||||
*/
|
||||
export interface ISessionInfo {
|
||||
/** Unique session/cascade ID */
|
||||
readonly id: string;
|
||||
|
||||
/** Session title (auto-generated or user-set) */
|
||||
readonly title: string;
|
||||
|
||||
/** When the session was created */
|
||||
readonly createdAt: Date;
|
||||
|
||||
/** When the session was last active */
|
||||
readonly lastActiveAt: Date;
|
||||
|
||||
/** Type of trajectory */
|
||||
readonly type: TrajectoryType;
|
||||
|
||||
/** Whether the session is currently active */
|
||||
readonly isActive: boolean;
|
||||
|
||||
/** Tags applied to this session */
|
||||
readonly tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent preferences from USS (Unified State Sync).
|
||||
*
|
||||
* All 16 sentinel keys verified from live state.vscdb on 2026-02-28.
|
||||
*/
|
||||
export interface IAgentPreferences {
|
||||
/** Terminal command auto-execution policy (terminalAutoExecutionPolicySentinelKey) */
|
||||
readonly terminalExecutionPolicy: TerminalExecutionPolicy;
|
||||
|
||||
/** Code change review policy (artifactReviewPolicySentinelKey) */
|
||||
readonly artifactReviewPolicy: ArtifactReviewPolicy;
|
||||
|
||||
/** Planning mode (planningModeSentinelKey) */
|
||||
readonly planningMode: number;
|
||||
|
||||
/** Whether strict/secure mode is enabled (secureModeSentinelKey) */
|
||||
readonly secureModeEnabled: boolean;
|
||||
|
||||
/** Whether terminal sandbox is enabled (enableTerminalSandboxSentinelKey) */
|
||||
readonly terminalSandboxEnabled: boolean;
|
||||
|
||||
/** Whether sandbox allows network access (sandboxAllowNetworkSentinelKey) */
|
||||
readonly sandboxAllowNetwork: boolean;
|
||||
|
||||
/** Whether shell integration is enabled (enableShellIntegrationSentinelKey) */
|
||||
readonly shellIntegrationEnabled: boolean;
|
||||
|
||||
/** Allow agent to access files outside workspace (allowAgentAccessNonWorkspaceFilesSentinelKey) */
|
||||
readonly allowNonWorkspaceFiles: boolean;
|
||||
|
||||
/** Allow Cascade to read .gitignore files (allowCascadeAccessGitignoreFilesSentinelKey) */
|
||||
readonly allowGitignoreAccess: boolean;
|
||||
|
||||
/** Explain and fix in current conversation (explainAndFixInCurrentConversationSentinelKey) */
|
||||
readonly explainFixInCurrentConvo: boolean;
|
||||
|
||||
/** Auto-continue on max generator invocations (autoContinueOnMaxGeneratorInvocationsSentinelKey) */
|
||||
readonly autoContinueOnMax: number;
|
||||
|
||||
/** Disable auto-open of edited files (disableAutoOpenEditedFilesSentinelKey) */
|
||||
readonly disableAutoOpenEdited: boolean;
|
||||
|
||||
/** Enable sounds for special events (enableSoundsForSpecialEventsSentinelKey) */
|
||||
readonly enableSounds: boolean;
|
||||
|
||||
/** Disable Cascade auto-fix for lint errors (disableCascadeAutoFixLintsSentinelKey) */
|
||||
readonly disableAutoFixLints: boolean;
|
||||
|
||||
/** Explicitly allowed terminal commands (terminalAllowedCommandsSentinelKey) */
|
||||
readonly allowedCommands: string[];
|
||||
|
||||
/** Explicitly denied terminal commands (terminalDeniedCommandsSentinelKey) */
|
||||
readonly deniedCommands: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model configuration.
|
||||
*/
|
||||
export interface IModelConfig {
|
||||
/** Model identifier */
|
||||
readonly id: string;
|
||||
|
||||
/** Human-readable model name */
|
||||
readonly name: string;
|
||||
|
||||
/** Whether this model is currently selected */
|
||||
readonly isActive: boolean;
|
||||
|
||||
/** Maximum context window size in tokens */
|
||||
readonly maxContextTokens: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a new Cascade session.
|
||||
*/
|
||||
export interface ICreateSessionOptions {
|
||||
/** Initial task/message to send */
|
||||
readonly task: string;
|
||||
|
||||
/** Whether to run in background (don't focus the panel) */
|
||||
readonly background?: boolean;
|
||||
|
||||
/** Model to use (defaults to current) */
|
||||
readonly model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent state from the Agent Manager.
|
||||
*/
|
||||
export interface IAgentState {
|
||||
/** Whether the agent manager is enabled */
|
||||
readonly isEnabled: boolean;
|
||||
|
||||
/** Whether the agent is currently processing */
|
||||
readonly isProcessing: boolean;
|
||||
|
||||
/** Active cascade/conversation ID */
|
||||
readonly activeCascadeId: string | null;
|
||||
|
||||
/** Current model in use */
|
||||
readonly currentModel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trajectory entry from getDiagnostics.recentTrajectories.
|
||||
*
|
||||
* VERIFIED 2026-02-28: getDiagnostics returns clean JSON array with:
|
||||
* { googleAgentId, trajectoryId, summary, lastStepIndex, lastModifiedTime }
|
||||
*/
|
||||
export interface ITrajectoryEntry {
|
||||
/** Conversation UUID = googleAgentId */
|
||||
readonly id: string;
|
||||
|
||||
/** Human-readable title = summary field */
|
||||
readonly title: string;
|
||||
|
||||
/** Current step index in this conversation */
|
||||
readonly stepCount: number;
|
||||
|
||||
/** Workspace URI (from USS protobuf fallback) */
|
||||
readonly workspaceUri: string;
|
||||
|
||||
/** Internal trajectory UUID (from getDiagnostics) */
|
||||
readonly trajectoryId?: string;
|
||||
|
||||
/** ISO timestamp of last modification (from getDiagnostics) */
|
||||
readonly lastModifiedTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics info from `antigravity.getDiagnostics`.
|
||||
*
|
||||
* VERIFIED: returns 176KB JSON string with 8 top-level keys:
|
||||
* isRemote, systemInfo, extensionLogs, rendererLogs,
|
||||
* mainThreadLogs, agentWindowConsoleLogs, languageServerLogs,
|
||||
* recentTrajectories.
|
||||
*/
|
||||
export interface IDiagnosticsInfo {
|
||||
/** Whether IDE is running remotely (SSH) */
|
||||
readonly isRemote: boolean;
|
||||
|
||||
/** System info */
|
||||
readonly systemInfo: {
|
||||
readonly operatingSystem: string;
|
||||
readonly timestamp: string;
|
||||
readonly userEmail: string;
|
||||
readonly userName: string;
|
||||
};
|
||||
|
||||
/** Raw JSON for fields not yet typed */
|
||||
readonly raw: Record<string, unknown>;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Antigravity SDK — Community SDK for Antigravity IDE.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { AntigravitySDK } from 'antigravity-sdk';
|
||||
*
|
||||
* export function activate(context: vscode.ExtensionContext) {
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* // Read preferences
|
||||
* const prefs = await sdk.cascade.getPreferences();
|
||||
* console.log('Terminal policy:', prefs.terminalExecutionPolicy);
|
||||
*
|
||||
* // List sessions
|
||||
* const sessions = await sdk.cascade.getSessions();
|
||||
* console.log(`${sessions.length} conversations`);
|
||||
*
|
||||
* // Get diagnostics
|
||||
* const diag = await sdk.cascade.getDiagnostics();
|
||||
* console.log(`User: ${diag.systemInfo.userName}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Core
|
||||
export {
|
||||
// Types
|
||||
TerminalExecutionPolicy,
|
||||
ArtifactReviewPolicy,
|
||||
CortexStepType,
|
||||
StepStatus,
|
||||
TrajectoryType,
|
||||
// Interfaces
|
||||
type ICortexStep,
|
||||
type IStepMetadata,
|
||||
type IChatMessage,
|
||||
type IContextInfo,
|
||||
type ITokenBreakdown,
|
||||
type ISessionInfo,
|
||||
type IAgentPreferences,
|
||||
type IModelConfig,
|
||||
type ICreateSessionOptions,
|
||||
type IAgentState,
|
||||
type ITrajectoryEntry,
|
||||
type IDiagnosticsInfo,
|
||||
} from './core/types';
|
||||
|
||||
export { Event, EventEmitter } from './core/events';
|
||||
export { IDisposable, DisposableStore, toDisposable } from './core/disposable';
|
||||
export {
|
||||
AntigravitySDKError,
|
||||
AntigravityNotFoundError,
|
||||
CommandExecutionError,
|
||||
StateReadError,
|
||||
SessionNotFoundError,
|
||||
} from './core/errors';
|
||||
export { Logger, LogLevel } from './core/logger';
|
||||
|
||||
// Transport
|
||||
export { CommandBridge, AntigravityCommands } from './transport/command-bridge';
|
||||
export { StateBridge, USSKeys } from './transport/state-bridge';
|
||||
export { EventMonitor, type IStateChange, type IStepCountChange, type IActiveSessionChange } from './transport/event-monitor';
|
||||
export { LSBridge, Models, type ModelId, type IHeadlessCascadeOptions, type ISendMessageOptions, type IConversationAnnotations } from './transport/ls-bridge';
|
||||
|
||||
// Cascade
|
||||
export { CascadeManager } from './cascade/cascade-manager';
|
||||
|
||||
// Integration
|
||||
export { IntegrationManager, IntegrityManager, TitleManager, IntegrationPoint } from './integration';
|
||||
export type {
|
||||
IntegrationConfig,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
IToastConfig,
|
||||
TurnMetric,
|
||||
} from './integration';
|
||||
|
||||
// SDK
|
||||
export { AntigravitySDK, type ISDKOptions } from './sdk';
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Integration module — re-exports.
|
||||
* @module integration
|
||||
*/
|
||||
export { IntegrationManager } from './integration-manager';
|
||||
export { IntegrityManager } from './integrity-manager';
|
||||
export { TitleManager } from './title-manager';
|
||||
export { IntegrationPoint } from './types';
|
||||
export type {
|
||||
IntegrationConfig,
|
||||
IIntegrationManager,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
IToastConfig,
|
||||
IToastRow,
|
||||
TurnMetric,
|
||||
} from './types';
|
||||
@@ -1,704 +0,0 @@
|
||||
/**
|
||||
* Integration Manager — Public API for UI integration into Agent View.
|
||||
*
|
||||
* Orchestrates ScriptGenerator and WorkbenchPatcher to provide
|
||||
* a clean, developer-friendly API.
|
||||
*
|
||||
* @module integration/integration-manager
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { IntegrationManager, IntegrationPoint } from 'antigravity-sdk';
|
||||
*
|
||||
* const integrator = new IntegrationManager();
|
||||
*
|
||||
* integrator.register({
|
||||
* id: 'myStats',
|
||||
* point: IntegrationPoint.TOP_BAR,
|
||||
* icon: '📊',
|
||||
* tooltip: 'Show Stats',
|
||||
* toast: {
|
||||
* title: 'My Extension Stats',
|
||||
* rows: [{ key: 'turns:', value: 'Dynamic data here' }],
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* integrator.register({
|
||||
* id: 'turnInfo',
|
||||
* point: IntegrationPoint.TURN_METADATA,
|
||||
* metrics: ['turnNumber', 'userCharCount', 'separator', 'aiCharCount', 'codeBlocks'],
|
||||
* });
|
||||
*
|
||||
* await integrator.install();
|
||||
* // Restart Antigravity to see changes
|
||||
* ```
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { Logger } from '../core/logger';
|
||||
import {
|
||||
IntegrationConfig,
|
||||
IntegrationPoint,
|
||||
IIntegrationManager,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
IToastConfig,
|
||||
} from './types';
|
||||
import { ScriptGenerator } from './script-generator';
|
||||
import { WorkbenchPatcher } from './workbench-patcher';
|
||||
import { IntegrityManager } from './integrity-manager';
|
||||
import { TitleManager } from './title-manager';
|
||||
import { generateTitleProxyCode } from './title-proxy';
|
||||
|
||||
const log = new Logger('IntegrationManager');
|
||||
|
||||
/**
|
||||
* Manages UI integrations into the Antigravity Agent View.
|
||||
*
|
||||
* Provides a declarative API to register integration points,
|
||||
* generates a self-contained JavaScript file, and installs it
|
||||
* into Antigravity's workbench.
|
||||
*
|
||||
* Features:
|
||||
* - **Theme-aware**: Adapts to dark/light mode automatically
|
||||
* - **Auto-repair**: Watches workbench.html and re-patches after updates
|
||||
* - **Dynamic update**: Re-generate script without re-patching workbench.html
|
||||
*/
|
||||
export class IntegrationManager implements IIntegrationManager, IDisposable {
|
||||
private readonly _configs: Map<string, IntegrationConfig> = new Map();
|
||||
private readonly _generator = new ScriptGenerator();
|
||||
private readonly _patcher: WorkbenchPatcher;
|
||||
private readonly _integrity: IntegrityManager;
|
||||
private readonly _titles = new TitleManager();
|
||||
private readonly _namespace: string;
|
||||
private _watcher: fs.FSWatcher | null = null;
|
||||
private _autoRepairDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||
private _titleProxyEnabled = false;
|
||||
|
||||
/**
|
||||
* @param namespace - Unique slug that isolates this extension's files.
|
||||
* Derived automatically from `context.extension.id` when using AntigravitySDK.
|
||||
* Multiple SDK-based extensions can coexist without conflicts.
|
||||
*/
|
||||
constructor(namespace: string = 'default') {
|
||||
this._namespace = namespace;
|
||||
this._patcher = new WorkbenchPatcher(namespace);
|
||||
this._integrity = new IntegrityManager(
|
||||
this._patcher.getWorkbenchDir(),
|
||||
namespace,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Registration ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Register a single integration point.
|
||||
*
|
||||
* @throws If an integration with the same ID already exists
|
||||
*/
|
||||
register(config: IntegrationConfig): void {
|
||||
if (this._configs.has(config.id)) {
|
||||
throw new Error(`Integration '${config.id}' is already registered`);
|
||||
}
|
||||
this._configs.set(config.id, config);
|
||||
log.debug(`Registered integration: ${config.id} (${config.point})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register multiple integration points at once.
|
||||
*/
|
||||
registerMany(configs: IntegrationConfig[]): void {
|
||||
for (const c of configs) {
|
||||
this.register(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a registered integration by ID.
|
||||
*/
|
||||
unregister(id: string): void {
|
||||
this._configs.delete(id);
|
||||
log.debug(`Unregistered integration: ${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered integrations.
|
||||
*/
|
||||
getRegistered(): ReadonlyArray<IntegrationConfig> {
|
||||
return Array.from(this._configs.values());
|
||||
}
|
||||
|
||||
// ─── Convenience methods (fluent API) ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Add a button to the top bar (near +, refresh icons).
|
||||
*/
|
||||
addTopBarButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.TOP_BAR,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a button to the top-right corner (before X).
|
||||
*/
|
||||
addTopRightButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.TOP_RIGHT,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a button next to the send/voice buttons.
|
||||
*/
|
||||
addInputButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.INPUT_AREA,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an icon to the bottom icon row (file, terminal, etc.).
|
||||
*/
|
||||
addBottomIcon(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.BOTTOM_ICONS,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable per-turn metadata display.
|
||||
*/
|
||||
addTurnMetadata(id: string, metrics: ITurnMetaIntegration['metrics'], clickable = true): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.TURN_METADATA,
|
||||
metrics,
|
||||
clickable,
|
||||
} as ITurnMetaIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add character count badges to user messages.
|
||||
*/
|
||||
addUserBadges(id: string, display: IUserBadgeIntegration['display'] = 'charCount'): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.USER_BADGE,
|
||||
display,
|
||||
} as IUserBadgeIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action button next to Good/Bad feedback.
|
||||
*/
|
||||
addBotAction(id: string, icon: string, label: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.BOT_ACTION,
|
||||
icon,
|
||||
label,
|
||||
toast,
|
||||
} as IBotActionIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item(s) to the 3-dot dropdown menu.
|
||||
*/
|
||||
addDropdownItem(id: string, label: string, icon?: string, toast?: IToastConfig, separator = false): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.DROPDOWN_MENU,
|
||||
label,
|
||||
icon,
|
||||
toast,
|
||||
separator,
|
||||
} as IDropdownIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable chat title interaction.
|
||||
*/
|
||||
addTitleInteraction(id: string, interaction: ITitleIntegration['interaction'] = 'dblclick', hint?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.CHAT_TITLE,
|
||||
interaction,
|
||||
hint,
|
||||
toast,
|
||||
} as ITitleIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
// ─── Title Proxy ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Enable the title proxy feature.
|
||||
*
|
||||
* Adds renderer-side code that intercepts the summaries provider
|
||||
* and injects custom chat titles. Uses structural matching to find
|
||||
* the provider (obfuscation-safe).
|
||||
*
|
||||
* After enabling, call `install()` or `updateScript()` to apply.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* sdk.integration.enableTitleProxy();
|
||||
* await sdk.integration.install();
|
||||
*
|
||||
* // Now rename from extension host:
|
||||
* sdk.integration.titles.rename(cascadeId, 'My Custom Title');
|
||||
* ```
|
||||
*/
|
||||
enableTitleProxy(): this {
|
||||
this._titleProxyEnabled = true;
|
||||
if (this._patcher.isAvailable()) {
|
||||
this._titles.initialize(this._patcher.getWorkbenchDir(), this._namespace);
|
||||
}
|
||||
log.info('Title proxy enabled');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the title manager for programmatic title control.
|
||||
*
|
||||
* Requires `enableTitleProxy()` to be called first.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* sdk.integration.titles.rename(cascadeId, 'My Title');
|
||||
* sdk.integration.titles.remove(cascadeId);
|
||||
* const all = sdk.integration.titles.getAll();
|
||||
* ```
|
||||
*/
|
||||
get titles(): TitleManager {
|
||||
if (!this._titleProxyEnabled) {
|
||||
log.warn('Title proxy not enabled. Call enableTitleProxy() first.');
|
||||
}
|
||||
return this._titles;
|
||||
}
|
||||
|
||||
// ─── Build & Install ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate the integration script from all registered configs.
|
||||
*
|
||||
* If title proxy is enabled, appends the title proxy renderer code.
|
||||
*
|
||||
* @returns Complete JavaScript code as a string
|
||||
*/
|
||||
build(): string {
|
||||
const configs = Array.from(this._configs.values());
|
||||
if (configs.length === 0 && !this._titleProxyEnabled) {
|
||||
throw new Error('No integration points registered and title proxy not enabled');
|
||||
}
|
||||
|
||||
let script = '';
|
||||
if (configs.length > 0) {
|
||||
log.info(`Building script for ${configs.length} integration(s)`);
|
||||
script = this._generator.generate(configs);
|
||||
}
|
||||
|
||||
if (this._titleProxyEnabled) {
|
||||
log.info('Appending title proxy code');
|
||||
script += '\n' + generateTitleProxyCode(this._namespace);
|
||||
}
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the generated script into workbench.html.
|
||||
*
|
||||
* For seamless hot-reload behavior, use `installSeamless()` instead.
|
||||
*
|
||||
* @returns true if the script content actually changed on disk
|
||||
*/
|
||||
async install(): Promise<boolean> {
|
||||
if (!this._patcher.isAvailable()) {
|
||||
throw new Error('Antigravity workbench not found. Is Antigravity installed?');
|
||||
}
|
||||
|
||||
const script = this.build();
|
||||
|
||||
// Read existing script to detect changes
|
||||
const scriptPath = this._patcher.getScriptPath();
|
||||
let oldContent = '';
|
||||
try {
|
||||
if (fs.existsSync(scriptPath)) {
|
||||
oldContent = fs.readFileSync(scriptPath, 'utf8');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
this._patcher.install(script);
|
||||
this._integrity.suppressCheck();
|
||||
this._patcher.writeHeartbeat();
|
||||
|
||||
const changed = oldContent !== script;
|
||||
log.info(
|
||||
`Installed integration (${this._configs.size} points, titleProxy: ${this._titleProxyEnabled}) -> ${scriptPath} [${changed ? 'CHANGED' : 'unchanged'}]`,
|
||||
);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seamless install — handles everything automatically.
|
||||
*
|
||||
* This is the **recommended** install method for extension developers.
|
||||
* It handles the entire lifecycle:
|
||||
*
|
||||
* 1. **First install:** Writes script + patches HTML + prompts user to reload
|
||||
* 2. **Update:** Compares content, if changed → auto-reloads window (no prompt)
|
||||
* 3. **No change:** Does nothing
|
||||
*
|
||||
* The developer never needs to think about reload.
|
||||
*
|
||||
* @param executeCommand - Function to execute VS Code commands
|
||||
* (pass `vscode.commands.executeCommand` or equivalent)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* sdk.integration.enableTitleProxy();
|
||||
* // That's it. SDK handles install, reload, everything.
|
||||
* await sdk.integration.installSeamless(
|
||||
* (cmd) => vscode.commands.executeCommand(cmd),
|
||||
* (msg, ...items) => vscode.window.showInformationMessage(msg, ...items),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async installSeamless(
|
||||
executeCommand: (command: string) => Thenable<any>,
|
||||
showMessage?: (message: string, ...items: string[]) => Thenable<string | undefined>,
|
||||
): Promise<void> {
|
||||
const wasInstalled = this._patcher.isInstalled();
|
||||
|
||||
// Snapshot old content before install
|
||||
const scriptPath = this._patcher.getScriptPath();
|
||||
let oldContent = '';
|
||||
try {
|
||||
if (fs.existsSync(scriptPath)) {
|
||||
oldContent = fs.readFileSync(scriptPath, 'utf8');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const changed = await this.install();
|
||||
|
||||
if (!wasInstalled) {
|
||||
// First install: prompt user
|
||||
log.info('First install. Prompting for reload.');
|
||||
if (showMessage) {
|
||||
const action = await showMessage(
|
||||
'Better Antigravity installed. Reload to activate.',
|
||||
'Reload Now',
|
||||
);
|
||||
if (action === 'Reload Now') {
|
||||
await executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
}
|
||||
} else if (changed) {
|
||||
// Update: auto-reload (no prompt)
|
||||
log.info('Script changed on disk. Auto-reloading window...');
|
||||
// Small delay to let extension finish activation
|
||||
setTimeout(() => executeCommand('workbench.action.reloadWindow'), 500);
|
||||
} else {
|
||||
log.debug('Script unchanged. No reload needed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the integration from workbench.html.
|
||||
*
|
||||
* ⚠️ Requires Antigravity restart to take effect.
|
||||
*/
|
||||
async uninstall(): Promise<void> {
|
||||
this._patcher.uninstall();
|
||||
this._integrity.releaseCheck();
|
||||
this._patcher.removeHeartbeat();
|
||||
this.disableAutoRepair();
|
||||
log.info('Uninstalled integration. Restart Antigravity to apply.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an integration is currently installed.
|
||||
*/
|
||||
isInstalled(): boolean {
|
||||
return this._patcher.isInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that the extension is active.
|
||||
*
|
||||
* Call this in your extension's `activate()` function.
|
||||
* The integration script checks for this heartbeat;
|
||||
* if it's missing or stale (>48h), the script won't start.
|
||||
*
|
||||
* This prevents orphaned integrations from running after
|
||||
* an extension is disabled or uninstalled.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export function activate(context: vscode.ExtensionContext) {
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* sdk.integration.signalActive();
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
signalActive(): void {
|
||||
this._patcher.writeHeartbeat();
|
||||
log.debug('Heartbeat refreshed');
|
||||
}
|
||||
|
||||
// ─── Dynamic Update ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Re-generate and overwrite the integration script without re-patching workbench.html.
|
||||
*
|
||||
* Use this after registering/unregistering integration points at runtime.
|
||||
* The script file is updated in-place; the next Antigravity restart
|
||||
* will pick up the changes. workbench.html <script> tag is unchanged.
|
||||
*
|
||||
* @returns true if script was updated
|
||||
*/
|
||||
updateScript(): boolean {
|
||||
if (!this._patcher.isInstalled()) {
|
||||
log.warn('Cannot update script — integration is not installed');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const script = this.build();
|
||||
fs.writeFileSync(this._patcher.getScriptPath(), script, 'utf8');
|
||||
log.info(`Script updated (${this._configs.size} points)`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
log.error('Failed to update script', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Auto-Repair ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Enable auto-repair: watches workbench.html for changes
|
||||
* and automatically re-applies the integration patch.
|
||||
*
|
||||
* This handles Antigravity updates that overwrite workbench.html.
|
||||
* The watcher detects when the file changes and re-patches it
|
||||
* if the integration marker is missing.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const integrator = new IntegrationManager();
|
||||
* integrator.useDemoPreset();
|
||||
* await integrator.install();
|
||||
* integrator.enableAutoRepair(); // Survive Antigravity updates
|
||||
* ```
|
||||
*/
|
||||
enableAutoRepair(): void {
|
||||
if (this._watcher) return;
|
||||
|
||||
const htmlPath = this._patcher.getWorkbenchDir() + '\\workbench.html';
|
||||
if (!fs.existsSync(htmlPath)) {
|
||||
log.warn('Cannot enable auto-repair — workbench.html not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._watcher = fs.watch(htmlPath, (eventType) => {
|
||||
if (eventType !== 'change') return;
|
||||
|
||||
// Debounce — Antigravity may write multiple times
|
||||
if (this._autoRepairDebounce) clearTimeout(this._autoRepairDebounce);
|
||||
this._autoRepairDebounce = setTimeout(() => {
|
||||
this._tryRepair();
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
log.info('Auto-repair enabled — watching workbench.html');
|
||||
} catch (err) {
|
||||
log.error('Failed to enable auto-repair', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-repair watcher.
|
||||
*/
|
||||
disableAutoRepair(): void {
|
||||
if (this._watcher) {
|
||||
this._watcher.close();
|
||||
this._watcher = null;
|
||||
log.info('Auto-repair disabled');
|
||||
}
|
||||
if (this._autoRepairDebounce) {
|
||||
clearTimeout(this._autoRepairDebounce);
|
||||
this._autoRepairDebounce = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether auto-repair is active.
|
||||
*/
|
||||
get isAutoRepairEnabled(): boolean {
|
||||
return this._watcher !== null;
|
||||
}
|
||||
|
||||
private _tryRepair(): void {
|
||||
try {
|
||||
if (this._patcher.isInstalled()) {
|
||||
log.debug('Auto-repair: integration still present, no action needed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._configs.size === 0) {
|
||||
log.debug('Auto-repair: no configs registered, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Auto-repair: integration lost (Antigravity update?), re-patching...');
|
||||
const script = this.build();
|
||||
this._patcher.install(script);
|
||||
this._integrity.repair();
|
||||
log.info('Auto-repair: re-patched successfully. Restart Antigravity.');
|
||||
} catch (err) {
|
||||
log.error('Auto-repair failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Preset ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Register the Demo preset — a complete demo of all 9 integration points.
|
||||
* Useful for testing and as a reference implementation.
|
||||
*/
|
||||
useDemoPreset(): this {
|
||||
this.addTopBarButton('demo_overview', '\u{1F4E1}', 'SDK: Session Overview', {
|
||||
title: 'Session Overview',
|
||||
badge: { text: 'TOP_BAR', bgColor: 'rgba(79,195,247,.2)', textColor: '#4fc3f7' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Header icon bar' },
|
||||
{ key: 'use case:', value: 'Session overview, navigation' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addTopRightButton('demo_perf', '\u26A1', 'SDK: Performance', {
|
||||
title: 'Performance',
|
||||
badge: { text: 'TOP_RIGHT', bgColor: 'rgba(255,193,7,.2)', textColor: '#ffd54f' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Top right, before close' },
|
||||
{ key: 'use case:', value: 'Status indicator' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addInputButton('demo_stats', '\u{1F4CA}', 'SDK: Stats', {
|
||||
title: 'Input Stats',
|
||||
badge: { text: 'INPUT_AREA', bgColor: 'rgba(76,175,80,.2)', textColor: '#81c784' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Next to send button' },
|
||||
{ key: 'use case:', value: 'Token counter, analytics' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addBottomIcon('demo_actions', '\u2630', 'SDK: Quick Actions', {
|
||||
title: 'Quick Actions',
|
||||
badge: { text: 'BOTTOM_ICONS', bgColor: 'rgba(255,152,0,.2)', textColor: '#ffb74d' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Bottom icon row' },
|
||||
{ key: 'use case:', value: 'Mode switches, quick actions' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addTurnMetadata('demo_turns', [
|
||||
'turnNumber',
|
||||
'userCharCount',
|
||||
'separator',
|
||||
'aiCharCount',
|
||||
'codeBlocks',
|
||||
'thinkingIndicator',
|
||||
]);
|
||||
|
||||
this.addUserBadges('demo_ubadge', 'charCount');
|
||||
|
||||
this.addBotAction('demo_inspect', '\u{1F50D}', 'inspect', {
|
||||
title: 'Response Inspector',
|
||||
badge: { text: 'BOT_ACTION', bgColor: 'rgba(156,39,176,.2)', textColor: '#ce93d8' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Next to Good/Bad' },
|
||||
{ key: 'use case:', value: 'Response analysis' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addDropdownItem('demo_menu_stats', 'SDK Stats', '\u{1F4CA}', {
|
||||
title: 'Extended Stats',
|
||||
badge: { text: 'DROPDOWN', bgColor: 'rgba(233,30,99,.2)', textColor: '#f48fb1' },
|
||||
rows: [
|
||||
{ key: 'location:', value: '3-dot dropdown menu' },
|
||||
{ key: 'use case:', value: 'Extended actions' },
|
||||
],
|
||||
}, true);
|
||||
|
||||
this.addDropdownItem('demo_menu_debug', 'SDK Debug', '\u{1F9EA}', {
|
||||
title: 'Debug Info',
|
||||
badge: { text: 'DEBUG', bgColor: 'rgba(255,87,34,.2)', textColor: '#ff8a65' },
|
||||
rows: [
|
||||
{ key: 'location:', value: '3-dot dropdown menu' },
|
||||
{ key: 'use case:', value: 'Debug, diagnostics' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addTitleInteraction('demo_title', 'dblclick', 'dblclick', {
|
||||
title: 'Chat Title',
|
||||
badge: { text: 'TITLE', bgColor: 'rgba(0,150,136,.2)', textColor: '#80cbc4' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Conversation title' },
|
||||
{ key: 'use case:', value: 'Rename, bookmark' },
|
||||
],
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// ─── Dispose ───────────────────────────────────────────────────────
|
||||
|
||||
dispose(): void {
|
||||
this.disableAutoRepair();
|
||||
this._configs.clear();
|
||||
this._titles.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* Integrity Manager — Suppress Antigravity's "corrupt installation" warnings.
|
||||
*
|
||||
* When the SDK patches workbench files, Antigravity's IntegrityService detects
|
||||
* checksum mismatches and shows two warnings:
|
||||
* 1. Console WARN ("Installation has been modified on disk")
|
||||
* 2. UI Notification ("Your Antigravity installation appears to be corrupt")
|
||||
*
|
||||
* This class updates ALL mismatched SHA256 hashes in product.json, so
|
||||
* IntegrityService sees isPure=true and produces no warnings at all.
|
||||
*
|
||||
* Handles not just workbench.html but also workbench.desktop.main.js (auto-run fix),
|
||||
* workbench-jetski-agent.html (agent manager patching), and any other modified files.
|
||||
*
|
||||
* Multi-extension coordination: a registry file (.ag-sdk-integrity.json)
|
||||
* in the workbench directory tracks active SDK namespaces and the original
|
||||
* hashes, so the last extension to uninstall restores the original state.
|
||||
*
|
||||
* @module integration/integrity-manager
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Logger } from '../core/logger';
|
||||
|
||||
const log = new Logger('IntegrityManager');
|
||||
|
||||
/** Coordination registry stored in the workbench directory. */
|
||||
interface IIntegrityRegistry {
|
||||
/** Active SDK namespace slugs. */
|
||||
namespaces: string[];
|
||||
/** Original product.json hashes for ALL checksummed files (before any patching). */
|
||||
originalHashes: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Registry filename — lives next to workbench.html. */
|
||||
const REGISTRY_FILENAME = '.ag-sdk-integrity.json';
|
||||
|
||||
/**
|
||||
* Manages integrity check suppression for Antigravity's IntegrityService.
|
||||
*
|
||||
* Call `suppressCheck()` after any file patching (workbench.html, main.js, etc.).
|
||||
* It scans ALL files listed in product.json checksums, recomputes hashes for
|
||||
* any that have changed, and updates product.json. IntegrityService will see
|
||||
* `isPure = true` on next restart, producing zero warnings.
|
||||
*/
|
||||
export class IntegrityManager {
|
||||
private readonly _productJsonPath: string;
|
||||
private readonly _appOutDir: string;
|
||||
private readonly _registryPath: string;
|
||||
private readonly _namespace: string;
|
||||
|
||||
/**
|
||||
* @param workbenchDir — Absolute path to the workbench directory
|
||||
* (e.g. `%LOCALAPPDATA%/Programs/Antigravity/resources/app/out/vs/code/electron-browser/workbench/`)
|
||||
* @param namespace — Unique slug for this extension (e.g. 'kanezal-better-antigravity')
|
||||
*/
|
||||
constructor(workbenchDir: string, namespace: string) {
|
||||
this._namespace = namespace;
|
||||
this._registryPath = path.join(workbenchDir, REGISTRY_FILENAME);
|
||||
|
||||
// product.json is at resources/app/product.json
|
||||
// workbenchDir is resources/app/out/vs/code/electron-browser/workbench/
|
||||
const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
|
||||
this._productJsonPath = path.join(appDir, 'product.json');
|
||||
this._appOutDir = path.join(appDir, 'out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress the integrity check by updating ALL mismatched hashes in product.json.
|
||||
*
|
||||
* Scans every file listed in product.json checksums, recomputes SHA256 for each,
|
||||
* and updates any that have changed. This handles not just workbench.html but also
|
||||
* workbench.desktop.main.js (auto-run fix), jetskiAgent files, etc.
|
||||
*
|
||||
* Call this after any file patching. Safe to call multiple times.
|
||||
*/
|
||||
suppressCheck(): void {
|
||||
try {
|
||||
if (!fs.existsSync(this._productJsonPath)) {
|
||||
log.warn(`product.json not found at ${this._productJsonPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const productJson = JSON.parse(fs.readFileSync(this._productJsonPath, 'utf8'));
|
||||
if (!productJson.checksums) {
|
||||
log.debug('No checksums in product.json — nothing to update');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Load or create registry, register this namespace
|
||||
const registry = this._readRegistry();
|
||||
if (!registry.namespaces.includes(this._namespace)) {
|
||||
registry.namespaces.push(this._namespace);
|
||||
}
|
||||
|
||||
// 2. Scan ALL checksummed files, save originals & update mismatches
|
||||
let updatedCount = 0;
|
||||
for (const [relPath, storedHash] of Object.entries(productJson.checksums) as [string, string][]) {
|
||||
const filePath = path.join(this._appOutDir, relPath);
|
||||
|
||||
let actualHash: string;
|
||||
try {
|
||||
const content = fs.readFileSync(filePath);
|
||||
actualHash = this._computeHash(content);
|
||||
} catch {
|
||||
// File not found — skip (don't break other checks)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actualHash !== storedHash) {
|
||||
// Save original hash if we haven't already
|
||||
if (!(relPath in registry.originalHashes)) {
|
||||
registry.originalHashes[relPath] = storedHash;
|
||||
log.debug(`Saved original hash for ${relPath}`);
|
||||
}
|
||||
|
||||
productJson.checksums[relPath] = actualHash;
|
||||
updatedCount++;
|
||||
log.info(`Updated hash: ${relPath} (${storedHash.substring(0, 8)}... -> ${actualHash.substring(0, 8)}...)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Write registry
|
||||
this._writeRegistry(registry);
|
||||
|
||||
// 4. Write product.json if anything changed
|
||||
if (updatedCount > 0) {
|
||||
fs.writeFileSync(this._productJsonPath, JSON.stringify(productJson, null, '\t'), 'utf8');
|
||||
log.info(`Updated ${updatedCount} hash(es) in product.json`);
|
||||
} else {
|
||||
log.debug('All hashes already match — no update needed');
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to suppress integrity check', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the integrity check suppression.
|
||||
*
|
||||
* Call this when uninstalling the integration. If no other SDK namespaces
|
||||
* remain active, restores all original hashes in product.json.
|
||||
*/
|
||||
releaseCheck(): void {
|
||||
try {
|
||||
const registry = this._readRegistry();
|
||||
|
||||
// Remove this namespace
|
||||
registry.namespaces = registry.namespaces.filter(ns => ns !== this._namespace);
|
||||
this._writeRegistry(registry);
|
||||
|
||||
if (registry.namespaces.length > 0) {
|
||||
// Other SDK extensions still active — recompute all hashes
|
||||
log.debug(`${registry.namespaces.length} other namespace(s) still active, recomputing hashes`);
|
||||
this.suppressCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
// Last extension uninstalling — restore ALL original hashes
|
||||
if (Object.keys(registry.originalHashes).length > 0) {
|
||||
this._restoreOriginalHashes(registry.originalHashes);
|
||||
log.info(`Restored ${Object.keys(registry.originalHashes).length} original hash(es)`);
|
||||
}
|
||||
|
||||
// Clean up registry file
|
||||
this._deleteRegistry();
|
||||
} catch (err) {
|
||||
log.error('Failed to release integrity check', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-apply integrity suppression after auto-repair.
|
||||
*
|
||||
* Call this after auto-repair has re-patched files
|
||||
* (e.g. after an AG update that overwrote workbench files).
|
||||
*/
|
||||
repair(): void {
|
||||
log.info('Repairing integrity check suppression...');
|
||||
this.suppressCheck();
|
||||
}
|
||||
|
||||
// ── Private helpers ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compute SHA256 hash matching Antigravity's ChecksumService format:
|
||||
* base64 WITHOUT trailing '=' padding.
|
||||
*/
|
||||
private _computeHash(content: Buffer): string {
|
||||
return crypto.createHash('sha256')
|
||||
.update(content)
|
||||
.digest('base64')
|
||||
.replace(/=+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all original hashes in product.json.
|
||||
*/
|
||||
private _restoreOriginalHashes(originalHashes: Record<string, string>): void {
|
||||
if (!fs.existsSync(this._productJsonPath)) return;
|
||||
|
||||
const productJson = JSON.parse(fs.readFileSync(this._productJsonPath, 'utf8'));
|
||||
if (!productJson.checksums) return;
|
||||
|
||||
for (const [relPath, hash] of Object.entries(originalHashes)) {
|
||||
if (relPath in productJson.checksums) {
|
||||
productJson.checksums[relPath] = hash;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(this._productJsonPath, JSON.stringify(productJson, null, '\t'), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the coordination registry from disk.
|
||||
*/
|
||||
private _readRegistry(): IIntegrityRegistry {
|
||||
try {
|
||||
if (fs.existsSync(this._registryPath)) {
|
||||
const raw = fs.readFileSync(this._registryPath, 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
// Migrate from old format (single originalHash) to new (originalHashes map)
|
||||
let originalHashes: Record<string, string> = {};
|
||||
if (data.originalHashes && typeof data.originalHashes === 'object') {
|
||||
originalHashes = data.originalHashes;
|
||||
} else if (typeof data.originalHash === 'string') {
|
||||
// Legacy v1.5.0 format: single hash for workbench.html
|
||||
originalHashes['vs/code/electron-browser/workbench/workbench.html'] = data.originalHash;
|
||||
}
|
||||
|
||||
return {
|
||||
namespaces: Array.isArray(data.namespaces) ? data.namespaces : [],
|
||||
originalHashes,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Corrupt or inaccessible — start fresh
|
||||
}
|
||||
return { namespaces: [], originalHashes: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the coordination registry to disk.
|
||||
*/
|
||||
private _writeRegistry(registry: IIntegrityRegistry): void {
|
||||
try {
|
||||
fs.writeFileSync(this._registryPath, JSON.stringify(registry, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
log.error('Failed to write integrity registry', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the coordination registry file.
|
||||
*/
|
||||
private _deleteRegistry(): void {
|
||||
try {
|
||||
if (fs.existsSync(this._registryPath)) {
|
||||
fs.unlinkSync(this._registryPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,554 +0,0 @@
|
||||
/**
|
||||
* Script Generator — Builds self-contained JS from integration configs.
|
||||
*
|
||||
* Generates a Trusted Types-safe integration script that:
|
||||
* - Uses ONLY createElement/textContent (no innerHTML)
|
||||
* - Uses MutationObserver for dynamic content
|
||||
* - Is fully self-contained (runs in renderer, no Node.js APIs)
|
||||
*
|
||||
* @module integration/script-generator
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
import { Selectors, AG_PREFIX, AG_DATA_ATTR } from './selectors';
|
||||
import {
|
||||
IntegrationConfig,
|
||||
IntegrationPoint,
|
||||
IToastConfig,
|
||||
IToastRow,
|
||||
TurnMetric,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Generates a self-contained JavaScript integration script
|
||||
* from an array of IntegrationConfig objects.
|
||||
*/
|
||||
export class ScriptGenerator {
|
||||
/**
|
||||
* Generate the complete integration script.
|
||||
*
|
||||
* @param configs — Registered integration configurations
|
||||
* @returns — Complete JS code as a string
|
||||
*/
|
||||
generate(configs: IntegrationConfig[]): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(this._header());
|
||||
parts.push(this._css(configs));
|
||||
parts.push(this._helpers());
|
||||
parts.push(this._toast());
|
||||
parts.push(this._stats());
|
||||
|
||||
// Generate code for each integration point
|
||||
const grouped = this._groupByPoint(configs);
|
||||
|
||||
for (const [point, cfgs] of Object.entries(grouped)) {
|
||||
parts.push(this._generatePoint(point as IntegrationPoint, cfgs));
|
||||
}
|
||||
|
||||
parts.push(this._mainLoop(Object.keys(grouped) as IntegrationPoint[]));
|
||||
parts.push(this._footer());
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
// ─── Grouping ──────────────────────────────────────────────────────
|
||||
|
||||
private _groupByPoint(configs: IntegrationConfig[]): Record<string, IntegrationConfig[]> {
|
||||
const groups: Record<string, IntegrationConfig[]> = {};
|
||||
for (const c of configs) {
|
||||
if (c.enabled === false) continue;
|
||||
if (!groups[c.point]) groups[c.point] = [];
|
||||
groups[c.point].push(c);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// ─── Code Sections ────────────────────────────────────────────────
|
||||
|
||||
private _header(): string {
|
||||
return `(function agSDK(){
|
||||
'use strict';
|
||||
if(window.__agSDK)return;
|
||||
window.__agSDK=true;
|
||||
|
||||
// ─── Theme Detection ───
|
||||
var _isDark=document.body.classList.contains('vscode-dark')||document.body.classList.contains('vscode-high-contrast');
|
||||
var _theme={
|
||||
bg:_isDark?'rgba(25,25,30,.95)':'rgba(245,245,250,.95)',
|
||||
fg:_isDark?'#ccc':'#333',
|
||||
fgDim:_isDark?'rgba(200,200,200,.45)':'rgba(80,80,80,.5)',
|
||||
fgHover:_isDark?'rgba(200,200,200,.8)':'rgba(40,40,40,.9)',
|
||||
accent:_isDark?'#4fc3f7':'#0288d1',
|
||||
accentBg:_isDark?'rgba(79,195,247,.12)':'rgba(2,136,209,.08)',
|
||||
success:_isDark?'#81c784':'#388e3c',
|
||||
successBg:_isDark?'rgba(76,175,80,.1)':'rgba(56,142,60,.06)',
|
||||
warn:_isDark?'#ffb74d':'#e65100',
|
||||
border:_isDark?'rgba(79,195,247,.06)':'rgba(0,0,0,.06)',
|
||||
borderHover:_isDark?'rgba(79,195,247,.2)':'rgba(2,136,209,.15)',
|
||||
sep:_isDark?'rgba(255,255,255,.06)':'rgba(0,0,0,.06)',
|
||||
shadow:_isDark?'rgba(0,0,0,.5)':'rgba(0,0,0,.15)',
|
||||
metaBg:_isDark?'linear-gradient(135deg,rgba(79,195,247,.03),rgba(156,39,176,.02))':'linear-gradient(135deg,rgba(2,136,209,.03),rgba(123,31,162,.02))',
|
||||
metaBgHover:_isDark?'linear-gradient(135deg,rgba(79,195,247,.07),rgba(156,39,176,.05))':'linear-gradient(135deg,rgba(2,136,209,.07),rgba(123,31,162,.05))'
|
||||
};
|
||||
// Watch for theme changes (VS Code toggles body classes)
|
||||
new MutationObserver(function(){var newDark=document.body.classList.contains('vscode-dark');if(newDark!==_isDark){location.reload();}}).observe(document.body,{attributes:true,attributeFilter:['class']});
|
||||
`;
|
||||
}
|
||||
|
||||
private _footer(): string {
|
||||
// The heartbeat file is in the same directory as the script.
|
||||
// We use sync XHR (allowed in renderer since we're in a script tag,
|
||||
// not a module) to check the file before starting.
|
||||
// Max age: 48 hours (172800000ms) — enough to survive normal restarts
|
||||
// but catches disabled extensions reliably.
|
||||
return `
|
||||
var _heartbeatMaxAge=172800000;
|
||||
function checkHeartbeat(){
|
||||
try{
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET','./ag-sdk-heartbeat?t='+Date.now(),false);
|
||||
xhr.send();
|
||||
if(xhr.status!==200)return false;
|
||||
var ts=parseInt(xhr.responseText,10);
|
||||
if(isNaN(ts))return false;
|
||||
return(Date.now()-ts)<_heartbeatMaxAge;
|
||||
}catch(e){return false;}
|
||||
}
|
||||
function boot(){
|
||||
if(!checkHeartbeat()){
|
||||
console.log('[AG SDK] Heartbeat missing or stale — extension disabled? Skipping.');
|
||||
return;
|
||||
}
|
||||
if(document.readyState==='complete')setTimeout(start,3000);
|
||||
else window.addEventListener('load',function(){setTimeout(start,3000);});
|
||||
}
|
||||
boot();
|
||||
})();`;
|
||||
}
|
||||
|
||||
private _css(configs: IntegrationConfig[]): string {
|
||||
// Only include CSS for points that are actually used
|
||||
const points = new Set(configs.map(c => c.point));
|
||||
|
||||
// All colors now use _theme variables for light/dark mode support
|
||||
// CSS is generated as a JS template that reads _theme at runtime
|
||||
return `
|
||||
// ─── Theme-Aware CSS ───
|
||||
var _cssRules=[
|
||||
'.${AG_PREFIX}meta{padding:3px 8px;background:'+_theme.metaBg+';border-top:1px solid '+_theme.border+';font-family:"Cascadia Code","Fira Code",monospace;font-size:9px;color:'+_theme.fgDim+';display:flex;align-items:center;gap:5px;flex-wrap:wrap;transition:all .2s;cursor:default;user-select:none;margin-top:2px;border-radius:0 0 6px 6px}',
|
||||
'.${AG_PREFIX}meta:hover{background:'+_theme.metaBgHover+';color:'+_theme.fgHover+'}',
|
||||
'.${AG_PREFIX}t{padding:1px 4px;border-radius:3px;font-size:8px;font-weight:700;letter-spacing:.3px}',
|
||||
'.${AG_PREFIX}u{background:'+_theme.successBg+';color:'+_theme.success+'}',
|
||||
'.${AG_PREFIX}b{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}k{color:'+_theme.fgDim+';font-size:8px}',
|
||||
'.${AG_PREFIX}v{color:'+_theme.fg+';font-size:8px;opacity:.55}',
|
||||
'.${AG_PREFIX}hi{color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}w{color:'+_theme.warn+'}',
|
||||
'.${AG_PREFIX}s{color:'+_theme.sep+'}',
|
||||
// Toast
|
||||
'.${AG_PREFIX}toast{position:fixed;bottom:80px;right:20px;background:'+_theme.bg+';border:1px solid '+_theme.borderHover+';border-radius:8px;padding:10px 14px;font-family:"Cascadia Code",monospace;font-size:10px;color:'+_theme.fg+';z-index:99999;max-width:320px;backdrop-filter:blur(10px);box-shadow:0 4px 24px '+_theme.shadow+';animation:${AG_PREFIX}fade .25s ease}',
|
||||
'@keyframes ${AG_PREFIX}fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}',
|
||||
'.${AG_PREFIX}toast-t{color:'+_theme.accent+';font-weight:700;margin-bottom:5px;font-size:11px;display:flex;align-items:center;gap:6px}',
|
||||
'.${AG_PREFIX}toast-r{display:flex;gap:8px;margin:1px 0}',
|
||||
'.${AG_PREFIX}toast-k{color:'+_theme.fgDim+';min-width:70px}',
|
||||
'.${AG_PREFIX}toast-v{color:'+_theme.fg+'}',
|
||||
'.${AG_PREFIX}toast-badge{font-size:8px;padding:1px 5px;border-radius:3px;font-weight:700}',
|
||||
// Buttons
|
||||
'.${AG_PREFIX}hdr{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:4px;cursor:pointer;color:'+_theme.fgDim+';font-size:9px;font-family:"Cascadia Code",monospace;transition:all .15s;user-select:none}',
|
||||
'.${AG_PREFIX}hdr:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}inp{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;border-radius:4px;cursor:pointer;color:'+_theme.fgDim+';font-size:11px;transition:all .15s;flex-shrink:0;padding:0 4px;font-family:"Cascadia Code",monospace}',
|
||||
'.${AG_PREFIX}inp:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}menu{padding:4px 8px;cursor:pointer;font-size:11px;color:'+_theme.fg+';opacity:.7;transition:all .12s;display:flex;align-items:center;gap:6px;white-space:nowrap}',
|
||||
'.${AG_PREFIX}menu:hover{background:'+_theme.accentBg+';color:'+_theme.accent+';opacity:1}',
|
||||
'.${AG_PREFIX}vote{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:3px;cursor:pointer;color:'+_theme.fgDim+';font-size:9px;font-family:"Cascadia Code",monospace;transition:all .15s;margin-left:4px}',
|
||||
'.${AG_PREFIX}vote:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}ubadge{display:inline-flex;align-items:center;gap:2px;padding:1px 5px;border-radius:3px;background:'+_theme.successBg+';cursor:pointer;color:'+_theme.success+';opacity:.4;font-size:8px;font-family:"Cascadia Code",monospace;transition:all .15s;margin-left:3px}',
|
||||
'.${AG_PREFIX}ubadge:hover{background:'+_theme.successBg+';color:'+_theme.success+';opacity:1}',
|
||||
'.${AG_PREFIX}title-hint{position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:8px;color:'+_theme.accent+';opacity:.3;pointer-events:none;font-family:"Cascadia Code",monospace;transition:opacity .2s}',
|
||||
'.${AG_PREFIX}title-wrap:hover .${AG_PREFIX}title-hint{opacity:1}'
|
||||
];
|
||||
var css=document.createElement('style');
|
||||
css.textContent=_cssRules.join('\\n');
|
||||
document.head.appendChild(css);
|
||||
`;
|
||||
}
|
||||
|
||||
private _helpers(): string {
|
||||
return `
|
||||
function mk(tag,cls,txt){var e=document.createElement(tag);if(cls)e.className=cls;if(txt!==undefined)e.textContent=txt;return e;}
|
||||
function fmt(n){return n>=1000?(n/1000).toFixed(1)+'k':''+n;}
|
||||
`;
|
||||
}
|
||||
|
||||
private _toast(): string {
|
||||
return `
|
||||
var _toastT=0;
|
||||
function toast(title,badge,rows){
|
||||
var old=document.querySelector('.${AG_PREFIX}toast');if(old)old.remove();
|
||||
var t=mk('div','${AG_PREFIX}toast');
|
||||
var hdr=mk('div','${AG_PREFIX}toast-t');
|
||||
hdr.appendChild(document.createTextNode(title));
|
||||
if(badge){var b=mk('span','${AG_PREFIX}toast-badge');b.textContent=badge[0];b.style.background=badge[1];b.style.color=badge[2];hdr.appendChild(b);}
|
||||
t.appendChild(hdr);
|
||||
rows.forEach(function(r){var row=mk('div','${AG_PREFIX}toast-r');row.appendChild(mk('span','${AG_PREFIX}toast-k',r[0]));row.appendChild(mk('span','${AG_PREFIX}toast-v',r[1]));t.appendChild(row);});
|
||||
document.body.appendChild(t);
|
||||
clearTimeout(_toastT);_toastT=setTimeout(function(){if(t.parentNode)t.remove();},6000);
|
||||
t.addEventListener('click',function(){t.remove();});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _stats(): string {
|
||||
return `
|
||||
function getStats(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});
|
||||
if(!c)return null;
|
||||
var turns=0,uC=0,bC=0,code=0;
|
||||
Array.from(c.children).forEach(function(ch){
|
||||
if(ch.getAttribute('${AG_DATA_ATTR}')||ch.children.length<1)return;
|
||||
turns++;
|
||||
uC+=(ch.children[0]?.textContent?.trim()||'').length;
|
||||
bC+=(ch.children[1]?.textContent?.trim()||'').length;
|
||||
code+=(ch.children[1]?.querySelectorAll('pre')?.length||0);
|
||||
});
|
||||
return{turns:turns,u:uC,b:bC,code:code};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Point generators ─────────────────────────────────────────────
|
||||
|
||||
private _generatePoint(point: IntegrationPoint, configs: IntegrationConfig[]): string {
|
||||
switch (point) {
|
||||
case IntegrationPoint.TOP_BAR:
|
||||
return this._genTopBar(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.TOP_RIGHT:
|
||||
return this._genTopRight(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.INPUT_AREA:
|
||||
return this._genInputArea(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.BOTTOM_ICONS:
|
||||
return this._genBottomIcons(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.TURN_METADATA:
|
||||
return this._genTurnMeta(configs as ITurnMetaIntegration[]);
|
||||
case IntegrationPoint.USER_BADGE:
|
||||
return this._genUserBadge(configs as IUserBadgeIntegration[]);
|
||||
case IntegrationPoint.BOT_ACTION:
|
||||
return this._genBotAction(configs as IBotActionIntegration[]);
|
||||
case IntegrationPoint.DROPDOWN_MENU:
|
||||
return this._genDropdown(configs as IDropdownIntegration[]);
|
||||
case IntegrationPoint.CHAT_TITLE:
|
||||
return this._genTitle(configs as ITitleIntegration[]);
|
||||
default:
|
||||
return `// Unknown point: ${point}`;
|
||||
}
|
||||
}
|
||||
|
||||
private _genToastCall(toast?: IToastConfig): string {
|
||||
if (!toast) return '';
|
||||
const badge = toast.badge
|
||||
? `[${JSON.stringify(toast.badge.text)},${JSON.stringify(toast.badge.bgColor)},${JSON.stringify(toast.badge.textColor)}]`
|
||||
: 'null';
|
||||
const rows = toast.rows
|
||||
.map(r => {
|
||||
if (r.dynamic) {
|
||||
return `[${JSON.stringify(r.key)},${r.value}]`;
|
||||
}
|
||||
return `[${JSON.stringify(r.key)},${JSON.stringify(r.value)}]`;
|
||||
})
|
||||
.join(',');
|
||||
return `toast(${JSON.stringify(toast.title)},${badge},[${rows}]);`;
|
||||
}
|
||||
|
||||
private _genTopBar(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn_${c.id}=mk('a','${AG_PREFIX}hdr ${AG_PREFIX}${c.id}');
|
||||
btn_${c.id}.textContent=${JSON.stringify(c.icon)};
|
||||
btn_${c.id}.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn_${c.id}.addEventListener('click',function(){${toastCall}});
|
||||
iconsArea.insertBefore(btn_${c.id},iconsArea.children[1]);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateTopBar(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
|
||||
var topBar=p.querySelector(${JSON.stringify(Selectors.TOP_BAR)});if(!topBar)return;
|
||||
var iconsArea=topBar.querySelector(${JSON.stringify(Selectors.TOP_ICONS)});
|
||||
if(!iconsArea||iconsArea.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genTopRight(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn_${c.id}=mk('a','${AG_PREFIX}hdr ${AG_PREFIX}${c.id}');
|
||||
btn_${c.id}.textContent=${JSON.stringify(c.icon)};
|
||||
btn_${c.id}.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn_${c.id}.addEventListener('click',function(){${toastCall}});
|
||||
iconsArea.insertBefore(btn_${c.id},iconsArea.lastElementChild);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateTopRight(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
|
||||
var topBar=p.querySelector(${JSON.stringify(Selectors.TOP_BAR)});if(!topBar)return;
|
||||
var iconsArea=topBar.querySelector(${JSON.stringify(Selectors.TOP_ICONS)});
|
||||
if(!iconsArea||iconsArea.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genInputArea(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn=mk('div','${AG_PREFIX}inp ${AG_PREFIX}${c.id}');
|
||||
btn.textContent=${JSON.stringify(c.icon)};
|
||||
btn.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn.addEventListener('click',function(){${toastCall}});
|
||||
btnRow.insertBefore(btn,btnRow.firstChild);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateInputArea(){
|
||||
var ib=document.querySelector(${JSON.stringify(Selectors.INPUT_BOX)});
|
||||
if(!ib||ib.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
var allBtns=ib.querySelectorAll('button,[role="button"]');
|
||||
if(allBtns.length===0)return;
|
||||
var btnRow=allBtns[allBtns.length-1].parentElement;if(!btnRow)return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genBottomIcons(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn=mk('div','${AG_PREFIX}inp ${AG_PREFIX}${c.id}');
|
||||
btn.textContent=${JSON.stringify(c.icon)};
|
||||
btn.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn.addEventListener('click',function(){${toastCall}});
|
||||
row.appendChild(btn);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateBottomIcons(){
|
||||
var ib=document.querySelector(${JSON.stringify(Selectors.INPUT_BOX)});
|
||||
if(!ib||ib.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
var rows=ib.querySelectorAll('.flex.items-center');
|
||||
var row=null;
|
||||
for(var i=0;i<rows.length;i++){if(rows[i].querySelectorAll('svg').length>=2){row=rows[i];}}
|
||||
if(!row)return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genTurnMeta(configs: ITurnMetaIntegration[]): string {
|
||||
// Take first config for metrics (single turn metadata style)
|
||||
const cfg = configs[0];
|
||||
const metricParts: string[] = [];
|
||||
|
||||
for (const m of cfg.metrics) {
|
||||
switch (m) {
|
||||
case 'turnNumber':
|
||||
metricParts.push(`meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}b','T'+tI));`);
|
||||
break;
|
||||
case 'userCharCount':
|
||||
metricParts.push(`if(uL>0){meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}u','USER'));meta.appendChild(mk('span','${AG_PREFIX}k',fmt(uL)));}`);
|
||||
break;
|
||||
case 'separator':
|
||||
metricParts.push(`if(uL>0&&bL>0)meta.appendChild(mk('span','${AG_PREFIX}s','\\u2502'));`);
|
||||
break;
|
||||
case 'aiCharCount':
|
||||
metricParts.push(`if(bL>0){meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}b','AI'));meta.appendChild(mk('span','${AG_PREFIX}k',fmt(bL)));}`);
|
||||
break;
|
||||
case 'codeBlocks':
|
||||
metricParts.push(`if(codes>0){meta.appendChild(mk('span','${AG_PREFIX}k','code:'));meta.appendChild(mk('span','${AG_PREFIX}v ${AG_PREFIX}w',''+codes));}`);
|
||||
break;
|
||||
case 'thinkingIndicator':
|
||||
metricParts.push(`if(brain)meta.appendChild(mk('span','${AG_PREFIX}v','\\u{1F9E0}'));`);
|
||||
break;
|
||||
case 'ratio':
|
||||
metricParts.push(`if(uL>0&&bL>0){meta.appendChild(mk('span','${AG_PREFIX}k',(bL/uL).toFixed(1)+'x'));}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const clickHandler = cfg.clickable !== false
|
||||
? `meta.addEventListener('click',function(){toast('Turn '+tI,null,[['user:',fmt(uL)],['AI:',fmt(bL)],['ratio:',uL>0?(bL/uL).toFixed(1)+'x':'\\u2014']]);});`
|
||||
: '';
|
||||
|
||||
return `
|
||||
function integrateTurnMeta(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
|
||||
var tI=0;
|
||||
Array.from(c.children).forEach(function(turn){
|
||||
if(turn.getAttribute('${AG_DATA_ATTR}')||turn.children.length<1)return;
|
||||
turn.setAttribute('${AG_DATA_ATTR}','1');
|
||||
tI++;var uL=(turn.children[0]?.textContent?.trim()||'').length;
|
||||
var bL=(turn.children[1]?.textContent?.trim()||'').length;
|
||||
if(uL===0&&bL===0)return;
|
||||
var codes=turn.children[1]?.querySelectorAll('pre')?.length||0;
|
||||
var brain=(turn.children[1]?.textContent||'').includes('Thought');
|
||||
var meta=mk('div','${AG_PREFIX}meta');
|
||||
${metricParts.join('\n ')}
|
||||
${clickHandler}
|
||||
turn.appendChild(meta);
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genUserBadge(configs: IUserBadgeIntegration[]): string {
|
||||
const cfg = configs[0];
|
||||
let displayExpr = 'fmt(uLen)+" ch"';
|
||||
if (cfg.display === 'wordCount') {
|
||||
displayExpr = '(txt.split(/\\\\s+/).length)+" w"';
|
||||
} else if (cfg.display === 'custom' && cfg.customFormat) {
|
||||
displayExpr = cfg.customFormat;
|
||||
}
|
||||
|
||||
return `
|
||||
function integrateUserBadges(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
|
||||
Array.from(c.children).forEach(function(turn,i){
|
||||
if(turn.getAttribute('${AG_DATA_ATTR}u')||turn.children.length<1)return;
|
||||
var bubble=turn.children[0]?.querySelector(${JSON.stringify(Selectors.USER_BUBBLE)});
|
||||
if(!bubble)return;
|
||||
var txt=turn.children[0]?.textContent?.trim()||'';
|
||||
var uLen=txt.length;if(uLen<5)return;
|
||||
turn.setAttribute('${AG_DATA_ATTR}u','1');
|
||||
var row=turn.children[0]?.querySelector('.flex.w-full,.flex.flex-row')||turn.children[0];
|
||||
var badge=mk('span','${AG_PREFIX}ubadge');
|
||||
badge.textContent=${displayExpr};
|
||||
badge.title='SDK: User message';
|
||||
row.appendChild(badge);
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genBotAction(configs: IBotActionIntegration[]): string {
|
||||
const items = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return `var b=mk('span','${AG_PREFIX}vote');b.textContent=${JSON.stringify(c.icon + ' ' + c.label)};
|
||||
b.addEventListener('click',function(ev){ev.stopPropagation();${toastCall}});
|
||||
row.appendChild(b);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateBotAction(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
|
||||
c.querySelectorAll('span,button,a,div').forEach(function(el){
|
||||
if(el.getAttribute('${AG_DATA_ATTR}v'))return;
|
||||
var txt=el.textContent?.trim();
|
||||
if(txt==='Good'||txt==='Bad'){
|
||||
var row=el.parentElement;if(!row||row.querySelector('.${AG_PREFIX}vote'))return;
|
||||
el.setAttribute('${AG_DATA_ATTR}v','1');
|
||||
${items.join('\n ')}
|
||||
}
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genDropdown(configs: IDropdownIntegration[]): string {
|
||||
const markers = JSON.stringify(Selectors.DROPDOWN_MARKER_TEXT);
|
||||
const items = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
const sep = c.separator
|
||||
? `var sep=mk('div','');sep.style.cssText='height:1px;background:rgba(255,255,255,.06);margin:4px 8px';dd.appendChild(sep);`
|
||||
: '';
|
||||
return `${sep}
|
||||
var mi=mk('div','${AG_PREFIX}menu');
|
||||
${c.icon ? `mi.appendChild(mk('span','',${JSON.stringify(c.icon)}));` : ''}
|
||||
mi.appendChild(document.createTextNode(${JSON.stringify(c.label)}));
|
||||
mi.addEventListener('click',function(){${toastCall}});
|
||||
dd.appendChild(mi);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateDropdown(){
|
||||
var dds=document.querySelectorAll('.rounded-bg.py-1,.rounded-lg.py-1');
|
||||
dds.forEach(function(dd){
|
||||
if(dd.getAttribute('${AG_DATA_ATTR}m'))return;
|
||||
var items=dd.querySelectorAll(${JSON.stringify(Selectors.DROPDOWN_ITEM)});
|
||||
var markers=${markers};
|
||||
var found=false;
|
||||
items.forEach(function(it){markers.forEach(function(m){if((it.textContent||'').includes(m))found=true;});});
|
||||
if(!found)return;
|
||||
dd.setAttribute('${AG_DATA_ATTR}m','1');
|
||||
${items.join('\n ')}
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genTitle(configs: ITitleIntegration[]): string {
|
||||
const cfg = configs[0];
|
||||
const toastCall = this._genToastCall(cfg.toast);
|
||||
const event = cfg.interaction || 'dblclick';
|
||||
|
||||
return `
|
||||
function integrateTitle(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
|
||||
var el=p.querySelector(${JSON.stringify(Selectors.TITLE)});
|
||||
if(!el||el.getAttribute('${AG_DATA_ATTR}t'))return;
|
||||
el.setAttribute('${AG_DATA_ATTR}t','1');
|
||||
el.style.cursor='pointer';
|
||||
el.classList.add('${AG_PREFIX}title-wrap');
|
||||
el.style.position='relative';
|
||||
${cfg.hint ? `var hint=mk('span','${AG_PREFIX}title-hint',${JSON.stringify(cfg.hint)});el.appendChild(hint);` : ''}
|
||||
el.addEventListener(${JSON.stringify(event)},function(){
|
||||
var title=el.textContent?.replace(${JSON.stringify(cfg.hint || '')},'')?.trim()||'';
|
||||
${toastCall || `toast('Chat',null,[['title:',title],['chars:',''+title.length]]);`}
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Main loop ────────────────────────────────────────────────────
|
||||
|
||||
private _mainLoop(points: IntegrationPoint[]): string {
|
||||
const fnMap: Record<string, string> = {
|
||||
[IntegrationPoint.TOP_BAR]: 'integrateTopBar',
|
||||
[IntegrationPoint.TOP_RIGHT]: 'integrateTopRight',
|
||||
[IntegrationPoint.INPUT_AREA]: 'integrateInputArea',
|
||||
[IntegrationPoint.BOTTOM_ICONS]: 'integrateBottomIcons',
|
||||
[IntegrationPoint.TURN_METADATA]: 'integrateTurnMeta',
|
||||
[IntegrationPoint.USER_BADGE]: 'integrateUserBadges',
|
||||
[IntegrationPoint.BOT_ACTION]: 'integrateBotAction',
|
||||
[IntegrationPoint.DROPDOWN_MENU]: 'integrateDropdown',
|
||||
[IntegrationPoint.CHAT_TITLE]: 'integrateTitle',
|
||||
};
|
||||
|
||||
const calls = points.map(p => ` ${fnMap[p]}();`).join('\n');
|
||||
|
||||
return `
|
||||
function fullScan(){
|
||||
${calls}
|
||||
}
|
||||
var _timer=0;
|
||||
function debounced(){clearTimeout(_timer);_timer=setTimeout(function(){requestAnimationFrame(fullScan);},400);}
|
||||
function start(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});
|
||||
if(!p){setTimeout(start,1000);return;}
|
||||
fullScan();
|
||||
new MutationObserver(debounced).observe(p,{childList:true,subtree:true});
|
||||
setInterval(fullScan,8000);
|
||||
console.log('[AG SDK] Active \\u2014 ${points.length} integration points');
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* DOM Selectors — Single source of truth for all Agent View selectors.
|
||||
*
|
||||
* Verified against Antigravity v1.107.0 DOM (2026-02-28).
|
||||
* If Antigravity updates break selectors, only THIS file needs updating.
|
||||
*
|
||||
* @module integration/selectors
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export const Selectors = {
|
||||
/** The entire agent side panel container */
|
||||
PANEL: '.antigravity-agent-side-panel',
|
||||
|
||||
/** Top bar with title and action icons */
|
||||
TOP_BAR: '.flex.items-center.justify-between',
|
||||
|
||||
/** Icons area in top bar (contains +, refresh, ..., X) */
|
||||
TOP_ICONS: '.flex.items-center.gap-2',
|
||||
|
||||
/** Chat title element */
|
||||
TITLE: '.flex.min-w-0.items-center.overflow-hidden',
|
||||
|
||||
/** Main conversation scroll area */
|
||||
CONVERSATION: '#conversation',
|
||||
|
||||
/** Message turns container (direct children are turns) */
|
||||
TURNS_CONTAINER: '#conversation .gap-y-3',
|
||||
|
||||
/** User message bubble (inside turn) */
|
||||
USER_BUBBLE: '.rounded-lg',
|
||||
|
||||
/** Input box container */
|
||||
INPUT_BOX: '#antigravity\\.agentSidePanelInputBox',
|
||||
|
||||
/** 3-dot dropdown menu (appears dynamically) */
|
||||
DROPDOWN_MARKER_TEXT: ['Customization', 'Export'],
|
||||
|
||||
/** Dropdown menu item class pattern */
|
||||
DROPDOWN_ITEM: '.cursor-pointer',
|
||||
|
||||
/** Good/Bad feedback text markers */
|
||||
FEEDBACK_MARKERS: ['Good', 'Bad'],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* CSS class prefixes used by SDK integrations.
|
||||
* Used to identify and clean up integrated elements.
|
||||
*/
|
||||
export const AG_PREFIX = 'ag-';
|
||||
|
||||
/**
|
||||
* Data attribute used to mark processed elements.
|
||||
*/
|
||||
export const AG_DATA_ATTR = 'data-ag-sdk';
|
||||
@@ -1,171 +0,0 @@
|
||||
/**
|
||||
* Title Manager — Extension-host API for managing chat titles.
|
||||
*
|
||||
* Allows extensions to programmatically rename conversations
|
||||
* by writing to a data file that the renderer-side title proxy reads.
|
||||
*
|
||||
* Also provides a direct localStorage synchronization mechanism
|
||||
* via the integration script's window.__agSDKTitles API.
|
||||
*
|
||||
* @module integration/title-manager
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* // Rename via extension host (writes data file, renderer picks up on next poll)
|
||||
* sdk.titles.rename('cascade-uuid', 'My Custom Title');
|
||||
*
|
||||
* // Get all custom titles
|
||||
* const titles = sdk.titles.getAll();
|
||||
*
|
||||
* // Remove a custom title (reverts to auto-generated summary)
|
||||
* sdk.titles.remove('cascade-uuid');
|
||||
* ```
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Logger } from '../core/logger';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { getTitlesDataFile } from './title-proxy';
|
||||
|
||||
const log = new Logger('TitleManager');
|
||||
|
||||
/**
|
||||
* Manages custom conversation titles from the extension host.
|
||||
*
|
||||
* Titles are persisted in a JSON file in the workbench directory.
|
||||
* The renderer-side title proxy reads this file and merges with localStorage.
|
||||
*/
|
||||
export class TitleManager implements IDisposable {
|
||||
private _titles: Record<string, string> = {};
|
||||
private _dataPath: string = '';
|
||||
private _initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize with the workbench directory path.
|
||||
*
|
||||
* @param workbenchDir - Path to workbench directory where data file is stored
|
||||
* @param namespace - Extension namespace for file isolation
|
||||
*/
|
||||
initialize(workbenchDir: string, namespace: string = 'default'): void {
|
||||
this._dataPath = path.join(workbenchDir, getTitlesDataFile(namespace));
|
||||
this._load();
|
||||
this._initialized = true;
|
||||
log.info(`Initialized, ${Object.keys(this._titles).length} custom titles loaded`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the manager is initialized.
|
||||
*/
|
||||
get isInitialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom title for a conversation.
|
||||
*
|
||||
* The title will be displayed in the Agent View title bar
|
||||
* and conversation list instead of the auto-generated summary.
|
||||
*
|
||||
* @param cascadeId - The conversation's cascade ID (UUID)
|
||||
* @param title - The custom title to display
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Rename the active conversation
|
||||
* const id = sdk.titles.getActiveCascadeId();
|
||||
* sdk.titles.rename(id, 'Project Alpha Discussion');
|
||||
* ```
|
||||
*/
|
||||
rename(cascadeId: string, title: string): void {
|
||||
if (!cascadeId) {
|
||||
log.warn('rename: cascadeId is required');
|
||||
return;
|
||||
}
|
||||
if (!title || !title.trim()) {
|
||||
log.warn('rename: title cannot be empty');
|
||||
return;
|
||||
}
|
||||
this._titles[cascadeId] = title.trim();
|
||||
this._save();
|
||||
log.debug(`Renamed ${cascadeId.substring(0, 8)}... -> "${title.trim()}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom title for a conversation.
|
||||
*
|
||||
* @param cascadeId - The conversation's cascade ID
|
||||
* @returns The custom title, or undefined if no custom title is set
|
||||
*/
|
||||
getTitle(cascadeId: string): string | undefined {
|
||||
return this._titles[cascadeId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all custom titles.
|
||||
*
|
||||
* @returns A copy of the titles map (cascadeId -> title)
|
||||
*/
|
||||
getAll(): Readonly<Record<string, string>> {
|
||||
return { ...this._titles };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a custom title, reverting to the auto-generated summary.
|
||||
*
|
||||
* @param cascadeId - The conversation's cascade ID
|
||||
*/
|
||||
remove(cascadeId: string): void {
|
||||
if (this._titles[cascadeId]) {
|
||||
delete this._titles[cascadeId];
|
||||
this._save();
|
||||
log.debug(`Removed title for ${cascadeId.substring(0, 8)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all custom titles.
|
||||
*/
|
||||
clear(): void {
|
||||
this._titles = {};
|
||||
this._save();
|
||||
log.debug('Cleared all custom titles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of custom titles.
|
||||
*/
|
||||
get count(): number {
|
||||
return Object.keys(this._titles).length;
|
||||
}
|
||||
|
||||
/** Load titles from the data file */
|
||||
private _load(): void {
|
||||
try {
|
||||
if (fs.existsSync(this._dataPath)) {
|
||||
const content = fs.readFileSync(this._dataPath, 'utf8');
|
||||
this._titles = JSON.parse(content) || {};
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(`Failed to load titles: ${err}`);
|
||||
this._titles = {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Save titles to the data file */
|
||||
private _save(): void {
|
||||
if (!this._dataPath) return;
|
||||
try {
|
||||
fs.writeFileSync(this._dataPath, JSON.stringify(this._titles, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
log.warn(`Failed to save titles: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// Nothing to clean up - titles persist on disk
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/**
|
||||
* Title Proxy — Renderer-side code for intercepting chat summaries.
|
||||
*
|
||||
* Generates JavaScript that runs in the workbench renderer process.
|
||||
* Uses Preact VNode context walk to find the summaries provider,
|
||||
* wraps getState() to inject custom titles from localStorage,
|
||||
* and captures onDidChange listeners for forced re-renders.
|
||||
*
|
||||
* All identifiers used here are STRUCTURALLY MATCHED, not hardcoded
|
||||
* minified variable names — this survives obfuscation changes.
|
||||
*
|
||||
* @module integration/title-proxy
|
||||
* @internal
|
||||
*/
|
||||
|
||||
/** localStorage key prefix for custom titles */
|
||||
const TITLES_STORAGE_PREFIX = 'ag-sdk-titles';
|
||||
|
||||
/** Data file prefix for extension-host to set titles */
|
||||
const TITLES_DATA_PREFIX = 'ag-sdk-titles';
|
||||
|
||||
/**
|
||||
* Generate the renderer-side title proxy JavaScript.
|
||||
*
|
||||
* This code:
|
||||
* 1. BFS walks the Preact VNode tree (limit 3000, arrays not counted)
|
||||
* 2. Finds summaries provider via structural matching
|
||||
* 3. Wraps provider.getState() to inject custom titles
|
||||
* 4. Captures onDidChange listeners for forced re-renders
|
||||
* 5. Reads custom titles from localStorage + data file
|
||||
* 6. Exposes window.__agSDKTitles API for inline rename
|
||||
*
|
||||
* @param dataFilePath - Relative path to the JSON data file (for extension-host titles)
|
||||
* @returns JavaScript source code
|
||||
*/
|
||||
export function generateTitleProxyCode(namespace: string = 'default'): string {
|
||||
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
const storageKey = `${TITLES_STORAGE_PREFIX}-${slug}`;
|
||||
const dataFile = `./${TITLES_DATA_PREFIX}-${slug}.json`;
|
||||
return `
|
||||
// ── AG SDK: Title Proxy ──────────────────────────────────────────
|
||||
// Intercepts summaries provider to inject custom chat titles.
|
||||
// Uses structural matching (obfuscation-safe).
|
||||
|
||||
(function initTitleProxy(){
|
||||
var PANEL_SEL='.antigravity-agent-side-panel';
|
||||
var TITLE_SEL='.flex.min-w-0.items-center.overflow-hidden';
|
||||
var STORAGE_KEY='${storageKey}';
|
||||
var DATA_FILE='${dataFile}';
|
||||
|
||||
var _provider=null;
|
||||
var _origGetState=null;
|
||||
var _listeners=[];
|
||||
var _customTitles={};
|
||||
var _searchTime=0;
|
||||
|
||||
// ── Load / Save ────────────────────────────────────────────────
|
||||
|
||||
function loadTitles(){
|
||||
// Step 1: Load from localStorage (sync, fast)
|
||||
try{_customTitles=JSON.parse(localStorage.getItem(STORAGE_KEY)||'{}');}catch(e){_customTitles={};}
|
||||
// Step 2: Merge extension-host titles from data file (async fetch)
|
||||
fetch(DATA_FILE).then(function(r){
|
||||
if(!r.ok)return;
|
||||
return r.text();
|
||||
}).then(function(text){
|
||||
if(!text)return;
|
||||
try{
|
||||
var extTitles=JSON.parse(text);
|
||||
if(extTitles&&typeof extTitles==='object'){
|
||||
for(var k in extTitles){_customTitles[k]=extTitles[k];}
|
||||
saveTitles();
|
||||
notifyListeners();
|
||||
}
|
||||
}catch(e){}
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
function saveTitles(){
|
||||
try{localStorage.setItem(STORAGE_KEY,JSON.stringify(_customTitles));}catch(e){}
|
||||
}
|
||||
|
||||
// ── Notify ─────────────────────────────────────────────────────
|
||||
|
||||
function notifyListeners(){
|
||||
for(var i=0;i<_listeners.length;i++){try{_listeners[i]();}catch(e){}}
|
||||
}
|
||||
|
||||
// ── Provider Wrapping ──────────────────────────────────────────
|
||||
|
||||
function wrapProvider(provider){
|
||||
if(provider.__agSDKWrapped)return;
|
||||
provider.__agSDKWrapped=true;
|
||||
_provider=provider;
|
||||
var origFn=provider.getState;
|
||||
_origGetState=origFn;
|
||||
|
||||
// Wrap getState to inject custom titles
|
||||
provider.getState=function(){
|
||||
var state=origFn.call(provider);
|
||||
if(!state||!state.summaries)return state;
|
||||
var hasOverrides=false;
|
||||
for(var cid in _customTitles){if(state.summaries[cid]){hasOverrides=true;break;}}
|
||||
if(!hasOverrides)return state;
|
||||
var ns={};
|
||||
for(var k in state.summaries)ns[k]=state.summaries[k];
|
||||
for(var cid in _customTitles){
|
||||
if(ns[cid]){
|
||||
var copy={};for(var p in ns[cid])copy[p]=ns[cid][p];
|
||||
copy.summary=_customTitles[cid];
|
||||
ns[cid]=copy;
|
||||
}
|
||||
}
|
||||
var newState={};for(var sk in state)newState[sk]=state[sk];
|
||||
newState.summaries=ns;
|
||||
return newState;
|
||||
};
|
||||
|
||||
// Intercept onDidChange to capture listeners
|
||||
var origOnDidChange=provider.onDidChange;
|
||||
provider.onDidChange=function(callback){
|
||||
_listeners.push(callback);
|
||||
var origDispose=origOnDidChange.call(this,callback);
|
||||
return{dispose:function(){
|
||||
var idx=_listeners.indexOf(callback);
|
||||
if(idx>=0)_listeners.splice(idx,1);
|
||||
origDispose.dispose();
|
||||
}};
|
||||
};
|
||||
|
||||
console.log('[AG SDK] Title proxy active, custom titles:', Object.keys(_customTitles).length);
|
||||
|
||||
// Force re-render so custom titles appear immediately
|
||||
// (without waiting for next native summaries update)
|
||||
setTimeout(function(){notifyListeners();},50);
|
||||
}
|
||||
|
||||
// ── VNode BFS Walk ─────────────────────────────────────────────
|
||||
|
||||
function findProvider(){
|
||||
if(_provider)return;
|
||||
var panel=document.querySelector(PANEL_SEL);
|
||||
if(!panel||!panel.__k)return;
|
||||
// Throttle only AFTER confirming panel exists (don't block retries when panel isn't mounted)
|
||||
var now=Date.now();
|
||||
if(_searchTime&&now-_searchTime<30000)return;
|
||||
_searchTime=now;
|
||||
var queue=[panel.__k],visited=0;
|
||||
while(queue.length>0&&visited<3000){
|
||||
var node=queue.shift();
|
||||
if(!node)continue;
|
||||
if(Array.isArray(node)){
|
||||
for(var ai=0;ai<node.length;ai++){if(node[ai])queue.push(node[ai]);}
|
||||
continue;
|
||||
}
|
||||
visited++;
|
||||
var comp=node.__c;
|
||||
if(comp&&comp.context&&typeof comp.context==='object'){
|
||||
for(var key in comp.context){
|
||||
try{
|
||||
var ctx=comp.context[key];
|
||||
if(!ctx||!ctx.props||!ctx.props.value)continue;
|
||||
var val=ctx.props.value;
|
||||
// Structural match: {provider: {getState() -> {summaries}}}
|
||||
if(val.provider&&typeof val.provider.getState==='function'){
|
||||
var ts=val.provider.getState();
|
||||
if(ts&&ts.summaries){wrapProvider(val.provider);return;}
|
||||
}
|
||||
// Structural match: {trajectorySummariesProvider: {getState() -> {summaries}}}
|
||||
if(val.trajectorySummariesProvider&&typeof val.trajectorySummariesProvider.getState==='function'){
|
||||
var ts2=val.trajectorySummariesProvider.getState();
|
||||
if(ts2&&ts2.summaries){wrapProvider(val.trajectorySummariesProvider);return;}
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
// Direct props match
|
||||
if(comp&&comp.props&&comp.props.trajectorySummariesProvider){
|
||||
var tsp=comp.props.trajectorySummariesProvider;
|
||||
if(typeof tsp.getState==='function'){
|
||||
try{var ts3=tsp.getState();
|
||||
if(ts3&&ts3.summaries){wrapProvider(tsp);return;}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
if(node.__k){
|
||||
if(Array.isArray(node.__k)){for(var ki=0;ki<node.__k.length;ki++){if(node.__k[ki])queue.push(node.__k[ki]);}}
|
||||
else{queue.push(node.__k);}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── CascadeId Resolution ───────────────────────────────────────
|
||||
|
||||
function findCascadeIdByTitle(text){
|
||||
if(!_origGetState)return '';
|
||||
try{
|
||||
var state=_origGetState.call(_provider);
|
||||
if(!state||!state.summaries)return '';
|
||||
// Reverse lookup custom titles first
|
||||
for(var cid in _customTitles){if(_customTitles[cid]===text)return cid;}
|
||||
// Match original summaries
|
||||
var bestId='',bestTime=0;
|
||||
for(var cid in state.summaries){
|
||||
var e=state.summaries[cid];
|
||||
if(e&&e.summary===text){
|
||||
var t=0;try{t=new Date(e.lastModifiedTime).getTime();}catch(e){}
|
||||
if(!bestId||t>bestTime){bestId=cid;bestTime=t;}
|
||||
}
|
||||
}
|
||||
return bestId;
|
||||
}catch(e){return '';}
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────
|
||||
|
||||
window.__agSDKTitles={
|
||||
rename:function(cascadeId,title){
|
||||
if(!cascadeId||!title)return false;
|
||||
_customTitles[cascadeId]=title;
|
||||
saveTitles();
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
renameByCurrentTitle:function(currentTitle,newTitle){
|
||||
var cid=findCascadeIdByTitle(currentTitle);
|
||||
if(!cid)return false;
|
||||
return this.rename(cid,newTitle);
|
||||
},
|
||||
remove:function(cascadeId){
|
||||
delete _customTitles[cascadeId];
|
||||
saveTitles();
|
||||
notifyListeners();
|
||||
},
|
||||
getTitle:function(cascadeId){return _customTitles[cascadeId]||null;},
|
||||
getAll:function(){var copy={};for(var k in _customTitles)copy[k]=_customTitles[k];return copy;},
|
||||
getActiveCascadeId:function(){
|
||||
var panel=document.querySelector(PANEL_SEL);
|
||||
if(!panel)return '';
|
||||
var titleEl=panel.querySelector(TITLE_SEL);
|
||||
if(!titleEl)return '';
|
||||
var text='';
|
||||
function findText(el){
|
||||
for(var i=0;i<el.childNodes.length;i++){
|
||||
var n=el.childNodes[i];
|
||||
if(n.nodeType===3&&n.textContent.trim().length>0)return n.textContent.trim();
|
||||
if(n.nodeType===1){var found=findText(n);if(found)return found;}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
text=findText(titleEl);
|
||||
return text?findCascadeIdByTitle(text):'';
|
||||
},
|
||||
isReady:function(){return !!_provider;},
|
||||
reload:function(){loadTitles();notifyListeners();}
|
||||
};
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────
|
||||
|
||||
loadTitles();
|
||||
|
||||
function poll(){
|
||||
findProvider();
|
||||
}
|
||||
|
||||
// Poll until provider found, then every 30s for recovery
|
||||
var pollTimer=setInterval(function(){poll();},2000);
|
||||
|
||||
// Initial attempt after DOM is ready
|
||||
if(document.querySelector(PANEL_SEL)){
|
||||
poll();
|
||||
}
|
||||
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data file name for extension-host titles.
|
||||
*/
|
||||
export function getTitlesDataFile(namespace: string = 'default'): string {
|
||||
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
return `${TITLES_DATA_PREFIX}-${slug}.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localStorage key used by the renderer.
|
||||
*/
|
||||
export function getTitlesStorageKey(namespace: string = 'default'): string {
|
||||
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
return `${TITLES_STORAGE_PREFIX}-${slug}`;
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
/**
|
||||
* Integration module types — standardized UI integration points
|
||||
* for the Antigravity Agent View.
|
||||
*
|
||||
* @module integration/types
|
||||
*/
|
||||
|
||||
// ─── Integration Points ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Standardized integration points in the Agent View UI.
|
||||
*
|
||||
* Each point corresponds to a specific DOM location in the
|
||||
* Antigravity chat interface (verified 2026-02-28).
|
||||
*/
|
||||
export enum IntegrationPoint {
|
||||
/** Top bar — next to +, refresh, ... icons */
|
||||
TOP_BAR = 'topBar',
|
||||
/** Top right corner — before the X (close) button */
|
||||
TOP_RIGHT = 'topRight',
|
||||
/** Input area — next to voice/send buttons */
|
||||
INPUT_AREA = 'inputArea',
|
||||
/** Bottom icon row — file, terminal, artifact, chrome icons */
|
||||
BOTTOM_ICONS = 'bottomIcons',
|
||||
/** Per-turn metadata — appended inside each conversation turn */
|
||||
TURN_METADATA = 'turnMeta',
|
||||
/** User message badge — small badge inside user message bubbles */
|
||||
USER_BADGE = 'userBadge',
|
||||
/** Bot response action — button next to Good/Bad feedback */
|
||||
BOT_ACTION = 'botAction',
|
||||
/** 3-dot dropdown menu — extra items in the overflow menu */
|
||||
DROPDOWN_MENU = 'dropdownMenu',
|
||||
/** Chat title bar — interaction on conversation title */
|
||||
CHAT_TITLE = 'chatTitle',
|
||||
}
|
||||
|
||||
// ─── Configuration Interfaces ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Base configuration for all integration points.
|
||||
*/
|
||||
export interface IIntegrationBase {
|
||||
/** Unique ID for this integration (prevents duplicates) */
|
||||
id: string;
|
||||
/** Which integration point to target */
|
||||
point: IntegrationPoint;
|
||||
/** Whether this integration is enabled (default: true) */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for button-type integrations (top bar, input area, etc.).
|
||||
*/
|
||||
export interface IButtonIntegration extends IIntegrationBase {
|
||||
point:
|
||||
| IntegrationPoint.TOP_BAR
|
||||
| IntegrationPoint.TOP_RIGHT
|
||||
| IntegrationPoint.INPUT_AREA
|
||||
| IntegrationPoint.BOTTOM_ICONS;
|
||||
/** Icon (emoji or text glyph) */
|
||||
icon: string;
|
||||
/** Tooltip text */
|
||||
tooltip?: string;
|
||||
/** Toast to show on click */
|
||||
toast?: IToastConfig;
|
||||
/** CSS class override */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for turn-level metadata integration.
|
||||
*/
|
||||
export interface ITurnMetaIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.TURN_METADATA;
|
||||
/** Which metrics to display */
|
||||
metrics: TurnMetric[];
|
||||
/** Whether turns are clickable to show details toast */
|
||||
clickable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for user message badges.
|
||||
*/
|
||||
export interface IUserBadgeIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.USER_BADGE;
|
||||
/** What to show in the badge */
|
||||
display: 'charCount' | 'wordCount' | 'custom';
|
||||
/** Custom formatter function body (receives `textLength` as arg) */
|
||||
customFormat?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for bot response action buttons.
|
||||
*/
|
||||
export interface IBotActionIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.BOT_ACTION;
|
||||
/** Icon */
|
||||
icon: string;
|
||||
/** Label text */
|
||||
label: string;
|
||||
/** Toast config on click */
|
||||
toast?: IToastConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for dropdown menu items.
|
||||
*/
|
||||
export interface IDropdownIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.DROPDOWN_MENU;
|
||||
/** Menu item icon */
|
||||
icon?: string;
|
||||
/** Menu item label */
|
||||
label: string;
|
||||
/** Add separator before this item */
|
||||
separator?: boolean;
|
||||
/** Toast config on click */
|
||||
toast?: IToastConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for chat title interaction.
|
||||
*/
|
||||
export interface ITitleIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.CHAT_TITLE;
|
||||
/** Interaction type */
|
||||
interaction: 'click' | 'dblclick' | 'hover';
|
||||
/** Hint text shown on hover */
|
||||
hint?: string;
|
||||
/** Toast config on interaction */
|
||||
toast?: IToastConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast popup configuration.
|
||||
*/
|
||||
export interface IToastConfig {
|
||||
/** Toast title */
|
||||
title: string;
|
||||
/** Badge label and colors */
|
||||
badge?: {
|
||||
text: string;
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
};
|
||||
/** Key-value rows to display */
|
||||
rows: IToastRow[];
|
||||
/** Auto-dismiss after N milliseconds (default: 6000) */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A row in a toast popup.
|
||||
*/
|
||||
export interface IToastRow {
|
||||
/** Label (left side) */
|
||||
key: string;
|
||||
/**
|
||||
* Value (right side).
|
||||
* Can be a static string or a dynamic expression.
|
||||
* Dynamic expressions are JS code that runs in the renderer,
|
||||
* with access to `getStats()` which returns conversation stats.
|
||||
*/
|
||||
value: string;
|
||||
/** If true, `value` is treated as a JS expression */
|
||||
dynamic?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metrics available for turn metadata display.
|
||||
*/
|
||||
export type TurnMetric =
|
||||
| 'turnNumber'
|
||||
| 'userCharCount'
|
||||
| 'aiCharCount'
|
||||
| 'codeBlocks'
|
||||
| 'thinkingIndicator'
|
||||
| 'ratio'
|
||||
| 'separator';
|
||||
|
||||
/**
|
||||
* Union type of all integration configurations.
|
||||
*/
|
||||
export type IntegrationConfig =
|
||||
| IButtonIntegration
|
||||
| ITurnMetaIntegration
|
||||
| IUserBadgeIntegration
|
||||
| IBotActionIntegration
|
||||
| IDropdownIntegration
|
||||
| ITitleIntegration;
|
||||
|
||||
// ─── Manager Interface ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Public interface for the Integration Manager.
|
||||
*/
|
||||
export interface IIntegrationManager {
|
||||
/** Register a single integration point */
|
||||
register(config: IntegrationConfig): void;
|
||||
/** Register multiple integration points at once */
|
||||
registerMany(configs: IntegrationConfig[]): void;
|
||||
/** Remove a registered integration by ID */
|
||||
unregister(id: string): void;
|
||||
/** Get all registered integrations */
|
||||
getRegistered(): ReadonlyArray<IntegrationConfig>;
|
||||
/** Generate the integration script from all registered configs */
|
||||
build(): string;
|
||||
/** Install the generated script into workbench.html. Returns true if content changed. */
|
||||
install(): Promise<boolean>;
|
||||
/** Remove the integration from workbench.html */
|
||||
uninstall(): Promise<void>;
|
||||
/** Check if an integration is currently installed */
|
||||
isInstalled(): boolean;
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
/**
|
||||
* Workbench Patcher — Install/uninstall integration scripts into workbench.html.
|
||||
*
|
||||
* Handles the file-level modification of Antigravity's workbench.html
|
||||
* to include/remove custom script tags.
|
||||
*
|
||||
* @module integration/workbench-patcher
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/** Default prefix for generated files */
|
||||
const FILE_PREFIX = 'ag-sdk';
|
||||
|
||||
/**
|
||||
* Manages patching/unpatching of Antigravity's workbench.html.
|
||||
*/
|
||||
export class WorkbenchPatcher {
|
||||
private readonly _workbenchDir: string;
|
||||
private readonly _workbenchHtml: string;
|
||||
private readonly _scriptPath: string;
|
||||
private readonly _heartbeatPath: string;
|
||||
private readonly _slug: string;
|
||||
|
||||
private readonly _markerStart: string;
|
||||
private readonly _markerEnd: string;
|
||||
|
||||
/**
|
||||
* @param namespace - Unique slug for this extension (e.g. 'kanezal-better-antigravity').
|
||||
* Used to namespace all generated files and HTML markers so multiple
|
||||
* SDK-based extensions can coexist without conflicts.
|
||||
*/
|
||||
constructor(namespace: string = 'default') {
|
||||
// Resolve Antigravity install path
|
||||
const appData = process.env.LOCALAPPDATA || '';
|
||||
this._workbenchDir = path.join(
|
||||
appData,
|
||||
'Programs',
|
||||
'Antigravity',
|
||||
'resources',
|
||||
'app',
|
||||
'out',
|
||||
'vs',
|
||||
'code',
|
||||
'electron-browser',
|
||||
'workbench',
|
||||
);
|
||||
this._workbenchHtml = path.join(this._workbenchDir, 'workbench.html');
|
||||
|
||||
this._slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
this._scriptPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}.js`);
|
||||
this._heartbeatPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}-heartbeat`);
|
||||
this._markerStart = `<!-- AG SDK [${this._slug}] -->`;
|
||||
this._markerEnd = `<!-- /AG SDK [${this._slug}] -->`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workbench.html exists and is accessible.
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
return fs.existsSync(this._workbenchHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if our integration is currently installed.
|
||||
*/
|
||||
isInstalled(): boolean {
|
||||
if (!this.isAvailable()) return false;
|
||||
try {
|
||||
const content = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
return content.includes(this._markerStart);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the integration script.
|
||||
*
|
||||
* 1. Writes the script file to the workbench directory
|
||||
* 2. Patches workbench.html to include a <script> tag
|
||||
*
|
||||
* @param scriptContent — The generated JavaScript code
|
||||
*/
|
||||
install(scriptContent: string): void {
|
||||
if (!this.isAvailable()) {
|
||||
throw new Error(`Workbench not found at: ${this._workbenchDir}`);
|
||||
}
|
||||
|
||||
// First uninstall any previous integration for THIS namespace
|
||||
if (this.isInstalled()) {
|
||||
this.uninstall();
|
||||
}
|
||||
|
||||
// Clean up legacy files from previous versions (non-namespaced)
|
||||
this._cleanupLegacyFiles();
|
||||
|
||||
// Write the script file
|
||||
fs.writeFileSync(this._scriptPath, scriptContent, 'utf8');
|
||||
|
||||
// Patch workbench.html
|
||||
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
|
||||
// Insert before </html>
|
||||
const scriptBasename = path.basename(this._scriptPath);
|
||||
const scriptTag = [
|
||||
this._markerStart,
|
||||
`<script src="./${scriptBasename}"></script>`,
|
||||
this._markerEnd,
|
||||
].join('\n');
|
||||
|
||||
html = html.replace('</html>', `${scriptTag}\n</html>`);
|
||||
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
|
||||
|
||||
// Create empty titles JSON if it doesn't exist (prevents console 404)
|
||||
const titlesPath = path.join(this._workbenchDir, `ag-sdk-titles-${this._slug}.json`);
|
||||
if (!fs.existsSync(titlesPath)) {
|
||||
fs.writeFileSync(titlesPath, '{}', 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the integration.
|
||||
*
|
||||
* 1. Removes the <script> tag from workbench.html
|
||||
* 2. Deletes the script file
|
||||
*/
|
||||
uninstall(): void {
|
||||
if (!this.isAvailable()) return;
|
||||
|
||||
// Remove from workbench.html
|
||||
try {
|
||||
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
const regex = new RegExp(
|
||||
`\\n?${escapeRegex(this._markerStart)}[\\s\\S]*?${escapeRegex(this._markerEnd)}\\n?`,
|
||||
'g',
|
||||
);
|
||||
html = html.replace(regex, '');
|
||||
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
||||
// Remove script file
|
||||
try {
|
||||
if (fs.existsSync(this._scriptPath)) {
|
||||
fs.unlinkSync(this._scriptPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write/refresh the heartbeat marker file.
|
||||
*
|
||||
* The generated script checks this file's modification time
|
||||
* to determine if the extension is still active. If the file
|
||||
* is missing or stale, the script will not start.
|
||||
*/
|
||||
writeHeartbeat(): void {
|
||||
try {
|
||||
fs.writeFileSync(this._heartbeatPath, Date.now().toString(), 'utf8');
|
||||
} catch {
|
||||
// Ignore — workbench dir may not be writable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the heartbeat marker file.
|
||||
*/
|
||||
removeHeartbeat(): void {
|
||||
try {
|
||||
if (fs.existsSync(this._heartbeatPath)) {
|
||||
fs.unlinkSync(this._heartbeatPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the heartbeat file.
|
||||
*/
|
||||
getHeartbeatPath(): string {
|
||||
return this._heartbeatPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the workbench directory.
|
||||
*/
|
||||
getWorkbenchDir(): string {
|
||||
return this._workbenchDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the script file.
|
||||
*/
|
||||
getScriptPath(): string {
|
||||
return this._scriptPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up legacy files from previous SDK versions.
|
||||
*
|
||||
* Removes non-namespaced files (from before namespace support)
|
||||
* and files with wrong namespace (e.g. 'undefined').
|
||||
*/
|
||||
private _cleanupLegacyFiles(): void {
|
||||
// Legacy file names that may exist from older versions
|
||||
const legacyFiles = [
|
||||
'ag-sdk-integrate.js',
|
||||
'ag-sdk-heartbeat',
|
||||
'ag-sdk-titles.json',
|
||||
'ag-sdk-titles-undefined.json',
|
||||
'ag-sdk-titles-default.json',
|
||||
];
|
||||
|
||||
for (const name of legacyFiles) {
|
||||
const p = path.join(this._workbenchDir, name);
|
||||
try {
|
||||
if (fs.existsSync(p)) fs.unlinkSync(p);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Remove legacy script tags from workbench.html
|
||||
try {
|
||||
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
let changed = false;
|
||||
|
||||
// Remove bare <script src="./ag-sdk-integrate.js"></script> lines
|
||||
const legacyTagRegex = /<script src="\.\/ag-sdk-integrate\.js"><\/script>\n?/g;
|
||||
if (legacyTagRegex.test(html)) {
|
||||
html = html.replace(legacyTagRegex, '');
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Remove old X-Ray SDK markers with no namespace
|
||||
const xrayRegex = /<!-- X-Ray SDK Integration -->\n?<script[^>]*ag-sdk-integrate[^>]*><\/script>\n?<!-- \/X-Ray SDK Integration -->\n?/g;
|
||||
if (xrayRegex.test(html)) {
|
||||
html = html.replace(xrayRegex, '');
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
/**
|
||||
* Main SDK entry point.
|
||||
*
|
||||
* Provides a unified interface to Antigravity's agent system
|
||||
* via verified transport layer (CommandBridge + StateBridge + EventMonitor).
|
||||
*
|
||||
* @module AntigravitySDK
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { AntigravitySDK } from 'antigravity-sdk';
|
||||
*
|
||||
* export function activate(context: vscode.ExtensionContext) {
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* // List conversations
|
||||
* const sessions = await sdk.cascade.getSessions();
|
||||
* console.log(`${sessions.length} conversations`);
|
||||
*
|
||||
* // Read preferences (all 16 sentinel values)
|
||||
* const prefs = await sdk.cascade.getPreferences();
|
||||
* console.log('Terminal policy:', prefs.terminalExecutionPolicy);
|
||||
*
|
||||
* // Monitor for new conversations
|
||||
* sdk.monitor.onNewConversation(() => {
|
||||
* console.log('New conversation detected!');
|
||||
* });
|
||||
* sdk.monitor.start(3000);
|
||||
*
|
||||
* // Clean up
|
||||
* context.subscriptions.push(sdk);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { DisposableStore, IDisposable } from './core/disposable';
|
||||
import { Logger, LogLevel } from './core/logger';
|
||||
import { AntigravityNotFoundError } from './core/errors';
|
||||
import { CommandBridge } from './transport/command-bridge';
|
||||
import { StateBridge } from './transport/state-bridge';
|
||||
import { EventMonitor } from './transport/event-monitor';
|
||||
import { LSBridge } from './transport/ls-bridge';
|
||||
import { CascadeManager } from './cascade/cascade-manager';
|
||||
import { IntegrationManager } from './integration/integration-manager';
|
||||
|
||||
const log = new Logger('SDK');
|
||||
|
||||
/**
|
||||
* SDK initialization options.
|
||||
*/
|
||||
export interface ISDKOptions {
|
||||
/** Enable debug logging */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main Antigravity SDK class.
|
||||
*
|
||||
* Provides access to:
|
||||
* - `commands` — Execute Antigravity internal commands
|
||||
* - `state` — Read agent preferences and state from USS
|
||||
* - `cascade` — Manage Cascade conversations, send messages, read preferences
|
||||
* - `monitor` — Watch for state changes (new conversations, preference updates)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
* const sessions = await sdk.cascade.getSessions();
|
||||
* ```
|
||||
*/
|
||||
export class AntigravitySDK implements IDisposable {
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private _initialized = false;
|
||||
|
||||
/** Command bridge for executing Antigravity commands */
|
||||
public readonly commands: CommandBridge;
|
||||
|
||||
/** State bridge for reading USS data */
|
||||
public readonly state: StateBridge;
|
||||
|
||||
/** Cascade manager for conversations, preferences, diagnostics */
|
||||
public readonly cascade: CascadeManager;
|
||||
|
||||
/** Event monitor for watching state changes */
|
||||
public readonly monitor: EventMonitor;
|
||||
|
||||
/** Integration manager for Agent View UI customization */
|
||||
public readonly integration: IntegrationManager;
|
||||
|
||||
/**
|
||||
* Language Server bridge for headless cascade operations.
|
||||
* Use this for background cascade creation without UI switching.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const id = await sdk.ls.createCascade({ text: 'Analyze coverage' });
|
||||
* await sdk.ls.sendMessage({ cascadeId: id, text: 'Focus on tests' });
|
||||
* await sdk.ls.focusCascade(id); // Only when ready to show
|
||||
* ```
|
||||
*/
|
||||
public readonly ls: LSBridge;
|
||||
|
||||
/**
|
||||
* Create a new Antigravity SDK instance.
|
||||
*
|
||||
* @param context - VS Code extension context
|
||||
* @param options - SDK options
|
||||
*/
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
options?: ISDKOptions,
|
||||
) {
|
||||
if (options?.debug) {
|
||||
Logger.setLevel(LogLevel.Debug);
|
||||
}
|
||||
|
||||
// Derive namespace from extension ID for file isolation
|
||||
// e.g. 'kanezal.better-antigravity' -> 'kanezal-better-antigravity'
|
||||
const namespace = this._context.extension.id.replace(/\./g, '-');
|
||||
|
||||
this.commands = this._disposables.add(new CommandBridge());
|
||||
this.state = this._disposables.add(new StateBridge());
|
||||
this.cascade = this._disposables.add(new CascadeManager(this.commands, this.state));
|
||||
this.monitor = this._disposables.add(new EventMonitor(this.state));
|
||||
this.integration = this._disposables.add(new IntegrationManager(namespace));
|
||||
this.ls = new LSBridge(
|
||||
<T = any>(cmd: string, ...args: any[]) => Promise.resolve(vscode.commands.executeCommand<T>(cmd, ...args))
|
||||
);
|
||||
|
||||
log.info(`SDK created (namespace: ${namespace})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the SDK and verify Antigravity is running.
|
||||
*
|
||||
* Call this before using any SDK features.
|
||||
*
|
||||
* @throws {AntigravityNotFoundError} If Antigravity is not detected
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Initializing SDK...');
|
||||
|
||||
// Verify we're running inside Antigravity
|
||||
const isAntigravity = await this._detectAntigravity();
|
||||
if (!isAntigravity) {
|
||||
throw new AntigravityNotFoundError();
|
||||
}
|
||||
|
||||
// Initialize state bridge (opens state.vscdb via sql.js)
|
||||
await this.state.initialize();
|
||||
|
||||
// Initialize cascade manager (loads session list)
|
||||
await this.cascade.initialize();
|
||||
|
||||
// Initialize LS bridge (discovers Language Server port + CSRF token)
|
||||
const lsOk = await this.ls.initialize();
|
||||
if (lsOk) {
|
||||
log.info(`LS bridge ready on port ${this.ls.port} (csrf: ${this.ls.hasCsrfToken ? 'ok' : 'missing'})`);
|
||||
} else {
|
||||
log.warn('LS bridge not available — use sdk.ls.setConnection(port, csrfToken) or command fallback');
|
||||
}
|
||||
|
||||
// Refresh integration heartbeat (so renderer script knows extension is active)
|
||||
this.integration.signalActive();
|
||||
|
||||
this._initialized = true;
|
||||
log.info('SDK initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SDK has been initialized.
|
||||
*/
|
||||
get isInitialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SDK version.
|
||||
*/
|
||||
get version(): string {
|
||||
try {
|
||||
return require('../package.json').version;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if we're running inside Antigravity IDE.
|
||||
*/
|
||||
private async _detectAntigravity(): Promise<boolean> {
|
||||
try {
|
||||
// Check for Antigravity-specific commands (VERIFIED naming)
|
||||
const commands = await this.commands.getAntigravityCommands();
|
||||
const hasAgentPanel = commands.includes('antigravity.agentPanel.open');
|
||||
|
||||
if (hasAgentPanel) {
|
||||
log.debug(`Detected Antigravity (${commands.length} commands)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: check env
|
||||
const appName = vscode.env.appName;
|
||||
if (appName?.toLowerCase().includes('antigravity')) {
|
||||
log.debug(`Detected Antigravity via appName: ${appName}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the SDK and all its resources.
|
||||
*/
|
||||
dispose(): void {
|
||||
log.info('Disposing SDK');
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
/**
|
||||
* Command Bridge — executes Antigravity internal commands via VS Code API.
|
||||
*
|
||||
* All commands go through `vscode.commands.executeCommand()` which is the
|
||||
* safe, official way to interact with Antigravity from extensions.
|
||||
*
|
||||
* VERIFIED: All commands listed below were confirmed to exist in
|
||||
* Antigravity v1.107.0 workbench.desktop.main.js and extension.js
|
||||
* on 2026-02-28.
|
||||
*
|
||||
* @module transport/command-bridge
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { CommandExecutionError } from '../core/errors';
|
||||
import { Logger } from '../core/logger';
|
||||
|
||||
const log = new Logger('CommandBridge');
|
||||
|
||||
/**
|
||||
* All known Antigravity commands, organized by category.
|
||||
*
|
||||
* Sources: workbench.desktop.main.js (160+ commands) + extension.js (45 commands)
|
||||
*/
|
||||
export const AntigravityCommands = {
|
||||
|
||||
// ─── Agent Panel & UI (VERIFIED: .open/.focus suffix required) ────────
|
||||
|
||||
/** Open the Cascade agent panel */
|
||||
OPEN_AGENT_PANEL: 'antigravity.agentPanel.open',
|
||||
/** Focus the Cascade agent panel */
|
||||
FOCUS_AGENT_PANEL: 'antigravity.agentPanel.focus',
|
||||
/** Open the agent side panel */
|
||||
OPEN_AGENT_SIDE_PANEL: 'antigravity.agentSidePanel.open',
|
||||
/** Focus the agent side panel */
|
||||
FOCUS_AGENT_SIDE_PANEL: 'antigravity.agentSidePanel.focus',
|
||||
/** Toggle side panel visibility */
|
||||
TOGGLE_SIDE_PANEL: 'antigravity.agentSidePanel.toggleVisibility',
|
||||
/** Open agent (generic) */
|
||||
OPEN_AGENT: 'antigravity.openAgent',
|
||||
/** Toggle chat focus */
|
||||
TOGGLE_CHAT_FOCUS: 'antigravity.toggleChatFocus',
|
||||
/** Switch between workspace editor and agent view */
|
||||
SWITCH_WORKSPACE_AGENT: 'antigravity.switchBetweenWorkspaceAndAgent',
|
||||
|
||||
// ─── Conversation Management (Critical for SDK) ──────────────────────
|
||||
|
||||
/** Start a new conversation */
|
||||
START_NEW_CONVERSATION: 'antigravity.startNewConversation',
|
||||
/** Send a prompt to the agent panel */
|
||||
SEND_PROMPT_TO_AGENT: 'antigravity.sendPromptToAgentPanel',
|
||||
/** Send text to chat */
|
||||
SEND_TEXT_TO_CHAT: 'antigravity.sendTextToChat',
|
||||
/** Send a chat action message */
|
||||
SEND_CHAT_ACTION: 'antigravity.sendChatActionMessage',
|
||||
/** Set which conversation is visible */
|
||||
SET_VISIBLE_CONVERSATION: 'antigravity.setVisibleConversation',
|
||||
/** Execute a cascade action */
|
||||
EXECUTE_CASCADE_ACTION: 'antigravity.executeCascadeAction',
|
||||
/** Broadcast conversation deletion to all windows */
|
||||
BROADCAST_CONVERSATION_DELETION: 'antigravity.broadcastConversationDeletion',
|
||||
/** Track that a background conversation was created */
|
||||
TRACK_BACKGROUND_CONVERSATION: 'antigravity.trackBackgroundConversationCreated',
|
||||
|
||||
// ─── Agent Step Control (VERIFIED) ────────────────────────────────────
|
||||
|
||||
/** Accept the current agent step */
|
||||
ACCEPT_AGENT_STEP: 'antigravity.agent.acceptAgentStep',
|
||||
/** Reject the current agent step */
|
||||
REJECT_AGENT_STEP: 'antigravity.agent.rejectAgentStep',
|
||||
/** Accept a pending command */
|
||||
COMMAND_ACCEPT: 'antigravity.command.accept',
|
||||
/** Reject a pending command */
|
||||
COMMAND_REJECT: 'antigravity.command.reject',
|
||||
/** Accept a terminal command */
|
||||
TERMINAL_ACCEPT: 'antigravity.terminalCommand.accept',
|
||||
/** Reject a terminal command */
|
||||
TERMINAL_REJECT: 'antigravity.terminalCommand.reject',
|
||||
/** Run a terminal command */
|
||||
TERMINAL_RUN: 'antigravity.terminalCommand.run',
|
||||
/** Open new conversation (prioritized) */
|
||||
OPEN_NEW_CONVERSATION: 'antigravity.prioritized.chat.openNewConversation',
|
||||
|
||||
// ─── Terminal Integration ─────────────────────────────────────────────
|
||||
|
||||
/** Notify terminal command started */
|
||||
TERMINAL_COMMAND_START: 'antigravity.onManagerTerminalCommandStart',
|
||||
/** Notify terminal command data */
|
||||
TERMINAL_COMMAND_DATA: 'antigravity.onManagerTerminalCommandData',
|
||||
/** Notify terminal command finished */
|
||||
TERMINAL_COMMAND_FINISH: 'antigravity.onManagerTerminalCommandFinish',
|
||||
/** Update last terminal command */
|
||||
UPDATE_TERMINAL_LAST_COMMAND: 'antigravity.updateTerminalLastCommand',
|
||||
/** Notify shell command completion */
|
||||
ON_SHELL_COMPLETION: 'antigravity.onShellCommandCompletion',
|
||||
/** Show managed terminal */
|
||||
SHOW_MANAGED_TERMINAL: 'antigravity.showManagedTerminal',
|
||||
/** Send terminal output to chat */
|
||||
SEND_TERMINAL_TO_CHAT: 'antigravity.sendTerminalToChat',
|
||||
/** Send terminal output to side panel */
|
||||
SEND_TERMINAL_TO_SIDE_PANEL: 'antigravity.sendTerminalToSidePanel',
|
||||
|
||||
// ─── Agent & Mode ─────────────────────────────────────────────────────
|
||||
|
||||
/** Initialize the agent */
|
||||
INITIALIZE_AGENT: 'antigravity.initializeAgent',
|
||||
|
||||
// ─── Conversation Picker & Workspace ──────────────────────────────────
|
||||
|
||||
/** Open conversation workspace picker */
|
||||
OPEN_CONVERSATION_PICKER: 'antigravity.openConversationWorkspaceQuickPick',
|
||||
/** Open conversation picker (alternative) */
|
||||
OPEN_CONV_PICKER_ALT: 'antigravity.openConversationPicker',
|
||||
/** Set working directories */
|
||||
SET_WORKING_DIRS: 'antigravity.setWorkingDirectories',
|
||||
|
||||
// ─── Review & Diff ────────────────────────────────────────────────────
|
||||
|
||||
/** Open review changes view */
|
||||
OPEN_REVIEW_CHANGES: 'antigravity.openReviewChanges',
|
||||
/** Open diff view */
|
||||
OPEN_DIFF_VIEW: 'antigravity.openDiffView',
|
||||
/** Open diff zones */
|
||||
OPEN_DIFF_ZONES: 'antigravity.openDiffZones',
|
||||
/** Close all diff zones */
|
||||
CLOSE_ALL_DIFF_ZONES: 'antigravity.closeAllDiffZones',
|
||||
|
||||
// ─── Rules & Workflows ────────────────────────────────────────────────
|
||||
|
||||
/** Create a new rule */
|
||||
CREATE_RULE: 'antigravity.createRule',
|
||||
/** Create a new workflow */
|
||||
CREATE_WORKFLOW: 'antigravity.createWorkflow',
|
||||
/** Create a global workflow */
|
||||
CREATE_GLOBAL_WORKFLOW: 'antigravity.createGlobalWorkflow',
|
||||
/** Open global rules */
|
||||
OPEN_GLOBAL_RULES: 'antigravity.openGlobalRules',
|
||||
/** Open workspace rules */
|
||||
OPEN_WORKSPACE_RULES: 'antigravity.openWorkspaceRules',
|
||||
|
||||
// ─── Plugins & MCP ────────────────────────────────────────────────────
|
||||
|
||||
/** Open configure plugins page */
|
||||
OPEN_CONFIGURE_PLUGINS: 'antigravity.openConfigurePluginsPage',
|
||||
/** Get Cascade plugin template */
|
||||
GET_PLUGIN_TEMPLATE: 'antigravity.getCascadePluginTemplate',
|
||||
/** Poll MCP server states */
|
||||
POLL_MCP_SERVERS: 'antigravity.pollMcpServerStates',
|
||||
/** Open MCP config file */
|
||||
OPEN_MCP_CONFIG: 'antigravity.openMcpConfigFile',
|
||||
/** Open MCP docs page */
|
||||
OPEN_MCP_DOCS: 'antigravity.openMcpDocsPage',
|
||||
/** Update plugin installation count */
|
||||
UPDATE_PLUGIN_COUNT: 'antigravity.updatePluginInstallationCount',
|
||||
|
||||
// ─── Autocomplete ─────────────────────────────────────────────────────
|
||||
|
||||
/** Enable autocomplete */
|
||||
ENABLE_AUTOCOMPLETE: 'antigravity.enableAutocomplete',
|
||||
/** Disable autocomplete */
|
||||
DISABLE_AUTOCOMPLETE: 'antigravity.disableAutocomplete',
|
||||
/** Accept completion */
|
||||
ACCEPT_COMPLETION: 'antigravity.acceptCompletion',
|
||||
/** Force supercomplete */
|
||||
FORCE_SUPERCOMPLETE: 'antigravity.forceSupercomplete',
|
||||
/** Snooze autocomplete temporarily */
|
||||
SNOOZE_AUTOCOMPLETE: 'antigravity.snoozeAutocomplete',
|
||||
/** Cancel snooze */
|
||||
CANCEL_SNOOZE: 'antigravity.cancelSnoozeAutocomplete',
|
||||
|
||||
// ─── Auth & Account ───────────────────────────────────────────────────
|
||||
|
||||
/** Login to Antigravity */
|
||||
LOGIN: 'antigravity.login',
|
||||
/** Cancel login */
|
||||
CANCEL_LOGIN: 'antigravity.cancelLogin',
|
||||
/** Handle auth refresh */
|
||||
HANDLE_AUTH_REFRESH: 'antigravity.handleAuthRefresh',
|
||||
/** Sign in to Antigravity */
|
||||
SIGN_IN: 'antigravity.SignInToAntigravity',
|
||||
|
||||
// ─── Diagnostics & Debug ──────────────────────────────────────────────
|
||||
|
||||
/** Get diagnostics info */
|
||||
GET_DIAGNOSTICS: 'antigravity.getDiagnostics',
|
||||
/** Download diagnostics bundle */
|
||||
DOWNLOAD_DIAGNOSTICS: 'antigravity.downloadDiagnostics',
|
||||
/** Capture traces */
|
||||
CAPTURE_TRACES: 'antigravity.captureTraces',
|
||||
/** Enable tracing */
|
||||
ENABLE_TRACING: 'antigravity.enableTracing',
|
||||
/** Clear and disable tracing */
|
||||
CLEAR_TRACING: 'antigravity.clearAndDisableTracing',
|
||||
/** Get manager trace */
|
||||
GET_MANAGER_TRACE: 'antigravity.getManagerTrace',
|
||||
/** Get workbench trace */
|
||||
GET_WORKBENCH_TRACE: 'antigravity.getWorkbenchTrace',
|
||||
/** Toggle debug info widget */
|
||||
TOGGLE_DEBUG_INFO: 'antigravity.toggleDebugInfoWidget',
|
||||
/** Open troubleshooting */
|
||||
OPEN_TROUBLESHOOTING: 'antigravity.openTroubleshooting',
|
||||
/** Open issue reporter */
|
||||
OPEN_ISSUE_REPORTER: 'antigravity.openIssueReporter',
|
||||
|
||||
// ─── Language Server ──────────────────────────────────────────────────
|
||||
|
||||
/** Restart the language server */
|
||||
RESTART_LANGUAGE_SERVER: 'antigravity.restartLanguageServer',
|
||||
/** Kill language server and reload window */
|
||||
KILL_LS_AND_RELOAD: 'antigravity.killLanguageServerAndReloadWindow',
|
||||
|
||||
// ─── Git & Commit ─────────────────────────────────────────────────────
|
||||
|
||||
/** Generate commit message via AI */
|
||||
GENERATE_COMMIT_MESSAGE: 'antigravity.generateCommitMessage',
|
||||
/** Cancel commit message generation */
|
||||
CANCEL_COMMIT_MESSAGE: 'antigravity.cancelGenerateCommitMessage',
|
||||
|
||||
// ─── Browser ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Open browser */
|
||||
OPEN_BROWSER: 'antigravity.openBrowser',
|
||||
/** Get browser onboarding port (returns number, e.g. 57401) */
|
||||
GET_BROWSER_PORT: 'antigravity.getBrowserOnboardingPort',
|
||||
|
||||
// ─── Settings & Import ────────────────────────────────────────────────
|
||||
|
||||
/** Open quick settings panel */
|
||||
OPEN_QUICK_SETTINGS: 'antigravity.openQuickSettingsPanel',
|
||||
/** Open customizations tab */
|
||||
OPEN_CUSTOMIZATIONS: 'antigravity.openCustomizationsTab',
|
||||
/** Import VS Code settings */
|
||||
IMPORT_VSCODE_SETTINGS: 'antigravity.importVSCodeSettings',
|
||||
/** Import VS Code extensions */
|
||||
IMPORT_VSCODE_EXTENSIONS: 'antigravity.importVSCodeExtensions',
|
||||
/** Import Cursor settings */
|
||||
IMPORT_CURSOR_SETTINGS: 'antigravity.importCursorSettings',
|
||||
/** Import Cursor extensions */
|
||||
IMPORT_CURSOR_EXTENSIONS: 'antigravity.importCursorExtensions',
|
||||
|
||||
// ─── Misc ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Reload window */
|
||||
RELOAD_WINDOW: 'antigravity.reloadWindow',
|
||||
/** Open documentation */
|
||||
OPEN_DOCS: 'antigravity.openDocs',
|
||||
/** Open changelog */
|
||||
OPEN_CHANGELOG: 'antigravity.openChangeLog',
|
||||
/** Explain and fix problem (from diagnostics) */
|
||||
EXPLAIN_AND_FIX: 'antigravity.explainAndFixProblem',
|
||||
/** Open a URL */
|
||||
OPEN_URL: 'antigravity.openGenericUrl',
|
||||
/** Editor mode settings */
|
||||
EDITOR_MODE_SETTINGS: 'antigravity.editorModeSettings',
|
||||
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Bridges between the SDK and Antigravity's command system.
|
||||
*
|
||||
* All interactions with Antigravity go through registered VS Code commands,
|
||||
* ensuring we never bypass the official extension API.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const bridge = new CommandBridge();
|
||||
*
|
||||
* // Open the agent panel
|
||||
* await bridge.execute(AntigravityCommands.OPEN_AGENT_PANEL);
|
||||
*
|
||||
* // Start a new conversation
|
||||
* await bridge.execute(AntigravityCommands.START_NEW_CONVERSATION);
|
||||
*
|
||||
* // Send a prompt
|
||||
* await bridge.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, 'Hello!');
|
||||
* ```
|
||||
*/
|
||||
export class CommandBridge implements IDisposable {
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* Execute an Antigravity command.
|
||||
*
|
||||
* @param command - The command ID to execute
|
||||
* @param args - Arguments to pass to the command
|
||||
* @returns The command's return value
|
||||
* @throws {CommandExecutionError} If the command fails
|
||||
*/
|
||||
async execute<T = unknown>(command: string, ...args: unknown[]): Promise<T> {
|
||||
if (this._disposed) {
|
||||
throw new CommandExecutionError(command, 'CommandBridge has been disposed');
|
||||
}
|
||||
|
||||
log.debug(`Executing: ${command}`, args.length > 0 ? args : '');
|
||||
|
||||
try {
|
||||
const result = await vscode.commands.executeCommand<T>(command, ...args);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Command failed: ${command}`, message);
|
||||
throw new CommandExecutionError(command, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command is registered and available.
|
||||
*
|
||||
* @param command - Command ID to check
|
||||
* @returns true if the command exists
|
||||
*/
|
||||
async isAvailable(command: string): Promise<boolean> {
|
||||
const commands = await vscode.commands.getCommands(true);
|
||||
return commands.includes(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered Antigravity commands.
|
||||
*
|
||||
* @returns List of command IDs starting with 'antigravity.'
|
||||
*/
|
||||
async getAntigravityCommands(): Promise<string[]> {
|
||||
const commands = await vscode.commands.getCommands(true);
|
||||
return commands.filter((cmd) => cmd.startsWith('antigravity.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a command handler.
|
||||
*
|
||||
* @param command - Command ID to register
|
||||
* @param handler - Function to handle the command
|
||||
* @returns Disposable to unregister the command
|
||||
*/
|
||||
register(command: string, handler: (...args: unknown[]) => unknown): IDisposable {
|
||||
return vscode.commands.registerCommand(command, handler);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/**
|
||||
* Event Monitor — polls state.vscdb and getDiagnostics for changes.
|
||||
*
|
||||
* Detects:
|
||||
* - USS key changes (trajectory summaries, preferences, etc.)
|
||||
* - Step count changes per session (via getDiagnostics.recentTrajectories)
|
||||
* - Active session switches
|
||||
* - New conversations
|
||||
*
|
||||
* @module transport/event-monitor
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { IDisposable, DisposableStore } from '../core/disposable';
|
||||
import { EventEmitter, Event } from '../core/events';
|
||||
import { Logger } from '../core/logger';
|
||||
import { StateBridge, USSKeys } from './state-bridge';
|
||||
|
||||
const log = new Logger('EventMonitor');
|
||||
|
||||
/**
|
||||
* USS key change event.
|
||||
*/
|
||||
export interface IStateChange {
|
||||
/** Which USS key changed */
|
||||
readonly key: string;
|
||||
/** New data size */
|
||||
readonly newSize: number;
|
||||
/** Previous data size */
|
||||
readonly previousSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step count change event — fired when the agent adds/processes steps.
|
||||
*/
|
||||
export interface IStepCountChange {
|
||||
/** Conversation UUID (googleAgentId) */
|
||||
readonly sessionId: string;
|
||||
/** Conversation title */
|
||||
readonly title: string;
|
||||
/** Previous step count */
|
||||
readonly previousCount: number;
|
||||
/** New step count */
|
||||
readonly newCount: number;
|
||||
/** Number of new steps added */
|
||||
readonly delta: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active session change event.
|
||||
*/
|
||||
export interface IActiveSessionChange {
|
||||
/** New active session ID */
|
||||
readonly sessionId: string;
|
||||
/** New active session title */
|
||||
readonly title: string;
|
||||
/** Previous active session ID (empty if first detection) */
|
||||
readonly previousSessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot of a trajectory from getDiagnostics.
|
||||
*/
|
||||
interface ITrajectorySnapshot {
|
||||
id: string;
|
||||
title: string;
|
||||
stepCount: number;
|
||||
lastModified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors Antigravity state for changes.
|
||||
*
|
||||
* Two polling modes:
|
||||
* 1. **USS polling** — watches state.vscdb keys for size changes (lightweight)
|
||||
* 2. **Trajectory polling** — watches getDiagnostics for step count changes (heavier, optional)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const monitor = new EventMonitor(stateBridge);
|
||||
*
|
||||
* // React to step changes (agent is working)
|
||||
* monitor.onStepCountChanged((e) => {
|
||||
* console.log(`${e.title}: +${e.delta} steps (now ${e.newCount})`);
|
||||
* });
|
||||
*
|
||||
* // React to conversation switches
|
||||
* monitor.onActiveSessionChanged((e) => {
|
||||
* console.log(`Switched to: ${e.title}`);
|
||||
* });
|
||||
*
|
||||
* monitor.start(3000);
|
||||
* ```
|
||||
*/
|
||||
export class EventMonitor implements IDisposable {
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private _ussTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _trajTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _ussSnapshots = new Map<string, number>();
|
||||
private _trajSnapshots = new Map<string, ITrajectorySnapshot>();
|
||||
private _activeSessionId = '';
|
||||
private _running = false;
|
||||
|
||||
// ─── USS Events ─────────────────────────────────────────────────────
|
||||
|
||||
private readonly _onStateChanged = this._disposables.add(new EventEmitter<IStateChange>());
|
||||
/** Fires when any monitored USS key changes size */
|
||||
public readonly onStateChanged: Event<IStateChange> = this._onStateChanged.event;
|
||||
|
||||
private readonly _onNewConversation = this._disposables.add(new EventEmitter<void>());
|
||||
/** Fires when trajectory summaries grow (new conversation likely) */
|
||||
public readonly onNewConversation: Event<void> = this._onNewConversation.event;
|
||||
|
||||
// ─── Trajectory Events ──────────────────────────────────────────────
|
||||
|
||||
private readonly _onStepCountChanged = this._disposables.add(new EventEmitter<IStepCountChange>());
|
||||
/** Fires when a session's step count changes (agent made progress) */
|
||||
public readonly onStepCountChanged: Event<IStepCountChange> = this._onStepCountChanged.event;
|
||||
|
||||
private readonly _onActiveSessionChanged = this._disposables.add(new EventEmitter<IActiveSessionChange>());
|
||||
/** Fires when the active (most recent) session changes */
|
||||
public readonly onActiveSessionChanged: Event<IActiveSessionChange> = this._onActiveSessionChanged.event;
|
||||
|
||||
/** Keys we monitor for USS changes */
|
||||
private readonly _watchedKeys = [
|
||||
USSKeys.TRAJECTORY_SUMMARIES,
|
||||
USSKeys.AGENT_PREFERENCES,
|
||||
USSKeys.USER_STATUS,
|
||||
];
|
||||
|
||||
constructor(private readonly _state: StateBridge) { }
|
||||
|
||||
/**
|
||||
* Start polling for state changes.
|
||||
*
|
||||
* @param intervalMs - USS polling interval (default: 3000ms)
|
||||
* @param trajectoryIntervalMs - Trajectory polling interval (default: 5000ms).
|
||||
* Set to 0 to disable trajectory polling (saves CPU).
|
||||
*/
|
||||
start(intervalMs: number = 3000, trajectoryIntervalMs: number = 5000): void {
|
||||
if (this._running) return;
|
||||
|
||||
this._running = true;
|
||||
log.info(`Starting event monitor (USS: ${intervalMs}ms, Traj: ${trajectoryIntervalMs}ms)`);
|
||||
|
||||
// Initial USS snapshot
|
||||
this._takeUSSSnapshot().catch(() => { });
|
||||
|
||||
// USS polling
|
||||
this._ussTimer = setInterval(async () => {
|
||||
try {
|
||||
await this._pollUSS();
|
||||
} catch (error) {
|
||||
log.error('USS poll error', error);
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
// Trajectory polling (optional, heavier)
|
||||
if (trajectoryIntervalMs > 0) {
|
||||
this._pollTrajectories().catch(() => { });
|
||||
|
||||
this._trajTimer = setInterval(async () => {
|
||||
try {
|
||||
await this._pollTrajectories();
|
||||
} catch (error) {
|
||||
log.error('Trajectory poll error', error);
|
||||
}
|
||||
}, trajectoryIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop polling.
|
||||
*/
|
||||
stop(): void {
|
||||
if (this._ussTimer) {
|
||||
clearInterval(this._ussTimer);
|
||||
this._ussTimer = null;
|
||||
}
|
||||
if (this._trajTimer) {
|
||||
clearInterval(this._trajTimer);
|
||||
this._trajTimer = null;
|
||||
}
|
||||
this._running = false;
|
||||
log.info('Event monitor stopped');
|
||||
}
|
||||
|
||||
/** Check if the monitor is currently running. */
|
||||
get isRunning(): boolean {
|
||||
return this._running;
|
||||
}
|
||||
|
||||
/** Get the currently active session ID. */
|
||||
get activeSessionId(): string {
|
||||
return this._activeSessionId;
|
||||
}
|
||||
|
||||
// ─── USS Polling ────────────────────────────────────────────────────
|
||||
|
||||
private async _takeUSSSnapshot(): Promise<void> {
|
||||
for (const key of this._watchedKeys) {
|
||||
try {
|
||||
const value = await this._state.getRawValue(key);
|
||||
this._ussSnapshots.set(key, value ? value.length : 0);
|
||||
} catch {
|
||||
this._ussSnapshots.set(key, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _pollUSS(): Promise<void> {
|
||||
for (const key of this._watchedKeys) {
|
||||
try {
|
||||
const value = await this._state.getRawValue(key);
|
||||
const newSize = value ? value.length : 0;
|
||||
const previousSize = this._ussSnapshots.get(key) ?? 0;
|
||||
|
||||
if (newSize !== previousSize) {
|
||||
log.debug(`USS change: ${key} (${previousSize} -> ${newSize})`);
|
||||
this._ussSnapshots.set(key, newSize);
|
||||
this._onStateChanged.fire({ key, newSize, previousSize });
|
||||
|
||||
if (key === USSKeys.TRAJECTORY_SUMMARIES && newSize > previousSize) {
|
||||
this._onNewConversation.fire();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip errors during polling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Trajectory Polling ─────────────────────────────────────────────
|
||||
|
||||
private async _pollTrajectories(): Promise<void> {
|
||||
let trajectories: Array<{
|
||||
googleAgentId: string;
|
||||
trajectoryId: string;
|
||||
summary: string;
|
||||
lastStepIndex: number;
|
||||
lastModifiedTime: string;
|
||||
}>;
|
||||
|
||||
try {
|
||||
const raw = await vscode.commands.executeCommand<string>('antigravity.getDiagnostics');
|
||||
if (!raw || typeof raw !== 'string') return;
|
||||
const diag = JSON.parse(raw);
|
||||
if (!Array.isArray(diag.recentTrajectories)) return;
|
||||
trajectories = diag.recentTrajectories;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for step count changes in each trajectory
|
||||
for (const traj of trajectories) {
|
||||
const id = traj.googleAgentId;
|
||||
if (!id) continue;
|
||||
|
||||
const prev = this._trajSnapshots.get(id);
|
||||
const newCount = traj.lastStepIndex ?? 0;
|
||||
|
||||
if (prev && prev.stepCount !== newCount) {
|
||||
const delta = newCount - prev.stepCount;
|
||||
log.debug(`Step change: "${traj.summary}" ${prev.stepCount} -> ${newCount} (+${delta})`);
|
||||
|
||||
this._onStepCountChanged.fire({
|
||||
sessionId: id,
|
||||
title: traj.summary ?? 'Untitled',
|
||||
previousCount: prev.stepCount,
|
||||
newCount,
|
||||
delta,
|
||||
});
|
||||
}
|
||||
|
||||
this._trajSnapshots.set(id, {
|
||||
id,
|
||||
title: traj.summary ?? 'Untitled',
|
||||
stepCount: newCount,
|
||||
lastModified: traj.lastModifiedTime ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
// Check for active session change (first entry = most recent)
|
||||
if (trajectories.length > 0) {
|
||||
const newActiveId = trajectories[0].googleAgentId;
|
||||
if (newActiveId && newActiveId !== this._activeSessionId) {
|
||||
const previousId = this._activeSessionId;
|
||||
this._activeSessionId = newActiveId;
|
||||
|
||||
// Only fire event after initial snapshot (not on first detection)
|
||||
if (previousId !== '') {
|
||||
log.debug(`Active session changed: "${trajectories[0].summary}"`);
|
||||
this._onActiveSessionChanged.fire({
|
||||
sessionId: newActiveId,
|
||||
title: trajectories[0].summary ?? 'Untitled',
|
||||
previousSessionId: previousId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Transport module re-exports.
|
||||
* @module transport
|
||||
*/
|
||||
|
||||
export { CommandBridge, AntigravityCommands } from './command-bridge';
|
||||
export { StateBridge, USSKeys } from './state-bridge';
|
||||
export { EventMonitor, type IStateChange } from './event-monitor';
|
||||
export { LSBridge, Models, type ModelId, type IHeadlessCascadeOptions, type ISendMessageOptions } from './ls-bridge';
|
||||
@@ -1,725 +0,0 @@
|
||||
/**
|
||||
* Language Server Bridge — Direct ConnectRPC calls to the local LS.
|
||||
*
|
||||
* UPDATED 2026-03-01 (v1.3.0):
|
||||
* Fixed CSRF token authentication (Issue #1).
|
||||
* The LS binary is launched with --csrf_token as a CLI argument.
|
||||
* Previous versions did not send this token, causing 401 "missing CSRF token".
|
||||
*
|
||||
* Discovery strategy (multi-layer):
|
||||
* 1. Process CLI args — extract --port and --csrf_token from LS process
|
||||
* 2. getDiagnostics console logs — fallback for port discovery
|
||||
* 3. Manual override — setConnection(port, csrfToken)
|
||||
*
|
||||
* Service: exa.language_server_pb.LanguageServerService
|
||||
* Protocol: HTTPS POST with JSON body + x-csrf-token header
|
||||
*
|
||||
* @module transport/ls-bridge
|
||||
*/
|
||||
|
||||
import { Logger } from '../core/logger';
|
||||
|
||||
const log = new Logger('LSBridge');
|
||||
|
||||
/** Known model IDs (verified 2026-02-28) */
|
||||
export const Models = {
|
||||
GEMINI_FLASH: 1018,
|
||||
GEMINI_PRO_LOW: 1164,
|
||||
GEMINI_PRO_HIGH: 1165,
|
||||
CLAUDE_SONNET: 1163,
|
||||
CLAUDE_OPUS: 1154,
|
||||
GPT_OSS: 342,
|
||||
} as const;
|
||||
|
||||
export type ModelId = typeof Models[keyof typeof Models] | number;
|
||||
|
||||
/** Options for creating a headless cascade */
|
||||
export interface IHeadlessCascadeOptions {
|
||||
/** Text prompt to send */
|
||||
text: string;
|
||||
/** Model ID (default: Gemini 3 Flash = 1018) */
|
||||
model?: ModelId;
|
||||
/** Planner type: 'conversational' (default) or 'normal' */
|
||||
plannerType?: 'conversational' | 'normal';
|
||||
}
|
||||
|
||||
/** Options for sending a message to existing cascade */
|
||||
export interface ISendMessageOptions {
|
||||
/** Target cascade ID */
|
||||
cascadeId: string;
|
||||
/** Text to send */
|
||||
text: string;
|
||||
/** Model ID (default: Gemini 3 Flash = 1018) */
|
||||
model?: ModelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversation annotation fields (from jetski_cortex.proto ConversationAnnotations).
|
||||
*
|
||||
* These are metadata annotations on a conversation that the user can set.
|
||||
* The LS stores these natively and they persist across sessions.
|
||||
*/
|
||||
export interface IConversationAnnotations {
|
||||
/** Custom user title -- overrides the auto-generated summary */
|
||||
title?: string;
|
||||
/** Tags/labels for organization */
|
||||
tags?: string[];
|
||||
/** Whether this conversation is archived */
|
||||
archived?: boolean;
|
||||
/** Whether this conversation is starred (pinned) */
|
||||
starred?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct bridge to the Language Server via ConnectRPC.
|
||||
*
|
||||
* Discovers the LS port and CSRF token from the LS process CLI args,
|
||||
* then makes authenticated HTTPS POST calls to the LS endpoints.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const ls = new LSBridge(commandBridge);
|
||||
* await ls.initialize();
|
||||
*
|
||||
* // Create a headless cascade
|
||||
* const cascadeId = await ls.createCascade({
|
||||
* text: 'Analyze test coverage',
|
||||
* model: Models.GEMINI_FLASH,
|
||||
* });
|
||||
*
|
||||
* // Send follow-up
|
||||
* await ls.sendMessage({ cascadeId, text: 'Focus on edge cases' });
|
||||
*
|
||||
* // Switch UI to it
|
||||
* await ls.focusCascade(cascadeId);
|
||||
* ```
|
||||
*/
|
||||
export class LSBridge {
|
||||
private _port: number | null = null;
|
||||
private _csrfToken: string | null = null;
|
||||
private _useTls: boolean = false;
|
||||
private _executeCommand: <T = any>(command: string, ...args: any[]) => Promise<T>;
|
||||
|
||||
constructor(executeCommand: <T = any>(command: string, ...args: any[]) => Promise<T>) {
|
||||
this._executeCommand = executeCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover the Language Server port and CSRF token.
|
||||
* Must be called before other methods.
|
||||
*
|
||||
* Discovery chain:
|
||||
* 1. Parse LS process CLI arguments (--port, --csrf_token)
|
||||
* 2. Fallback: getDiagnostics console logs (port only)
|
||||
* 3. Manual: call setConnection() after initialize() returns false
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
// Strategy 1: discover from LS process CLI args (port + CSRF)
|
||||
const fromProcess = await this._discoverFromProcess();
|
||||
if (fromProcess) {
|
||||
this._port = fromProcess.port;
|
||||
this._csrfToken = fromProcess.csrfToken;
|
||||
this._useTls = fromProcess.useTls;
|
||||
log.info(`LS discovered from process: port=${this._port}, tls=${this._useTls}, csrf=${this._csrfToken ? 'found' : 'missing'}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Strategy 2: fallback to getDiagnostics logs (port only, no CSRF)
|
||||
this._port = await this._discoverPortFromDiagnostics();
|
||||
if (this._port) {
|
||||
log.warn(`LS port from diagnostics: ${this._port}, but CSRF token not found — RPC calls may fail with 401`);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn('Could not discover LS connection. Use setConnection(port, csrfToken) manually.');
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether the bridge is ready (port discovered) */
|
||||
get isReady(): boolean {
|
||||
return this._port !== null;
|
||||
}
|
||||
|
||||
/** The discovered LS port */
|
||||
get port(): number | null {
|
||||
return this._port;
|
||||
}
|
||||
|
||||
/** Whether CSRF token is available */
|
||||
get hasCsrfToken(): boolean {
|
||||
return this._csrfToken !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually set the LS connection parameters.
|
||||
*
|
||||
* Use this when auto-discovery fails (e.g., non-standard install,
|
||||
* or you've discovered the port/token through other means like `lsof`).
|
||||
*
|
||||
* @param port - LS port number
|
||||
* @param csrfToken - CSRF token from LS process CLI args
|
||||
* @param useTls - Whether to use HTTPS (default: false, extension_server uses HTTP)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const ls = new LSBridge(commandBridge);
|
||||
* const ok = await ls.initialize();
|
||||
* if (!ok) {
|
||||
* // Manual fallback: get port and csrf from your own discovery
|
||||
* ls.setConnection(54321, 'abc123-csrf-token');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
setConnection(port: number, csrfToken: string, useTls: boolean = false): void {
|
||||
this._port = port;
|
||||
this._csrfToken = csrfToken;
|
||||
this._useTls = useTls;
|
||||
log.info(`LS connection set manually: port=${port}, tls=${useTls}, csrf=${csrfToken ? 'provided' : 'empty'}`);
|
||||
}
|
||||
|
||||
// ─── Headless Cascade API ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a new cascade and optionally send a message.
|
||||
* Fully headless — no UI panel opened, no conversation switched.
|
||||
*
|
||||
* @returns cascadeId or null on failure
|
||||
*/
|
||||
async createCascade(options: IHeadlessCascadeOptions): Promise<string | null> {
|
||||
this._ensureReady();
|
||||
|
||||
// Step 1: StartCascade
|
||||
const startResp = await this._rpc('StartCascade', { source: 0 });
|
||||
const cascadeId = startResp?.cascadeId;
|
||||
if (!cascadeId) {
|
||||
log.error('StartCascade returned no cascadeId');
|
||||
return null;
|
||||
}
|
||||
log.info(`Cascade created: ${cascadeId}`);
|
||||
|
||||
// Step 2: SendUserCascadeMessage
|
||||
if (options.text) {
|
||||
await this._sendMessage(cascadeId, options.text, options.model, options.plannerType);
|
||||
log.info(`Message sent to: ${cascadeId}`);
|
||||
}
|
||||
|
||||
return cascadeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to an existing cascade.
|
||||
*
|
||||
* @returns true if sent successfully
|
||||
*/
|
||||
async sendMessage(options: ISendMessageOptions): Promise<boolean> {
|
||||
this._ensureReady();
|
||||
await this._sendMessage(options.cascadeId, options.text, options.model);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the UI to show a specific cascade conversation.
|
||||
*/
|
||||
async focusCascade(cascadeId: string): Promise<void> {
|
||||
this._ensureReady();
|
||||
await this._rpc('SmartFocusConversation', { cascadeId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running cascade invocation.
|
||||
*/
|
||||
async cancelCascade(cascadeId: string): Promise<void> {
|
||||
this._ensureReady();
|
||||
await this._rpc('CancelCascadeInvocation', { cascadeId });
|
||||
}
|
||||
|
||||
// ─── Conversation Annotations API ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Native conversation annotations (verified from jetski_cortex.proto).
|
||||
*
|
||||
* ConversationAnnotations protobuf fields:
|
||||
* - title (string) — custom user title, overrides auto-summary
|
||||
* - tags (string[]) — tags/labels
|
||||
* - archived (bool) — archive status
|
||||
* - starred (bool) — pinned/starred
|
||||
* - last_user_view_time (Timestamp)
|
||||
*
|
||||
* @param cascadeId - Conversation ID
|
||||
* @param annotations - Partial annotation fields to set
|
||||
* @param merge - If true, merge with existing annotations (default: true)
|
||||
*/
|
||||
async updateAnnotations(
|
||||
cascadeId: string,
|
||||
annotations: IConversationAnnotations,
|
||||
merge: boolean = true,
|
||||
): Promise<void> {
|
||||
this._ensureReady();
|
||||
|
||||
// Convert camelCase to snake_case for protobuf
|
||||
const proto: Record<string, any> = {};
|
||||
if (annotations.title !== undefined) proto.title = annotations.title;
|
||||
if (annotations.starred !== undefined) proto.starred = annotations.starred;
|
||||
if (annotations.archived !== undefined) proto.archived = annotations.archived;
|
||||
if (annotations.tags !== undefined) proto.tags = annotations.tags;
|
||||
|
||||
await this._rpc('UpdateConversationAnnotations', {
|
||||
cascadeId,
|
||||
annotations: proto,
|
||||
mergeAnnotations: merge,
|
||||
});
|
||||
log.info(`Annotations updated for ${cascadeId.substring(0, 8)}...`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom title for a conversation.
|
||||
*
|
||||
* This sets the `title` field in ConversationAnnotations.
|
||||
* When set, this title should be displayed instead of the
|
||||
* auto-generated `summary` from the LLM.
|
||||
*
|
||||
* @param cascadeId - Conversation ID
|
||||
* @param title - Custom title to set
|
||||
*/
|
||||
async setTitle(cascadeId: string, title: string): Promise<void> {
|
||||
await this.updateAnnotations(cascadeId, { title });
|
||||
}
|
||||
|
||||
/**
|
||||
* Star (pin) or unstar a conversation.
|
||||
*
|
||||
* This sets the `starred` field in ConversationAnnotations.
|
||||
*
|
||||
* @param cascadeId - Conversation ID
|
||||
* @param starred - true to star, false to unstar
|
||||
*/
|
||||
async setStar(cascadeId: string, starred: boolean): Promise<void> {
|
||||
await this.updateAnnotations(cascadeId, { starred });
|
||||
}
|
||||
|
||||
// ─── Conversation Read API ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get details of a specific conversation.
|
||||
*/
|
||||
async getConversation(cascadeId: string): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc('GetConversation', { cascadeId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cascade trajectories (conversation list).
|
||||
*/
|
||||
async listCascades(): Promise<any> {
|
||||
this._ensureReady();
|
||||
const resp = await this._rpc('GetAllCascadeTrajectories', {});
|
||||
return resp?.trajectorySummaries ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trajectory descriptions (lighter than full trajectories).
|
||||
* Returns { trajectories: [...] }.
|
||||
*/
|
||||
async getTrajectoryDescriptions(): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc('GetUserTrajectoryDescriptions', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user status (tier, models, etc.)
|
||||
*/
|
||||
async getUserStatus(): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc('GetUserStatus', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a raw RPC call to any LS method.
|
||||
* @param method - RPC method name (e.g. 'StartCascade')
|
||||
* @param payload - JSON payload
|
||||
*/
|
||||
async rawRPC(method: string, payload: any): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc(method, payload);
|
||||
}
|
||||
|
||||
// ─── Internal ────────────────────────────────────────────────────
|
||||
|
||||
private _ensureReady(): void {
|
||||
if (!this._port) {
|
||||
throw new Error('LSBridge not initialized. Call initialize() first.');
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendMessage(
|
||||
cascadeId: string,
|
||||
text: string,
|
||||
model?: ModelId,
|
||||
plannerType?: string,
|
||||
): Promise<void> {
|
||||
const payload: any = {
|
||||
cascadeId,
|
||||
items: [{ chunk: { case: 'text', value: text } }],
|
||||
cascadeConfig: {
|
||||
plannerConfig: {
|
||||
plannerTypeConfig: {
|
||||
case: plannerType || 'conversational',
|
||||
value: {},
|
||||
},
|
||||
requestedModel: {
|
||||
choice: { case: 'model', value: model || Models.GEMINI_FLASH },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await this._rpc('SendUserCascadeMessage', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover LS port and CSRF token from the Language Server process.
|
||||
*
|
||||
* VERIFIED 2026-03-01 from Antigravity extension.js source:
|
||||
*
|
||||
* 1. CSRF header is "x-codeium-csrf-token" (NOT x-csrf-token)
|
||||
* 2. CSRF value is --csrf_token from CLI (NOT --extension_server_csrf_token)
|
||||
* 3. ConnectRPC endpoint is on httpsPort (HTTPS) or httpPort (HTTP)
|
||||
* These ports are NOT in CLI args (--random_port flag means random).
|
||||
* We discover them via netstat/PID, excluding extension_server_port.
|
||||
*
|
||||
* Source code proof:
|
||||
* n.header.set("x-codeium-csrf-token", e) // header name
|
||||
* address = `127.0.0.1:${te.httpsPort}` // ConnectRPC address
|
||||
* csrfToken = a = d.randomUUID() → --csrf_token // token source
|
||||
* t.headers["x-codeium-csrf-token"] === this.csrfToken ? ... : 403
|
||||
*
|
||||
* Discovery: 2 phases
|
||||
* Phase 1: Get-CimInstance/ps → PID, --csrf_token, --extension_server_port
|
||||
* Phase 2: netstat → find LISTENING ports for PID, exclude ext_server_port
|
||||
*/
|
||||
private async _discoverFromProcess(): Promise<{ port: number; csrfToken: string; useTls: boolean } | null> {
|
||||
try {
|
||||
const platform = process.platform;
|
||||
|
||||
// Phase 1: find LS process, extract PID, csrf_token, extension_server_port
|
||||
let processInfo = await this._findLSProcess(platform);
|
||||
if (!processInfo) {
|
||||
log.debug('No LS processes found');
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug(`LS process found: PID=${processInfo.pid}, csrf=present, ext_port=${processInfo.extPort}`);
|
||||
|
||||
// Phase 2: find actual ConnectRPC port via netstat
|
||||
const connectPort = await this._findConnectPort(platform, processInfo.pid, processInfo.extPort);
|
||||
if (!connectPort) {
|
||||
log.debug('Could not find ConnectRPC port via netstat, trying extension_server_port as fallback');
|
||||
// Fallback: try extension_server_port with HTTP
|
||||
if (processInfo.extPort) {
|
||||
return { port: processInfo.extPort, csrfToken: processInfo.csrfToken, useTls: false };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
port: connectPort.port,
|
||||
csrfToken: processInfo.csrfToken,
|
||||
useTls: connectPort.tls,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
log.debug('Process discovery failed', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1: Find the LS process for this workspace.
|
||||
*/
|
||||
private async _findLSProcess(
|
||||
platform: string,
|
||||
): Promise<{ pid: number; csrfToken: string; extPort: number } | null> {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
let output: string;
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Use -EncodedCommand to avoid all PowerShell escaping issues with $_ and quotes
|
||||
const psScript = "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'language_server' -and $_.CommandLine -match 'csrf_token' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }";
|
||||
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
|
||||
const result = await execAsync(
|
||||
`powershell.exe -NoProfile -EncodedCommand ${encoded}`,
|
||||
{ encoding: 'utf8', timeout: 10000, windowsHide: true },
|
||||
);
|
||||
output = result.stdout;
|
||||
} else {
|
||||
const result = await execAsync(
|
||||
'ps -eo pid,args 2>/dev/null | grep language_server | grep csrf_token | grep -v grep',
|
||||
{ encoding: 'utf8', timeout: 5000 },
|
||||
);
|
||||
output = result.stdout;
|
||||
}
|
||||
|
||||
const lines = output.split('\n').filter((l: string) => l.trim().length > 0);
|
||||
if (lines.length === 0) return null;
|
||||
|
||||
const workspaceHint = this._getWorkspaceHint();
|
||||
let bestLine: string | null = null;
|
||||
|
||||
if (workspaceHint) {
|
||||
for (const line of lines) {
|
||||
if (line.includes(workspaceHint)) {
|
||||
bestLine = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bestLine) bestLine = lines[0];
|
||||
|
||||
// Extract PID (first field before | on Windows, first token on Unix)
|
||||
let pid: number;
|
||||
if (platform === 'win32') {
|
||||
pid = parseInt(bestLine.split('|')[0].trim(), 10);
|
||||
} else {
|
||||
pid = parseInt(bestLine.trim().split(/\s+/)[0], 10);
|
||||
}
|
||||
|
||||
const csrfToken = this._extractArg(bestLine, 'csrf_token');
|
||||
const extPortStr = this._extractArg(bestLine, 'extension_server_port');
|
||||
const extPort = extPortStr ? parseInt(extPortStr, 10) : 0;
|
||||
|
||||
if (!csrfToken || isNaN(pid)) return null;
|
||||
|
||||
return { pid, csrfToken, extPort };
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Find ConnectRPC port via netstat.
|
||||
*
|
||||
* The LS process listens on multiple ports:
|
||||
* - httpsPort (HTTPS, ConnectRPC) ← this is what we want
|
||||
* - httpPort (HTTP, ConnectRPC) ← also works
|
||||
* - lspPort (LSP JSON-RPC)
|
||||
* - extension_server_port is separate (for Extension Host IPC)
|
||||
*
|
||||
* We find all LISTENING ports for the LS PID, exclude ext_server_port,
|
||||
* then try HTTPS first (preferred), fall back to HTTP.
|
||||
*/
|
||||
private async _findConnectPort(
|
||||
platform: string,
|
||||
pid: number,
|
||||
extPort: number,
|
||||
): Promise<{ port: number; tls: boolean } | null> {
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
let output: string;
|
||||
|
||||
if (platform === 'win32') {
|
||||
const result = await execAsync(
|
||||
`netstat -aon | findstr "LISTENING" | findstr "${pid}"`,
|
||||
{ encoding: 'utf8', timeout: 5000, windowsHide: true },
|
||||
);
|
||||
output = result.stdout;
|
||||
} else {
|
||||
const result = await execAsync(
|
||||
`ss -tlnp 2>/dev/null | grep "pid=${pid}" || netstat -tlnp 2>/dev/null | grep "${pid}"`,
|
||||
{ encoding: 'utf8', timeout: 5000 },
|
||||
);
|
||||
output = result.stdout;
|
||||
}
|
||||
|
||||
// Extract all listening ports for this PID
|
||||
const portMatches = output.matchAll(/127\.0\.0\.1:(\d+)/g);
|
||||
const ports: number[] = [];
|
||||
for (const m of portMatches) {
|
||||
const p = parseInt(m[1], 10);
|
||||
// Exclude extension_server_port
|
||||
if (p !== extPort && !ports.includes(p)) {
|
||||
ports.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (ports.length === 0) return null;
|
||||
|
||||
log.debug(`LS ports (excl ext ${extPort}): ${ports.join(', ')}`);
|
||||
|
||||
// Try to identify httpsPort vs httpPort by probing
|
||||
// Strategy: try HTTPS first on each port (httpsPort is preferred)
|
||||
for (const port of ports) {
|
||||
const tls = await this._probePort(port, true);
|
||||
if (tls) return { port, tls: true };
|
||||
}
|
||||
|
||||
// Fallback: try HTTP
|
||||
for (const port of ports) {
|
||||
const http = await this._probePort(port, false);
|
||||
if (http) return { port, tls: false };
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
log.debug('netstat port discovery failed', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick probe: check if a port accepts ConnectRPC requests.
|
||||
* Returns true if the port responds (even with error) on the given protocol.
|
||||
*/
|
||||
private _probePort(port: number, useTls: boolean): Promise<boolean> {
|
||||
const mod = useTls ? require('https') : require('http');
|
||||
const proto = useTls ? 'https' : 'http';
|
||||
return new Promise((resolve) => {
|
||||
const req = mod.request(`${proto}://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/GetUserStatus`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Content-Length': 2 },
|
||||
rejectUnauthorized: false,
|
||||
timeout: 2000,
|
||||
}, (res: any) => {
|
||||
// 401 = correct endpoint, just missing CSRF (expected)
|
||||
// 200 = also correct (unlikely without CSRF but possible)
|
||||
resolve(res.statusCode === 401 || res.statusCode === 200);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
req.on('timeout', () => { req.destroy(); resolve(false); });
|
||||
req.write('{}');
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a workspace hint string used to match the correct LS process.
|
||||
*
|
||||
* The LS process has --workspace_id like:
|
||||
* file_d_3A_programming_better_antigravity
|
||||
* which is an encoded version of the workspace URI.
|
||||
*/
|
||||
private _getWorkspaceHint(): string {
|
||||
try {
|
||||
const vscode = require('vscode');
|
||||
const folders = vscode.workspace?.workspaceFolders;
|
||||
if (folders && folders.length > 0) {
|
||||
// Convert workspace path to LS workspace_id format
|
||||
// e.g., "d:\programming\better-antigravity" -> "better_antigravity"
|
||||
// (LS uses underscored path segments)
|
||||
const folder = folders[0].uri.fsPath;
|
||||
const parts = folder.replace(/\\/g, '/').split('/');
|
||||
// Use last 2-3 segments for matching
|
||||
return parts.slice(-2).join('_').replace(/[-.\s]/g, '_').toLowerCase();
|
||||
}
|
||||
} catch {
|
||||
// vscode not available (e.g., testing)
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a CLI argument value from a command-line string.
|
||||
* Supports both --key=value and --key value formats.
|
||||
*/
|
||||
private _extractArg(cmdLine: string, argName: string): string | null {
|
||||
// --argName=value
|
||||
const eqMatch = cmdLine.match(new RegExp(`--${argName}=([^\\s"]+)`));
|
||||
if (eqMatch) return eqMatch[1];
|
||||
|
||||
// --argName value
|
||||
const spaceMatch = cmdLine.match(new RegExp(`--${argName}\\s+([^\\s"]+)`));
|
||||
if (spaceMatch) return spaceMatch[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: discover port from getDiagnostics console logs.
|
||||
* NOTE: This does NOT discover the CSRF token.
|
||||
* In recent Antigravity versions, the port URL may no longer appear in logs.
|
||||
*/
|
||||
private async _discoverPortFromDiagnostics(): Promise<number | null> {
|
||||
try {
|
||||
const raw = await this._executeCommand<string>('antigravity.getDiagnostics');
|
||||
if (!raw || typeof raw !== 'string') return null;
|
||||
const diag = JSON.parse(raw);
|
||||
|
||||
const logs: string = diag.agentWindowConsoleLogs || '';
|
||||
|
||||
// Pattern: 127.0.0.1:{port}/exa.language_server_pb
|
||||
const m1 = logs.match(/127\.0\.0\.1:(\d+)\/exa\.language_server_pb/);
|
||||
if (m1) return parseInt(m1[1], 10);
|
||||
|
||||
// Fallback: any 127.0.0.1:{port} in HTTPS context
|
||||
const m2 = logs.match(/https?:\/\/127\.0\.0\.1:(\d+)/);
|
||||
if (m2) return parseInt(m2[1], 10);
|
||||
|
||||
// Check mainThreadLogs for port info
|
||||
if (diag.mainThreadLogs) {
|
||||
const mainLogs = typeof diag.mainThreadLogs === 'string'
|
||||
? diag.mainThreadLogs
|
||||
: JSON.stringify(diag.mainThreadLogs);
|
||||
const m3 = mainLogs.match(/127\.0\.0\.1:(\d+)/);
|
||||
if (m3) return parseInt(m3[1], 10);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to discover LS port from diagnostics', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated RPC call to the Language Server.
|
||||
* Sends x-csrf-token header when available.
|
||||
*
|
||||
* VERIFIED 2026-03-01:
|
||||
* - extension_server_port uses plain HTTP (no TLS)
|
||||
* - Main LS port (--random_port) uses HTTPS with self-signed cert
|
||||
*/
|
||||
private async _rpc(method: string, payload: any): Promise<any> {
|
||||
const httpModule = this._useTls ? require('https') : require('http');
|
||||
const protocol = this._useTls ? 'https' : 'http';
|
||||
const url = `${protocol}://127.0.0.1:${this._port}/exa.language_server_pb.LanguageServerService/${method}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = JSON.stringify(payload);
|
||||
const headers: Record<string, string | number> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
};
|
||||
|
||||
// CSRF header: "x-codeium-csrf-token" (verified from extension.js source)
|
||||
if (this._csrfToken) {
|
||||
headers['x-codeium-csrf-token'] = this._csrfToken;
|
||||
}
|
||||
|
||||
const reqOptions: any = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
};
|
||||
|
||||
// Self-signed TLS when using HTTPS
|
||||
if (this._useTls) {
|
||||
reqOptions.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const req = httpModule.request(url, reqOptions, (res: any) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: string) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch { resolve(data); }
|
||||
} else {
|
||||
const hint = res.statusCode === 401
|
||||
? ' (CSRF token may be invalid or missing -- try setConnection() with the correct token)'
|
||||
: '';
|
||||
reject(new Error(`LS ${method}: ${res.statusCode} -- ${data.substring(0, 200)}${hint}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (err: Error) => reject(err));
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
/**
|
||||
* State Bridge — reads Antigravity's USS state from the SQLite database.
|
||||
*
|
||||
* Antigravity stores settings, conversation metadata, and agent preferences
|
||||
* in `state.vscdb` (SQLite). This bridge provides read-only access to that data.
|
||||
*
|
||||
* VERIFIED against live state.vscdb on 2026-02-28.
|
||||
*
|
||||
* @module transport/state-bridge
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { StateReadError } from '../core/errors';
|
||||
import { Logger } from '../core/logger';
|
||||
import type {
|
||||
IAgentPreferences,
|
||||
TerminalExecutionPolicy,
|
||||
ArtifactReviewPolicy,
|
||||
} from '../core/types';
|
||||
|
||||
const log = new Logger('StateBridge');
|
||||
|
||||
/**
|
||||
* USS (Unified State Sync) keys in state.vscdb.
|
||||
*
|
||||
* VERIFIED: All keys listed below were confirmed to exist
|
||||
* in a live Antigravity v1.107.0 installation on 2026-02-28.
|
||||
* Values are Base64-encoded protobuf unless noted otherwise.
|
||||
*/
|
||||
export const USSKeys = {
|
||||
/** Agent preferences — terminal policy, review policy, secure mode, etc. (1020 bytes) */
|
||||
AGENT_PREFERENCES: 'antigravityUnifiedStateSync.agentPreferences',
|
||||
|
||||
/** Conversation/trajectory summaries — titles, timestamps, workspace URIs (74KB+) */
|
||||
TRAJECTORY_SUMMARIES: 'antigravityUnifiedStateSync.trajectorySummaries',
|
||||
|
||||
/** Agent manager window state (192 bytes) */
|
||||
AGENT_MANAGER_WINDOW: 'antigravityUnifiedStateSync.agentManagerWindow',
|
||||
|
||||
/** Enterprise override store (56 bytes) */
|
||||
OVERRIDE_STORE: 'antigravityUnifiedStateSync.overrideStore',
|
||||
|
||||
/** Model preferences — selected model, sentinel key */
|
||||
MODEL_PREFERENCES: 'antigravityUnifiedStateSync.modelPreferences',
|
||||
|
||||
/** Artifact review state (1204 bytes) */
|
||||
ARTIFACT_REVIEW: 'antigravityUnifiedStateSync.artifactReview',
|
||||
|
||||
/** Browser preferences (380 bytes) */
|
||||
BROWSER_PREFERENCES: 'antigravityUnifiedStateSync.browserPreferences',
|
||||
|
||||
/** Editor preferences (108 bytes) */
|
||||
EDITOR_PREFERENCES: 'antigravityUnifiedStateSync.editorPreferences',
|
||||
|
||||
/** Tab preferences (404 bytes) */
|
||||
TAB_PREFERENCES: 'antigravityUnifiedStateSync.tabPreferences',
|
||||
|
||||
/** Window preferences (44 bytes) */
|
||||
WINDOW_PREFERENCES: 'antigravityUnifiedStateSync.windowPreferences',
|
||||
|
||||
/** Scratch/playground workspaces (268 bytes) */
|
||||
SCRATCH_WORKSPACES: 'antigravityUnifiedStateSync.scratchWorkspaces',
|
||||
|
||||
/** Sidebar workspaces — recent workspace list (5604 bytes) */
|
||||
SIDEBAR_WORKSPACES: 'antigravityUnifiedStateSync.sidebarWorkspaces',
|
||||
|
||||
/** User status info (5196 bytes) */
|
||||
USER_STATUS: 'antigravityUnifiedStateSync.userStatus',
|
||||
|
||||
/** Model credits/usage info */
|
||||
MODEL_CREDITS: 'antigravityUnifiedStateSync.modelCredits',
|
||||
|
||||
/** Onboarding state (140 bytes) */
|
||||
ONBOARDING: 'antigravityUnifiedStateSync.onboarding',
|
||||
|
||||
/** Seen NUX (new user experience) IDs (76 bytes) */
|
||||
SEEN_NUX_IDS: 'antigravityUnifiedStateSync.seenNuxIds',
|
||||
|
||||
// ⚠️ Jetski-specific state (separate sync namespace)
|
||||
/** Agent manager initialization state — contains auth tokens, workspace map (5144 bytes) */
|
||||
AGENT_MANAGER_INIT: 'jetskiStateSync.agentManagerInitState',
|
||||
|
||||
// ⚠️ Non-USS but relevant keys
|
||||
/** All user settings — JSON format */
|
||||
ALL_USER_SETTINGS: 'antigravityUserSettings.allUserSettings',
|
||||
|
||||
/** Allowed model configs for commands */
|
||||
ALLOWED_COMMAND_MODEL_CONFIGS: 'antigravity_allowed_command_model_configs',
|
||||
|
||||
/** Chat session store index (JSON: {"version":1,"entries":{}}) */
|
||||
CHAT_SESSION_INDEX: 'chat.ChatSessionStore.index',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Keys that contain sensitive data and MUST NOT be exposed through the SDK.
|
||||
*
|
||||
* VERIFIED 2026-02-28:
|
||||
* - oauthToken: OAuth access token (732 bytes)
|
||||
* - agentManagerInitState: Contains LIVE ya29.* access token + g1//* refresh token!
|
||||
* - antigravityAuthStatus: Auth status
|
||||
*/
|
||||
const SENSITIVE_KEYS = new Set([
|
||||
'antigravityUnifiedStateSync.oauthToken',
|
||||
'jetskiStateSync.agentManagerInitState',
|
||||
'antigravityAuthStatus',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Protobuf sentinel keys found in agentPreferences.
|
||||
*
|
||||
* ALL 16 sentinel keys verified from live state.vscdb on 2026-02-28.
|
||||
* Each sentinel key string is followed by a small Base64 value encoding
|
||||
* a protobuf varint (the actual preference value).
|
||||
*/
|
||||
const SENTINEL_KEYS = {
|
||||
PLANNING_MODE: 'planningModeSentinelKey',
|
||||
ARTIFACT_REVIEW_POLICY: 'artifactReviewPolicySentinelKey',
|
||||
TERMINAL_AUTO_EXECUTION_POLICY: 'terminalAutoExecutionPolicySentinelKey',
|
||||
TERMINAL_ALLOWED_COMMANDS: 'terminalAllowedCommandsSentinelKey',
|
||||
TERMINAL_DENIED_COMMANDS: 'terminalDeniedCommandsSentinelKey',
|
||||
ALLOW_NON_WORKSPACE_FILES: 'allowAgentAccessNonWorkspaceFilesSentinelKey',
|
||||
ALLOW_GITIGNORE_ACCESS: 'allowCascadeAccessGitignoreFilesSentinelKey',
|
||||
SECURE_MODE: 'secureModeSentinelKey',
|
||||
EXPLAIN_FIX_IN_CONVO: 'explainAndFixInCurrentConversationSentinelKey',
|
||||
AUTO_CONTINUE_ON_MAX: 'autoContinueOnMaxGeneratorInvocationsSentinelKey',
|
||||
DISABLE_AUTO_OPEN_EDITED: 'disableAutoOpenEditedFilesSentinelKey',
|
||||
ENABLE_SOUNDS: 'enableSoundsForSpecialEventsSentinelKey',
|
||||
DISABLE_AUTO_FIX_LINTS: 'disableCascadeAutoFixLintsSentinelKey',
|
||||
ENABLE_SHELL_INTEGRATION: 'enableShellIntegrationSentinelKey',
|
||||
SANDBOX_ALLOW_NETWORK: 'sandboxAllowNetworkSentinelKey',
|
||||
ENABLE_TERMINAL_SANDBOX: 'enableTerminalSandboxSentinelKey',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Reads Antigravity's internal state from the SQLite database.
|
||||
*
|
||||
* Uses **sql.js** (pure JavaScript SQLite, compiled to WASM) which is
|
||||
* verified to work in Antigravity's Extension Host (unlike better-sqlite3
|
||||
* which fails due to ABI mismatch with Electron v22.21.1 / ABI v140).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const bridge = new StateBridge();
|
||||
* await bridge.initialize();
|
||||
*
|
||||
* const prefs = await bridge.getAgentPreferences();
|
||||
* console.log(prefs.terminalExecutionPolicy);
|
||||
* ```
|
||||
*/
|
||||
export class StateBridge implements IDisposable {
|
||||
private _dbPath: string | null = null;
|
||||
private _db: any = null; // sql.js Database instance
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* Initialize the state bridge by locating and opening state database.
|
||||
*
|
||||
* @throws {StateReadError} If the database cannot be found
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
const dbPath = this._findStateDb();
|
||||
|
||||
if (!dbPath) {
|
||||
throw new StateReadError('state.vscdb', 'Could not locate Antigravity state database');
|
||||
}
|
||||
|
||||
this._dbPath = dbPath;
|
||||
|
||||
// Open with sql.js (pure JS — verified working in Extension Host)
|
||||
try {
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Try to load sql.js from multiple locations:
|
||||
// 1. Adjacent sql-wasm.js (for VSIX bundles where consumer copies it to dist/)
|
||||
// 2. Standard require('sql.js') (for npm install / dev setups)
|
||||
let initSqlJs: any;
|
||||
const localSqlJs = path.join(__dirname, 'sql-wasm.js');
|
||||
if (fs.existsSync(localSqlJs)) {
|
||||
initSqlJs = require(localSqlJs);
|
||||
} else {
|
||||
initSqlJs = require('sql.js');
|
||||
}
|
||||
|
||||
// Auto-locate sql-wasm.wasm — try multiple paths so devs
|
||||
// don't need to manually copy anything after `npm install`
|
||||
const candidates = [
|
||||
// 1. Adjacent to this file (if wasm was bundled/copied to dist/)
|
||||
path.join(__dirname, 'sql-wasm.wasm'),
|
||||
// 2. sql.js package dist/ (standard npm install)
|
||||
path.resolve(__dirname, '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
|
||||
// 3. Hoisted node_modules (monorepo / npm workspaces)
|
||||
path.resolve(__dirname, '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
|
||||
// 4. Walk up to find it (deep hoisting)
|
||||
path.resolve(__dirname, '..', '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
|
||||
];
|
||||
|
||||
// Try require.resolve — works in all layouts
|
||||
try {
|
||||
const sqlJsMain = require.resolve('sql.js');
|
||||
candidates.unshift(path.join(path.dirname(sqlJsMain), 'sql-wasm.wasm'));
|
||||
} catch {
|
||||
// sql.js might not have a resolvable main in all setups
|
||||
}
|
||||
|
||||
let wasmPath: string | null = null;
|
||||
for (const p of candidates) {
|
||||
if (fs.existsSync(p)) {
|
||||
wasmPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasmPath) {
|
||||
throw new Error('sql-wasm.wasm not found in any expected location');
|
||||
}
|
||||
|
||||
const SQL = await initSqlJs({
|
||||
locateFile: () => wasmPath!,
|
||||
});
|
||||
const fileBuffer = fs.readFileSync(dbPath);
|
||||
this._db = new SQL.Database(fileBuffer);
|
||||
log.info(`State database opened via sql.js: ${dbPath}`);
|
||||
} catch (error) {
|
||||
log.warn('sql.js not available, will use child_process fallback', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a raw value from the state database.
|
||||
*
|
||||
* @param key - The SQLite key to read
|
||||
* @returns The raw string value, or null if not found
|
||||
* @throws {StateReadError} If the key is sensitive or read fails
|
||||
*/
|
||||
async getRawValue(key: string): Promise<string | null> {
|
||||
if (this._disposed) {
|
||||
throw new StateReadError(key, 'StateBridge has been disposed');
|
||||
}
|
||||
|
||||
if (!this._dbPath) {
|
||||
throw new StateReadError(key, 'StateBridge not initialized');
|
||||
}
|
||||
|
||||
// Block access to sensitive keys
|
||||
if (SENSITIVE_KEYS.has(key)) {
|
||||
throw new StateReadError(key, 'Access to sensitive keys is blocked by the SDK for security');
|
||||
}
|
||||
|
||||
try {
|
||||
if (this._db) {
|
||||
return this._querySqlJs(key);
|
||||
}
|
||||
return await this._queryChildProcess(key);
|
||||
} catch (error) {
|
||||
if (error instanceof StateReadError) throw error;
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
throw new StateReadError(key, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent preferences from USS.
|
||||
*
|
||||
* @returns Parsed agent preferences
|
||||
*/
|
||||
async getAgentPreferences(): Promise<IAgentPreferences> {
|
||||
const raw = await this.getRawValue(USSKeys.AGENT_PREFERENCES);
|
||||
|
||||
if (!raw) {
|
||||
log.warn('No agent preferences found, returning defaults');
|
||||
return this._defaultPreferences();
|
||||
}
|
||||
|
||||
try {
|
||||
return this._parseAgentPreferences(raw);
|
||||
} catch (error) {
|
||||
log.error('Failed to parse preferences, returning defaults', error);
|
||||
return this._defaultPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all stored USS keys from the state database.
|
||||
*
|
||||
* @returns List of key names related to Antigravity (excludes sensitive keys)
|
||||
*/
|
||||
async getAntigravityKeys(): Promise<string[]> {
|
||||
if (!this._dbPath) {
|
||||
throw new StateReadError('*', 'StateBridge not initialized');
|
||||
}
|
||||
|
||||
let keys: string[];
|
||||
|
||||
if (this._db) {
|
||||
const result = this._db.exec(
|
||||
"SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%' OR key LIKE 'chat.%'",
|
||||
);
|
||||
keys = result.length > 0 ? result[0].values.map((r: any[]) => r[0] as string) : [];
|
||||
} else {
|
||||
const result = await this._queryChildProcess('*');
|
||||
keys = result ? result.split('\n').map((l: string) => l.trim()).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
// Filter out sensitive keys
|
||||
return keys.filter((k) => !SENSITIVE_KEYS.has(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* Query using sql.js (in-process, pure JS).
|
||||
*/
|
||||
private _querySqlJs(key: string): string | null {
|
||||
const stmt = this._db.prepare('SELECT value FROM ItemTable WHERE key = $key');
|
||||
stmt.bind({ $key: key });
|
||||
if (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
stmt.free();
|
||||
return (row.value as string) ?? null;
|
||||
}
|
||||
stmt.free();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query using child_process sqlite3 CLI (fallback).
|
||||
*/
|
||||
private async _queryChildProcess(key: string): Promise<string | null> {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
const sql =
|
||||
key === '*'
|
||||
? "SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%'"
|
||||
: `SELECT value FROM ItemTable WHERE key = '${key.replace(/'/g, "''")}'`;
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(`sqlite3 "${this._dbPath}" "${sql}"`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
});
|
||||
return stdout.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the state.vscdb file across platforms.
|
||||
*/
|
||||
private _findStateDb(): string | null {
|
||||
const candidates: string[] = [];
|
||||
|
||||
// Windows (VERIFIED: this is the correct path)
|
||||
const appData = process.env.APPDATA;
|
||||
if (appData) {
|
||||
candidates.push(path.join(appData, 'Antigravity', 'User', 'globalStorage', 'state.vscdb'));
|
||||
}
|
||||
|
||||
// macOS
|
||||
const home = process.env.HOME;
|
||||
if (home) {
|
||||
candidates.push(
|
||||
path.join(
|
||||
home,
|
||||
'Library',
|
||||
'Application Support',
|
||||
'Antigravity',
|
||||
'User',
|
||||
'globalStorage',
|
||||
'state.vscdb',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Linux
|
||||
if (home) {
|
||||
candidates.push(
|
||||
path.join(home, '.config', 'Antigravity', 'User', 'globalStorage', 'state.vscdb'),
|
||||
);
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse agent preferences from Base64(Protobuf).
|
||||
*
|
||||
* The protobuf structure uses "sentinel keys" as string fields:
|
||||
* - `planningModeSentinelKey` → nested message with Base64(varint)
|
||||
* - `terminalAutoExecutionPolicySentinelKey` → nested message with Base64(varint)
|
||||
* - `artifactReviewPolicySentinelKey` → nested message with Base64(varint)
|
||||
*
|
||||
* Each sentinel value is itself a small Base64 string (e.g., "EAM=" = varint 3 = EAGER).
|
||||
*/
|
||||
private _parseAgentPreferences(raw: string): IAgentPreferences {
|
||||
const buffer = Buffer.from(raw, 'base64');
|
||||
const text = buffer.toString('utf8');
|
||||
|
||||
// Extract all sentinel values
|
||||
const terminalPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.TERMINAL_AUTO_EXECUTION_POLICY);
|
||||
const artifactPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.ARTIFACT_REVIEW_POLICY);
|
||||
const planningMode = this._extractSentinelValue(text, SENTINEL_KEYS.PLANNING_MODE);
|
||||
const secureMode = this._extractSentinelValue(text, SENTINEL_KEYS.SECURE_MODE);
|
||||
const terminalSandbox = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_TERMINAL_SANDBOX);
|
||||
const sandboxNetwork = this._extractSentinelValue(text, SENTINEL_KEYS.SANDBOX_ALLOW_NETWORK);
|
||||
const shellIntegration = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SHELL_INTEGRATION);
|
||||
const nonWorkspaceFiles = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_NON_WORKSPACE_FILES);
|
||||
const gitignoreAccess = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_GITIGNORE_ACCESS);
|
||||
const explainFix = this._extractSentinelValue(text, SENTINEL_KEYS.EXPLAIN_FIX_IN_CONVO);
|
||||
const autoContinue = this._extractSentinelValue(text, SENTINEL_KEYS.AUTO_CONTINUE_ON_MAX);
|
||||
const disableAutoOpen = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_OPEN_EDITED);
|
||||
const enableSounds = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SOUNDS);
|
||||
const disableAutoFix = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_FIX_LINTS);
|
||||
|
||||
return {
|
||||
terminalExecutionPolicy: (terminalPolicy ?? 1) as TerminalExecutionPolicy,
|
||||
artifactReviewPolicy: (artifactPolicy ?? 1) as ArtifactReviewPolicy,
|
||||
planningMode: planningMode ?? 0,
|
||||
secureModeEnabled: (secureMode ?? 0) === 1,
|
||||
terminalSandboxEnabled: (terminalSandbox ?? 0) === 1,
|
||||
sandboxAllowNetwork: (sandboxNetwork ?? 0) === 1,
|
||||
shellIntegrationEnabled: (shellIntegration ?? 1) === 1,
|
||||
allowNonWorkspaceFiles: (nonWorkspaceFiles ?? 0) === 1,
|
||||
allowGitignoreAccess: (gitignoreAccess ?? 0) === 1,
|
||||
explainFixInCurrentConvo: (explainFix ?? 0) === 1,
|
||||
autoContinueOnMax: autoContinue ?? 0,
|
||||
disableAutoOpenEdited: (disableAutoOpen ?? 0) === 1,
|
||||
enableSounds: (enableSounds ?? 0) === 1,
|
||||
disableAutoFixLints: (disableAutoFix ?? 0) === 1,
|
||||
allowedCommands: [],
|
||||
deniedCommands: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a varint value from a protobuf sentinel key.
|
||||
*
|
||||
* The structure is: sentinel_key_string followed by a small
|
||||
* Base64 value like "EAM=" (which decodes to a protobuf varint).
|
||||
*
|
||||
* Known mappings:
|
||||
* - "CAE=" → field 1, value 1 (OFF / ALWAYS)
|
||||
* - "EAI=" → field 2, value 2 (AUTO / TURBO)
|
||||
* - "EAM=" → field 2, value 3 (EAGER / AUTO)
|
||||
*/
|
||||
private _extractSentinelValue(text: string, sentinelKey: string): number | null {
|
||||
const idx = text.indexOf(sentinelKey);
|
||||
if (idx === -1) return null;
|
||||
|
||||
// After the sentinel key, look for a small Base64 fragment
|
||||
const after = text.substring(idx + sentinelKey.length, idx + sentinelKey.length + 30);
|
||||
|
||||
// Match a Base64 chunk (typically 4-8 chars ending with =)
|
||||
const b64Match = after.match(/([A-Za-z0-9+/]{2,8}={0,2})/);
|
||||
if (!b64Match) return null;
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(b64Match[1], 'base64');
|
||||
// Protobuf varint: last byte of the value
|
||||
// For simple single-byte varints, the value is in the lower 7 bits
|
||||
if (decoded.length >= 2) {
|
||||
// The first byte is (field_number << 3 | wire_type)
|
||||
// The second byte is the actual value
|
||||
return decoded[1];
|
||||
} else if (decoded.length === 1) {
|
||||
return decoded[0];
|
||||
}
|
||||
} catch {
|
||||
// Not valid base64
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private _defaultPreferences(): IAgentPreferences {
|
||||
return {
|
||||
terminalExecutionPolicy: 1 as TerminalExecutionPolicy, // OFF
|
||||
artifactReviewPolicy: 1 as ArtifactReviewPolicy, // ALWAYS
|
||||
planningMode: 0,
|
||||
secureModeEnabled: false,
|
||||
terminalSandboxEnabled: false,
|
||||
sandboxAllowNetwork: false,
|
||||
shellIntegrationEnabled: true,
|
||||
allowNonWorkspaceFiles: false,
|
||||
allowGitignoreAccess: false,
|
||||
explainFixInCurrentConvo: false,
|
||||
autoContinueOnMax: 0,
|
||||
disableAutoOpenEdited: false,
|
||||
enableSounds: false,
|
||||
disableAutoFixLints: false,
|
||||
allowedCommands: [],
|
||||
deniedCommands: [],
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposed = true;
|
||||
|
||||
if (this._db) {
|
||||
try {
|
||||
this._db.close();
|
||||
} catch {
|
||||
// Ignore close errors
|
||||
}
|
||||
this._db = null;
|
||||
}
|
||||
|
||||
this._dbPath = null;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"ES2020"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"docs-site"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: ['vscode'],
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
});
|
||||
127
auth.py
127
auth.py
@@ -1,127 +0,0 @@
|
||||
"""Authentication module — JWT token management for WebSocket Hub.
|
||||
|
||||
Two-stage auth:
|
||||
1. Extension connects with a registration code (built into .vsix at build time)
|
||||
2. Hub validates the code and issues a short-lived JWT session token
|
||||
3. Subsequent reconnections use the JWT token directly
|
||||
|
||||
The master secret is stored server-side only (GRAVITY_HUB_SECRET env var).
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Defaults
|
||||
DEFAULT_TOKEN_TTL = 86400 # 24 hours
|
||||
DEFAULT_REGISTRATION_CODE = "" # Set via GRAVITY_REGISTRATION_CODE env var
|
||||
|
||||
|
||||
class TokenManager:
|
||||
"""Manages JWT-like token creation and verification.
|
||||
|
||||
Uses HMAC-SHA256 for signing. Tokens contain:
|
||||
- project: project name scope
|
||||
- pc: PC identifier (hostname or custom name)
|
||||
- iat: issued at (unix timestamp)
|
||||
- exp: expiration (unix timestamp)
|
||||
"""
|
||||
|
||||
def __init__(self, secret: str = "", registration_code: str = ""):
|
||||
self.secret = secret or os.getenv("GRAVITY_HUB_SECRET", "")
|
||||
self.registration_code = registration_code or os.getenv(
|
||||
"GRAVITY_REGISTRATION_CODE", DEFAULT_REGISTRATION_CODE
|
||||
)
|
||||
if not self.secret:
|
||||
# Auto-generate a secret if not set (ephemeral — tokens invalid after restart)
|
||||
self.secret = hashlib.sha256(os.urandom(32)).hexdigest()
|
||||
logger.warning(
|
||||
"[AUTH] No GRAVITY_HUB_SECRET set — generated ephemeral secret. "
|
||||
"Tokens will be invalid after server restart."
|
||||
)
|
||||
|
||||
def validate_registration_code(self, code: str) -> bool:
|
||||
"""Check if the provided registration code matches."""
|
||||
if not self.registration_code:
|
||||
# No registration code configured → allow all connections
|
||||
logger.warning("[AUTH] No registration code configured — accepting all")
|
||||
return True
|
||||
return hmac.compare_digest(code, self.registration_code)
|
||||
|
||||
def create_token(
|
||||
self, project: str, pc_name: str, ttl: int = DEFAULT_TOKEN_TTL
|
||||
) -> str:
|
||||
"""Create a signed token for a specific project and PC.
|
||||
|
||||
Returns a base64-encoded string: {header}.{payload}.{signature}
|
||||
"""
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
"project": project,
|
||||
"pc": pc_name,
|
||||
"iat": now,
|
||||
"exp": now + ttl,
|
||||
}
|
||||
payload_b64 = _b64_encode(json.dumps(payload))
|
||||
signature = self._sign(payload_b64)
|
||||
return f"{payload_b64}.{signature}"
|
||||
|
||||
def verify_token(self, token: str) -> dict | None:
|
||||
"""Verify and decode a token.
|
||||
|
||||
Returns the payload dict if valid, None if invalid or expired.
|
||||
"""
|
||||
try:
|
||||
parts = token.split(".")
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
|
||||
payload_b64, signature = parts
|
||||
expected_sig = self._sign(payload_b64)
|
||||
|
||||
if not hmac.compare_digest(signature, expected_sig):
|
||||
logger.warning("[AUTH] Invalid token signature")
|
||||
return None
|
||||
|
||||
payload = json.loads(_b64_decode(payload_b64))
|
||||
|
||||
# Check expiration
|
||||
if payload.get("exp", 0) < time.time():
|
||||
logger.info(f"[AUTH] Token expired for {payload.get('pc', '?')}")
|
||||
return None
|
||||
|
||||
return payload
|
||||
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||
logger.warning(f"[AUTH] Token decode error: {e}")
|
||||
return None
|
||||
|
||||
def _sign(self, data: str) -> str:
|
||||
"""HMAC-SHA256 sign and return base64."""
|
||||
sig = hmac.new(
|
||||
self.secret.encode("utf-8"),
|
||||
data.encode("utf-8"),
|
||||
hashlib.sha256,
|
||||
).digest()
|
||||
return _b64_encode_bytes(sig)
|
||||
|
||||
|
||||
def _b64_encode(data: str) -> str:
|
||||
"""URL-safe base64 encode a string, no padding."""
|
||||
return urlsafe_b64encode(data.encode("utf-8")).rstrip(b"=").decode("ascii")
|
||||
|
||||
|
||||
def _b64_encode_bytes(data: bytes) -> str:
|
||||
"""URL-safe base64 encode bytes, no padding."""
|
||||
return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
||||
|
||||
|
||||
def _b64_decode(data: str) -> str:
|
||||
"""URL-safe base64 decode, handles missing padding."""
|
||||
padded = data + "=" * (4 - len(data) % 4)
|
||||
return urlsafe_b64decode(padded).decode("utf-8")
|
||||
2
better-antigravity-main/.github/FUNDING.yml
vendored
2
better-antigravity-main/.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
custom:
|
||||
- https://github.com/Kanezal/better-antigravity#support
|
||||
10
better-antigravity-main/.gitignore
vendored
10
better-antigravity-main/.gitignore
vendored
@@ -1,10 +0,0 @@
|
||||
GEMINI.md
|
||||
node_modules/
|
||||
dist/
|
||||
out/
|
||||
.env
|
||||
*.vsix
|
||||
*.bak
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -1,12 +0,0 @@
|
||||
.env
|
||||
publish-ovsx.mjs
|
||||
.git/
|
||||
.gitignore
|
||||
node_modules/
|
||||
out/
|
||||
src/
|
||||
build.mjs
|
||||
tsconfig.json
|
||||
*.bak
|
||||
*.log
|
||||
*.map
|
||||
@@ -1,105 +0,0 @@
|
||||
# Fixes — Technical Details
|
||||
|
||||
Detailed root cause analysis and patch descriptions for each fix in Better Antigravity.
|
||||
|
||||
---
|
||||
|
||||
## Auto-Run Fix
|
||||
|
||||
**Status:** Working
|
||||
**Affected versions:** 1.107.0+
|
||||
**Files patched:** `workbench.desktop.main.js`, `jetskiAgent.js`
|
||||
|
||||
### The Problem
|
||||
|
||||
You set **Settings -> Agent -> Terminal Execution -> "Always Proceed"**, but Antigravity **still asks you to click "Run"** on every single terminal command. Every. Single. Time.
|
||||
|
||||
The setting saves correctly, Strict Mode is off -- it just doesn't work.
|
||||
|
||||
### Root Cause
|
||||
|
||||
Found in the source code: the `run_command` step renderer component has an `onChange` handler that auto-confirms commands when you switch the dropdown to "Always run" **on a specific step**. But there's **no `useEffect` hook** that checks the saved policy at mount time and auto-confirms **new steps**.
|
||||
|
||||
In other words: the UI reads your setting, displays the correct dropdown value, but never actually acts on it automatically.
|
||||
|
||||
```javascript
|
||||
// What exists (only fires on dropdown CHANGE):
|
||||
y = Mt(_ => {
|
||||
setTerminalAutoExecutionPolicy(_),
|
||||
_ === EAGER && confirm(true) // <- only when you manually switch
|
||||
}, [])
|
||||
|
||||
// What's MISSING (should fire on component mount):
|
||||
useEffect(() => {
|
||||
if (policy === EAGER && !secureMode) confirm(true) // <- auto-confirm new steps
|
||||
}, [])
|
||||
```
|
||||
|
||||
### How the Patch Works
|
||||
|
||||
The patcher uses **structural regex matching** to find the `onChange` handler in the minified source. It matches the code by shape, not by variable names -- so it works even when Antigravity re-minifies on update.
|
||||
|
||||
**Step 1: Find the onChange handler**
|
||||
|
||||
Pattern: `<callback>=<useCallback>((<arg>)=>{<setFn>(<arg>),<arg>===<ENUM>.EAGER&&<confirm>(!0)},[...])`
|
||||
|
||||
This matches the handler structurally:
|
||||
- An assignment to a variable
|
||||
- A `useCallback` call
|
||||
- Arrow function with one argument
|
||||
- Two expressions: set state + check EAGER and confirm
|
||||
|
||||
**Step 2: Extract variable names from context**
|
||||
|
||||
From the surrounding 3000 characters, extract:
|
||||
- `policyVar`: `<var>=<something>?.terminalAutoExecutionPolicy??<ENUM>.OFF`
|
||||
- `secureVar`: `<var>=<something>?.secureModeEnabled??!1`
|
||||
- `useEffectFn`: the most frequently used short-named function matching the `fn(()=>{...})` pattern (frequency analysis)
|
||||
|
||||
**Step 3: Generate and inject the patch**
|
||||
|
||||
```javascript
|
||||
/*BA:autorun*/<useEffect>(()=>{<policyVar>===<ENUM>.EAGER&&!<secureVar>&&<confirm>(!0)},[])
|
||||
```
|
||||
|
||||
The patch is injected immediately after the `onChange` handler's closing bracket.
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
Antigravity "Always Proceed" Auto-Run Fix
|
||||
|
||||
C:\Users\user\AppData\Local\Programs\Antigravity
|
||||
Version: 1.107.0 (IDE 1.19.5)
|
||||
|
||||
[workbench] Found onChange at offset 12362782
|
||||
callback=Mt, enum=Dhe, confirm=b
|
||||
policyVar=u
|
||||
secureVar=d
|
||||
useEffect=mn (confidence: 30 hits)
|
||||
[workbench] Patched (+43 bytes)
|
||||
[jetskiAgent] Found onChange at offset 8388797
|
||||
callback=ve, enum=rx, confirm=F
|
||||
policyVar=d
|
||||
secureVar=f
|
||||
useEffect=At (confidence: 55 hits)
|
||||
[jetskiAgent] Patched (+42 bytes)
|
||||
|
||||
Done! Restart Antigravity.
|
||||
```
|
||||
|
||||
### Safety
|
||||
|
||||
- Original files are saved as `.ba-backup` before patching
|
||||
- The patch marker `/*BA:autorun*/` prevents double-patching
|
||||
- Only **adds** code, never removes existing logic
|
||||
- `--revert` restores the original file from backup
|
||||
- Async I/O in the extension prevents blocking the Extension Host
|
||||
|
||||
### Why two files?
|
||||
|
||||
The `run_command` step renderer exists in **two** bundles:
|
||||
1. `workbench.desktop.main.js` -- the main workbench bundle (~15MB)
|
||||
2. `jetskiAgent.js` -- the Cascade chat panel webview (~10MB)
|
||||
|
||||
Both contain the same bug with slightly different minified variable names. The structural matcher handles both transparently.
|
||||
@@ -1,59 +0,0 @@
|
||||
# Legal Notice
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This project is an unofficial, community-maintained collection of patches for
|
||||
[Antigravity IDE](https://antigravity.dev). It is **not affiliated with,
|
||||
endorsed by, or sponsored by Google LLC or any of its subsidiaries.**
|
||||
|
||||
## Nature of the Project
|
||||
|
||||
Better Antigravity provides **bugfix patches** that restore documented, expected
|
||||
functionality in Antigravity IDE. Specifically:
|
||||
|
||||
- The "Always Proceed" terminal execution policy is documented to auto-execute
|
||||
commands, but does not function as described. Our patch restores this behavior.
|
||||
- All patches are non-destructive: they create automatic backups and can be
|
||||
fully reverted at any time.
|
||||
- No data is collected, transmitted, or shared with any party.
|
||||
|
||||
## Compliance
|
||||
|
||||
- This project **does not access** Google's backend servers, APIs, or
|
||||
authentication systems.
|
||||
- This project **does not extract** AI models, training data, or proprietary
|
||||
algorithms.
|
||||
- This project **does not bypass** security features, licensing, or
|
||||
usage restrictions.
|
||||
- All modifications are local to the user's machine and affect only the
|
||||
client-side UI behavior.
|
||||
|
||||
## Interoperability
|
||||
|
||||
Where applicable, this project relies on the right to achieve interoperability
|
||||
as provided by:
|
||||
|
||||
- **EU Software Directive** (Directive 2009/24/EC), Article 6
|
||||
- **UK Copyright, Designs and Patents Act 1988**, Section 50B
|
||||
- Similar provisions in other jurisdictions
|
||||
|
||||
## User Responsibility
|
||||
|
||||
Users are responsible for ensuring their use of this software complies with
|
||||
applicable terms of service and local laws. By using this software, you
|
||||
acknowledge that:
|
||||
|
||||
1. You are applying modifications to software on your own machine at your
|
||||
own risk.
|
||||
2. Backups of original files are created automatically and can be restored.
|
||||
3. This project may stop working after Antigravity updates — in that case,
|
||||
revert and wait for an updated patch.
|
||||
|
||||
## Takedown
|
||||
|
||||
If Google or the Antigravity team requests removal of this project, we will
|
||||
comply promptly. Contact: [open a GitHub issue](https://github.com/Kanezal/better-antigravity/issues).
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the [GNU Affero General Public License v3.0](LICENSE).
|
||||
@@ -1,644 +0,0 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they receive
|
||||
widespread use, become available for other developers to incorporate.
|
||||
Many developers of free software are heartened and encouraged by the
|
||||
resulting cooperation. However, in the case of software used on network
|
||||
servers, this result may fail to come about. The GNU General Public
|
||||
License permits making a modified version and letting the public access
|
||||
it on a server without ever releasing its source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding Source
|
||||
of the work are being offered to the general public at no charge under
|
||||
subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied by
|
||||
the Installation Information. But this requirement does not apply if
|
||||
neither you nor any third party retains the ability to install modified
|
||||
object code on the User Product (for example, the work has been
|
||||
installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in source
|
||||
code form), and must require no special password or key for unpacking,
|
||||
reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions; the
|
||||
above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
|
||||
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR
|
||||
DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR
|
||||
A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
|
||||
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Better Antigravity
|
||||
Copyright (C) 2026 Kanezal
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@@ -1,209 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
# Better Antigravity
|
||||
|
||||
**Community-driven fixes and improvements for [Antigravity IDE](https://antigravity.dev)**
|
||||
|
||||
[](https://open-vsx.org/extension/kanezal/better-antigravity)
|
||||
[](https://www.npmjs.com/package/better-antigravity)
|
||||
[](LICENSE)
|
||||
[](https://antigravity.dev)
|
||||
[](https://github.com/Kanezal/better-antigravity#support)
|
||||
|
||||
*Antigravity is great. We just make it a little better.*
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## What is this?
|
||||
|
||||
Better Antigravity is both a **VS Code extension** and an **npm CLI tool** that fixes known bugs and adds quality-of-life features to Antigravity IDE.
|
||||
|
||||
| Channel | What it does | Install |
|
||||
|---------|-------------|---------|
|
||||
| **Extension** | Auto-applies fixes on startup + chat rename + SDK features | [Open VSX](https://open-vsx.org/extension/kanezal/better-antigravity) |
|
||||
| **CLI** | Quick one-off patching via `npx` (no extension install needed) | `npx better-antigravity auto-run` |
|
||||
|
||||
> [!NOTE]
|
||||
> The extension includes everything the CLI does, plus extra features powered by the [Antigravity SDK](https://www.npmjs.com/package/antigravity-sdk). If you install the extension, you don't need the CLI.
|
||||
|
||||
---
|
||||
|
||||
## Install (Extension)
|
||||
|
||||
Search for **"Better Antigravity"** in the Extensions panel, or install from [Open VSX](https://open-vsx.org/extension/kanezal/better-antigravity).
|
||||
|
||||
Manual install:
|
||||
|
||||
```bash
|
||||
antigravity --install-extension better-antigravity-0.5.0.vsix --force
|
||||
```
|
||||
|
||||
On activation the extension will:
|
||||
1. **Auto-apply the auto-run fix** (silent, no prompt)
|
||||
2. **Initialize the SDK** for chat rename and future features
|
||||
3. **Install the integration script** (prompts for reload on first install, auto-reloads on updates)
|
||||
4. **Suppress integrity warnings** ("corrupt installation" notification silenced automatically)
|
||||
|
||||
---
|
||||
|
||||
## Install (CLI only)
|
||||
|
||||
If you just want the auto-run fix without installing an extension:
|
||||
|
||||
```bash
|
||||
npx better-antigravity auto-run # apply fix
|
||||
npx better-antigravity auto-run --check # check status
|
||||
npx better-antigravity auto-run --revert # revert to original
|
||||
```
|
||||
|
||||
Custom install path (if Antigravity is not in the default location):
|
||||
|
||||
```bash
|
||||
npx better-antigravity auto-run --path "D:\Antigravity"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Auto-Run Fix
|
||||
|
||||
**The problem:** You set **Settings -> Agent -> Terminal Execution -> "Always Proceed"**, but Antigravity **still asks you to click "Run"** on every terminal command.
|
||||
|
||||
**Root cause:** The `run_command` step renderer has an `onChange` handler that auto-confirms when you switch the dropdown, but there's **no `useEffect`** that checks the saved policy at mount time.
|
||||
|
||||
```javascript
|
||||
// What exists (only fires on dropdown CHANGE):
|
||||
onChange = useCallback(_ => {
|
||||
setPolicy(_), _ === EAGER && confirm(true)
|
||||
}, [])
|
||||
|
||||
// What's MISSING (should fire on mount):
|
||||
useEffect(() => {
|
||||
if (policy === EAGER && !secureMode) confirm(true)
|
||||
}, [])
|
||||
```
|
||||
|
||||
**The fix:** Our patcher adds the missing `useEffect`. It uses **structural regex matching** (not hardcoded variable names) so it works across Antigravity versions.
|
||||
|
||||
> For the full root cause analysis, pattern matching explanation, and example output, see **[FIXES.md](FIXES.md)**.
|
||||
|
||||
### Chat Rename (Extension only)
|
||||
|
||||
Rename conversations to custom titles via the [Antigravity SDK](https://www.npmjs.com/package/antigravity-sdk) title proxy. Custom titles override the auto-generated summaries in the sidebar.
|
||||
|
||||
### Integrity Check Suppression (Extension only)
|
||||
|
||||
When the SDK patches workbench.html, Antigravity shows a sticky "Your installation appears to be corrupt" warning with no dismiss button. As of v0.4.0, the extension automatically updates the checksum in `product.json` after patching so IntegrityService sees `isPure = true`. No warnings on next restart.
|
||||
|
||||
Multiple SDK-based extensions are coordinated automatically -- the original checksum is restored only when the last extension uninstalls.
|
||||
|
||||
### Status Command (Extension only)
|
||||
|
||||
`Ctrl+Shift+P` -> **"Better Antigravity: Show Status"** to see:
|
||||
- SDK initialization state
|
||||
- Language Server connection
|
||||
- Integration script status
|
||||
- Auto-run fix status per file
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `Better Antigravity: Show Status` | Show extension and fix status |
|
||||
| `Better Antigravity: Revert Auto-Run Fix` | Restore original files from backup |
|
||||
|
||||
---
|
||||
|
||||
## Safety
|
||||
|
||||
- **Automatic backups** -- original files saved as `.ba-backup` before patching
|
||||
- **One-command revert** -- CLI `--revert` or extension command
|
||||
- **Non-destructive** -- patches only add code, never remove existing logic
|
||||
- **Version-resilient** -- structural regex matching, not hardcoded variable names
|
||||
- **Async I/O** -- file operations don't block the extension host
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Antigravity Version | Status |
|
||||
|---------------------|--------|
|
||||
| 1.107.0 | Tested |
|
||||
| Other versions | Should work (dynamic pattern matching) |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
better-antigravity/
|
||||
├── src/
|
||||
│ ├── extension.ts # Extension entry point (thin orchestrator)
|
||||
│ ├── auto-run.ts # Auto-run fix logic (async, no vscode dependency)
|
||||
│ └── commands.ts # VS Code command handlers
|
||||
├── fixes/
|
||||
│ └── auto-run-fix/
|
||||
│ └── patch.js # Standalone CLI patcher
|
||||
├── cli.js # npx entry point
|
||||
├── build.mjs # esbuild config
|
||||
├── publish-ovsx.mjs # Open VSX publish script
|
||||
└── package.json # Dual: npm package + VS Code extension
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build # Compile extension
|
||||
npm run watch # Watch mode
|
||||
npm run package # Build VSIX -> out/
|
||||
npm run publish:ovsx # Publish to Open VSX (reads .env)
|
||||
```
|
||||
|
||||
The extension depends on [antigravity-sdk](https://www.npmjs.com/package/antigravity-sdk) from the monorepo sibling directory. The build script aliases it automatically.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Found another Antigravity bug? Have a fix? PRs are welcome.
|
||||
|
||||
### Adding a new fix:
|
||||
|
||||
1. Create a folder under `fixes/` with a descriptive name
|
||||
2. Include a `patch.js` that supports `--check` and `--revert` flags
|
||||
3. Use structural pattern matching, not hardcoded variable names
|
||||
4. Update this README's feature table
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> [!WARNING]
|
||||
> This project is not affiliated with Google or the Antigravity team. These are community patches and improvements. If Antigravity updates and the patches break, simply revert and re-apply (or wait for an updated patch).
|
||||
|
||||
**Always report bugs officially** at [antigravity.google/support](https://antigravity.google/support) -- community patches are temporary solutions, not replacements for official fixes.
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Support
|
||||
|
||||
If you find this project useful and want to support its development, you can send **USDT** to:
|
||||
|
||||
| Network | Address |
|
||||
|---------|---------|
|
||||
| **TON** | `UQCjVh3C3mZc44GjT2IDsS4pmeOoUgRNxWMcb85NS5Bz_v1d` |
|
||||
| **TRON (TRC20)** | `TH3JKGjNrSDCsjkkSuneaSMZoJYF7CNTXD` |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](LICENSE)
|
||||
@@ -1,57 +0,0 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const isWatch = process.argv.includes('--watch');
|
||||
|
||||
/** @type {esbuild.BuildOptions} */
|
||||
const config = {
|
||||
entryPoints: ['src/extension.ts'],
|
||||
bundle: true,
|
||||
outfile: 'dist/extension.js',
|
||||
external: ['vscode'],
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'es2020',
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
// Resolve antigravity-sdk from monorepo sibling
|
||||
alias: {
|
||||
'antigravity-sdk': path.resolve('..', 'antigravity-sdk', 'dist', 'index.js'),
|
||||
},
|
||||
};
|
||||
|
||||
// Ensure dist/ exists
|
||||
if (!fs.existsSync('dist')) fs.mkdirSync('dist');
|
||||
|
||||
// Copy sql-wasm.wasm AND sql-wasm.js to dist/ (required by antigravity-sdk's StateBridge)
|
||||
const sqlFiles = ['sql-wasm.wasm', 'sql-wasm.js'];
|
||||
for (const sqlFile of sqlFiles) {
|
||||
const searchPaths = [
|
||||
path.join('node_modules', 'sql.js', 'dist', sqlFile),
|
||||
path.join('..', 'antigravity-sdk', 'node_modules', 'sql.js', 'dist', sqlFile),
|
||||
];
|
||||
|
||||
let copied = false;
|
||||
for (const src of searchPaths) {
|
||||
if (fs.existsSync(src)) {
|
||||
fs.copyFileSync(src, path.join('dist', sqlFile));
|
||||
console.log(`Copied ${sqlFile} from ${src}`);
|
||||
copied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!copied) {
|
||||
console.error(`ERROR: ${sqlFile} not found. Run "npm install" first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isWatch) {
|
||||
const ctx = await esbuild.context(config);
|
||||
await ctx.watch();
|
||||
console.log('Watching...');
|
||||
} else {
|
||||
await esbuild.build(config);
|
||||
console.log('Build complete');
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* better-antigravity CLI
|
||||
* Usage:
|
||||
* npx better-antigravity — list available fixes
|
||||
* npx better-antigravity auto-run — apply auto-run fix
|
||||
* npx better-antigravity auto-run --check — check status
|
||||
* npx better-antigravity auto-run --revert — revert fix
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const fixes = {
|
||||
'auto-run': {
|
||||
script: path.join(__dirname, 'fixes', 'auto-run-fix', 'patch.js'),
|
||||
description: '"Always Proceed" terminal policy doesn\'t auto-execute commands'
|
||||
}
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const fixName = args[0];
|
||||
const flags = args.slice(1);
|
||||
|
||||
// Header
|
||||
console.log('');
|
||||
console.log(' better-antigravity — community fixes for Antigravity IDE');
|
||||
console.log(' https://github.com/Kanezal/better-antigravity');
|
||||
console.log('');
|
||||
|
||||
if (!fixName || fixName === '--help' || fixName === '-h') {
|
||||
console.log(' Available fixes:');
|
||||
console.log('');
|
||||
for (const [name, fix] of Object.entries(fixes)) {
|
||||
console.log(` ${name.padEnd(15)} ${fix.description}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(' Usage:');
|
||||
console.log(' npx better-antigravity <fix-name> Apply fix');
|
||||
console.log(' npx better-antigravity <fix-name> --check Check status');
|
||||
console.log(' npx better-antigravity <fix-name> --revert Revert fix');
|
||||
console.log(' npx better-antigravity <fix-name> --path <dir> Use custom install path');
|
||||
console.log('');
|
||||
console.log(' The tool auto-detects Antigravity in: CWD, PATH, Registry, default locations.');
|
||||
console.log(' Use --path if auto-detection fails (e.g. custom install on another drive).');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const fix = fixes[fixName];
|
||||
if (!fix) {
|
||||
console.log(` Unknown fix: "${fixName}"`);
|
||||
console.log(` Available: ${Object.keys(fixes).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(fix.script)) {
|
||||
console.log(` Fix script not found: ${fix.script}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Forward to the fix script with flags
|
||||
process.argv = [process.argv[0], fix.script, ...flags];
|
||||
require(fix.script);
|
||||
@@ -1,400 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Antigravity "Always Proceed" Auto-Run Fix
|
||||
* ==========================================
|
||||
*
|
||||
* Fixes a bug where the "Always Proceed" terminal execution policy doesn't
|
||||
* actually auto-execute commands. Uses regex patterns to find code structures
|
||||
* regardless of minified variable names — works across versions.
|
||||
*
|
||||
* Usage:
|
||||
* node patch.js - Apply patch
|
||||
* node patch.js --revert - Restore original files
|
||||
* node patch.js --check - Check patch status
|
||||
*
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// ─── Installation Detection ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validates that a candidate directory is a real Antigravity installation
|
||||
* by checking for the workbench main JS file.
|
||||
*/
|
||||
function isAntigravityDir(dir) {
|
||||
if (!dir) return false;
|
||||
try {
|
||||
const workbench = path.join(dir, 'resources', 'app', 'out', 'vs', 'workbench', 'workbench.desktop.main.js');
|
||||
return fs.existsSync(workbench);
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a directory looks like the Antigravity installation root
|
||||
* (contains Antigravity.exe or antigravity binary).
|
||||
*/
|
||||
function looksLikeAntigravityRoot(dir) {
|
||||
if (!dir) return false;
|
||||
try {
|
||||
const exe = process.platform === 'win32' ? 'Antigravity.exe' : 'antigravity';
|
||||
return fs.existsSync(path.join(dir, exe));
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find Antigravity installation path from Windows Registry.
|
||||
* InnoSetup writes uninstall info to HKCU or HKLM.
|
||||
*/
|
||||
function findFromRegistry() {
|
||||
if (process.platform !== 'win32') return null;
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
// InnoSetup typically writes to this key; try HKCU first, then HKLM
|
||||
const regPaths = [
|
||||
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1',
|
||||
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1',
|
||||
'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1',
|
||||
];
|
||||
for (const regPath of regPaths) {
|
||||
try {
|
||||
const output = execSync(
|
||||
`reg query "${regPath}" /v InstallLocation`,
|
||||
{ encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }
|
||||
);
|
||||
const match = output.match(/InstallLocation\s+REG_SZ\s+(.+)/i);
|
||||
if (match) {
|
||||
const dir = match[1].trim().replace(/\\$/, '');
|
||||
if (isAntigravityDir(dir)) return dir;
|
||||
}
|
||||
} catch { /* key not found, try next */ }
|
||||
}
|
||||
} catch { /* child_process failed */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find Antigravity by looking at PATH entries for the executable.
|
||||
*/
|
||||
function findFromPath() {
|
||||
try {
|
||||
const pathDirs = (process.env.PATH || '').split(path.delimiter);
|
||||
const exe = process.platform === 'win32' ? 'Antigravity.exe' : 'antigravity';
|
||||
for (const dir of pathDirs) {
|
||||
if (!dir) continue;
|
||||
if (fs.existsSync(path.join(dir, exe))) {
|
||||
// The exe could be in the root or in a bin/ subdirectory
|
||||
if (isAntigravityDir(dir)) return dir;
|
||||
const parent = path.dirname(dir);
|
||||
if (isAntigravityDir(parent)) return parent;
|
||||
}
|
||||
}
|
||||
} catch { /* PATH parsing failed */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function findAntigravityPath() {
|
||||
// 1. Check CWD and its ancestors (user may run from install dir or a subdir)
|
||||
let dir = process.cwd();
|
||||
const root = path.parse(dir).root;
|
||||
while (dir && dir !== root) {
|
||||
if (looksLikeAntigravityRoot(dir) && isAntigravityDir(dir)) return dir;
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
|
||||
// 2. Check PATH
|
||||
const fromPath = findFromPath();
|
||||
if (fromPath) return fromPath;
|
||||
|
||||
// 3. Check Windows Registry (InnoSetup uninstall keys)
|
||||
const fromReg = findFromRegistry();
|
||||
if (fromReg) return fromReg;
|
||||
|
||||
// 4. Hardcoded well-known locations
|
||||
const candidates = [];
|
||||
if (process.platform === 'win32') {
|
||||
candidates.push(
|
||||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Antigravity'),
|
||||
path.join(process.env.PROGRAMFILES || '', 'Antigravity'),
|
||||
);
|
||||
} else if (process.platform === 'darwin') {
|
||||
candidates.push(
|
||||
'/Applications/Antigravity.app/Contents/Resources',
|
||||
path.join(os.homedir(), 'Applications', 'Antigravity.app', 'Contents', 'Resources')
|
||||
);
|
||||
} else {
|
||||
candidates.push('/usr/share/antigravity', '/opt/antigravity',
|
||||
path.join(os.homedir(), '.local', 'share', 'antigravity'));
|
||||
}
|
||||
for (const c of candidates) {
|
||||
if (isAntigravityDir(c)) return c;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Smart Pattern Matching ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Finds the onChange handler for terminalAutoExecutionPolicy and extracts
|
||||
* variable names from context, regardless of minification.
|
||||
*
|
||||
* Pattern we're looking for (structure, not exact names):
|
||||
* <VAR_CONFIRM>=<useCallback>((<ARG>)=>{
|
||||
* <stepHandler>?.setTerminalAutoExecutionPolicy?.(<ARG>),
|
||||
* <ARG>===<ENUM>.EAGER&&<CONFIRM_FN>(!0)
|
||||
* },[...])
|
||||
*
|
||||
* From the surrounding context we also extract:
|
||||
* <POLICY_VAR> = <stepHandler>?.terminalAutoExecutionPolicy ?? <ENUM>.OFF
|
||||
* <SECURE_VAR> = <stepHandler>?.secureModeEnabled ?? !1
|
||||
*/
|
||||
function analyzeFile(content, label) {
|
||||
// 1. Find the onChange handler: contains setTerminalAutoExecutionPolicy AND .EAGER
|
||||
// Pattern: VARNAME=CALLBACK(ARG=>{...setTerminalAutoExecutionPolicy...,ARG===ENUM.EAGER&&CONFIRM(!0)},[...])
|
||||
const onChangeRe = /(\w+)=(\w+)\((\w+)=>\{\w+\?\.setTerminalAutoExecutionPolicy\?\.\(\3\),\3===(\w+)\.EAGER&&(\w+)\(!0\)\},\[[\w,]*\]\)/;
|
||||
const onChangeMatch = content.match(onChangeRe);
|
||||
|
||||
if (!onChangeMatch) {
|
||||
console.log(` ❌ [${label}] Could not find onChange handler pattern`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const [fullMatch, assignVar, callbackAlias, argName, enumAlias, confirmFn] = onChangeMatch;
|
||||
const matchIndex = content.indexOf(fullMatch);
|
||||
|
||||
console.log(` 📋 [${label}] Found onChange at offset ${matchIndex}`);
|
||||
console.log(` callback=${callbackAlias}, enum=${enumAlias}, confirm=${confirmFn}`);
|
||||
|
||||
// 2. Find policy variable: VARNAME=HANDLER?.terminalAutoExecutionPolicy??ENUM.OFF
|
||||
const policyRe = new RegExp(`(\\w+)=\\w+\\?\\.terminalAutoExecutionPolicy\\?\\?${enumAlias}\\.OFF`);
|
||||
const policyMatch = content.substring(Math.max(0, matchIndex - 2000), matchIndex).match(policyRe);
|
||||
|
||||
if (!policyMatch) {
|
||||
console.log(` ❌ [${label}] Could not find policy variable`);
|
||||
return null;
|
||||
}
|
||||
const policyVar = policyMatch[1];
|
||||
console.log(` policyVar=${policyVar}`);
|
||||
|
||||
// 3. Find secureMode variable: VARNAME=HANDLER?.secureModeEnabled??!1
|
||||
const secureRe = /(\w+)=\w+\?\.secureModeEnabled\?\?!1/;
|
||||
const secureMatch = content.substring(Math.max(0, matchIndex - 2000), matchIndex).match(secureRe);
|
||||
|
||||
if (!secureMatch) {
|
||||
console.log(` ❌ [${label}] Could not find secureMode variable`);
|
||||
return null;
|
||||
}
|
||||
const secureVar = secureMatch[1];
|
||||
console.log(` secureVar=${secureVar}`);
|
||||
|
||||
// 4. Find useEffect alias: look for ALIAS(()=>{...},[...]) calls nearby (not useCallback/useMemo)
|
||||
const nearbyCode = content.substring(Math.max(0, matchIndex - 5000), matchIndex + 5000);
|
||||
const effectCandidates = {};
|
||||
const effectRe = /\b(\w{2,3})\(\(\)=>\{[^}]{3,80}\},\[/g;
|
||||
let m;
|
||||
while ((m = effectRe.exec(nearbyCode)) !== null) {
|
||||
const alias = m[1];
|
||||
if (alias !== callbackAlias && alias !== 'var' && alias !== 'new') {
|
||||
effectCandidates[alias] = (effectCandidates[alias] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check broader file for common useEffect patterns (with cleanup return)
|
||||
const cleanupRe = /\b(\w{2,3})\(\(\)=>\{[^}]*return\s*\(\)=>/g;
|
||||
while ((m = cleanupRe.exec(content)) !== null) {
|
||||
const alias = m[1];
|
||||
if (alias !== callbackAlias) {
|
||||
effectCandidates[alias] = (effectCandidates[alias] || 0) + 5; // higher weight
|
||||
}
|
||||
}
|
||||
|
||||
// Remove known non-useEffect aliases (useMemo patterns)
|
||||
// useMemo: alias(()=>EXPRESSION,[deps]) — returns a value, often assigned
|
||||
// useEffect: alias(()=>{STATEMENTS},[deps]) — no return value
|
||||
|
||||
// Pick the most common candidate
|
||||
let useEffectAlias = null;
|
||||
let maxCount = 0;
|
||||
for (const [alias, count] of Object.entries(effectCandidates)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
useEffectAlias = alias;
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEffectAlias) {
|
||||
console.log(` ❌ [${label}] Could not determine useEffect alias`);
|
||||
return null;
|
||||
}
|
||||
console.log(` useEffect=${useEffectAlias} (confidence: ${maxCount} hits)`);
|
||||
|
||||
// 5. Build patch
|
||||
const patchCode = `_aep=${useEffectAlias}(()=>{${policyVar}===${enumAlias}.EAGER&&!${secureVar}&&${confirmFn}(!0)},[]),`;
|
||||
|
||||
return {
|
||||
target: fullMatch,
|
||||
replacement: patchCode + fullMatch,
|
||||
patchMarker: `_aep=${useEffectAlias}(()=>{${policyVar}===${enumAlias}.EAGER`,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
// ─── File Operations ────────────────────────────────────────────────────────
|
||||
|
||||
function patchFile(filePath, label) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(` ❌ [${label}] File not found: ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Check if already patched
|
||||
if (content.includes('_aep=')) {
|
||||
const existingPatch = content.match(/_aep=\w+\(\(\)=>\{[^}]+EAGER[^}]+\},\[\]\)/);
|
||||
if (existingPatch) {
|
||||
console.log(` ⏭️ [${label}] Already patched`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const analysis = analyzeFile(content, label);
|
||||
if (!analysis) return false;
|
||||
|
||||
// Verify target is unique
|
||||
const count = content.split(analysis.target).length - 1;
|
||||
if (count !== 1) {
|
||||
console.log(` ❌ [${label}] Target found ${count} times (expected 1)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Backup
|
||||
if (!fs.existsSync(filePath + '.bak')) {
|
||||
fs.copyFileSync(filePath, filePath + '.bak');
|
||||
console.log(` 📦 [${label}] Backup created`);
|
||||
}
|
||||
|
||||
// Apply
|
||||
const patched = content.replace(analysis.target, analysis.replacement);
|
||||
fs.writeFileSync(filePath, patched, 'utf8');
|
||||
|
||||
const diff = fs.statSync(filePath).size - fs.statSync(filePath + '.bak').size;
|
||||
console.log(` ✅ [${label}] Patched (+${diff} bytes)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function revertFile(filePath, label) {
|
||||
const bak = filePath + '.bak';
|
||||
if (!fs.existsSync(bak)) {
|
||||
console.log(` ⏭️ [${label}] No backup, skipping`);
|
||||
return;
|
||||
}
|
||||
fs.copyFileSync(bak, filePath);
|
||||
console.log(` ✅ [${label}] Restored`);
|
||||
}
|
||||
|
||||
function checkFile(filePath, label) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(` ❌ [${label}] Not found`);
|
||||
return false;
|
||||
}
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const patched = content.includes('_aep=') && /_aep=\w+\(\(\)=>\{[^}]+EAGER/.test(content);
|
||||
const hasBak = fs.existsSync(filePath + '.bak');
|
||||
|
||||
if (patched) {
|
||||
console.log(` ✅ [${label}] PATCHED` + (hasBak ? ' (backup exists)' : ''));
|
||||
} else {
|
||||
const analysis = analyzeFile(content, label);
|
||||
if (analysis) {
|
||||
console.log(` ⬜ [${label}] NOT PATCHED (patchable)`);
|
||||
} else {
|
||||
console.log(` ⚠️ [${label}] NOT PATCHED (may be incompatible)`);
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
|
||||
// ─── Version Info ───────────────────────────────────────────────────────────
|
||||
|
||||
function getVersion(basePath) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(basePath, 'resources', 'app', 'package.json'), 'utf8'));
|
||||
const product = JSON.parse(fs.readFileSync(path.join(basePath, 'resources', 'app', 'product.json'), 'utf8'));
|
||||
return `${pkg.version} (IDE ${product.ideVersion})`;
|
||||
} catch { return 'unknown'; }
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const action = args.includes('--revert') ? 'revert' : args.includes('--check') ? 'check' : 'apply';
|
||||
|
||||
// Parse --path flag
|
||||
let explicitPath = null;
|
||||
const pathIdx = args.indexOf('--path');
|
||||
if (pathIdx !== -1 && args[pathIdx + 1]) {
|
||||
explicitPath = path.resolve(args[pathIdx + 1]);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('╔══════════════════════════════════════════════════╗');
|
||||
console.log('║ Antigravity "Always Proceed" Auto-Run Fix ║');
|
||||
console.log('╚══════════════════════════════════════════════════╝');
|
||||
|
||||
let basePath;
|
||||
if (explicitPath) {
|
||||
if (!isAntigravityDir(explicitPath)) {
|
||||
console.log(`\n\u274C --path "${explicitPath}" does not look like an Antigravity installation.`);
|
||||
console.log(' Expected to find: resources/app/out/vs/workbench/workbench.desktop.main.js');
|
||||
process.exit(1);
|
||||
}
|
||||
basePath = explicitPath;
|
||||
} else {
|
||||
basePath = findAntigravityPath();
|
||||
}
|
||||
|
||||
if (!basePath) {
|
||||
console.log('\n\u274C Antigravity installation not found!');
|
||||
console.log('');
|
||||
console.log(' Try one of:');
|
||||
console.log(' 1. Run from the Antigravity install directory:');
|
||||
console.log(' cd "C:\\Path\\To\\Antigravity" && npx better-antigravity auto-run');
|
||||
console.log(' 2. Specify the path explicitly:');
|
||||
console.log(' npx better-antigravity auto-run --path "D:\\Antigravity"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n📍 ${basePath}`);
|
||||
console.log(`📦 Version: ${getVersion(basePath)}`);
|
||||
console.log('');
|
||||
|
||||
const files = [
|
||||
{ path: path.join(basePath, 'resources', 'app', 'out', 'vs', 'workbench', 'workbench.desktop.main.js'), label: 'workbench' },
|
||||
{ path: path.join(basePath, 'resources', 'app', 'out', 'jetskiAgent', 'main.js'), label: 'jetskiAgent' },
|
||||
];
|
||||
|
||||
switch (action) {
|
||||
case 'check':
|
||||
files.forEach(f => checkFile(f.path, f.label));
|
||||
break;
|
||||
case 'revert':
|
||||
files.forEach(f => revertFile(f.path, f.label));
|
||||
console.log('\n✨ Restored! Restart Antigravity.');
|
||||
break;
|
||||
case 'apply':
|
||||
const ok = files.every(f => patchFile(f.path, f.label));
|
||||
console.log(ok
|
||||
? '\n✨ Done! Restart Antigravity.\n💡 Run with --revert to undo.\n⚠️ Re-run after Antigravity updates.'
|
||||
: '\n⚠️ Some patches failed.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
503
better-antigravity-main/package-lock.json
generated
503
better-antigravity-main/package-lock.json
generated
@@ -1,503 +0,0 @@
|
||||
{
|
||||
"name": "better-antigravity",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "better-antigravity",
|
||||
"version": "0.2.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"antigravity-sdk": "^1.3.0",
|
||||
"sql.js": "^1.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"better-antigravity": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"esbuild": "^0.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"vscode": "^1.85.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz",
|
||||
"integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/vscode": {
|
||||
"version": "1.109.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz",
|
||||
"integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/antigravity-sdk": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/antigravity-sdk/-/antigravity-sdk-1.3.0.tgz",
|
||||
"integrity": "sha512-AonqXNmtnkYYib/pSCcDlxnVxLsNIafIbBQxwTV0zHt6RZBjG8ejknkJAhd8hRyilMefMTOE28oPzTblby2K2A==",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"sql.js": "^1.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/vscode": "^1.85.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.0.tgz",
|
||||
"integrity": "sha512-NXYh+kFqLiYRCNAaHD0PcbjFgXyjuolEKLMk5vRt2DgPENtF1kkNzzMlg42dUk5wIsH8MhUzsRhaUxIisoSlZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
{
|
||||
"name": "better-antigravity",
|
||||
"displayName": "Better Antigravity",
|
||||
"description": "Community-driven fixes and improvements for Antigravity IDE — auto-run fix, chat rename, and more",
|
||||
"version": "0.6.0",
|
||||
"publisher": "kanezal",
|
||||
"icon": "static/BA-background.png",
|
||||
"galleryBanner": {
|
||||
"color": "#1a1a1a",
|
||||
"theme": "dark"
|
||||
},
|
||||
"markdown": "github",
|
||||
"badges": [
|
||||
{
|
||||
"url": "https://img.shields.io/npm/v/better-antigravity",
|
||||
"href": "https://www.npmjs.com/package/better-antigravity",
|
||||
"description": "npm version"
|
||||
},
|
||||
{
|
||||
"url": "https://img.shields.io/badge/License-AGPL--3.0-blue.svg",
|
||||
"href": "https://github.com/Kanezal/better-antigravity/blob/main/LICENSE",
|
||||
"description": "License: AGPL-3.0"
|
||||
},
|
||||
{
|
||||
"url": "https://img.shields.io/badge/Antigravity-v1.107.0+-blue.svg",
|
||||
"href": "https://antigravity.dev",
|
||||
"description": "Antigravity compatibility"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.85.0",
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
"bin": {
|
||||
"better-antigravity": "cli.js"
|
||||
},
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "better-antigravity.status",
|
||||
"title": "Better Antigravity: Show Status"
|
||||
},
|
||||
{
|
||||
"command": "better-antigravity.revertAutoRun",
|
||||
"title": "Better Antigravity: Revert Auto-Run Fix"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
"watch": "node build.mjs --watch",
|
||||
"prepackage": "node -e \"require('fs').mkdirSync('out',{recursive:true})\"",
|
||||
"package": "npm run prepackage && npx @vscode/vsce package --no-dependencies --out out/better-antigravity.vsix",
|
||||
"publish:ovsx": "node publish-ovsx.mjs",
|
||||
"fix:auto-run": "node fixes/auto-run-fix/patch.js",
|
||||
"fix:auto-run:check": "node fixes/auto-run-fix/patch.js --check",
|
||||
"fix:auto-run:revert": "node fixes/auto-run-fix/patch.js --revert"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Kanezal/better-antigravity"
|
||||
},
|
||||
"homepage": "https://github.com/Kanezal/better-antigravity#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Kanezal/better-antigravity/issues"
|
||||
},
|
||||
"author": "Kanezal",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"keywords": [
|
||||
"antigravity",
|
||||
"antigravity-ide",
|
||||
"google-antigravity",
|
||||
"fix",
|
||||
"auto-run",
|
||||
"rename-chat",
|
||||
"community"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"esbuild": "^0.20.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"antigravity-sdk": "^1.5.0",
|
||||
"sql.js": "^1.14.0"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Publish to Open VSX using token from .env
|
||||
*
|
||||
* Usage:
|
||||
* node publish-ovsx.mjs — publish VSIX
|
||||
* node publish-ovsx.mjs create-namespace — create publisher namespace (first time only)
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require('./package.json');
|
||||
const cmd = process.argv[2] || 'publish';
|
||||
|
||||
// Read token from .env
|
||||
let pat;
|
||||
try {
|
||||
const env = readFileSync('.env', 'utf8');
|
||||
const match = env.match(/OVSX_PAT=(.+)/);
|
||||
if (!match) throw new Error('OVSX_PAT not found in .env');
|
||||
pat = match[1].trim();
|
||||
} catch (err) {
|
||||
console.error('ERROR: Could not read .env file. Create .env with OVSX_PAT=<token>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
if (cmd === 'create-namespace') {
|
||||
console.log(`Creating namespace "${pkg.publisher}" on Open VSX...`);
|
||||
execSync(`npx ovsx create-namespace ${pkg.publisher} --pat ${pat}`, { stdio: 'inherit' });
|
||||
console.log('Namespace created!');
|
||||
} else {
|
||||
const vsixFile = `out/better-antigravity.vsix`;
|
||||
console.log(`Publishing ${vsixFile} to Open VSX...`);
|
||||
execSync(`npx ovsx publish ${vsixFile} --pat ${pat}`, { stdio: 'inherit' });
|
||||
console.log('Done!');
|
||||
}
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
/**
|
||||
* Auto-Run Fix — Patches the "Always Proceed" terminal policy to actually auto-execute.
|
||||
*
|
||||
* Uses structural regex matching to find the onChange handler in minified code
|
||||
* and injects a missing useEffect that auto-confirms commands when policy is EAGER.
|
||||
*
|
||||
* Works across AG versions because it matches code STRUCTURE, not variable NAMES.
|
||||
*
|
||||
* @module auto-run
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as fsp from 'fs/promises';
|
||||
|
||||
/** Marker comment to identify our patches */
|
||||
const PATCH_MARKER = '/*BA:autorun*/';
|
||||
|
||||
/**
|
||||
* Resolve the Antigravity workbench directory.
|
||||
*/
|
||||
export function getWorkbenchDir(): string | null {
|
||||
const appData = process.env.LOCALAPPDATA || '';
|
||||
const dir = path.join(
|
||||
appData,
|
||||
'Programs', 'Antigravity', 'resources', 'app', 'out',
|
||||
'vs', 'code', 'electron-browser', 'workbench',
|
||||
);
|
||||
return fs.existsSync(dir) ? dir : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target files that need the auto-run patch.
|
||||
*/
|
||||
export function getTargetFiles(workbenchDir: string): Array<{ path: string; label: string }> {
|
||||
return [
|
||||
{ path: path.join(workbenchDir, 'workbench.desktop.main.js'), label: 'workbench' },
|
||||
{ path: path.join(workbenchDir, 'jetskiAgent.js'), label: 'jetskiAgent' },
|
||||
].filter(f => fs.existsSync(f.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file already has the auto-run patch applied.
|
||||
*/
|
||||
export async function isPatched(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
// Read only first 50 bytes of the marker area via a small buffer scan
|
||||
// The marker is injected mid-file, so we must read the full file.
|
||||
// Use async to avoid blocking extension host.
|
||||
const content = await fsp.readFile(filePath, 'utf8');
|
||||
return content.includes(PATCH_MARKER);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a file to find the onChange handler and extract variable names.
|
||||
*
|
||||
* Returns null if pattern not found (file may already be fixed by AG update).
|
||||
*/
|
||||
function analyzeFile(content: string): AnalysisResult | null {
|
||||
// Find onChange handler for terminalAutoExecutionPolicy
|
||||
// Pattern: <callback>=<useCallback>((<arg>)=>{<setFn>(<arg>),<arg>===<ENUM>.EAGER&&<confirm>(true)},[...])
|
||||
const onChangeRegex = /(\w+)=(\w+)\((\(\w+\))=>\{(\w+)\(\w+\),\w+===(\w+)\.EAGER&&(\w+)\(!0\)\},\[/g;
|
||||
const match = onChangeRegex.exec(content);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const [fullMatch, , , , , enumName, confirmFn] = match;
|
||||
const insertPos = match.index + fullMatch.length;
|
||||
|
||||
// Extract context variables from surrounding code
|
||||
const contextStart = Math.max(0, match.index - 3000);
|
||||
const contextEnd = Math.min(content.length, match.index + 3000);
|
||||
const context = content.substring(contextStart, contextEnd);
|
||||
|
||||
// policyVar: <var>=<something>?.terminalAutoExecutionPolicy??<ENUM>.OFF
|
||||
const policyMatch = /(\w+)=\w+\?\.terminalAutoExecutionPolicy\?\?(\w+)\.OFF/.exec(context);
|
||||
// secureVar: <var>=<something>?.secureModeEnabled??!1
|
||||
const secureMatch = /(\w+)=\w+\?\.secureModeEnabled\?\?!1/.exec(context);
|
||||
|
||||
if (!policyMatch || !secureMatch) return null;
|
||||
|
||||
const policyVar = policyMatch[1];
|
||||
const secureVar = secureMatch[1];
|
||||
|
||||
// Find useEffect — most frequently used short-named function in the scope
|
||||
const useEffectFn = findUseEffect(context, [confirmFn]);
|
||||
|
||||
if (!useEffectFn) return null;
|
||||
|
||||
// Find insertion point: after the useCallback closing
|
||||
const afterOnChange = content.indexOf('])', insertPos);
|
||||
if (afterOnChange === -1) return null;
|
||||
|
||||
const insertAt = content.indexOf(';', afterOnChange);
|
||||
if (insertAt === -1) return null;
|
||||
|
||||
return {
|
||||
enumName,
|
||||
confirmFn,
|
||||
policyVar,
|
||||
secureVar,
|
||||
useEffectFn,
|
||||
insertAt: insertAt + 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the useEffect function name by frequency analysis.
|
||||
*/
|
||||
function findUseEffect(context: string, exclude: string[]): string | null {
|
||||
const candidates: Record<string, number> = {};
|
||||
const regex = /(\w{1,3})\(\(\)=>\{/g;
|
||||
let m;
|
||||
|
||||
while ((m = regex.exec(context)) !== null) {
|
||||
const fn = m[1];
|
||||
if (fn.length <= 3 && !exclude.includes(fn)) {
|
||||
candidates[fn] = (candidates[fn] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let best = '';
|
||||
let maxCount = 0;
|
||||
for (const [fn, count] of Object.entries(candidates)) {
|
||||
if (count > maxCount) {
|
||||
best = fn;
|
||||
maxCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return best || null;
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
enumName: string;
|
||||
confirmFn: string;
|
||||
policyVar: string;
|
||||
secureVar: string;
|
||||
useEffectFn: string;
|
||||
insertAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the auto-run patch to a single file.
|
||||
*
|
||||
* @returns Patch status message
|
||||
*/
|
||||
export async function patchFile(filePath: string, label: string): Promise<PatchResult> {
|
||||
try {
|
||||
let content = await fsp.readFile(filePath, 'utf8');
|
||||
|
||||
if (content.includes(PATCH_MARKER)) {
|
||||
return { success: true, label, status: 'already-patched' };
|
||||
}
|
||||
|
||||
const analysis = analyzeFile(content);
|
||||
if (!analysis) {
|
||||
return { success: false, label, status: 'pattern-not-found' };
|
||||
}
|
||||
|
||||
const { enumName, confirmFn, policyVar, secureVar, useEffectFn, insertAt } = analysis;
|
||||
|
||||
// Build the patch
|
||||
const patch = `${PATCH_MARKER}${useEffectFn}(()=>{${policyVar}===${enumName}.EAGER&&!${secureVar}&&${confirmFn}(!0)},[])`;
|
||||
|
||||
// Create backup (only if one doesn't exist)
|
||||
const backup = filePath + '.ba-backup';
|
||||
try { await fsp.access(backup); } catch {
|
||||
await fsp.copyFile(filePath, backup);
|
||||
}
|
||||
|
||||
// Insert
|
||||
content = content.substring(0, insertAt) + patch + content.substring(insertAt);
|
||||
await fsp.writeFile(filePath, content, 'utf8');
|
||||
|
||||
return { success: true, label, status: 'patched', bytesAdded: patch.length };
|
||||
} catch (err: any) {
|
||||
return { success: false, label, status: 'error', error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the auto-run patch on a single file.
|
||||
*/
|
||||
export function revertFile(filePath: string, label: string): PatchResult {
|
||||
const backup = filePath + '.ba-backup';
|
||||
if (!fs.existsSync(backup)) {
|
||||
return { success: false, label, status: 'no-backup' };
|
||||
}
|
||||
|
||||
try {
|
||||
fs.copyFileSync(backup, filePath);
|
||||
fs.unlinkSync(backup);
|
||||
return { success: true, label, status: 'reverted' };
|
||||
} catch (err: any) {
|
||||
return { success: false, label, status: 'error', error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export interface PatchResult {
|
||||
success: boolean;
|
||||
label: string;
|
||||
status: 'patched' | 'already-patched' | 'pattern-not-found' | 'reverted' | 'no-backup' | 'error';
|
||||
bytesAdded?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-apply the fix to all target files.
|
||||
*
|
||||
* @returns Array of results for each file
|
||||
*/
|
||||
export async function autoApply(): Promise<PatchResult[]> {
|
||||
const dir = getWorkbenchDir();
|
||||
if (!dir) return [];
|
||||
|
||||
const files = getTargetFiles(dir);
|
||||
return Promise.all(files.map(f => patchFile(f.path, f.label)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert all target files from backups.
|
||||
*
|
||||
* @returns Number of files reverted
|
||||
*/
|
||||
export function revertAll(): PatchResult[] {
|
||||
const dir = getWorkbenchDir();
|
||||
if (!dir) return [];
|
||||
|
||||
const files = getTargetFiles(dir);
|
||||
return files.map(f => revertFile(f.path, f.label));
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Better Antigravity — VS Code command handlers.
|
||||
*
|
||||
* Each exported function is a command handler registered in extension.ts.
|
||||
*
|
||||
* @module commands
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fsp from 'fs/promises';
|
||||
import { AntigravitySDK } from 'antigravity-sdk';
|
||||
import { getWorkbenchDir, getTargetFiles, isPatched, revertAll } from './auto-run';
|
||||
|
||||
/**
|
||||
* Show extension status in the output channel.
|
||||
*/
|
||||
export async function status(sdk: AntigravitySDK | null, output: vscode.OutputChannel): Promise<void> {
|
||||
const lines = [
|
||||
'=== Better Antigravity ===',
|
||||
'',
|
||||
`SDK: ${sdk?.isInitialized ? `v${sdk.version}` : 'not initialized'}`,
|
||||
`LS: ${sdk?.ls?.isReady ? `port ${sdk.ls.port}` : 'not ready'}`,
|
||||
`UI: ${sdk?.integration.isInstalled() ? 'installed' : 'not installed'}`,
|
||||
`Titles: ${sdk?.integration.titles.count ?? 0} custom`,
|
||||
];
|
||||
|
||||
const dir = getWorkbenchDir();
|
||||
if (dir) {
|
||||
const files = getTargetFiles(dir);
|
||||
for (const f of files) {
|
||||
const patched = await isPatched(f.path);
|
||||
lines.push(`AutoRun: ${f.label} = ${patched ? 'fixed' : 'not fixed'}`);
|
||||
}
|
||||
} else {
|
||||
lines.push('AutoRun: workbench directory not found');
|
||||
}
|
||||
|
||||
output.appendLine(lines.join('\n'));
|
||||
output.show(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the auto-run fix and prompt for reload.
|
||||
*
|
||||
* Also clears V8 Code Cache to prevent stale cached patched code
|
||||
* from being loaded by Electron (which causes grey screen).
|
||||
*/
|
||||
export async function revertAutoRun(): Promise<void> {
|
||||
const dir = getWorkbenchDir();
|
||||
if (!dir) {
|
||||
vscode.window.showErrorMessage('Workbench directory not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const results = revertAll();
|
||||
const reverted = results.filter(r => r.status === 'reverted').length;
|
||||
|
||||
if (reverted > 0) {
|
||||
// Clear V8 Code Cache — stale cache after revert causes grey screen
|
||||
const appData = process.env.APPDATA || '';
|
||||
const cacheDirs = [
|
||||
path.join(appData, 'Antigravity', 'CachedData'),
|
||||
path.join(appData, 'Antigravity', 'GPUCache'),
|
||||
path.join(appData, 'Antigravity', 'Code Cache'),
|
||||
];
|
||||
for (const d of cacheDirs) {
|
||||
try { await fsp.rm(d, { recursive: true, force: true }); } catch { /* may not exist */ }
|
||||
}
|
||||
|
||||
const action = await vscode.window.showInformationMessage(
|
||||
`Auto-run fix reverted (${reverted} file(s)). Caches cleared. Reload to apply.`,
|
||||
'Reload Now',
|
||||
);
|
||||
if (action === 'Reload Now') {
|
||||
vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
} else {
|
||||
vscode.window.showInformationMessage('No backups found. Nothing to revert.');
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Better Antigravity — Extension entry point.
|
||||
*
|
||||
* Thin orchestrator: wires up modules, no business logic here.
|
||||
*
|
||||
* @module extension
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { AntigravitySDK } from 'antigravity-sdk';
|
||||
import { autoApply } from './auto-run';
|
||||
import { status, revertAutoRun } from './commands';
|
||||
|
||||
let sdk: AntigravitySDK | null = null;
|
||||
let output: vscode.OutputChannel;
|
||||
|
||||
function log(msg: string): void {
|
||||
const ts = new Date().toISOString().substring(11, 19);
|
||||
output?.appendLine(`[${ts}] ${msg}`);
|
||||
}
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
output = vscode.window.createOutputChannel('Better Antigravity');
|
||||
context.subscriptions.push(output);
|
||||
log('Activating...');
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('better-antigravity.status', () => status(sdk, output)),
|
||||
vscode.commands.registerCommand('better-antigravity.revertAutoRun', revertAutoRun),
|
||||
);
|
||||
|
||||
// ── Auto-Run Fix (async, non-blocking, no prompt) ─────────────────
|
||||
autoApply().then(fixResults => {
|
||||
for (const r of fixResults) {
|
||||
log(`[auto-run] ${r.label}: ${r.status}${r.bytesAdded ? ` (+${r.bytesAdded}b)` : ''}${r.error ? ` -- ${r.error}` : ''}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── SDK Init ─────────────────────────────────────────────────────
|
||||
try {
|
||||
sdk = new AntigravitySDK(context);
|
||||
await sdk.initialize();
|
||||
log(`SDK v${sdk.version} initialized`);
|
||||
|
||||
// Title proxy for chat rename
|
||||
sdk.integration.enableTitleProxy();
|
||||
|
||||
// Seamless install (handles first-time prompt + auto-reload on update)
|
||||
await sdk.integration.installSeamless(
|
||||
(cmd) => vscode.commands.executeCommand(cmd),
|
||||
(msg, ...items) => vscode.window.showInformationMessage(msg, ...items),
|
||||
);
|
||||
|
||||
// Heartbeat (keeps renderer script alive)
|
||||
const hbTimer = setInterval(() => sdk?.integration.signalActive(), 30_000);
|
||||
context.subscriptions.push({ dispose: () => clearInterval(hbTimer) });
|
||||
|
||||
// Auto-repair (re-patch after AG updates)
|
||||
sdk.integration.enableAutoRepair();
|
||||
|
||||
log('Active');
|
||||
} catch (err: any) {
|
||||
log(`SDK init failed: ${err.message}`);
|
||||
log('Running in degraded mode (auto-run fix only)');
|
||||
}
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
sdk?.dispose();
|
||||
sdk = null;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"ES2020"
|
||||
],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"fixes"
|
||||
]
|
||||
}
|
||||
816
bot.py
816
bot.py
@@ -5,19 +5,12 @@ Multi-project channel architecture:
|
||||
- Each conversation maps to a project via conv_to_project dict
|
||||
- Extension registers projects via bridge/pending/ files
|
||||
- Commands include project_name for routing to correct IDE window
|
||||
|
||||
Multi-PC UX:
|
||||
- When multiple AG instances are active, messages get instance numbers (PC #1, #2)
|
||||
- Users can target specific instances with !N <message> (e.g. !2 hello)
|
||||
- When only one instance is active, natural conversation without numbers
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
@@ -30,7 +23,8 @@ from parser import (
|
||||
md_to_discord_text,
|
||||
format_task_embed_text,
|
||||
)
|
||||
from models import BrainEvent, EventType, ApprovalRequest, UserResponse
|
||||
from watcher import BrainEvent, EventType
|
||||
from bridge import BridgeProtocol, ApprovalRequest, UserResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,98 +32,23 @@ logger = logging.getLogger(__name__)
|
||||
# ─── Discord UI Components ──────────────────────────────────────────
|
||||
|
||||
class ApprovalView(discord.ui.View):
|
||||
"""Discord buttons for approving/rejecting Antigravity actions.
|
||||
"""Discord buttons for approving/rejecting Antigravity actions."""
|
||||
|
||||
Supports two modes:
|
||||
1. Legacy: ✅ 승인 / ❌ 거부 (when no buttons array)
|
||||
2. Multi-choice: dynamic buttons from pending's buttons array
|
||||
(e.g., ✅ Allow Once / ✅ Allow This Conversation / ❌ Deny)
|
||||
"""
|
||||
|
||||
def __init__(self, request: ApprovalRequest,
|
||||
buttons: list[dict] | None = None, hub=None):
|
||||
super().__init__(timeout=1800) # 30 minutes
|
||||
self.hub = hub # WSHub instance for WS response routing
|
||||
def __init__(self, bridge: BridgeProtocol, request: ApprovalRequest):
|
||||
super().__init__(timeout=300)
|
||||
self.bridge = bridge
|
||||
self.request = request
|
||||
self.responded = False
|
||||
self.buttons_data = buttons
|
||||
|
||||
if buttons and len(buttons) > 1:
|
||||
# Multi-choice mode: remove the default decorated buttons first
|
||||
# (they are added by @discord.ui.button at class definition time)
|
||||
self.clear_items()
|
||||
|
||||
# Add a Discord button for each option
|
||||
for btn_info in buttons:
|
||||
btn_text = btn_info.get("text", "?")
|
||||
btn_index = btn_info.get("index", 0)
|
||||
is_reject = btn_text.lower() in ("deny", "reject", "cancel",
|
||||
"reject all", "decline",
|
||||
"dismiss", "stop")
|
||||
style = discord.ButtonStyle.red if is_reject else discord.ButtonStyle.green
|
||||
emoji = "❌" if is_reject else "✅"
|
||||
|
||||
button = discord.ui.Button(
|
||||
label=f"{emoji} {btn_text}",
|
||||
style=style,
|
||||
custom_id=f"choice_{request.request_id}_{btn_index}",
|
||||
)
|
||||
# Bind the callback with closure over btn_index and btn_text
|
||||
button.callback = self._make_choice_callback(btn_index, btn_text,
|
||||
is_reject)
|
||||
self.add_item(button)
|
||||
# else: use the default @discord.ui.button decorated methods below
|
||||
|
||||
def _make_choice_callback(self, btn_index: int, btn_text: str,
|
||||
is_reject: bool):
|
||||
async def callback(interaction: discord.Interaction):
|
||||
if self.responded:
|
||||
await interaction.response.send_message("이미 응답됨",
|
||||
ephemeral=True)
|
||||
return
|
||||
self.responded = True
|
||||
response_data = {
|
||||
"request_id": self.request.request_id,
|
||||
"approved": not is_reject,
|
||||
"button_index": btn_index,
|
||||
"step_type": getattr(self.request, 'step_type', ''),
|
||||
"project_name": getattr(self.request, 'project_name', ''),
|
||||
}
|
||||
# Hub WS route (primary — reaches remote Extensions)
|
||||
delivered = False
|
||||
if self.hub:
|
||||
await self.hub.send_response_to_pending_owner(self.request.request_id, {
|
||||
"type": "response", "data": response_data,
|
||||
})
|
||||
embed = interaction.message.embeds[0] if interaction.message.embeds else None
|
||||
if embed:
|
||||
color = discord.Color.red() if is_reject else discord.Color.green()
|
||||
embed.color = color
|
||||
emoji = "❌" if is_reject else "✅"
|
||||
embed.set_footer(
|
||||
text=f"{emoji} {btn_text} by {interaction.user.display_name}"
|
||||
)
|
||||
await interaction.response.edit_message(embed=embed, view=None)
|
||||
return callback
|
||||
|
||||
@discord.ui.button(label="✅ 승인", style=discord.ButtonStyle.green)
|
||||
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
# Only active in legacy mode (no buttons array)
|
||||
if self.buttons_data and len(self.buttons_data) > 1:
|
||||
return # multi-choice mode handles via dynamic buttons
|
||||
if self.responded:
|
||||
await interaction.response.send_message("이미 응답됨", ephemeral=True)
|
||||
return
|
||||
self.responded = True
|
||||
response_data = {
|
||||
"request_id": self.request.request_id, "approved": True,
|
||||
"step_type": getattr(self.request, 'step_type', ''),
|
||||
"project_name": getattr(self.request, 'project_name', ''),
|
||||
}
|
||||
if self.hub:
|
||||
await self.hub.send_response_to_pending_owner(self.request.request_id, {
|
||||
"type": "response", "data": response_data,
|
||||
})
|
||||
self.bridge.write_response(UserResponse(
|
||||
request_id=self.request.request_id, approved=True,
|
||||
))
|
||||
embed = interaction.message.embeds[0] if interaction.message.embeds else None
|
||||
if embed:
|
||||
embed.color = discord.Color.green()
|
||||
@@ -138,22 +57,13 @@ class ApprovalView(discord.ui.View):
|
||||
|
||||
@discord.ui.button(label="❌ 거부", style=discord.ButtonStyle.red)
|
||||
async def reject(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
# Only active in legacy mode (no buttons array)
|
||||
if self.buttons_data and len(self.buttons_data) > 1:
|
||||
return # multi-choice mode handles via dynamic buttons
|
||||
if self.responded:
|
||||
await interaction.response.send_message("이미 응답됨", ephemeral=True)
|
||||
return
|
||||
self.responded = True
|
||||
response_data = {
|
||||
"request_id": self.request.request_id, "approved": False,
|
||||
"step_type": getattr(self.request, 'step_type', ''),
|
||||
"project_name": getattr(self.request, 'project_name', ''),
|
||||
}
|
||||
if self.hub:
|
||||
await self.hub.send_response_to_pending_owner(self.request.request_id, {
|
||||
"type": "response", "data": response_data,
|
||||
})
|
||||
self.bridge.write_response(UserResponse(
|
||||
request_id=self.request.request_id, approved=False,
|
||||
))
|
||||
embed = interaction.message.embeds[0] if interaction.message.embeds else None
|
||||
if embed:
|
||||
embed.color = discord.Color.red()
|
||||
@@ -161,14 +71,10 @@ class ApprovalView(discord.ui.View):
|
||||
await interaction.response.edit_message(embed=embed, view=None)
|
||||
|
||||
async def on_timeout(self):
|
||||
if not self.responded and self.hub:
|
||||
await self.hub.send_response_to_pending_owner(self.request.request_id, {
|
||||
"type": "response", "data": {
|
||||
"request_id": self.request.request_id, "approved": False,
|
||||
"step_type": getattr(self.request, 'step_type', ''),
|
||||
"project_name": getattr(self.request, 'project_name', ''),
|
||||
}
|
||||
})
|
||||
if not self.responded:
|
||||
self.bridge.write_response(UserResponse(
|
||||
request_id=self.request.request_id, approved=False,
|
||||
))
|
||||
|
||||
|
||||
# ─── Bot ─────────────────────────────────────────────────────────────
|
||||
@@ -193,56 +99,12 @@ class GravityBot(commands.Bot):
|
||||
self.conv_to_project: dict[str, str] = {} # conv_id → project
|
||||
self.channel_to_project: dict[int, str] = {} # channel.id → project
|
||||
self.session_status_messages: dict[str, int] = {} # conv_id → msg_id
|
||||
self._sent_approval_ids: dict[str, bool] = {} # request_id → bool
|
||||
self._deferred_ids: dict[str, int] = {} # request_id → defer count
|
||||
self._sent_commands: dict[str, str] = {} # request_id → command text (for MERGE edit detection)
|
||||
self._sent_approval_ids: set[str] = set()
|
||||
self._ready_event = asyncio.Event()
|
||||
self._channel_lock = asyncio.Lock()
|
||||
self.bridge = BridgeProtocol()
|
||||
self.session_category: discord.CategoryChannel | None = None
|
||||
self.guild: discord.Guild | None = None
|
||||
self.auto_approve_projects: set[str] = set() # projects with auto-approve enabled
|
||||
self._processed_message_ids: deque[int] = deque(maxlen=200) # dedup for Gateway event replay
|
||||
self._approval_messages: dict[str, int] = {} # FIX #4: request_id → discord message_id (for auto_resolved lookup)
|
||||
self._last_auto_toggle: dict[str, float] = {} # project → timestamp (dedup for !auto embed)
|
||||
self.gateway = None # Set by main.py in gateway mode
|
||||
self.hub = None # Set by main.py in gateway mode (WSHub instance)
|
||||
|
||||
def _write_command(self, project: str, text: str, *,
|
||||
target_instance: int | None = None, **kwargs):
|
||||
"""Write command to Extension via Hub WS (primary) or file bridge (fallback).
|
||||
|
||||
When Hub is connected, ONLY use WS to prevent duplicate delivery.
|
||||
File bridge + Gateway are legacy fallbacks for when Hub is unavailable.
|
||||
|
||||
Args:
|
||||
target_instance: If set, send only to this instance number (via Hub).
|
||||
If None, broadcast to all instances.
|
||||
"""
|
||||
cmd_data = {
|
||||
"text": text,
|
||||
"project_name": kwargs.get('project_name', project),
|
||||
}
|
||||
|
||||
# Hub route (primary)
|
||||
if self.hub:
|
||||
import time as _time
|
||||
cmd_data["id"] = str(int(_time.time() * 1000))
|
||||
msg = {"type": "command", "data": cmd_data}
|
||||
if target_instance is not None:
|
||||
asyncio.create_task(
|
||||
self.hub.send_to_instance(project, target_instance, msg)
|
||||
)
|
||||
else:
|
||||
asyncio.create_task(
|
||||
self.hub.broadcast_to_project(project, msg)
|
||||
)
|
||||
|
||||
def _cap_dict(self, d: dict, max_size: int = 5000):
|
||||
"""Prevent memory leaks by capping dictionary sizes using insertion order (oldest first)."""
|
||||
if len(d) >= max_size:
|
||||
to_remove = len(d) - max_size + max_size // 10 # remove 10%
|
||||
for k in list(d.keys())[:to_remove]:
|
||||
d.pop(k, None)
|
||||
|
||||
@staticmethod
|
||||
def _make_channel_name(project_name: str) -> str:
|
||||
@@ -251,9 +113,9 @@ class GravityBot(commands.Bot):
|
||||
|
||||
async def setup_hook(self):
|
||||
self.loop.create_task(self._process_events())
|
||||
self.pending_approval_scanner.start()
|
||||
self.chat_snapshot_scanner.start()
|
||||
self._register_slash_commands()
|
||||
# Register Hub handlers (if Hub is available, set after setup_hook by main.py)
|
||||
asyncio.get_event_loop().call_soon(self._register_hub_handlers)
|
||||
logger.info("Bot setup complete")
|
||||
|
||||
def _register_slash_commands(self):
|
||||
@@ -265,7 +127,7 @@ class GravityBot(commands.Bot):
|
||||
if not project:
|
||||
await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True)
|
||||
return
|
||||
self._write_command(project, "!stop", project_name=project)
|
||||
self.bridge.write_command(project, "!stop", project_name=project)
|
||||
await interaction.response.send_message(
|
||||
embed=discord.Embed(
|
||||
title="⏹️ AI 작업 중지",
|
||||
@@ -275,19 +137,13 @@ class GravityBot(commands.Bot):
|
||||
)
|
||||
|
||||
@self.tree.command(name="auto", description="자동 승인 토글")
|
||||
async def slash_auto(interaction: discord.Interaction):
|
||||
async def slash_auto(interaction: discord.Interaction, mode: str):
|
||||
project = self.channel_to_project.get(interaction.channel_id)
|
||||
if not project:
|
||||
await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True)
|
||||
return
|
||||
# Toggle
|
||||
if project in self.auto_approve_projects:
|
||||
self.auto_approve_projects.discard(project)
|
||||
enabled = False
|
||||
else:
|
||||
self.auto_approve_projects.add(project)
|
||||
enabled = True
|
||||
self._write_command(project, f"!auto {'on' if enabled else 'off'}", project_name=project)
|
||||
enabled = mode.lower() in ("on", "true", "1")
|
||||
self.bridge.write_command(project, f"!auto {'on' if enabled else 'off'}", project_name=project)
|
||||
emoji = "🟢" if enabled else "🔴"
|
||||
await interaction.response.send_message(
|
||||
embed=discord.Embed(
|
||||
@@ -303,7 +159,7 @@ class GravityBot(commands.Bot):
|
||||
if not project:
|
||||
await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True)
|
||||
return
|
||||
self._write_command(project, message, project_name=project)
|
||||
self.bridge.write_command(project, message, project_name=project)
|
||||
await interaction.response.send_message(
|
||||
embed=discord.Embed(
|
||||
description=f"📨 → **{project}** IDE에 전달됨\n`{message[:100]}`",
|
||||
@@ -333,12 +189,57 @@ class GravityBot(commands.Bot):
|
||||
logger.error("No permission to create category!")
|
||||
return
|
||||
|
||||
# Start WS Hub processors by ensuring ready gate is open
|
||||
# Discover existing project channels
|
||||
await self._discover_channels()
|
||||
|
||||
# Load conversation → project registrations from Extension
|
||||
self._load_registrations()
|
||||
|
||||
# Sync slash commands to guild
|
||||
try:
|
||||
self.tree.copy_global_to(guild=self.guild)
|
||||
synced = await self.tree.sync(guild=self.guild)
|
||||
logger.info(f"Synced {len(synced)} slash commands to guild")
|
||||
except Exception as e:
|
||||
logger.warning(f"Slash command sync failed: {e}")
|
||||
|
||||
# Open the gate
|
||||
self._ready_event.set()
|
||||
logger.info("Ready gate opened — event processing enabled")
|
||||
|
||||
# Start scanner loops
|
||||
if not self.pending_approval_scanner.is_running():
|
||||
self.pending_approval_scanner.start()
|
||||
if not self.chat_snapshot_scanner.is_running():
|
||||
self.chat_snapshot_scanner.start()
|
||||
logger.info("Scanner loops started")
|
||||
|
||||
# ─── Channel Management ──────────────────────────────────────────
|
||||
|
||||
def _load_registrations(self):
|
||||
"""Read bridge/register/ to learn conversation → project mappings."""
|
||||
register_dir = self.bridge.bridge_dir / "register"
|
||||
if not register_dir.exists():
|
||||
return
|
||||
|
||||
count = 0
|
||||
for f in register_dir.glob("*.json"):
|
||||
try:
|
||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||
conv_id = data.get("conversation_id", "")
|
||||
project = data.get("project_name", "")
|
||||
if conv_id and project:
|
||||
self.conv_to_project[conv_id] = project
|
||||
count += 1
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
# Only log when count changes
|
||||
prev = getattr(self, '_last_reg_count', -1)
|
||||
if count != prev:
|
||||
self._last_reg_count = count
|
||||
if count:
|
||||
logger.info(f"Loaded {count} conversation→project registrations")
|
||||
|
||||
# ─── Channel Management ──────────────────────────────────────────
|
||||
|
||||
@@ -359,38 +260,32 @@ class GravityBot(commands.Bot):
|
||||
logger.info(f"Discovered {len(self.project_channels)} project channels")
|
||||
|
||||
async def _get_channel(self, project_name: str) -> discord.TextChannel:
|
||||
"""Get or create a channel for a project.
|
||||
|
||||
Uses guild.channels cache first (NO API call), only locks + creates
|
||||
if channel truly doesn't exist. This prevents O(N) fetch_channels()
|
||||
API calls when multiple projects arrive simultaneously.
|
||||
"""
|
||||
"""Get or create a channel for a project. Lock-protected."""
|
||||
if project_name in self.project_channels:
|
||||
return self.project_channels[project_name]
|
||||
|
||||
if not self.session_category:
|
||||
logger.error(f"[CHANNEL] session_category is None — cannot get channel for project={project_name}")
|
||||
return None
|
||||
|
||||
channel_name = self._make_channel_name(project_name)
|
||||
|
||||
# 1. Check guild channel cache (NO API call — instant)
|
||||
existing = discord.utils.get(
|
||||
self.guild.channels, name=channel_name,
|
||||
category_id=self.session_category.id,
|
||||
)
|
||||
if existing and isinstance(existing, discord.TextChannel):
|
||||
self.project_channels[project_name] = existing
|
||||
self.channel_to_project[existing.id] = project_name
|
||||
logger.info(f"Found channel (cache): #{channel_name}")
|
||||
return existing
|
||||
|
||||
# 2. Only lock + API call if truly creating new channel
|
||||
async with self._channel_lock:
|
||||
# Double-check after lock (another coroutine may have created it)
|
||||
# Double-check after lock
|
||||
if project_name in self.project_channels:
|
||||
return self.project_channels[project_name]
|
||||
|
||||
channel_name = self._make_channel_name(project_name)
|
||||
|
||||
# Search existing channels FIRST (prevents duplicates)
|
||||
try:
|
||||
all_channels = await self.guild.fetch_channels()
|
||||
for ch in all_channels:
|
||||
if (isinstance(ch, discord.TextChannel)
|
||||
and ch.name == channel_name
|
||||
and ch.category_id == self.session_category.id):
|
||||
self.project_channels[project_name] = ch
|
||||
self.channel_to_project[ch.id] = project_name
|
||||
logger.info(f"Found existing channel: #{channel_name}")
|
||||
return ch
|
||||
except Exception as e:
|
||||
logger.warning(f"fetch_channels failed: {e}")
|
||||
|
||||
# No existing channel — create new
|
||||
try:
|
||||
ch = await self.guild.create_text_channel(
|
||||
name=channel_name,
|
||||
@@ -412,9 +307,6 @@ class GravityBot(commands.Bot):
|
||||
except discord.errors.Forbidden:
|
||||
logger.error(f"No permission to create channel: {channel_name}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"[CHANNEL] Failed to create channel #{channel_name}: {e}")
|
||||
return None
|
||||
|
||||
def _resolve_project(self, conversation_id: str) -> str:
|
||||
"""Get project name for a conversation. Falls back to default."""
|
||||
@@ -506,93 +398,110 @@ class GravityBot(commands.Bot):
|
||||
event_label = "생성" if event.event_type == EventType.FILE_CREATED else "업데이트"
|
||||
|
||||
full_content = event.content.strip()
|
||||
if not full_content:
|
||||
full_content = "(빈 파일)"
|
||||
CHUNK_SIZE = 4000 # Discord embed desc limit is 4096
|
||||
|
||||
FILE_ATTACH_THRESHOLD = 4000 # Above this, send as file attachment
|
||||
# Split into chunks for long content
|
||||
chunks = []
|
||||
while full_content:
|
||||
chunks.append(full_content[:CHUNK_SIZE])
|
||||
full_content = full_content[CHUNK_SIZE:]
|
||||
|
||||
if len(full_content) > FILE_ATTACH_THRESHOLD:
|
||||
# Long content → summary embed + file attachment
|
||||
# Extract first meaningful paragraph for summary
|
||||
summary_lines = []
|
||||
for line in full_content.split('\n'):
|
||||
if line.strip():
|
||||
summary_lines.append(line.strip())
|
||||
if len('\n'.join(summary_lines)) > 300:
|
||||
break
|
||||
summary = '\n'.join(summary_lines[:5])
|
||||
if len(summary) > 500:
|
||||
summary = summary[:500] + '...'
|
||||
if not chunks:
|
||||
chunks = ["(빈 파일)"]
|
||||
|
||||
# First chunk with title
|
||||
embed = discord.Embed(
|
||||
title=f"{label} ({event_label}됨)",
|
||||
description=chunks[0],
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
|
||||
await channel.send(embed=embed)
|
||||
|
||||
# Additional chunks if content is long
|
||||
for i, chunk in enumerate(chunks[1:], 2):
|
||||
embed = discord.Embed(
|
||||
title=f"{label} ({event_label}됨)",
|
||||
description=f"{summary}\n\n📎 *전체 내용은 첨부 파일을 확인하세요* ({len(full_content):,}자)",
|
||||
title=f"{label} (계속 {i}/{len(chunks)})",
|
||||
description=chunk,
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
|
||||
|
||||
# Create in-memory file attachment
|
||||
import io
|
||||
file_bytes = full_content.encode('utf-8')
|
||||
discord_file = discord.File(
|
||||
io.BytesIO(file_bytes),
|
||||
filename=event.file_name,
|
||||
)
|
||||
await channel.send(embed=embed, file=discord_file)
|
||||
else:
|
||||
# Short content → inline embed (original behavior)
|
||||
embed = discord.Embed(
|
||||
title=f"{label} ({event_label}됨)",
|
||||
description=full_content,
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
|
||||
await channel.send(embed=embed)
|
||||
|
||||
# ─── Approval Scanner ────────────────────────────────────────────
|
||||
|
||||
@tasks.loop(seconds=3)
|
||||
async def pending_approval_scanner(self):
|
||||
"""Scan bridge/pending/ for new approval requests + reload registrations."""
|
||||
try:
|
||||
# Reload conv→project registrations each cycle
|
||||
self._load_registrations()
|
||||
|
||||
# Ensure channels exist for all registered projects
|
||||
for project in set(self.conv_to_project.values()):
|
||||
if project not in self.project_channels:
|
||||
await self._get_channel(project)
|
||||
logger.info(f"Auto-created channel for registered project: {project}")
|
||||
|
||||
requests = self.bridge.get_pending_requests()
|
||||
for req in requests:
|
||||
if req.request_id in self._sent_approval_ids:
|
||||
continue
|
||||
if req.discord_message_id != 0:
|
||||
continue
|
||||
|
||||
# ─── Discord → IDE Text Relay + Multi-PC UX ───────────────────────────
|
||||
# Learn project mapping from pending approval
|
||||
project = req.project_name or Config.PROJECT_NAME
|
||||
if req.conversation_id and req.conversation_id != '__global__':
|
||||
self.conv_to_project[req.conversation_id] = project
|
||||
|
||||
def _get_instance_header(self, project: str, instance_number: int) -> str:
|
||||
"""Format instance header based on active count.
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
self._sent_approval_ids.add(req.request_id)
|
||||
await self._send_approval_request(channel, req)
|
||||
except Exception as e:
|
||||
logger.error(f"Error scanning approvals: {e}")
|
||||
|
||||
Single instance: empty string (natural conversation)
|
||||
Multiple instances: **[PC #N]** prefix
|
||||
"""
|
||||
if not self.hub:
|
||||
return ""
|
||||
active = self.hub.get_active_count(project)
|
||||
if active <= 1:
|
||||
return ""
|
||||
return f"**[PC #{instance_number}]** "
|
||||
@pending_approval_scanner.before_loop
|
||||
async def before_scanner(self):
|
||||
await self.wait_until_ready()
|
||||
|
||||
def _parse_instance_target(self, text: str) -> tuple[int | None, str]:
|
||||
"""Parse !N prefix from message text.
|
||||
async def _send_approval_request(
|
||||
self, channel: discord.TextChannel, request: ApprovalRequest
|
||||
):
|
||||
embed = discord.Embed(
|
||||
title="⚠️ 승인 요청",
|
||||
description=(
|
||||
f"**명령어:**\n```\n{request.command[:1000]}\n```\n"
|
||||
f"{request.description[:500]}"
|
||||
),
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"ID: {request.request_id}")
|
||||
|
||||
Returns (target_instance, remaining_text).
|
||||
'!2 hello' -> (2, 'hello')
|
||||
'hello' -> (None, 'hello')
|
||||
'!stop' -> (None, '!stop') # special commands not treated as targeting
|
||||
"""
|
||||
match = re.match(r'^!(\d+)\s+(.+)', text, re.DOTALL)
|
||||
if match:
|
||||
return int(match.group(1)), match.group(2).strip()
|
||||
return None, text
|
||||
view = ApprovalView(self.bridge, request)
|
||||
msg = await channel.send(embed=embed, view=view)
|
||||
|
||||
pending_file = self.bridge.pending_dir / f"{request.request_id}.json"
|
||||
if pending_file.exists():
|
||||
try:
|
||||
data = json.loads(pending_file.read_text(encoding="utf-8-sig"))
|
||||
data["discord_message_id"] = msg.id
|
||||
pending_file.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
logger.info(f"Sent approval request: {request.request_id[:12]}")
|
||||
|
||||
# ─── Discord → IDE Text Relay ─────────────────────────────────────
|
||||
|
||||
async def on_message(self, message: discord.Message):
|
||||
if message.author == self.user:
|
||||
return
|
||||
|
||||
# Dedup: Discord Gateway can deliver MESSAGE_CREATE twice on reconnection
|
||||
if message.id in self._processed_message_ids:
|
||||
return
|
||||
self._processed_message_ids.append(message.id)
|
||||
|
||||
# Determine project from channel
|
||||
project = self.channel_to_project.get(message.channel.id)
|
||||
if not project:
|
||||
@@ -601,362 +510,83 @@ class GravityBot(commands.Bot):
|
||||
|
||||
text = message.content.strip()
|
||||
|
||||
# Parse !N instance targeting (before special commands)
|
||||
target_instance, actual_text = self._parse_instance_target(text)
|
||||
|
||||
# Special command: !stop — cancel AI work
|
||||
if actual_text == "!stop":
|
||||
self._write_command(project, "!stop", target_instance=target_instance,
|
||||
project_name=project)
|
||||
target_label = f" (PC #{target_instance})" if target_instance else ""
|
||||
if text == "!stop":
|
||||
self.bridge.write_command(project, "!stop", project_name=project)
|
||||
embed = discord.Embed(
|
||||
title="⏹️ AI 작업 중지",
|
||||
description=f"프로젝트: **{project}**{target_label}\n중지 요청을 Extension에 전달했습니다.",
|
||||
description=f"프로젝트: **{project}**\n중지 요청을 Extension에 전달했습니다.",
|
||||
color=discord.Color.orange(),
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
# Special command: !auto — toggle auto-approve
|
||||
if actual_text == "!auto":
|
||||
# Dedup: skip if toggled within 5s for same project (Gateway event replay)
|
||||
now = time.time()
|
||||
last = self._last_auto_toggle.get(project, 0)
|
||||
if now - last < 5.0:
|
||||
logger.info(f"[AUTO] Dedup: skipping duplicate !auto for {project} ({now-last:.1f}s ago)")
|
||||
return
|
||||
self._last_auto_toggle[project] = now
|
||||
|
||||
# Toggle per-project auto-approve
|
||||
if project in self.auto_approve_projects:
|
||||
self.auto_approve_projects.discard(project)
|
||||
enabled = False
|
||||
else:
|
||||
self.auto_approve_projects.add(project)
|
||||
enabled = True
|
||||
self._write_command(project, f"!auto {'on' if enabled else 'off'}",
|
||||
target_instance=target_instance, project_name=project)
|
||||
# Special command: !auto on/off
|
||||
if text in ("!auto on", "!auto off"):
|
||||
self.bridge.write_command(project, text, project_name=project)
|
||||
enabled = text == "!auto on"
|
||||
emoji = "🟢" if enabled else "🔴"
|
||||
mode = "자동 승인" if enabled else "수동 승인"
|
||||
embed = discord.Embed(
|
||||
title=f"{emoji} {mode} 모드",
|
||||
description=f"프로젝트: **{project}**\n"
|
||||
f"모든 승인 요청이 {'자동으로 승인됩니다' if enabled else '수동 확인이 필요합니다'}",
|
||||
f"`chat.tools.autoApprove = {enabled}`\n"
|
||||
f"`chat.agent.autoApprove = {enabled}`",
|
||||
color=discord.Color.green() if enabled else discord.Color.red(),
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
# General text relay — routed by project (+ optional instance targeting)
|
||||
if actual_text:
|
||||
self._write_command(project, actual_text, target_instance=target_instance,
|
||||
project_name=project)
|
||||
# General text relay — routed by project
|
||||
if text:
|
||||
self.bridge.write_command(project, text, project_name=project)
|
||||
await message.add_reaction("📨")
|
||||
target_label = f" PC #{target_instance}" if target_instance else ""
|
||||
embed = discord.Embed(
|
||||
description=f"📨 → **{project}**{target_label} IDE에 전달됨\n`{actual_text[:100]}`",
|
||||
description=f"📨 → **{project}** IDE에 전달됨\n`{text[:100]}`",
|
||||
color=discord.Color.blurple(),
|
||||
)
|
||||
await message.channel.send(embed=embed, delete_after=10)
|
||||
|
||||
await self.process_commands(message)
|
||||
|
||||
# ─── Hub Event Handlers ──────────────────────────────────────────
|
||||
|
||||
def _register_hub_handlers(self):
|
||||
"""Register callbacks on the Hub for Extension->Bot messages."""
|
||||
if not self.hub:
|
||||
return
|
||||
self.hub.set_bot_handlers(
|
||||
on_pending=self._hub_on_pending,
|
||||
on_chat=self._hub_on_chat,
|
||||
on_register=self._hub_on_register,
|
||||
on_auto_resolve=self._hub_on_auto_resolve,
|
||||
on_brain_event=self._hub_on_brain_event,
|
||||
)
|
||||
logger.info("[BOT] Hub handlers registered")
|
||||
|
||||
async def _hub_on_pending(self, project: str, data: dict):
|
||||
"""Handle pending approval from Hub (Extension->Hub->Bot)."""
|
||||
try:
|
||||
request_id = data.get("request_id", "")
|
||||
if not request_id:
|
||||
return
|
||||
|
||||
# Skip if already sent
|
||||
if request_id in self._sent_approval_ids:
|
||||
return
|
||||
|
||||
# Check auto_resolved / auto_approved status
|
||||
status = data.get("status", "pending")
|
||||
if status in ("auto_resolved", "expired"):
|
||||
await self._handle_auto_resolved(request_id, status)
|
||||
return
|
||||
if status == "auto_approved":
|
||||
# Bridge-level auto-approve (e.g. "Always run") — show notification only
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
cmd_text = data.get("command", "")[:200]
|
||||
desc_text = data.get("description", "")[:300]
|
||||
embed = discord.Embed(
|
||||
title="🤖 자동 승인됨 (Always run)",
|
||||
description=f"✅ **{cmd_text}**" + (f"\n```\n{desc_text}\n```" if desc_text and len(desc_text) > 3 else ""),
|
||||
color=discord.Color.green(),
|
||||
)
|
||||
embed.set_footer(text=f"auto-approve | {request_id[:12]}")
|
||||
await channel.send(embed=embed)
|
||||
self._cap_dict(self._sent_approval_ids)
|
||||
self._sent_approval_ids[request_id] = True
|
||||
logger.info(f"[HUB-PENDING] Auto-approved (Always run): {request_id[:12]} project={project}")
|
||||
return
|
||||
|
||||
instance_number = data.get("_instance_number", 0)
|
||||
pc_name = data.get("_pc_name", "")
|
||||
header = self._get_instance_header(project, instance_number)
|
||||
|
||||
# Build approval request
|
||||
request = ApprovalRequest(
|
||||
request_id=request_id,
|
||||
conversation_id=data.get("conversation_id", ""),
|
||||
command=data.get("command", ""),
|
||||
description=data.get("description", ""),
|
||||
timestamp=data.get("timestamp", time.time()),
|
||||
project_name=project,
|
||||
step_type=data.get("step_type", ""),
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Auto-approve check
|
||||
if project in self.auto_approve_projects:
|
||||
await self._auto_approve_via_hub(request)
|
||||
return
|
||||
|
||||
# Send to Discord
|
||||
channel = await self._get_channel(project)
|
||||
if not channel:
|
||||
logger.warning(f"[HUB-PENDING] No channel for project={project}")
|
||||
return
|
||||
|
||||
buttons = data.get("buttons", [])
|
||||
desc_parts = []
|
||||
if header:
|
||||
desc_parts.append(header)
|
||||
|
||||
# Clean command text (remove "Running2" artifacts → "Running 2")
|
||||
cmd_text = request.command[:200]
|
||||
import re
|
||||
cmd_text = re.sub(r'Running(\d)', r'Running \1', cmd_text)
|
||||
desc_parts.append(f"**명령:** `{cmd_text}`")
|
||||
|
||||
if buttons:
|
||||
btn_names = [b.get("text", "?") for b in buttons]
|
||||
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
|
||||
|
||||
# Clean description: strip noise headers and garbage
|
||||
desc_raw = request.description or ""
|
||||
# Remove old-style headers
|
||||
desc_raw = re.sub(r'\[AI 본문 요약\]\s*', '', desc_raw)
|
||||
desc_raw = re.sub(r'\[결행 명령\]\s*', '', desc_raw)
|
||||
# Remove lines that are clearly noise
|
||||
desc_lines = desc_raw.split('\n')
|
||||
clean_desc_lines = []
|
||||
for dline in desc_lines:
|
||||
dline_stripped = dline.strip()
|
||||
if not dline_stripped:
|
||||
continue
|
||||
# Skip UI artifacts
|
||||
if dline_stripped in ('chevron_right', 'chevron_left', 'close', 'check',
|
||||
'content_copy', 'expand_more', 'expand_less',
|
||||
'Show more', 'Show less', 'Copy', 'Edit', 'Copied!'):
|
||||
continue
|
||||
# Skip "Thought for Xs"
|
||||
if re.match(r'^Thought for \d+', dline_stripped):
|
||||
continue
|
||||
# Skip TypeScript declarations and file paths
|
||||
if re.match(r'^(declare|import|export)\s+(class|function|interface|type|enum|const)', dline_stripped):
|
||||
continue
|
||||
if re.search(r'\.ts:\d+:', dline_stripped):
|
||||
continue
|
||||
if re.search(r'extension.*src.*sdk', dline_stripped, re.IGNORECASE):
|
||||
continue
|
||||
clean_desc_lines.append(dline_stripped)
|
||||
|
||||
clean_desc = '\n'.join(clean_desc_lines).strip()
|
||||
if clean_desc and len(clean_desc) > 3:
|
||||
# Truncate and wrap in code block for readability
|
||||
if len(clean_desc) > 300:
|
||||
clean_desc = clean_desc[:300] + '…'
|
||||
desc_parts.append(f"```\n{clean_desc}\n```")
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"⚠️ 승인 요청 — {request.step_type or 'action'}",
|
||||
description="\n".join(desc_parts),
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"ID: {request_id}")
|
||||
|
||||
view = ApprovalView(request, buttons=buttons, hub=self.hub)
|
||||
msg = await channel.send(
|
||||
content=f"🔔 **새로운 승인 요청이 도착했습니다** (ID: {request_id[:8]})",
|
||||
embed=embed,
|
||||
view=view
|
||||
)
|
||||
|
||||
self._cap_dict(self._sent_approval_ids)
|
||||
self._sent_approval_ids[request_id] = True
|
||||
|
||||
self._cap_dict(self._approval_messages)
|
||||
self._approval_messages[request_id] = msg.id
|
||||
logger.info(f"[HUB-PENDING] Sent approval: {request_id[:12]} project={project} | URL: {msg.jump_url}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[HUB-PENDING] Error: {e}")
|
||||
|
||||
async def _auto_approve_via_hub(self, request: ApprovalRequest):
|
||||
"""Auto-approve a pending request via Hub."""
|
||||
self._cap_dict(self._sent_approval_ids)
|
||||
self._sent_approval_ids[request.request_id] = True
|
||||
|
||||
if self.hub:
|
||||
await self.hub.send_response_to_pending_owner(request.request_id, {
|
||||
"type": "response",
|
||||
"data": {
|
||||
"request_id": request.request_id,
|
||||
"approved": True,
|
||||
"button_index": 0,
|
||||
"step_type": request.step_type,
|
||||
"project_name": request.project_name,
|
||||
},
|
||||
})
|
||||
# Send compact auto-approved embed to Discord (was missing — caused silent approvals)
|
||||
channel = await self._get_channel(request.project_name)
|
||||
if channel:
|
||||
try:
|
||||
embed = discord.Embed(
|
||||
title="🤖 자동 승인됨",
|
||||
description=f"✅ **{request.command}**\n\n```\n{request.description[:2000]}\n```" if getattr(request, "description", "") else f"✅ **{request.command}**",
|
||||
color=discord.Color.green(),
|
||||
)
|
||||
embed.set_footer(text=f"auto-approve | {request.request_id[:12]}")
|
||||
await channel.send(embed=embed)
|
||||
except Exception as e:
|
||||
logger.error(f"[HUB-AUTO] Discord send failed: {e}")
|
||||
logger.info(f"[HUB-AUTO] Auto-approved: {request.request_id[:12]} project={request.project_name}")
|
||||
|
||||
async def _hub_on_chat(self, project: str, data: dict):
|
||||
"""Handle chat snapshot from Hub (Extension->Hub->Bot->Discord)."""
|
||||
try:
|
||||
content = data.get("content", "")
|
||||
attached_files = data.get("attached_files", [])
|
||||
if not content and not attached_files:
|
||||
return
|
||||
|
||||
instance_number = data.get("_instance_number", 0)
|
||||
header = self._get_instance_header(project, instance_number)
|
||||
|
||||
channel = await self._get_channel(project)
|
||||
if not channel:
|
||||
return
|
||||
|
||||
import io as _io
|
||||
discord_files = []
|
||||
for af in attached_files:
|
||||
af_name = af.get("name", "document.md")
|
||||
af_content = af.get("content", "")
|
||||
if af_content:
|
||||
discord_files.append(discord.File(
|
||||
_io.BytesIO(af_content.encode("utf-8")),
|
||||
filename=af_name,
|
||||
))
|
||||
|
||||
display_content = f"{header}{content}" if header else content
|
||||
|
||||
FILE_ATTACH_THRESHOLD = 4000
|
||||
if len(display_content) > FILE_ATTACH_THRESHOLD:
|
||||
summary = display_content[:500].rsplit('\n', 1)[0]
|
||||
embed = discord.Embed(
|
||||
title="💬 AI 대화 내용",
|
||||
description=f"{summary}\n\n📎 *전체 내용은 첨부 파일 참조* ({len(content):,}자)",
|
||||
color=discord.Color.purple(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
discord_files.append(discord.File(
|
||||
_io.BytesIO(content.encode("utf-8")),
|
||||
filename="chat_message.md",
|
||||
))
|
||||
await channel.send(embed=embed, files=discord_files)
|
||||
else:
|
||||
embed = discord.Embed(
|
||||
title="💬 AI 대화 내용",
|
||||
description=display_content,
|
||||
color=discord.Color.purple(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
await channel.send(
|
||||
embed=embed,
|
||||
files=discord_files if discord_files else discord.utils.MISSING,
|
||||
)
|
||||
|
||||
logger.info(f"[HUB-CHAT] Sent to #{channel.name} ({len(content)} chars)")
|
||||
except Exception as e:
|
||||
logger.error(f"[HUB-CHAT] Error: {e}")
|
||||
|
||||
async def _hub_on_register(self, data: dict):
|
||||
"""Handle session registration from Hub."""
|
||||
conv_id = data.get("conversation_id", "")
|
||||
project = data.get("project_name", "")
|
||||
if conv_id and project:
|
||||
self.conv_to_project[conv_id] = project
|
||||
logger.info(f"[HUB-REG] {conv_id[:8]} → {project}")
|
||||
|
||||
async def _hub_on_auto_resolve(self, project: str, data: dict):
|
||||
"""Handle auto_resolve notification from Hub."""
|
||||
request_id = data.get("request_id", "")
|
||||
if request_id:
|
||||
await self._handle_auto_resolved(request_id, "auto_resolved")
|
||||
|
||||
async def _hub_on_brain_event(self, project: str, data: dict):
|
||||
"""Handle brain event from Hub (Extension->Hub->Bot->Discord)."""
|
||||
try:
|
||||
from models import BrainEvent, EventType
|
||||
event = BrainEvent(
|
||||
event_type=EventType(data.get("event_type", "file_changed")),
|
||||
conversation_id=data.get("conversation_id", ""),
|
||||
file_name=data.get("file_name", ""),
|
||||
file_path=None,
|
||||
content=data.get("content", ""),
|
||||
timestamp=data.get("timestamp", time.time()),
|
||||
)
|
||||
await self.event_queue.put(event)
|
||||
except Exception as e:
|
||||
logger.error(f"[HUB-EVENT] Error: {e}")
|
||||
|
||||
async def _handle_auto_resolved(self, request_id: str, status: str):
|
||||
"""Edit Discord message to show auto-resolved/expired status."""
|
||||
msg_id = self._approval_messages.get(request_id)
|
||||
if not msg_id:
|
||||
return
|
||||
# Find the channel containing this message
|
||||
for channel in self.project_channels.values():
|
||||
try:
|
||||
msg = await channel.fetch_message(msg_id)
|
||||
embed = msg.embeds[0] if msg.embeds else None
|
||||
if embed:
|
||||
if status == "auto_resolved":
|
||||
embed.color = discord.Color.green()
|
||||
embed.set_footer(text="✅ 자동 해결됨")
|
||||
else:
|
||||
embed.color = discord.Color.greyple()
|
||||
embed.set_footer(text="⏰ 만료됨")
|
||||
await msg.edit(embed=embed, view=None)
|
||||
self._approval_messages.pop(request_id, None)
|
||||
break
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
continue
|
||||
except Exception:
|
||||
break
|
||||
|
||||
# ─── Chat Snapshot Scanner ─────────────────────────────────────────
|
||||
|
||||
@tasks.loop(seconds=5)
|
||||
async def chat_snapshot_scanner(self):
|
||||
"""Scan bridge/chat_snapshots/ for AI response dumps."""
|
||||
try:
|
||||
snapshot_dir = self.bridge.bridge_dir / "chat_snapshots"
|
||||
if not snapshot_dir.exists():
|
||||
return
|
||||
|
||||
for f in snapshot_dir.glob("*.json"):
|
||||
try:
|
||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||
project = data.get("project_name", Config.PROJECT_NAME)
|
||||
content = data.get("content", "")
|
||||
|
||||
if content:
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
# Split long content
|
||||
CHUNK = 4000
|
||||
chunks = [content[i:i+CHUNK] for i in range(0, len(content), CHUNK)]
|
||||
for i, chunk in enumerate(chunks):
|
||||
title = "💬 AI 대화 내용" if i == 0 else f"💬 (계속 {i+1}/{len(chunks)})"
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
description=chunk,
|
||||
color=discord.Color.purple(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
f.unlink() # Cleanup
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
logger.warning(f"Bad chat snapshot {f.name}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error scanning chat snapshots: {e}")
|
||||
|
||||
@chat_snapshot_scanner.before_loop
|
||||
async def before_chat_scanner(self):
|
||||
await self.wait_until_ready()
|
||||
|
||||
132
bridge.py
Normal file
132
bridge.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Bridge protocol — file-based communication between Discord bot and Antigravity.
|
||||
|
||||
Bridge directory: ~/.gemini/antigravity/bridge/
|
||||
Structure:
|
||||
bridge/
|
||||
pending/ ← Bot writes approval requests for Discord
|
||||
response/ ← Bot writes user responses from Discord
|
||||
commands/ ← Bot writes user text input from Discord
|
||||
|
||||
Protocol:
|
||||
1. VS Code Extension detects pending approval → writes JSON to pending/
|
||||
2. Bot reads pending/ → sends Discord message with ✅/❌ buttons
|
||||
3. User clicks button → Bot writes JSON to response/
|
||||
4. VS Code Extension reads response/ → executes action
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
|
||||
from config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApprovalStatus(Enum):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
TIMEOUT = "timeout"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApprovalRequest:
|
||||
"""An approval request from Antigravity."""
|
||||
request_id: str
|
||||
conversation_id: str
|
||||
command: str # The command/action needing approval
|
||||
description: str # Human-readable description
|
||||
timestamp: float
|
||||
status: str = "pending"
|
||||
discord_message_id: int = 0
|
||||
project_name: str = "" # Project routing key
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserResponse:
|
||||
"""A user response from Discord."""
|
||||
request_id: str
|
||||
approved: bool
|
||||
user_input: str = ""
|
||||
timestamp: float = 0
|
||||
|
||||
|
||||
class BridgeProtocol:
|
||||
"""Manages the file-based bridge protocol."""
|
||||
|
||||
def __init__(self):
|
||||
self.bridge_dir = Config.BRAIN_PATH.parent / "bridge"
|
||||
self.pending_dir = self.bridge_dir / "pending"
|
||||
self.response_dir = self.bridge_dir / "response"
|
||||
self.commands_dir = self.bridge_dir / "commands"
|
||||
|
||||
# Create directories
|
||||
for d in [self.pending_dir, self.response_dir, self.commands_dir]:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"Bridge protocol initialized: {self.bridge_dir}")
|
||||
|
||||
def get_pending_requests(self) -> list[ApprovalRequest]:
|
||||
"""Read all pending approval requests."""
|
||||
requests = []
|
||||
fields = {f.name for f in ApprovalRequest.__dataclass_fields__.values()}
|
||||
for f in self.pending_dir.glob("*.json"):
|
||||
try:
|
||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||
if data.get("status") == "pending":
|
||||
# Filter to known fields only
|
||||
filtered = {k: v for k, v in data.items() if k in fields}
|
||||
requests.append(ApprovalRequest(**filtered))
|
||||
except (json.JSONDecodeError, TypeError, OSError) as e:
|
||||
logger.warning(f"Bad pending request {f.name}: {e}")
|
||||
return requests
|
||||
|
||||
def write_response(self, response: UserResponse):
|
||||
"""Write a user response to the response directory."""
|
||||
response.timestamp = time.time()
|
||||
filename = f"{response.request_id}.json"
|
||||
filepath = self.response_dir / filename
|
||||
|
||||
filepath.write_text(
|
||||
json.dumps(asdict(response), ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
logger.info(f"Response written: {filename} (approved={response.approved})")
|
||||
|
||||
# Mark pending request as processed
|
||||
pending_file = self.pending_dir / filename
|
||||
if pending_file.exists():
|
||||
try:
|
||||
data = json.loads(pending_file.read_text(encoding="utf-8"))
|
||||
data["status"] = "approved" if response.approved else "rejected"
|
||||
pending_file.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
def write_command(self, conversation_id: str, text: str, *, project_name: str = ""):
|
||||
"""Write a user text command for Antigravity to consume."""
|
||||
cmd_id = f"{int(time.time() * 1000)}"
|
||||
filepath = self.commands_dir / f"{cmd_id}.json"
|
||||
|
||||
data = {
|
||||
"id": cmd_id,
|
||||
"conversation_id": conversation_id,
|
||||
"project_name": project_name,
|
||||
"text": text,
|
||||
"timestamp": time.time(),
|
||||
"consumed": False,
|
||||
}
|
||||
|
||||
filepath.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
logger.info(f"Command written: {cmd_id} → project={project_name}")
|
||||
return cmd_id
|
||||
23
config.py
23
config.py
@@ -16,11 +16,10 @@ class Config:
|
||||
DISCORD_GUILD_ID: int = int(os.getenv("DISCORD_GUILD_ID") or "0")
|
||||
|
||||
# Antigravity Brain path
|
||||
# NOTE: os.getenv returns "" (not None) when .env has BRAIN_PATH= (empty value).
|
||||
# Path("") resolves to "." (CWD), which is WRONG. Use `or` to handle both None and "".
|
||||
BRAIN_PATH: Path = Path(
|
||||
os.getenv("BRAIN_PATH") or os.path.expanduser("~/.gemini/antigravity/brain")
|
||||
)
|
||||
BRAIN_PATH: Path = Path(os.getenv(
|
||||
"BRAIN_PATH",
|
||||
os.path.expanduser("~/.gemini/antigravity/brain")
|
||||
))
|
||||
|
||||
# Watcher settings
|
||||
DEBOUNCE_SECONDS: float = float(os.getenv("DEBOUNCE_SECONDS", "5"))
|
||||
@@ -32,9 +31,6 @@ class Config:
|
||||
"walkthrough.md",
|
||||
}
|
||||
|
||||
# Extension-based monitoring: any file with these extensions in brain/{conv}/ is watched
|
||||
WATCHED_EXTENSIONS: set = {".md"}
|
||||
|
||||
# Discord message limits
|
||||
DISCORD_MSG_LIMIT: int = 2000
|
||||
DISCORD_EMBED_DESC_LIMIT: int = 4096
|
||||
@@ -43,14 +39,6 @@ class Config:
|
||||
CHANNEL_PREFIX: str = "AG"
|
||||
PROJECT_NAME: str = os.getenv("PROJECT_NAME", "gravity_control")
|
||||
|
||||
# Bot mode: 'local' (file-based bridge) or 'gateway' (WS Hub + HTTP API)
|
||||
BOT_MODE: str = os.getenv("BOT_MODE", "local")
|
||||
GATEWAY_API_KEY: str = os.getenv("GATEWAY_API_KEY", "")
|
||||
|
||||
# WebSocket Hub
|
||||
GRAVITY_HUB_SECRET: str = os.getenv("GRAVITY_HUB_SECRET", "") # JWT signing secret
|
||||
GRAVITY_REGISTRATION_CODE: str = os.getenv("GRAVITY_REGISTRATION_CODE", "") # Extension auth
|
||||
|
||||
@classmethod
|
||||
def validate(cls) -> list[str]:
|
||||
"""Return list of configuration errors."""
|
||||
@@ -59,7 +47,6 @@ class Config:
|
||||
errors.append("DISCORD_TOKEN is not set")
|
||||
if not cls.DISCORD_GUILD_ID:
|
||||
errors.append("DISCORD_GUILD_ID is not set")
|
||||
# Gateway mode doesn't need local BRAIN_PATH
|
||||
if cls.BOT_MODE != 'gateway' and not cls.BRAIN_PATH.exists():
|
||||
if not cls.BRAIN_PATH.exists():
|
||||
errors.append(f"BRAIN_PATH does not exist: {cls.BRAIN_PATH}")
|
||||
return errors
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
services:
|
||||
gateway:
|
||||
build: .
|
||||
container_name: gravity-gateway
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8585:8585"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- BOT_MODE=gateway
|
||||
- GATEWAY_PORT=8585
|
||||
- BRAIN_PATH=/app/data/brain
|
||||
volumes:
|
||||
- gateway-data:/app/data
|
||||
networks:
|
||||
- default
|
||||
- proxy-net
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: 3
|
||||
|
||||
volumes:
|
||||
gateway-data:
|
||||
|
||||
networks:
|
||||
proxy-net:
|
||||
external: true
|
||||
@@ -1,30 +0,0 @@
|
||||
services:
|
||||
gateway:
|
||||
build: .
|
||||
container_name: gravity-gateway
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8585:8585"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- BOT_MODE=gateway
|
||||
- GATEWAY_PORT=8585
|
||||
- BRAIN_PATH=/app/data/brain
|
||||
volumes:
|
||||
- gateway-data:/app/data
|
||||
networks:
|
||||
- default
|
||||
- proxy-net
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: 3
|
||||
|
||||
volumes:
|
||||
gateway-data:
|
||||
|
||||
networks:
|
||||
proxy-net:
|
||||
external: true
|
||||
@@ -1,185 +0,0 @@
|
||||
# Gravity Bridge — 승인 시스템 완전 Flow 가이드
|
||||
|
||||
> **Last Updated**: 2026-03-16 (v0.3.12)
|
||||
> **SSOT**: 이 문서는 승인 시스템의 전체 데이터 플로우와 상태 관리를 설명합니다.
|
||||
> **수정 시**: known-issues.md와 동기화 필수
|
||||
|
||||
---
|
||||
|
||||
## 1. 시스템 아키텍처 개요
|
||||
|
||||
```
|
||||
AG IDE (Antigravity)
|
||||
├── Extension (extension.ts) ← Bridge 핵심
|
||||
│ ├── setupMonitor() ← 5초 폴링 (GetAllCascadeTrajectories)
|
||||
│ ├── step_probe ← WAITING step 감지 (GetCascadeTrajectorySteps)
|
||||
│ ├── DOM Observer ← 렌더러 스크립트 (버튼 감지)
|
||||
│ ├── processResponseFile() ← Discord 응답 처리
|
||||
│ ├── writePendingApproval() ← pending 파일 생성 (dedup 포함)
|
||||
│ └── tryApprovalStrategies() ← RPC 실행
|
||||
├── bridge/ (파일 시스템)
|
||||
│ ├── pending/*.json ← 승인 대기 목록
|
||||
│ ├── response/*.json ← Discord 응답
|
||||
│ ├── snapshot/*.json ← 채팅 릴레이
|
||||
│ └── register/*.json ← 세션-프로젝트 매핑
|
||||
└── Bot (bot.py) ← Discord 통신
|
||||
├── pending_approval_scanner ← 3초 폴링
|
||||
├── auto_approve_scanner ← !auto 토글
|
||||
└── snapshot_scanner ← 채팅 릴레이
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 데이터 플로우: 승인 요청 → 응답
|
||||
|
||||
### 2.1 Pending 생성 경로 (2개)
|
||||
|
||||
#### 경로 A: Step Probe → `writePendingApproval()`
|
||||
```
|
||||
1. setupMonitor() 5초 폴링 → GetAllCascadeTrajectories
|
||||
2. RUNNING + delta=0 + modTime 미변경 → stall 감지
|
||||
3. consecutiveIdleCount >= 1 && !stallProbed
|
||||
4. GetCascadeTrajectorySteps → WAITING step 발견
|
||||
5. si !== lastPendingStepIndex 확인 (dedup)
|
||||
6. writePendingApproval() 호출
|
||||
├── recentPendingSteps 메모리 dedup 체크 (60초 TTL)
|
||||
├── 기존 pending 파일 dedup 체크 (15초 윈도우)
|
||||
└── pending 파일 생성 + recentPendingSteps에 기록
|
||||
7. stallProbed = true, lastPendingStepIndex = si
|
||||
```
|
||||
|
||||
#### 경로 B: DOM Observer → HTTP POST `/pending`
|
||||
```
|
||||
1. 렌더러 MutationObserver → 버튼 감지 (Run, Accept, Allow 등)
|
||||
2. FALSE_POSITIVE_RE 필터 (Proceed, Continue, Deny 등 차단)
|
||||
3. "Run"은 sessionStalled=true && lastPendingStepIndex < 0 일 때만 통과
|
||||
4. HTTP POST /pending → Extension HTTP 핸들러 (L738-812)
|
||||
5. 파일 직접 생성 (writePendingApproval() 우회!)
|
||||
⚠️ recentPendingSteps 메모리 dedup 미적용
|
||||
```
|
||||
|
||||
> **주의**: 경로 B는 `writePendingApproval()`의 메모리 dedup을 우회합니다. 하지만 `lastPendingStepIndex >= 0`일 때 "Run" 필터(L757)와 15초 파일 기반 dedup이 방어합니다.
|
||||
|
||||
### 2.2 Response 처리 경로
|
||||
|
||||
```
|
||||
1. Bot pending_approval_scanner → pending 파일 발견
|
||||
2. auto-approve OR Discord 버튼 → write_response() 호출
|
||||
├── response/*.json 생성
|
||||
└── pending/*.json 삭제 (!)
|
||||
3. Extension response watcher (fs.watch + 3초 폴링 fallback)
|
||||
→ processResponseFile() (300ms 딜레이)
|
||||
4. processResponseFile():
|
||||
├── 파일 존재 확인 (HTTP handler가 먼저 삭제했을 수 있음)
|
||||
├── stale timeout 필터 (2분)
|
||||
├── auto_resolved/expired 상태 skip
|
||||
├── project_name 필터
|
||||
└── tryApprovalStrategies() → RPC 실행
|
||||
5. sawRunningAfterPending = true (v0.3.12 핵심 수정)
|
||||
6. response 파일 삭제 (DOM observer 경로는 HTTP handler에 위임)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 상태 변수 완전 참조
|
||||
|
||||
### 3.1 모듈 레벨 변수 (extension.ts)
|
||||
|
||||
| 변수 | 위치 | 역할 | 설정 | 리셋 |
|
||||
|------|------|------|------|------|
|
||||
| `lastPendingStepIndex` | L707 | 마지막으로 pending을 생성한 step index | step_probe(L2047,2108), error_probe(L2178) | delta>0(L1972), session change(L1841) |
|
||||
| `stallProbed` | L708 | 현재 stall에서 probe 완료 여부 | step_probe(L2046,2107,2177) | delta>0(L1980), modTime changed(L1986), session change(L1842) |
|
||||
| `sawRunningAfterPending` | L709 | pending 후 delta>0 발생 여부 (auto_resolve gate) | delta>0(L1979), **processResponseFile(L2622)** | step_probe(L2049,2110) |
|
||||
| `sessionStalled` | L706 | AG가 stall 상태인지 | idle count≥1(L1993) | delta>0(L1937), not WAITING(L2135) |
|
||||
| `recentPendingSteps` | L54 | 메모리 기반 pending dedup Map | writePendingApproval(L2787,2837) | delta>0(L1974), TTL 60초 |
|
||||
|
||||
### 3.2 setupMonitor() 로컬 변수
|
||||
|
||||
| 변수 | 역할 |
|
||||
|------|------|
|
||||
| `consecutiveIdleCount` | 연속 idle poll 수 (stall 감지 debounce) |
|
||||
| `lastPendingTime` | 마지막 pending 생성 시간 |
|
||||
| `lastModTime` | 마지막 modifiedTime (thinking vs approval 구분) |
|
||||
| `wasRunning` | RUNNING→IDLE 전이 추적 |
|
||||
| `lastResponseCaptureStep` | 응답 캡처 dedup |
|
||||
|
||||
---
|
||||
|
||||
## 4. 핵심 상태 전이 다이어그램
|
||||
|
||||
```
|
||||
[IDLE] ──step진행(delta>0)──→ [RUNNING]
|
||||
│
|
||||
delta=0 + modTime 변동 → [THINKING] (stall 카운터 리셋)
|
||||
delta=0 + modTime 고정 → [STALLED]
|
||||
│
|
||||
!stallProbed → step_probe 실행
|
||||
│
|
||||
WAITING 발견 → [PENDING_CREATED]
|
||||
(stallProbed=true, lastPendingStepIndex=si)
|
||||
│
|
||||
┌──────────────────────────────────┤
|
||||
▼ ▼
|
||||
[DISCORD_APPROVED] [AG_LOCAL_APPROVED]
|
||||
processResponseFile() delta > 0 + !sawRunningAfterPending
|
||||
sawRunningAfterPending=true → auto_resolve → Discord 알림
|
||||
│ │
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
[STEP_PROGRESSED]
|
||||
delta > 0 → 전체 리셋
|
||||
lastPendingStepIndex = -1
|
||||
stallProbed = false
|
||||
sawRunningAfterPending = true
|
||||
recentPendingSteps 클리어
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. v0.3.12 수정 — 왜 `sawRunningAfterPending = true`인가
|
||||
|
||||
### 5.1 이전 문제: 무한 루프 (v0.3.11 이전)
|
||||
|
||||
processResponseFile이 `lastPendingStepIndex = -1`로 리셋 → step_probe가 같은 WAITING step 재감지 → 새 pending → auto-approve → response → 다시 리셋 → **무한 루프**
|
||||
|
||||
### 5.2 v0.3.11 시도: 모든 리셋 제거
|
||||
|
||||
`lastPendingStepIndex`와 `stallProbed` 리셋을 완전 제거 → **무한 루프 해소**, 하지만:
|
||||
- known-issues L479 회귀: Discord 승인 후 AG 진행 시 `sawRunningAfterPending=false` + `lastPendingStepIndex>=0` → auto_resolve 중복 알림
|
||||
- `stallProbed` 영구 잠금 우려 (실제로는 delta>0에서 자연 리셋)
|
||||
|
||||
### 5.3 v0.3.12 해결: `sawRunningAfterPending = true`
|
||||
|
||||
Discord 승인 response 처리 후 `sawRunningAfterPending = true`만 설정:
|
||||
1. ✅ 무한 루프 방지: `lastPendingStepIndex` 유지 → dedup 작동
|
||||
2. ✅ auto_resolve 중복 방지: `sawRunningAfterPending = true` → L1939 조건 FALSE
|
||||
3. ✅ stallProbed 자연 리셋: delta>0에서 L1980
|
||||
4. ✅ 신호 수집 무영향: step_probe, GetAllCascadeTrajectories 코드 미변경
|
||||
|
||||
---
|
||||
|
||||
## 6. 위험 지점 목록 (수정 시 반드시 확인)
|
||||
|
||||
| 코드 위치 | 위험 | 확인 사항 |
|
||||
|-----------|------|----------|
|
||||
| processResponseFile 리셋 (L2607+) | 무한 루프 or auto_resolve 중복 | `sawRunningAfterPending = true`만 설정. `lastPendingStepIndex`와 `stallProbed`는 건드리지 말 것 |
|
||||
| HTTP POST /pending (L738-812) | DOM observer 경로가 writePendingApproval() 우회 | "Run" 필터(L757)와 파일 기반 dedup이 방어 |
|
||||
| bridge.py write_response (L460-461) | pending 파일 삭제 | 메모리 dedup(recentPendingSteps)이 재생성 방지 |
|
||||
| auto_resolve (L1939-1977) | 중복 알림 | `sawRunningAfterPending` gate 확인 |
|
||||
| step_probe offset (L2025-2070) | 775-step 리밋 | stepOffset으로 최신 step 조회 |
|
||||
| session change (L1832-1854) | 모든 상태 초기화 | lastPendingStepIndex, stallProbed, sawRunningAfterPending 모두 리셋 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 과거 이슈 교차 참조
|
||||
|
||||
| 이슈 (known-issues.md) | 방어 코드 | 상태 |
|
||||
|------------------------|----------|------|
|
||||
| L252: 중복 승인 요청 | writePendingApproval dedup (15초 윈도우 + 메모리 dedup) | ✅ 해결 |
|
||||
| L264: pending 무한 누적 | write_response()에서 삭제 + 5분 age filter | ✅ 해결 |
|
||||
| L288: DOM observer ENOENT | isDomObserver 분기 삭제 (L2619) | ✅ 해결 |
|
||||
| L384: 크로스 프로젝트 MERGE | project_name 가드 (L2774) | ✅ 해결 |
|
||||
| L444: DEDUP 크로스 세션 | conversation_id 가드 (L2794) | ✅ 해결 |
|
||||
| L474-479: auto_resolve 중복 | `sawRunningAfterPending = true` (v0.3.12) | ✅ 해결 |
|
||||
| L493: Double-Fire auto-approve | Extension auto-approve 경로 제거, Bot 단일 경로 | ✅ 해결 |
|
||||
| L499: Deny false positive | FALSE_POSITIVE_RE + Bot reject guard | ✅ 해결 |
|
||||
@@ -1,23 +1,5 @@
|
||||
# 2026-03-08 Devlog — Bridge 프로토콜 수정 + 딥 디버깅
|
||||
# 2026-03-08 Devlog — Bridge 프로토콜 수정
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 1 | 01:00 | Extension↔Bot 프로토콜 불일치 3건 수정 + sql-wasm 번들링 | `e4dc1b1` | 🔧 |
|
||||
| 2 | 01:45~02:25 | Discord Bridge 디버깅: step 구조 파악, 승인 버튼, AI 텍스트 릴레이 | `0c3d6cd` | ✅ |
|
||||
| 3 | 05:30 | 모든 WAITING step relay 구현 + Step type 전체 매핑 (775 steps, 17 types) | - | ✅ |
|
||||
| 4 | 06:10 | 채널 등록 자동화 (writeRegistration) + Bot 파이프라인 검증 | - | ✅ |
|
||||
| 5 | 06:30 | **근본 원인 발견**: getDiagnostics.lastStepIndex stale 문제 | - | ✅ |
|
||||
| 6 | 06:45 | SDK 소스 전체 분석 (antigravity-sdk v1.6.0 — EventMonitor, CascadeManager) | - | ✅ |
|
||||
| 7 | 06:55 | **PRIMARY RELAY 재작성** — rawRPC 직접 5초 폴링으로 전환 | - | 🔧 |
|
||||
| 8 | 07:30 | **GetAllCascadeTrajectories** 기반 릴레이 — NOTIFY/TASK 정상 동작 확인 | `854f33b` | ✅ |
|
||||
| 9 | 07:50 | SDK EventMonitor 제거 — ERR_CONNECTION_REFUSED 원인 차단 (-404 lines) | `f6ae9c8` | ✅ |
|
||||
| 10 | 08:00 | GetCascadeTrajectorySteps 완전 제거 + stall-based WAITING 감지 | `9b9c9c7` | ✅ |
|
||||
| 11 | 08:10 | Stall 감지 calibration + VS Code 명령어 기반 승인 핸들러 | `f1f9a0b` | 🔧 |
|
||||
| 12 | 11:30~14:35 | 승인 로직 정밀 디버깅: IDLE→stall 전환, lastModifiedTime 구분, RPC/Commands 전수 테스트, ResolveOutstandingSteps cancel 발견 | - | 🔧 |
|
||||
| 13 | 15:00~16:52 | Multi-window 격리 (v0.3.1→0.3.4): 세션 필터, per-project 포트, 등록 경쟁 조건 수정, DOM Observer 렌더러 디버깅 | - | 🔧 |
|
||||
| 14 | 17:01~17:38 | **근본 원인 발견**: product.json 체크섬 불일치 → vscode-file:// 원본 캐시 서빙. 체크섬 수동 업데이트로 수정 | - | 🔧 |
|
||||
| 15 | 17:50~18:30 | **v0.3.5**: 포트 디스커버리 수정 (결정론적 포트 + 하드코딩), 인라인 스크립트 전환 (`<script src>` → `<script>inline</script>`), product.json 자동 체크섬 업데이트 | - | 🔧 |
|
||||
| 16 | 19:00~19:48 | 렌더러 스크립트 로딩 디버깅: sync XHR→async fetch 변환, 설치경로 불일치 발견, vscode-file:// 커스텀 파일 서빙 불가 확인, Electron 풀 재시작 필요 발견 | - | 🔧 |
|
||||
| 17 | 19:53~20:00 | **AG 재시작 성공**: GB Observer Bridge connected (port 34332), Allow Once/Allow This Conversation 감지 정상 동작 확인 | - | ✅ |
|
||||
| 18 | 20:00~20:15 | **승인 감지 최적화**: latestToolCallStep 즉시 감지 (30초→5초), DOM scan 범위 확장 (Accept all/Reject all), stall→100초 fallback | - | 🔧 |
|
||||
| 19 | 21:30~22:55 | **E2E 디버깅**: response 파일 race condition 수정, Run 버튼 regex 패턴 수정(`^Run$`→`^Run`), renderer 스크립트 소스 혼동 발견(3곳), Run 버튼은 webview iframe 내부로 DOM observer 접근 불가 확인 | - | 🔧 |
|
||||
| 1 | 01:00 | Extension↔Bot 프로토콜 불일치 3건 수정 + sql-wasm 번들링 | 커밋예정 | 🔧 |
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# 2026-03-09 Devlog
|
||||
|
||||
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||
|---|------|----------|------|------|
|
||||
| 001 | 08:00~09:17 | 승인 실행 메커니즘 연구 + step-type별 VS Code 명령 분기 구현 | included in 002 | ✅ |
|
||||
| 002 | 09:21~15:07 | SDK 승인 명령 미등록 확정 + Renderer DOM Click 구현 | `4497e96` | ✅ |
|
||||
| 003 | 15:32~17:59 | Renderer v3 deep DOM traversal (iframe/webview/shadow 관통) | `32bf5ae` | ✅ |
|
||||
| 004 | 18:08~18:23 | Deep inspect HTTP endpoint (/deep-inspect) + 렌더러 재귀 인스펙터 | `a07d9d3` | ✅ |
|
||||
| 005 | 18:30~19:28 | workbench.html inline v3 패치 누락 수정 + pre-patch 검증 | `b61cff1` | ✅ |
|
||||
| 006 | 19:38~19:56 | V8 CachedData 진단 + 캐시 삭제 (renderer 미실행 근본 원인) | docs only | ✅ |
|
||||
| 007 | 20:04~20:28 | CSP script-src `'unsafe-inline'` 패치 (renderer 미실행 진짜 근본 원인) | `08077e8` | ✅ |
|
||||
| 008 | 21:00~21:30 | **E2E 승인 플로우 성공 검증** — AG 재시작 후 renderer v3 실행 확인 + Discord 승인→명령 실행 | `520d36e` | ✅ |
|
||||
| 009 | 21:33~22:28 | 승인 플로우 튜닝 — dedup + 텍스트 정제 + stall fallback 제거 + reject 안전화 | `18b3734` | ✅ |
|
||||
| 010 | 22:38~23:10 | E2E 검증 + Retry/Dismiss/Reject all 버튼 패턴 추가 + V8 캐시 삭제 | `4ba65f9` | ✅ |
|
||||
| 011 | 23:11~23:20 | agent_guide 템플릿 통합 — 워크플로우 교체 + 플레이스홀더 적용 + 중복 helper 정리 | `4ba65f9` | ✅ |
|
||||
| 012 | 23:30~00:31 | 승인 플로우 안정화 — pending 누적/false positive/MERGE dedup/auto_resolve/timeout | `` | 🔧 |
|
||||
@@ -1,19 +0,0 @@
|
||||
# 2026-03-10 Devlog
|
||||
|
||||
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||
|---|------|----------|------|------|
|
||||
| 001 | 06:12~06:30 | Discord 승인 ENOENT race condition 수정 + 버튼 그룹화 (multi-choice) | `aab1cfb` | 🔧 |
|
||||
| 002 | 21:00~02:30 | 승인 메시지 전문 표시 + 연속 승인 감지 + file_permission scope 라우팅 | `75a3482`~`c9b4fd4` | ✅ |
|
||||
| 003 | 12:30~13:30 | 3버튼 UI + Run 중복 필터 + dedup + 인자 값 표시 | `14d2acf`~`47dbd38` | ✅ |
|
||||
| 004 | 13:30~13:55 | auto_resolved 동기화 + expired 카드 업데이트 + DOM step_index | `048ffd9` | ✅ |
|
||||
| 005 | 13:55~14:10 | #253 전체 대화 릴레이 — 사용자 메시지 + 에러 알림 | `17dd665`~`b500120` | ✅ |
|
||||
| 006 | 14:00~15:00 | Discord 에코필터 + 리로드 재전송 방지 + diff review 알림 | `82b727a`~`8fbf6bf` | ✅ |
|
||||
| 007 | 15:00~15:55 | step_type 패스스루 체인 수정 + file_permission 자동감지 | `7982263`~`d1586c5` | ✅ |
|
||||
| 008 | 16:45~17:20 | Single active project lock + stale REJECT 필터 + Vikunja 태스크 정리 | `186875a`~`95d4f85` | ✅ |
|
||||
| 009 | 17:20~17:47 | v0.3.6 릴리스 — VSIX 빌드 + start_bot.bat 런처 | `bd46bea` | ✅ |
|
||||
| 010 | 18:00~18:30 | v0.3.7 — file_permission 3-button 주입 + active_project.lock 제거 (멀티프로젝트) | `27deb2a` | ✅ |
|
||||
| 011 | 18:50~19:29 | v0.3.8 — workspace URI 기반 세션 필터링 (멀티프로젝트 격리 완성) | `ae91134` | ✅ |
|
||||
| 012 | 19:30~20:35 | 크로스 프로젝트 response watcher 우회 수정 + file_permission write 도구 3-button 매핑 | `3b834e0` | ✅ |
|
||||
| 013 | 21:04~22:19 | Deriva 신호 진단 + RUNNING 세션 우선 선택 + IDLE 채널 자동 생성 제거 | `6179c4d` | ✅ |
|
||||
| 014 | 22:23~22:47 | SDK LS 프로세스 대소문자 매칭 버그 수정 — variet-agent 신호 미도달 해결 | `21fd309` | ✅ |
|
||||
| 015 | 23:46~23:57 | v0.3.9 — SDK JS 파일 VSIX 미포함 수정 + start_bot.bat Python 경로 우선순위 | `71aa80d` | ✅ |
|
||||
@@ -1,14 +0,0 @@
|
||||
# 2026-03-11 Devlog
|
||||
|
||||
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||
|---|------|----------|------|------|
|
||||
| 001 | 00:00~00:20 | Discord 릴레이 미작동 진단 — config.py BRAIN_PATH 빈문자열 버그 수정 | `pending` | ✅ |
|
||||
| 002 | 00:20~01:05 | 크로스 프로젝트 pending DEDUP MERGE 버그 진단 및 수정 (project_name 가드 3곳) | `pending` | ✅ |
|
||||
| 003 | 09:25~09:33 | Auto-approve 기능 감사 (미구현 확인) + Vikunja 태스크 #304, #305 등록 | `pending` | ✅ |
|
||||
| 004 | 10:00~10:35 | P1: `!auto` 토글 자동 승인 구현 (bot.py + extension.ts) | `pending` | ✅ |
|
||||
| 005 | 10:35~10:45 | P2: BridgeTransport 추상화 (bridge.py 리팩토링 + config/main 모드 설정) | `pending` | ✅ |
|
||||
| 006 | 10:43~10:55 | 사용 가이드 작성 (docs/usage-guide.md) + tech-stack.md Python 경로 기록 | `c130399` | ✅ |
|
||||
| 007 | 19:28~19:35 | Gateway HTTP API + Docker (Dockerfile, docker-compose, Caddyfile) | `6dbbb57` | ✅ |
|
||||
| 008 | 19:35~19:50 | Gateway 보안: API Key 인증 미들웨어 + Caddy HTTPS + .env.example | `95da3e9` | ✅ |
|
||||
| 009 | 19:50~20:10 | RemoteTransport + CollectorBridge 구현 — Collector↔Gateway HTTP 통신 | `95c2905` | ✅ |
|
||||
| 010 | 21:30~23:48 | 아키텍처 감사: aiohttp 전환 + 보안 + 기능 누락 수정 + 나노 검증 | `d7ed454` | ✅ |
|
||||
@@ -1,9 +0,0 @@
|
||||
# 2026-03-12 Devlog
|
||||
|
||||
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||
|---|------|----------|------|------|
|
||||
| 001 | 00:34~00:47 | 429 Rate Limit 무한 루프 디버깅 — 지수 백오프 + rate limit 완화 + Collector 폴링 보호 | `d9b36cf` | ✅ |
|
||||
| 002 | 16:45~17:04 | workbench.html 0-byte 파괴 복구 — 멀티 인스턴스 race condition 방지 안전 가드 추가 | `a9feee6` | ✅ |
|
||||
| 003 | 17:10~17:55 | workbench.html 크로스 복원 CSS 깨짐 수정 — pre-patch backup + requiredMarker 구조 검증 + .orig 자동 복원 | `6d8c6f1` | ✅ |
|
||||
| 004 | 19:46~21:13 | Collector 멀티 프로젝트 command 폴링 버그 수정 + rate limit burst throttle | `ae51d28` `bcc29f9` | ✅ |
|
||||
| 005 | 22:12~22:58 | Rate limit 구조적 수정 — 점진적 백오프 + adaptive 폴링 + burst-friendly 윈도우 + stale pending 정리 | `56de714` | 🔧 |
|
||||
@@ -1,6 +0,0 @@
|
||||
# Devlog — 2026-03-13
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 001 | 08:56 | Discord 아티팩트 알림 개선 — truncation 확대, 파일 첨부 전송, 동적 .md 감시 | `e5a05e3` | ✅ |
|
||||
| 002 | 19:53 | Collector 성능 최적화 — mtime 프리체크, 프로젝트 캐시, re-forward 수정, 폴링 간격 조정 | `d4a2016` | ✅ |
|
||||
@@ -1,13 +0,0 @@
|
||||
# 2026-03-15 Devlog
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 001 | 07:00~08:16 | 승인 신호 누락 진단 & 5건 버그 수정 (DEDUP collision, fs.watch fail, default 보호, auto 확인, msg dedup) | `40e3cd5` | ✅ |
|
||||
| 002 | 08:25~08:31 | Extension v0.3.10 버전 범프 & VSIX 빌드 | `10caae1` | ✅ |
|
||||
| 003 | 10:00~10:41 | 승인 라이프사이클 race condition 4건 수정 (HTML lock, pending status skip, auto_resolve Discord 알림, Bot approval_messages) | `f962036` | ✅ |
|
||||
| 004 | 10:41~10:53 | 성능 최적화 3건 (pollResponseGroup 1500ms, renderer adaptive idle, Bot single-pass scanner) + VSIX 빌드 | `ae0509f` | ✅ |
|
||||
| 005 | 15:17~17:09 | 크로스 프로젝트 신호 오염 진단 & 승인 플로우 아키텍처 수정 — DEDUP project_name 가드, double-fire auto-approve 제거, 실패 RPC 전략 30+개 삭제 (v0.3.11) | `6739f8f` | ✅ |
|
||||
| 006 | 18:32~18:51 | Auto-approve 크래시 수정 — DOM Observer Deny false positive 필터 + Bot reject-word 가드 + AGENT.md 규칙 #10 추가 | `5e5f515` | ✅ |
|
||||
| 007 | 22:00~22:52 | 시스템 전체 감사 + 5개 파일 버그 수정 (PATS Deny 트리거 제거, auto_resolved 채팅 병합, UUID 파일명 충돌방지, IP rate limit 누수, bot.py deque) + VSIX 빌드/배포 | `c9f44af` | ✅ |
|
||||
| 008 | 23:18~23:27 | AGENT.md 로컬적 사고 방지 규칙 추가 — NEVER #10 강화(반증 의무), NEVER #11(기계적 적용 금지), ALWAYS #9(프로젝트 이력 교차 참조), Bug Report Protocol 분리 | `9b93ee9` | ✅ |
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# 2026-03-16 Devlog
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 001 | 07:30~11:10 | 승인 상태 관리 근본 원인 분석 + v0.3.12 수정 (sawRunningAfterPending gate) + approval-flow.md 시스템 Flow 문서 + known-issues 2건 추가 | `2d9fe96` | ✅ |
|
||||
| 002 | 13:25~14:20 | diff_review 핸들러 2-strategy 리팩토링 + 배포 불일치 발견/수정 + pending 순서 8초 지연 + 1차 테스트 (버튼 OK, RPC 미배포→재배포) + known-issues 2건 | `f302984` | ✅ |
|
||||
| 003 | 15:18~16:55 | diff_review steps=[] 근본 원인 분석 + 인메모리 캐시 (v0.3.13) + 3차 E2E (RPC SUCCESS but no-op) + 4가지 파라미터 실험 배포 | `00b9491` | ✅ |
|
||||
| 004 | 17:05~18:00 | AG 소스 역분석 — `AcknowledgeCascadeCodeEdit`→`acknowledgeCodeActionStep` 메서드명 오류 발견 + v0.3.14 3단계 전략 배포 + known-issues 2건 업데이트 | `5a1d4f0` | ✅ |
|
||||
| 005 | 18:13~18:43 | v0.3.14 E2E 테스트 → RPC 3개 전략 모두 실패 확인 + v0.3.15 agentAcceptAllInFile 전환 배포 + known-issues 업데이트 | `0fdf668` | ✅ |
|
||||
| 006 | 18:47~19:09 | v0.3.15 diff_review E2E 2회 성공 + 이중 승인 수정 + IDLE 종료 알림 + !auto 이중 메시지 수정 (v0.3.16) + known-issues 2건 | `3cd7122` | ✅ |
|
||||
| 007 | 19:17~20:38 | Discord 알림 누락 디버깅 — Bot snapshot 로깅 추가 + 병렬 WAITING step break 제거 + 서버 Docker 재배포 3회 + known-issues 2건 | `7f079a5` | ✅ |
|
||||
| 008 | 20:50~23:06 | 크로스 프로젝트 알림 폭주 + pending 139개 누적 + diff_review brain/ 거짓양성 — 근본 원인 6건 분석 + Watcher 프로젝트 필터 + Collector stale 정리 + Extension brain/ 제외 + known-issues 3건 | `e3f8fb9` | ✅ |
|
||||
@@ -1,28 +0,0 @@
|
||||
# Devlog — 2026-03-17
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 009 | 00:00~06:38 | Extension 모듈 분리 + Hub 통합 테스트 + VSIX v0.4.0 빌드 | `5f795b9` | ✅ |
|
||||
| 010 | 06:50~07:39 | 문서 전면 재작성 + 서버 배포 + WS 호환 수정 | `6ea3211` | ✅ |
|
||||
| 011 | 07:44~08:18 | VSIX v0.4.0 E2E 사전 검증 + WS 프록시 수정 | — | 🔧 |
|
||||
| 012 | 09:00~17:44 | VSIX E2E: workspaceUri, 이중발송, ApprovalRequest, ApprovalView WS, 응답 라우팅 | `2eea5fa` | ✅ |
|
||||
| 013 | 18:05~18:45 | Extension 모듈 분리 #398: http-bridge, html-patcher, command-handler 추출 (1296→650줄) | `6640d42` | ✅ |
|
||||
| 014 | 18:45~20:35 | WS+File dual-delivery 수정 + 에코 릴레이 수정 + VSIX v0.4.4 빌드 | `0da6291` | ✅ |
|
||||
| 015 | 20:45~21:00 | Accept All WS regression 수정 + auto_approve 이중쓰기 수정 + VSIX v0.4.5 | `47cc838` | ✅ |
|
||||
| 016 | 21:00~21:27 | 통신 아키텍처 나노단위 감사: writeRegistration 이중쓰기 + ApprovalView fallback + scanner 최적화 | — | ✅ |
|
||||
| 017 | 21:35~21:53 | Hub pending_owners 생명주기 수정: WS 재연결 시 승인 응답 소실 방지 (reconnect reassign + fallback routing) | `9ccfa83` | ✅ |
|
||||
|
||||
### #010 상세
|
||||
- **문서**: architecture.md(250줄), tech-stack.md(100줄), conventions.md(100줄) 전면 재작성 + Wiki 동기화
|
||||
- **태스크 정리**: #296 폐기, #396~#400 신규 5건 등록
|
||||
- **서버 배포**: docker-compose.yml 서버 실제 구성 반영, Caddyfile 제거, ag.variet.net 도메인 확인
|
||||
- **WS 호환**: ws-client.ts 브라우저 WebSocket API 호환 (.onopen/.onmessage) 수정
|
||||
- **Known issue**: VS Code 캐시로 Extension 코드 반영 지연 — 완전 재시작 필요
|
||||
|
||||
### #011 상세
|
||||
- **WS 프록시 수정**: NPM(openresty)에서 WebSocket Support 활성화 → 101 Switching Protocols 확인
|
||||
- **WS 인증 검증**: `wss://ag.variet.net/ws` → auth_ok, conn_id 발급, instance=#1 확인
|
||||
- **VSIX 설치**: v0.4.0 설치 확인, v0.3.16 제거, ws 모듈 수동 복사
|
||||
- **AG 설정**: `settings.json`에 hubUrl + registrationCode 설정
|
||||
- **ws 번들**: `.vscodeignore`에 `!node_modules/ws/**` 추가, `package.json`에 ws dependency
|
||||
- **미완료**: AG 재시작 후 Extension→Hub→Bot→Discord 실제 E2E 검증 필요
|
||||
@@ -1,8 +0,0 @@
|
||||
# 2026-03-18 Devlog
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 1 | 11:00 | v0.5.0 Collector 제거 + dead code 정리 + HttpBridgeContext 버그 수정 | `e763117` | ✅ |
|
||||
| 2 | 14:00 | bot.py unit tests 27건 — _write_command, _hub_on_pending, ApprovalView | `a41062b` | ✅ |
|
||||
| 3 | 14:30 | step-probe.ts 모듈 분리 → approval-handler.ts (1597→1017+411줄) + dead code 제거 | `17978a7` | ✅ |
|
||||
| 4 | 15:30 | 코드베이스 건강도 분석 + 통신 레이어 전수 감사 (8파일/7메시지타입) → 수정 필요 0건 | — | ✅ |
|
||||
@@ -1,6 +0,0 @@
|
||||
# 2026-03-19 Devlog
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 1 | 07:30 | v0.5.1 browser_subagent Allow RPC 매핑 수정 + .env 정리 | `549af6d` | ✅ |
|
||||
| 2 | 10:35 | v0.5.2 Idle→Resume 신호 소실 3중 버그 수정: auth_fail 재연결, pending_owners 보존, step-probe 리셋 | `5aad82c` | ✅ |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user