diff --git a/.agents/workflows/services.md b/.agents/workflows/services.md index c7b23e1..01b0535 100644 --- a/.agents/workflows/services.md +++ b/.agents/workflows/services.md @@ -12,7 +12,7 @@ description: Gravity Web 프로젝트 연동 서비스 URL, API 키, 프로젝 | 항목 | 값 | |------|-----| | **Node.js** | 시스템 설치 (`node`, `npm`) | -| **Python (helper)** | `C:\ProgramData\miniforge3\envs\deriva\python.exe` | +| **Python (helper)** | `C:\ProgramData\miniforge3\envs\gravity_web\python.exe` | | **프로젝트 루트** | `c:\Users\Certes\Desktop\gravity_web` | | **Shell** | PowerShell (`curl` = `Invoke-WebRequest` 별칭이므로 반드시 **`curl.exe`** 사용) | | **서버 실행** | `cd server && cmd /c node index.js` (port 3300) | diff --git a/server/cdp-client.js b/server/cdp-client.js index 4571c23..d30573a 100644 --- a/server/cdp-client.js +++ b/server/cdp-client.js @@ -2,6 +2,15 @@ * CDP Client — Antigravity IDE와 Chrome DevTools Protocol로 통신 * * 채팅 DOM 스크래핑, 메시지 전송, 상태 모니터링 + * + * Antigravity DOM 구조 (v1.107.0 기준): + * .antigravity-agent-side-panel + * └ #conversation ← 대화 스크롤 영역 + * ├ .overflow-y-auto ← 메시지 스크롤 컨테이너 + * │ └ (메시지 블록들) + * └ .text-ide-message-block-bot-color ← 입력 영역 래퍼 + * └ #antigravity.agentSidePanelInputBox + * └ [contenteditable][role="textbox"] ← 입력창 */ const CDP = require('chrome-remote-interface'); @@ -25,10 +34,14 @@ class CDPClient { try { // CDP 타겟 목록에서 올바른 페이지 찾기 const targets = await CDP.List({ host: this.host, port: this.port }); - - // Antigravity 메인 윈도우 타겟 찾기 (type: 'page') - const pageTarget = targets.find(t => t.type === 'page' && !t.url.includes('devtools')); - + + // Antigravity 메인 워크벤치 타겟 (jetski-agent 런치패드 제외) + const pageTarget = targets.find(t => + t.type === 'page' && + t.url.includes('workbench.html') && + !t.url.includes('jetski') + ) || targets.find(t => t.type === 'page' && !t.url.includes('devtools')); + if (!pageTarget) { throw new Error('Antigravity 페이지 타겟을 찾을 수 없습니다'); } @@ -39,8 +52,8 @@ class CDPClient { target: pageTarget, }); - const { Runtime, DOM, Page } = this.client; - + const { Runtime, DOM, Page, Input } = this.client; + await Promise.all([ Runtime.enable(), DOM.enable(), @@ -82,8 +95,7 @@ class CDPClient { /** * 채팅 영역의 DOM을 스크래핑 * - * Antigravity의 채팅 컨테이너를 JS로 탐색하여 HTML 반환 - * DOM 셀렉터는 실제 Antigravity 버전에 따라 조정 필요 + * Antigravity의 에이전트 사이드 패널에서 대화 내용을 추출 */ async scrapeChatDOM() { if (!this.connected || !this.client) return null; @@ -92,13 +104,10 @@ class CDPClient { const { result } = await this.client.Runtime.evaluate({ expression: ` (function() { - // Antigravity 채팅 컨테이너 셀렉터들 (우선순위 순) + // Antigravity 실제 DOM 셀렉터 (우선순위 순) const selectors = [ - '.chat-messages-container', - '[class*="chat"][class*="container"]', - '[class*="conversation"]', - '[class*="messages"]', - '.monaco-workbench .part.panel .content', + '#conversation', + '.antigravity-agent-side-panel', ]; for (const sel of selectors) { @@ -108,8 +117,7 @@ class CDPClient { } } - // 폴백: body 전체(디버깅용) - return ''; + return ''; })() `, returnByValue: true, @@ -124,6 +132,8 @@ class CDPClient { /** * Antigravity 채팅 입력창에 메시지를 전송 + * + * Antigravity는 textarea가 아닌 contenteditable div를 사용함 */ async sendMessage(text) { if (!this.connected || !this.client) { @@ -131,24 +141,23 @@ class CDPClient { } try { - // 1) 입력창 찾기 및 포커스 + // 1) contenteditable 입력창 찾기 및 포커스 const { result: focusResult } = await this.client.Runtime.evaluate({ expression: ` (function() { + // Antigravity 입력창 셀렉터 (우선순위 순) const selectors = [ - 'textarea[class*="chat"]', - '[class*="chat"] textarea', - '[class*="input"] textarea', - 'textarea[placeholder]', - '.chat-input textarea', - '[contenteditable="true"]', + '#antigravity\\\\.agentSidePanelInputBox [contenteditable="true"][role="textbox"]', + '.antigravity-agent-side-panel [contenteditable="true"][role="textbox"]', + '#conversation [contenteditable="true"]', + '[contenteditable="true"][role="textbox"]', ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el) { el.focus(); - return { found: true, tag: el.tagName, sel: sel }; + return { found: true, sel: sel }; } } return { found: false }; @@ -161,25 +170,24 @@ class CDPClient { return { success: false, error: '채팅 입력창을 찾을 수 없습니다' }; } - // 2) 텍스트 입력 (clipboardData 방식 - 가장 범용적) + // 2) 텍스트 입력 (contenteditable용 — execCommand 방식) await this.client.Runtime.evaluate({ expression: ` (function() { const el = document.querySelector('${focusResult.value.sel}'); if (!el) return; - // contenteditable인 경우 - if (el.contentEditable === 'true') { - el.textContent = ${JSON.stringify(text)}; - el.dispatchEvent(new Event('input', { bubbles: true })); - } else { - // textarea인 경우 — React setState 호환 - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLTextAreaElement.prototype, 'value' - ).set; - nativeInputValueSetter.call(el, ${JSON.stringify(text)}); - el.dispatchEvent(new Event('input', { bubbles: true })); - } + el.focus(); + + // 기존 내용 선택 후 삭제 + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(el); + selection.removeAllRanges(); + selection.addRange(range); + + // 텍스트 삽입 (React/Preact 호환) + document.execCommand('insertText', false, ${JSON.stringify(text)}); })() `, returnByValue: true, @@ -214,7 +222,7 @@ class CDPClient { */ startPolling(intervalMs = 1000) { this.stopPolling(); - + this.pollInterval = setInterval(async () => { if (!this.connected) { this.stopPolling();