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.
Files changed (76) hide show
  1. package/README.md +362 -0
  2. package/app/api/v1/artifacts/[convId]/[filename]/route.ts +75 -0
  3. package/app/api/v1/artifacts/[convId]/route.ts +47 -0
  4. package/app/api/v1/artifacts/active/[filename]/route.ts +50 -0
  5. package/app/api/v1/artifacts/active/route.ts +89 -0
  6. package/app/api/v1/artifacts/route.ts +43 -0
  7. package/app/api/v1/chat/action/route.ts +30 -0
  8. package/app/api/v1/chat/approve/route.ts +21 -0
  9. package/app/api/v1/chat/history/route.ts +23 -0
  10. package/app/api/v1/chat/mode/route.ts +59 -0
  11. package/app/api/v1/chat/new/route.ts +21 -0
  12. package/app/api/v1/chat/reject/route.ts +21 -0
  13. package/app/api/v1/chat/route.ts +105 -0
  14. package/app/api/v1/chat/state/route.ts +23 -0
  15. package/app/api/v1/chat/stream/route.ts +258 -0
  16. package/app/api/v1/conversations/active/route.ts +117 -0
  17. package/app/api/v1/conversations/route.ts +189 -0
  18. package/app/api/v1/conversations/select/route.ts +114 -0
  19. package/app/api/v1/debug/dom/route.ts +30 -0
  20. package/app/api/v1/debug/scrape/route.ts +56 -0
  21. package/app/api/v1/health/route.ts +13 -0
  22. package/app/api/v1/windows/cdp-start/route.ts +32 -0
  23. package/app/api/v1/windows/cdp-status/route.ts +32 -0
  24. package/app/api/v1/windows/close/route.ts +67 -0
  25. package/app/api/v1/windows/open/route.ts +49 -0
  26. package/app/api/v1/windows/recent/route.ts +25 -0
  27. package/app/api/v1/windows/route.ts +27 -0
  28. package/app/api/v1/windows/select/route.ts +35 -0
  29. package/app/debug/page.tsx +228 -0
  30. package/app/favicon.ico +0 -0
  31. package/app/globals.css +1234 -0
  32. package/app/layout.tsx +42 -0
  33. package/app/page.tsx +10 -0
  34. package/bin/cli.js +601 -0
  35. package/components/agent-message.tsx +63 -0
  36. package/components/artifact-panel.tsx +133 -0
  37. package/components/chat-container.tsx +82 -0
  38. package/components/chat-input.tsx +92 -0
  39. package/components/conversation-selector.tsx +97 -0
  40. package/components/header.tsx +302 -0
  41. package/components/hitl-dialog.tsx +23 -0
  42. package/components/message-list.tsx +41 -0
  43. package/components/thinking-block.tsx +14 -0
  44. package/components/tool-call-card.tsx +75 -0
  45. package/components/typing-indicator.tsx +11 -0
  46. package/components/user-message.tsx +13 -0
  47. package/components/welcome-screen.tsx +38 -0
  48. package/hooks/use-artifacts.ts +85 -0
  49. package/hooks/use-chat.ts +278 -0
  50. package/hooks/use-conversations.ts +190 -0
  51. package/lib/actions/hitl.ts +113 -0
  52. package/lib/actions/new-chat.ts +116 -0
  53. package/lib/actions/send-message.ts +31 -0
  54. package/lib/actions/switch-conversation.ts +92 -0
  55. package/lib/cdp/connection.ts +95 -0
  56. package/lib/cdp/process-manager.ts +327 -0
  57. package/lib/cdp/recent-projects.ts +137 -0
  58. package/lib/cdp/selectors.ts +11 -0
  59. package/lib/context.ts +38 -0
  60. package/lib/init.ts +48 -0
  61. package/lib/logger.ts +32 -0
  62. package/lib/scraper/agent-mode.ts +122 -0
  63. package/lib/scraper/agent-state.ts +756 -0
  64. package/lib/scraper/chat-history.ts +138 -0
  65. package/lib/scraper/ide-conversations.ts +124 -0
  66. package/lib/sse/diff-states.ts +141 -0
  67. package/lib/types.ts +146 -0
  68. package/lib/utils.ts +7 -0
  69. package/next.config.ts +7 -0
  70. package/package.json +50 -0
  71. package/public/file.svg +1 -0
  72. package/public/globe.svg +1 -0
  73. package/public/next.svg +1 -0
  74. package/public/vercel.svg +1 -0
  75. package/public/window.svg +1 -0
  76. 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;