antigravity-chat-proxy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +362 -0
- package/app/api/v1/artifacts/[convId]/[filename]/route.ts +75 -0
- package/app/api/v1/artifacts/[convId]/route.ts +47 -0
- package/app/api/v1/artifacts/active/[filename]/route.ts +50 -0
- package/app/api/v1/artifacts/active/route.ts +89 -0
- package/app/api/v1/artifacts/route.ts +43 -0
- package/app/api/v1/chat/action/route.ts +30 -0
- package/app/api/v1/chat/approve/route.ts +21 -0
- package/app/api/v1/chat/history/route.ts +23 -0
- package/app/api/v1/chat/mode/route.ts +59 -0
- package/app/api/v1/chat/new/route.ts +21 -0
- package/app/api/v1/chat/reject/route.ts +21 -0
- package/app/api/v1/chat/route.ts +105 -0
- package/app/api/v1/chat/state/route.ts +23 -0
- package/app/api/v1/chat/stream/route.ts +258 -0
- package/app/api/v1/conversations/active/route.ts +117 -0
- package/app/api/v1/conversations/route.ts +189 -0
- package/app/api/v1/conversations/select/route.ts +114 -0
- package/app/api/v1/debug/dom/route.ts +30 -0
- package/app/api/v1/debug/scrape/route.ts +56 -0
- package/app/api/v1/health/route.ts +13 -0
- package/app/api/v1/windows/cdp-start/route.ts +32 -0
- package/app/api/v1/windows/cdp-status/route.ts +32 -0
- package/app/api/v1/windows/close/route.ts +67 -0
- package/app/api/v1/windows/open/route.ts +49 -0
- package/app/api/v1/windows/recent/route.ts +25 -0
- package/app/api/v1/windows/route.ts +27 -0
- package/app/api/v1/windows/select/route.ts +35 -0
- package/app/debug/page.tsx +228 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +1234 -0
- package/app/layout.tsx +42 -0
- package/app/page.tsx +10 -0
- package/bin/cli.js +601 -0
- package/components/agent-message.tsx +63 -0
- package/components/artifact-panel.tsx +133 -0
- package/components/chat-container.tsx +82 -0
- package/components/chat-input.tsx +92 -0
- package/components/conversation-selector.tsx +97 -0
- package/components/header.tsx +302 -0
- package/components/hitl-dialog.tsx +23 -0
- package/components/message-list.tsx +41 -0
- package/components/thinking-block.tsx +14 -0
- package/components/tool-call-card.tsx +75 -0
- package/components/typing-indicator.tsx +11 -0
- package/components/user-message.tsx +13 -0
- package/components/welcome-screen.tsx +38 -0
- package/hooks/use-artifacts.ts +85 -0
- package/hooks/use-chat.ts +278 -0
- package/hooks/use-conversations.ts +190 -0
- package/lib/actions/hitl.ts +113 -0
- package/lib/actions/new-chat.ts +116 -0
- package/lib/actions/send-message.ts +31 -0
- package/lib/actions/switch-conversation.ts +92 -0
- package/lib/cdp/connection.ts +95 -0
- package/lib/cdp/process-manager.ts +327 -0
- package/lib/cdp/recent-projects.ts +137 -0
- package/lib/cdp/selectors.ts +11 -0
- package/lib/context.ts +38 -0
- package/lib/init.ts +48 -0
- package/lib/logger.ts +32 -0
- package/lib/scraper/agent-mode.ts +122 -0
- package/lib/scraper/agent-state.ts +756 -0
- package/lib/scraper/chat-history.ts +138 -0
- package/lib/scraper/ide-conversations.ts +124 -0
- package/lib/sse/diff-states.ts +141 -0
- package/lib/types.ts +146 -0
- package/lib/utils.ts +7 -0
- package/next.config.ts +7 -0
- package/package.json +50 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send a chat message by typing into the Antigravity input and pressing Enter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SELECTORS } from '../cdp/selectors';
|
|
6
|
+
import { sleep } from '../utils';
|
|
7
|
+
import { logger } from '@/lib/logger';
|
|
8
|
+
import type { ProxyContext } from '../types';
|
|
9
|
+
|
|
10
|
+
export async function sendMessage(ctx: ProxyContext, text: string) {
|
|
11
|
+
if (!ctx.workbenchPage) throw new Error('Not connected to Antigravity');
|
|
12
|
+
|
|
13
|
+
logger.info(`[Chat] Sending message (${text.length} chars)...`);
|
|
14
|
+
|
|
15
|
+
await ctx.workbenchPage.click(SELECTORS.chatInput);
|
|
16
|
+
await sleep(200);
|
|
17
|
+
|
|
18
|
+
await ctx.workbenchPage.evaluate((sel: string) => {
|
|
19
|
+
const el = document.querySelector(sel);
|
|
20
|
+
if (el) {
|
|
21
|
+
el.textContent = '';
|
|
22
|
+
(el as HTMLElement).focus();
|
|
23
|
+
}
|
|
24
|
+
}, SELECTORS.chatInput);
|
|
25
|
+
|
|
26
|
+
await ctx.workbenchPage.keyboard.type(text);
|
|
27
|
+
await sleep(300);
|
|
28
|
+
|
|
29
|
+
await ctx.workbenchPage.keyboard.press('Enter');
|
|
30
|
+
logger.info(`[Chat] Sent.`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switch the active conversation in the IDE using CDP.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { sleep } from '../utils';
|
|
6
|
+
import { logger } from '@/lib/logger';
|
|
7
|
+
import type { ProxyContext } from '../types';
|
|
8
|
+
|
|
9
|
+
export async function switchIdeConversation(
|
|
10
|
+
ctx: ProxyContext,
|
|
11
|
+
conversationTitle: string
|
|
12
|
+
): Promise<boolean> {
|
|
13
|
+
if (!ctx.workbenchPage || !conversationTitle) return false;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const success = await ctx.workbenchPage.evaluate(
|
|
17
|
+
async (targetTitle: string) => {
|
|
18
|
+
const historyBtn = document.querySelector('a[data-past-conversations-toggle="true"]');
|
|
19
|
+
if (!historyBtn) return false;
|
|
20
|
+
|
|
21
|
+
let isAlreadyOpen = !!document.querySelector('.text-quickinput-foreground.opacity-50');
|
|
22
|
+
|
|
23
|
+
if (!isAlreadyOpen) {
|
|
24
|
+
historyBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
|
|
25
|
+
historyBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
|
|
26
|
+
historyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < 20; i++) {
|
|
29
|
+
await new Promise(r => setTimeout(r, 100));
|
|
30
|
+
if (document.querySelector('.text-quickinput-foreground.opacity-50')) {
|
|
31
|
+
isAlreadyOpen = true;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isAlreadyOpen) return false;
|
|
38
|
+
|
|
39
|
+
const rowSelector = '.cursor-pointer.flex.items-center.justify-between.rounded-md.text-quickinput-foreground';
|
|
40
|
+
const rowElements = Array.from(document.querySelectorAll(rowSelector));
|
|
41
|
+
|
|
42
|
+
let matchedRow: Element | null = null;
|
|
43
|
+
for (const row of rowElements) {
|
|
44
|
+
const titleEl = row.querySelector('.truncate span');
|
|
45
|
+
const title = titleEl ? titleEl.textContent?.trim() : row.textContent?.trim();
|
|
46
|
+
|
|
47
|
+
if (title === targetTitle) {
|
|
48
|
+
matchedRow = row;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (matchedRow) {
|
|
54
|
+
matchedRow.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, view: window }));
|
|
55
|
+
matchedRow.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
|
|
56
|
+
matchedRow.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, view: window }));
|
|
57
|
+
matchedRow.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
|
|
58
|
+
matchedRow.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
59
|
+
|
|
60
|
+
// The modal usually auto-closes on click.
|
|
61
|
+
// If it doesn't, we can simulate an escape or click body.
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
const stillOpen = !!document.querySelector('.text-quickinput-foreground.opacity-50');
|
|
64
|
+
if (stillOpen && historyBtn) {
|
|
65
|
+
historyBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
|
|
66
|
+
historyBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
|
|
67
|
+
historyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
68
|
+
}
|
|
69
|
+
}, 300);
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If not found, close modal
|
|
75
|
+
historyBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
|
|
76
|
+
historyBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
|
|
77
|
+
historyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
},
|
|
81
|
+
conversationTitle
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (success) {
|
|
85
|
+
await sleep(1000); // Give it a second to load the new conversation
|
|
86
|
+
}
|
|
87
|
+
return success;
|
|
88
|
+
} catch (e: any) {
|
|
89
|
+
logger.error('[Action] Error switching IDE conversation:', e);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP (Chrome DevTools Protocol) connection management.
|
|
3
|
+
* Handles connecting to Antigravity's Electron app via Puppeteer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import puppeteer, { Browser } from 'puppeteer-core';
|
|
7
|
+
import { logger } from '../logger';
|
|
8
|
+
import type { ProxyContext } from '../types';
|
|
9
|
+
|
|
10
|
+
const CDP_PORT_RAW = process.env.CDP_PORT || '9223';
|
|
11
|
+
const CDP_PORT = parseInt(CDP_PORT_RAW, 10);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Discover all workbench pages in the Electron app.
|
|
15
|
+
*/
|
|
16
|
+
export async function discoverWorkbenches(ctx: ProxyContext) {
|
|
17
|
+
if (isNaN(CDP_PORT) || CDP_PORT <= 0 || CDP_PORT > 65535) {
|
|
18
|
+
throw new Error(`[CDP] Invalid CDP_PORT configured: "${CDP_PORT_RAW}". Please check your .env.local file.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!ctx.browser || !ctx.browser.isConnected()) {
|
|
22
|
+
logger.info(`[CDP] Initializing connection on port ${CDP_PORT}...`);
|
|
23
|
+
try {
|
|
24
|
+
ctx.browser = await puppeteer.connect({
|
|
25
|
+
browserURL: `http://localhost:${CDP_PORT}`,
|
|
26
|
+
defaultViewport: null,
|
|
27
|
+
});
|
|
28
|
+
ctx.browser.on('disconnected', () => {
|
|
29
|
+
logger.info('[CDP] Browser disconnected. Resetting context...');
|
|
30
|
+
ctx.browser = null;
|
|
31
|
+
ctx.workbenchPage = null;
|
|
32
|
+
ctx.allWorkbenches = [];
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger.error(`[CDP] Failed to connect on port ${CDP_PORT}. Ensure IDE is running with --remote-debugging-port=${CDP_PORT}`);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const pages = await ctx.browser.pages();
|
|
40
|
+
|
|
41
|
+
ctx.allWorkbenches = [];
|
|
42
|
+
for (const p of pages) {
|
|
43
|
+
const url = p.url();
|
|
44
|
+
if (url.includes('workbench.html') && !url.includes('jetski')) {
|
|
45
|
+
const title = await p.title();
|
|
46
|
+
ctx.allWorkbenches.push({ page: p, title, url });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return ctx.allWorkbenches;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Connect to the default (or env-specified) workbench window.
|
|
54
|
+
*/
|
|
55
|
+
export async function connectToWorkbench(ctx: ProxyContext) {
|
|
56
|
+
await discoverWorkbenches(ctx);
|
|
57
|
+
|
|
58
|
+
if (ctx.allWorkbenches.length === 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'No workbench pages found. Is Antigravity running with --remote-debugging-port=9223?'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logger.info(`[CDP] Found ${ctx.allWorkbenches.length} Antigravity window(s).`);
|
|
65
|
+
for (let i = 0; i < ctx.allWorkbenches.length; i++) {
|
|
66
|
+
logger.info(` [${i}] ${ctx.allWorkbenches[i].title}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const targetIdx = parseInt(process.env.PROXY_PAGE || '0', 10);
|
|
70
|
+
ctx.activeWindowIdx = targetIdx;
|
|
71
|
+
ctx.workbenchPage =
|
|
72
|
+
ctx.allWorkbenches[targetIdx]?.page || ctx.allWorkbenches[0].page;
|
|
73
|
+
|
|
74
|
+
ctx.workbenchPage.on('close', () => {
|
|
75
|
+
logger.info('[CDP] Workbench page closed. Resetting context...');
|
|
76
|
+
ctx.workbenchPage = null;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
logger.info(`[CDP] Active window index set to ${ctx.activeWindowIdx}. Connected to: "${ctx.allWorkbenches[ctx.activeWindowIdx]?.title || 'unknown'}"`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Switch to a different workbench window by index.
|
|
84
|
+
*/
|
|
85
|
+
export function selectWindow(ctx: ProxyContext, idx: number) {
|
|
86
|
+
if (idx < 0 || idx >= ctx.allWorkbenches.length) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Invalid window index ${idx}. Available: 0-${ctx.allWorkbenches.length - 1}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
ctx.activeWindowIdx = idx;
|
|
92
|
+
ctx.workbenchPage = ctx.allWorkbenches[idx].page;
|
|
93
|
+
logger.info(`[CDP] Switched to window [${idx}] ${ctx.allWorkbenches[idx].title}`);
|
|
94
|
+
return ctx.allWorkbenches[idx];
|
|
95
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity Process Manager (Cross-Platform)
|
|
3
|
+
*
|
|
4
|
+
* Manages the Antigravity IDE process lifecycle:
|
|
5
|
+
* - Check if CDP server is active
|
|
6
|
+
* - Start the Antigravity binary with remote debugging
|
|
7
|
+
* - Open new windows in a specific directory
|
|
8
|
+
* - Close individual windows
|
|
9
|
+
*
|
|
10
|
+
* Supports: Linux, macOS, and Windows
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { exec, spawn, ChildProcess } from 'child_process';
|
|
14
|
+
import { promisify } from 'util';
|
|
15
|
+
import { existsSync, statSync } from 'fs';
|
|
16
|
+
import { resolve } from 'path';
|
|
17
|
+
import { logger } from '../logger';
|
|
18
|
+
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
|
|
21
|
+
const CDP_PORT_RAW = process.env.CDP_PORT || '9223';
|
|
22
|
+
const CDP_PORT = parseInt(CDP_PORT_RAW, 10);
|
|
23
|
+
const IS_WIN = process.platform === 'win32';
|
|
24
|
+
const IS_MAC = process.platform === 'darwin';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the Antigravity binary path based on the OS.
|
|
28
|
+
* Can be overridden with ANTIGRAVITY_BINARY env var.
|
|
29
|
+
*/
|
|
30
|
+
function getAntigravityBinary(): string {
|
|
31
|
+
// Allow explicit override via env
|
|
32
|
+
if (process.env.ANTIGRAVITY_BINARY) {
|
|
33
|
+
return process.env.ANTIGRAVITY_BINARY;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (IS_WIN) {
|
|
37
|
+
// Windows: check common install locations
|
|
38
|
+
const candidates = [
|
|
39
|
+
resolve(process.env.LOCALAPPDATA || '', 'Programs', 'Antigravity', 'Antigravity.exe'),
|
|
40
|
+
resolve(process.env.PROGRAMFILES || 'C:\\Program Files', 'Google', 'Antigravity', 'Antigravity.exe'),
|
|
41
|
+
resolve(process.env.PROGRAMFILES || 'C:\\Program Files', 'Antigravity', 'Antigravity.exe'),
|
|
42
|
+
];
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
if (candidate && existsSync(candidate)) return candidate;
|
|
45
|
+
}
|
|
46
|
+
// Fallback — assume it's in PATH
|
|
47
|
+
return 'Antigravity.exe';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (IS_MAC) {
|
|
51
|
+
// macOS: standard .app bundle location
|
|
52
|
+
const macBinary = '/Applications/Antigravity.app/Contents/MacOS/Antigravity';
|
|
53
|
+
if (existsSync(macBinary)) return macBinary;
|
|
54
|
+
// Also check user Applications folder
|
|
55
|
+
const userMacBinary = resolve(process.env.HOME || '', 'Applications', 'Antigravity.app', 'Contents', 'MacOS', 'Antigravity');
|
|
56
|
+
if (existsSync(userMacBinary)) return userMacBinary;
|
|
57
|
+
return macBinary; // fallback to standard path
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Linux
|
|
61
|
+
return '/usr/share/antigravity/antigravity';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Kill all existing Antigravity processes (cross-platform).
|
|
66
|
+
*/
|
|
67
|
+
async function killAllAntigravity(): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
if (IS_WIN) {
|
|
70
|
+
await execAsync('taskkill /F /IM Antigravity.exe 2>nul || exit 0');
|
|
71
|
+
} else {
|
|
72
|
+
await execAsync('killall antigravity 2>/dev/null || true');
|
|
73
|
+
}
|
|
74
|
+
logger.info('[ProcessManager] Killed existing Antigravity processes.');
|
|
75
|
+
// Wait for process cleanup
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
77
|
+
} catch {
|
|
78
|
+
// Ignore errors - process might not exist
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Track spawned processes so we can clean up if needed */
|
|
83
|
+
let spawnedProcess: ChildProcess | null = null;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if the CDP server is accessible by pinging /json endpoint.
|
|
87
|
+
*/
|
|
88
|
+
export async function isCdpServerActive(): Promise<{
|
|
89
|
+
active: boolean;
|
|
90
|
+
windowCount: number;
|
|
91
|
+
error?: string;
|
|
92
|
+
}> {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`http://localhost:${CDP_PORT}/json`);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
return { active: false, windowCount: 0, error: `HTTP ${response.status}` };
|
|
97
|
+
}
|
|
98
|
+
const pages = await response.json() as any[];
|
|
99
|
+
const workbenches = pages.filter(
|
|
100
|
+
(p: any) => p.url?.includes('workbench.html') && !p.url?.includes('jetski')
|
|
101
|
+
);
|
|
102
|
+
return { active: true, windowCount: workbenches.length };
|
|
103
|
+
} catch (e: any) {
|
|
104
|
+
return { active: false, windowCount: 0, error: e.message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Start the Antigravity binary with CDP enabled.
|
|
110
|
+
*
|
|
111
|
+
* IMPORTANT: From the KI, we know that if ANY Antigravity window exists,
|
|
112
|
+
* new windows will merge into the existing Electron process and immediately
|
|
113
|
+
* shut down the CDP server. So we must ensure a clean slate.
|
|
114
|
+
*
|
|
115
|
+
* @param projectDir - The directory to open in Antigravity
|
|
116
|
+
* @param killExisting - If true, kill all existing Antigravity processes first
|
|
117
|
+
*/
|
|
118
|
+
export async function startCdpServer(
|
|
119
|
+
projectDir: string = '.',
|
|
120
|
+
killExisting: boolean = false,
|
|
121
|
+
): Promise<{
|
|
122
|
+
success: boolean;
|
|
123
|
+
message: string;
|
|
124
|
+
pid?: number;
|
|
125
|
+
}> {
|
|
126
|
+
// Check if CDP is already active
|
|
127
|
+
const status = await isCdpServerActive();
|
|
128
|
+
if (status.active) {
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
message: `CDP server already active on port ${CDP_PORT} with ${status.windowCount} window(s).`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Kill existing Antigravity instances if requested (required for clean CDP start)
|
|
136
|
+
if (killExisting) {
|
|
137
|
+
await killAllAntigravity();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const binaryPath = getAntigravityBinary();
|
|
141
|
+
// resolve() handles both absolute ('/home/user/proj') and relative ('my-proj') paths correctly
|
|
142
|
+
const absoluteDir = resolve(projectDir || '.');
|
|
143
|
+
|
|
144
|
+
// Validate the directory exists
|
|
145
|
+
try {
|
|
146
|
+
const stat = statSync(absoluteDir);
|
|
147
|
+
if (!stat.isDirectory()) {
|
|
148
|
+
return { success: false, message: `"${absoluteDir}" is not a directory.` };
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
return { success: false, message: `Directory not found: "${absoluteDir}"` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Spawn the Antigravity binary
|
|
155
|
+
try {
|
|
156
|
+
logger.info(`[ProcessManager] Starting Antigravity: ${binaryPath} --remote-debugging-port=${CDP_PORT} --new-window ${absoluteDir}`);
|
|
157
|
+
logger.info(`[ProcessManager] Platform: ${process.platform}, Binary: ${binaryPath}`);
|
|
158
|
+
|
|
159
|
+
spawnedProcess = spawn(
|
|
160
|
+
binaryPath,
|
|
161
|
+
[`--remote-debugging-port=${CDP_PORT}`, '--new-window', absoluteDir],
|
|
162
|
+
{
|
|
163
|
+
detached: !IS_WIN, // detached not needed on Windows
|
|
164
|
+
stdio: 'ignore',
|
|
165
|
+
env: { ...process.env },
|
|
166
|
+
// On Windows, use shell to resolve .exe correctly
|
|
167
|
+
shell: IS_WIN,
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Unref so the process doesn't keep Node alive
|
|
172
|
+
spawnedProcess.unref();
|
|
173
|
+
|
|
174
|
+
const pid = spawnedProcess.pid;
|
|
175
|
+
|
|
176
|
+
spawnedProcess.on('error', (err) => {
|
|
177
|
+
logger.error(`[ProcessManager] Antigravity process error: ${err.message}`);
|
|
178
|
+
spawnedProcess = null;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
spawnedProcess.on('exit', (code) => {
|
|
182
|
+
logger.info(`[ProcessManager] Antigravity process exited with code ${code}`);
|
|
183
|
+
spawnedProcess = null;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Wait for CDP to become available (poll up to 15 seconds)
|
|
187
|
+
const started = await waitForCdp(15000);
|
|
188
|
+
if (started) {
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
message: `Antigravity started with CDP on port ${CDP_PORT}.`,
|
|
192
|
+
pid: pid || undefined,
|
|
193
|
+
};
|
|
194
|
+
} else {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
message: `Antigravity process started but CDP server did not become available on port ${CDP_PORT} within 15s.`,
|
|
198
|
+
pid: pid || undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
} catch (e: any) {
|
|
202
|
+
logger.error(`[ProcessManager] Failed to start Antigravity: ${e.message}`);
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
message: `Failed to start Antigravity: ${e.message}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Open a new Antigravity window with a specified directory.
|
|
212
|
+
*
|
|
213
|
+
* If CDP is already active (Antigravity is running), this will open
|
|
214
|
+
* a new window in the existing Electron process.
|
|
215
|
+
* If CDP is NOT active, it will start Antigravity fresh.
|
|
216
|
+
*/
|
|
217
|
+
export async function openNewWindow(projectDir: string): Promise<{
|
|
218
|
+
success: boolean;
|
|
219
|
+
message: string;
|
|
220
|
+
}> {
|
|
221
|
+
// resolve() handles both absolute ('/home/user/proj') and relative ('my-proj') paths correctly
|
|
222
|
+
const absoluteDir = resolve(projectDir);
|
|
223
|
+
|
|
224
|
+
// Validate the directory exists before trying to open
|
|
225
|
+
try {
|
|
226
|
+
const stat = statSync(absoluteDir);
|
|
227
|
+
if (!stat.isDirectory()) {
|
|
228
|
+
return { success: false, message: `"${absoluteDir}" is not a directory. Please provide a folder path.` };
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
return { success: false, message: `Directory not found: "${absoluteDir}". Make sure the path exists.` };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const status = await isCdpServerActive();
|
|
235
|
+
const binaryPath = getAntigravityBinary();
|
|
236
|
+
|
|
237
|
+
if (status.active) {
|
|
238
|
+
// Antigravity is already running — open a new window via CLI
|
|
239
|
+
// This will open a new window in the same Electron process
|
|
240
|
+
try {
|
|
241
|
+
logger.info(`[ProcessManager] Opening new window for: ${absoluteDir}`);
|
|
242
|
+
const child = spawn(
|
|
243
|
+
binaryPath,
|
|
244
|
+
['--new-window', absoluteDir],
|
|
245
|
+
{
|
|
246
|
+
detached: !IS_WIN,
|
|
247
|
+
stdio: 'ignore',
|
|
248
|
+
env: { ...process.env },
|
|
249
|
+
shell: IS_WIN,
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
child.unref();
|
|
253
|
+
|
|
254
|
+
// Wait for the new window to appear
|
|
255
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
message: `Opened new window for "${absoluteDir}".`,
|
|
259
|
+
};
|
|
260
|
+
} catch (e: any) {
|
|
261
|
+
return { success: false, message: `Failed to open window: ${e.message}` };
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
// No CDP server — start fresh with this directory
|
|
265
|
+
const result = await startCdpServer(projectDir, false);
|
|
266
|
+
return { success: result.success, message: result.message };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Close a specific workbench window by its CDP page index.
|
|
272
|
+
* Uses the CDP protocol to close the page's target.
|
|
273
|
+
*/
|
|
274
|
+
export async function closeWindow(targetId: string): Promise<{
|
|
275
|
+
success: boolean;
|
|
276
|
+
message: string;
|
|
277
|
+
}> {
|
|
278
|
+
try {
|
|
279
|
+
const response = await fetch(`http://localhost:${CDP_PORT}/json/close/${targetId}`);
|
|
280
|
+
if (response.ok) {
|
|
281
|
+
return { success: true, message: 'Window closed successfully.' };
|
|
282
|
+
} else {
|
|
283
|
+
return { success: false, message: `Failed to close window: HTTP ${response.status}` };
|
|
284
|
+
}
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
return { success: false, message: `Failed to close window: ${e.message}` };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get detailed info about all CDP targets (workbench windows).
|
|
292
|
+
*/
|
|
293
|
+
export async function getWindowTargets(): Promise<{
|
|
294
|
+
targets: Array<{ id: string; title: string; url: string }>;
|
|
295
|
+
error?: string;
|
|
296
|
+
}> {
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(`http://localhost:${CDP_PORT}/json`);
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
return { targets: [], error: `HTTP ${response.status}` };
|
|
301
|
+
}
|
|
302
|
+
const pages = await response.json() as any[];
|
|
303
|
+
const workbenches = pages
|
|
304
|
+
.filter((p: any) => p.url?.includes('workbench.html') && !p.url?.includes('jetski'))
|
|
305
|
+
.map((p: any) => ({
|
|
306
|
+
id: p.id,
|
|
307
|
+
title: p.title || 'Untitled',
|
|
308
|
+
url: p.url,
|
|
309
|
+
}));
|
|
310
|
+
return { targets: workbenches };
|
|
311
|
+
} catch (e: any) {
|
|
312
|
+
return { targets: [], error: e.message };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Poll until CDP server is accessible.
|
|
318
|
+
*/
|
|
319
|
+
async function waitForCdp(timeoutMs: number): Promise<boolean> {
|
|
320
|
+
const start = Date.now();
|
|
321
|
+
while (Date.now() - start < timeoutMs) {
|
|
322
|
+
const status = await isCdpServerActive();
|
|
323
|
+
if (status.active) return true;
|
|
324
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recent Projects Reader (Cross-Platform)
|
|
3
|
+
*
|
|
4
|
+
* Reads Antigravity's workspaceStorage to discover recently opened projects.
|
|
5
|
+
* Each workspace entry is stored as a subdirectory containing a workspace.json
|
|
6
|
+
* with the folder URI.
|
|
7
|
+
*
|
|
8
|
+
* Config paths by OS:
|
|
9
|
+
* Linux: ~/.config/Antigravity/User/workspaceStorage/
|
|
10
|
+
* macOS: ~/Library/Application Support/Antigravity/User/workspaceStorage/
|
|
11
|
+
* Windows: %APPDATA%/Antigravity/User/workspaceStorage/
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
|
|
15
|
+
import { join, basename } from 'path';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
import { logger } from '../logger';
|
|
18
|
+
|
|
19
|
+
export interface RecentProject {
|
|
20
|
+
/** Absolute filesystem path to the project directory */
|
|
21
|
+
path: string;
|
|
22
|
+
/** Short display name (last segment of the path) */
|
|
23
|
+
name: string;
|
|
24
|
+
/** ISO timestamp of last time this workspace was active */
|
|
25
|
+
lastOpened: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the Antigravity workspaceStorage directory based on the OS.
|
|
30
|
+
*/
|
|
31
|
+
function getWorkspaceStoragePath(): string {
|
|
32
|
+
const home = homedir();
|
|
33
|
+
const platform = process.platform;
|
|
34
|
+
|
|
35
|
+
if (platform === 'win32') {
|
|
36
|
+
return join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'Antigravity', 'User', 'workspaceStorage');
|
|
37
|
+
}
|
|
38
|
+
if (platform === 'darwin') {
|
|
39
|
+
return join(home, 'Library', 'Application Support', 'Antigravity', 'User', 'workspaceStorage');
|
|
40
|
+
}
|
|
41
|
+
// Linux
|
|
42
|
+
return join(process.env.XDG_CONFIG_HOME || join(home, '.config'), 'Antigravity', 'User', 'workspaceStorage');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read all workspace entries and return recent projects sorted by last-opened (desc).
|
|
47
|
+
*
|
|
48
|
+
* Filters out:
|
|
49
|
+
* - Remote workspaces (vscode-remote://)
|
|
50
|
+
* - Playground directories (/.gemini/antigravity/playground/)
|
|
51
|
+
* - Directories that no longer exist on disk
|
|
52
|
+
*/
|
|
53
|
+
export function getRecentProjects(limit: number = 15): RecentProject[] {
|
|
54
|
+
const storagePath = getWorkspaceStoragePath();
|
|
55
|
+
|
|
56
|
+
if (!existsSync(storagePath)) {
|
|
57
|
+
logger.warn(`[RecentProjects] Workspace storage not found at: ${storagePath}`);
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const entries: RecentProject[] = [];
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const dirs = readdirSync(storagePath, { withFileTypes: true });
|
|
65
|
+
|
|
66
|
+
for (const dir of dirs) {
|
|
67
|
+
if (!dir.isDirectory()) continue;
|
|
68
|
+
|
|
69
|
+
const wsJsonPath = join(storagePath, dir.name, 'workspace.json');
|
|
70
|
+
if (!existsSync(wsJsonPath)) continue;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const raw = readFileSync(wsJsonPath, 'utf-8');
|
|
74
|
+
const data = JSON.parse(raw);
|
|
75
|
+
const folderUri: string | undefined = data.folder;
|
|
76
|
+
|
|
77
|
+
if (!folderUri) continue;
|
|
78
|
+
|
|
79
|
+
// Skip remote workspaces
|
|
80
|
+
if (folderUri.startsWith('vscode-remote://')) continue;
|
|
81
|
+
|
|
82
|
+
// Extract the filesystem path from file:// URI
|
|
83
|
+
let fsPath: string;
|
|
84
|
+
if (folderUri.startsWith('file://')) {
|
|
85
|
+
fsPath = decodeURIComponent(new URL(folderUri).pathname);
|
|
86
|
+
// On Windows, strip leading / from /C:/Users/...
|
|
87
|
+
if (process.platform === 'win32' && fsPath.startsWith('/') && fsPath[2] === ':') {
|
|
88
|
+
fsPath = fsPath.substring(1);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
fsPath = folderUri;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Skip playground directories
|
|
95
|
+
if (fsPath.includes('/playground/') || fsPath.includes('\\playground\\')) continue;
|
|
96
|
+
|
|
97
|
+
// Skip directories that no longer exist
|
|
98
|
+
try {
|
|
99
|
+
const s = statSync(fsPath);
|
|
100
|
+
if (!s.isDirectory()) continue;
|
|
101
|
+
} catch {
|
|
102
|
+
continue; // Directory doesn't exist anymore
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Use the workspace storage directory's mtime as lastOpened
|
|
106
|
+
const dirPath = join(storagePath, dir.name);
|
|
107
|
+
const dirStat = statSync(dirPath);
|
|
108
|
+
|
|
109
|
+
entries.push({
|
|
110
|
+
path: fsPath,
|
|
111
|
+
name: basename(fsPath),
|
|
112
|
+
lastOpened: dirStat.mtime.toISOString(),
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
// Skip malformed workspace.json files
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (e: any) {
|
|
119
|
+
logger.error(`[RecentProjects] Failed to read workspace storage: ${e.message}`);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Sort by lastOpened descending (most recent first)
|
|
124
|
+
entries.sort((a, b) => new Date(b.lastOpened).getTime() - new Date(a.lastOpened).getTime());
|
|
125
|
+
|
|
126
|
+
// Deduplicate by path (keep the most recent entry)
|
|
127
|
+
const seen = new Set<string>();
|
|
128
|
+
const deduped: RecentProject[] = [];
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!seen.has(entry.path)) {
|
|
131
|
+
seen.add(entry.path);
|
|
132
|
+
deduped.push(entry);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return deduped.slice(0, limit);
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM selectors for the Antigravity agent side panel.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SELECTORS = {
|
|
6
|
+
chatInput:
|
|
7
|
+
'#antigravity\\.agentSidePanelInputBox [contenteditable="true"][role="textbox"]',
|
|
8
|
+
messageList: '#conversation > div:first-child .mx-auto.w-full',
|
|
9
|
+
conversation: '#conversation',
|
|
10
|
+
spinner: '.antigravity-agent-side-panel .animate-spin',
|
|
11
|
+
} as const;
|