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,189 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { getIdeConversations } from '@/lib/scraper/ide-conversations';
|
|
8
|
+
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
const BRAIN_DIR = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
|
|
12
|
+
|
|
13
|
+
function extractTitle(convDir: string): string | null {
|
|
14
|
+
const taskFile = path.join(convDir, 'task.md');
|
|
15
|
+
let title = null;
|
|
16
|
+
try {
|
|
17
|
+
if (fs.existsSync(taskFile)) {
|
|
18
|
+
const content = fs.readFileSync(taskFile, 'utf-8');
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (trimmed.startsWith('# ')) {
|
|
23
|
+
title = trimmed.slice(2).trim();
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch { /* ignore */ }
|
|
29
|
+
return title;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getConversationFiles(convDir: string) {
|
|
33
|
+
try {
|
|
34
|
+
const results: any[] = [];
|
|
35
|
+
const entries = fs.readdirSync(convDir, { recursive: true, withFileTypes: true });
|
|
36
|
+
for (const d of entries) {
|
|
37
|
+
if (!d.isFile() || !d.name.endsWith('.md')) continue;
|
|
38
|
+
|
|
39
|
+
// Node 20+ uses parentPath, older Node might use path. Fallback to convDir just in case
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
const parentDir = d.parentPath || d.path || convDir;
|
|
42
|
+
const fullPath = path.join(parentDir, d.name);
|
|
43
|
+
const relPath = path.relative(convDir, fullPath).replace(/\\/g, '/');
|
|
44
|
+
|
|
45
|
+
if (relPath.split('/').some(p => p.startsWith('.'))) continue;
|
|
46
|
+
|
|
47
|
+
const stat = fs.statSync(fullPath);
|
|
48
|
+
results.push({ name: relPath, size: stat.size, mtime: stat.mtime.toISOString() });
|
|
49
|
+
}
|
|
50
|
+
return results;
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Compute a simple word overlap score for fuzzy string matching
|
|
57
|
+
function getMatchScore(s1: string, s2: string): number {
|
|
58
|
+
if (!s1 || !s2) return 0;
|
|
59
|
+
const words1 = s1.toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(w => w.length > 2);
|
|
60
|
+
const words2 = s2.toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(w => w.length > 2);
|
|
61
|
+
|
|
62
|
+
if (words1.length === 0 || words2.length === 0) return 0;
|
|
63
|
+
|
|
64
|
+
let matches = 0;
|
|
65
|
+
for (const w of words1) {
|
|
66
|
+
if (words2.includes(w)) matches++;
|
|
67
|
+
}
|
|
68
|
+
return matches / Math.min(words1.length, words2.length);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* GET /api/v1/conversations — list conversations from the IDE's history panel.
|
|
73
|
+
* These are per-window (only shows conversations for the active window).
|
|
74
|
+
*/
|
|
75
|
+
export async function GET() {
|
|
76
|
+
await ensureCdpConnection();
|
|
77
|
+
|
|
78
|
+
if (!ctx.workbenchPage) {
|
|
79
|
+
return NextResponse.json({ conversations: [] });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const ideConversations = await getIdeConversations(ctx);
|
|
84
|
+
|
|
85
|
+
// Get the globally active backend title
|
|
86
|
+
let globalActiveTitle: string | null = ctx.activeTitle || null;
|
|
87
|
+
if (ctx.activeConversationId) {
|
|
88
|
+
const dirPath = path.join(BRAIN_DIR, ctx.activeConversationId);
|
|
89
|
+
const title = extractTitle(dirPath);
|
|
90
|
+
if (title) globalActiveTitle = title;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Pre-calculate BRAIN metadata so we can map it to IDE conversations
|
|
94
|
+
const brainData: any[] = [];
|
|
95
|
+
if (fs.existsSync(BRAIN_DIR)) {
|
|
96
|
+
const entries = fs.readdirSync(BRAIN_DIR, { withFileTypes: true });
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
99
|
+
const dirPath = path.join(BRAIN_DIR, entry.name);
|
|
100
|
+
const files = getConversationFiles(dirPath);
|
|
101
|
+
const title = extractTitle(dirPath);
|
|
102
|
+
const latestMtime = files.reduce((max, f) => {
|
|
103
|
+
const t = new Date(f.mtime).getTime();
|
|
104
|
+
return t > max ? t : max;
|
|
105
|
+
}, 0);
|
|
106
|
+
brainData.push({ id: entry.name, title, files, mtime: new Date(latestMtime).toISOString() });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let foundActive = false;
|
|
112
|
+
let conversations = ideConversations.map((c) => {
|
|
113
|
+
const isActive = globalActiveTitle
|
|
114
|
+
? (c.title === globalActiveTitle || c.title.includes(globalActiveTitle) || globalActiveTitle.includes(c.title))
|
|
115
|
+
: c.active;
|
|
116
|
+
if (isActive) foundActive = true;
|
|
117
|
+
|
|
118
|
+
// Map IDE title back to Brain
|
|
119
|
+
let mappedId = c.index.toString();
|
|
120
|
+
let files: any[] = [];
|
|
121
|
+
let mtime: string | undefined = undefined;
|
|
122
|
+
|
|
123
|
+
let bestMatch: any = null;
|
|
124
|
+
for (const bd of brainData) {
|
|
125
|
+
if (!bd.title) continue;
|
|
126
|
+
if (bd.title === c.title || bd.title.includes(c.title) || c.title.includes(bd.title)) {
|
|
127
|
+
mappedId = bd.id;
|
|
128
|
+
files = bd.files;
|
|
129
|
+
mtime = bd.mtime;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const score = getMatchScore(bd.title, c.title);
|
|
133
|
+
const bdtime = new Date(bd.mtime).getTime();
|
|
134
|
+
if (score > 0 && (!bestMatch || score > bestMatch.score || (score === bestMatch.score && bdtime > bestMatch.time))) {
|
|
135
|
+
bestMatch = { ...bd, score, time: bdtime };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (mappedId === c.index.toString() && bestMatch && bestMatch.score >= 0.2) {
|
|
140
|
+
mappedId = bestMatch.id;
|
|
141
|
+
files = bestMatch.files;
|
|
142
|
+
mtime = bestMatch.mtime;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
id: mappedId,
|
|
147
|
+
title: c.title,
|
|
148
|
+
active: isActive,
|
|
149
|
+
index: c.index,
|
|
150
|
+
files,
|
|
151
|
+
mtime
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// If the globally active conversation isn't in the IDE dropdown yet, but we know it's active
|
|
156
|
+
if (!foundActive && globalActiveTitle) {
|
|
157
|
+
let files: any[] = [];
|
|
158
|
+
let mtime: string | undefined = undefined;
|
|
159
|
+
let mappedId = '-1';
|
|
160
|
+
|
|
161
|
+
if (ctx.activeConversationId) {
|
|
162
|
+
const bd = brainData.find(b => b.id === ctx.activeConversationId);
|
|
163
|
+
if (bd) {
|
|
164
|
+
mappedId = bd.id;
|
|
165
|
+
files = bd.files;
|
|
166
|
+
mtime = bd.mtime;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
conversations.unshift({
|
|
171
|
+
id: mappedId,
|
|
172
|
+
title: globalActiveTitle,
|
|
173
|
+
active: true,
|
|
174
|
+
index: -1,
|
|
175
|
+
files,
|
|
176
|
+
mtime
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Fallback: if nothing is active, default back to the first one like the IDE does
|
|
181
|
+
if (!foundActive && conversations.length > 0 && !globalActiveTitle) {
|
|
182
|
+
conversations[0].active = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return NextResponse.json({ conversations });
|
|
186
|
+
} catch (err: any) {
|
|
187
|
+
return NextResponse.json({ error: err.message }, { status: 500 });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { switchIdeConversation } from '@/lib/actions/switch-conversation';
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
export const dynamic = 'force-dynamic';
|
|
11
|
+
|
|
12
|
+
const BRAIN_DIR = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
|
|
13
|
+
|
|
14
|
+
function extractTitle(convDir: string): string | null {
|
|
15
|
+
const taskFile = path.join(convDir, 'task.md');
|
|
16
|
+
let title = null;
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(taskFile)) {
|
|
19
|
+
const content = fs.readFileSync(taskFile, 'utf-8');
|
|
20
|
+
const lines = content.split('\n');
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (trimmed.startsWith('# ')) {
|
|
24
|
+
title = trimmed.slice(2).trim();
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch { /* ignore */ }
|
|
30
|
+
return title;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Compute a simple word overlap score for fuzzy string matching
|
|
34
|
+
function getMatchScore(s1: string, s2: string): number {
|
|
35
|
+
if (!s1 || !s2) return 0;
|
|
36
|
+
const words1 = s1.toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(w => w.length > 2);
|
|
37
|
+
const words2 = s2.toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(w => w.length > 2);
|
|
38
|
+
|
|
39
|
+
if (words1.length === 0 || words2.length === 0) return 0;
|
|
40
|
+
|
|
41
|
+
let matches = 0;
|
|
42
|
+
for (const w of words1) {
|
|
43
|
+
if (words2.includes(w)) matches++;
|
|
44
|
+
}
|
|
45
|
+
return matches / Math.min(words1.length, words2.length); // 0.0 to 1.0 (overlap ratio)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* POST /api/v1/conversations/select — switch conversation in the IDE.
|
|
50
|
+
* Accepts { title: string }
|
|
51
|
+
*/
|
|
52
|
+
export async function POST(request: NextRequest) {
|
|
53
|
+
await ensureCdpConnection();
|
|
54
|
+
|
|
55
|
+
const body = await request.json();
|
|
56
|
+
const { title } = body;
|
|
57
|
+
if (!title) {
|
|
58
|
+
return NextResponse.json({ error: 'title is required' }, { status: 400 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!ctx.workbenchPage) {
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{ error: 'Not connected to Antigravity' },
|
|
64
|
+
{ status: 503 }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// 1. Switch the IDE UI
|
|
70
|
+
const success = await switchIdeConversation(ctx, title);
|
|
71
|
+
|
|
72
|
+
// 2. Add memory cache of the selected title, in case we can't find the UUID
|
|
73
|
+
ctx.activeTitle = title;
|
|
74
|
+
ctx.activeConversationId = null; // Reset it
|
|
75
|
+
|
|
76
|
+
// 3. Map the title back to a brain directory so the artifact panel works
|
|
77
|
+
if (fs.existsSync(BRAIN_DIR)) {
|
|
78
|
+
const entries = fs.readdirSync(BRAIN_DIR, { withFileTypes: true });
|
|
79
|
+
|
|
80
|
+
let bestMatch: { id: string; score: number; mtime: number } | null = null;
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
84
|
+
const dirPath = path.join(BRAIN_DIR, entry.name);
|
|
85
|
+
const convTitle = extractTitle(dirPath);
|
|
86
|
+
|
|
87
|
+
if (convTitle) {
|
|
88
|
+
// Exact or substring match
|
|
89
|
+
if (convTitle === title || convTitle.includes(title) || title.includes(convTitle)) {
|
|
90
|
+
ctx.activeConversationId = entry.name;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fuzzy match scoring
|
|
95
|
+
const score = getMatchScore(convTitle, title);
|
|
96
|
+
const mtime = fs.statSync(dirPath).mtimeMs;
|
|
97
|
+
if (score > 0 && (!bestMatch || score > bestMatch.score || (score === bestMatch.score && mtime > bestMatch.mtime))) {
|
|
98
|
+
bestMatch = { id: entry.name, score, mtime };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Fallback to best fuzzy match if no exact match found
|
|
105
|
+
if (!ctx.activeConversationId && bestMatch && bestMatch.score >= 0.2) {
|
|
106
|
+
ctx.activeConversationId = bestMatch.id;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return NextResponse.json({ success, title, activeConversationId: ctx.activeConversationId });
|
|
111
|
+
} catch (e: any) {
|
|
112
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/v1/debug/dom — dump raw HTML of the agent panel for debugging.
|
|
9
|
+
*/
|
|
10
|
+
export async function GET() {
|
|
11
|
+
await ensureCdpConnection();
|
|
12
|
+
if (!ctx.workbenchPage) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: 'Not connected to Antigravity' },
|
|
15
|
+
{ status: 503 }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const html = await ctx.workbenchPage.evaluate(() => {
|
|
21
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
22
|
+
return panel ? (panel as HTMLElement).innerHTML : 'Panel not found';
|
|
23
|
+
});
|
|
24
|
+
return new Response(html, {
|
|
25
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
26
|
+
});
|
|
27
|
+
} catch (e: any) {
|
|
28
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { getFullAgentState } from '@/lib/scraper/agent-state';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/v1/debug/scrape — Returns both the raw last-turn HTML
|
|
10
|
+
* and the full parsed agent state for debugging.
|
|
11
|
+
*/
|
|
12
|
+
export async function GET() {
|
|
13
|
+
await ensureCdpConnection();
|
|
14
|
+
if (!ctx.workbenchPage) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: 'Not connected to Antigravity' },
|
|
17
|
+
{ status: 503 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// 1. Get parsed state via the scraper
|
|
23
|
+
const parsedState = await getFullAgentState(ctx);
|
|
24
|
+
|
|
25
|
+
// 2. Get raw HTML of the last turn from the panel
|
|
26
|
+
const rawHTML = await ctx.workbenchPage.evaluate(() => {
|
|
27
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
28
|
+
if (!panel) return '<p>Panel not found</p>';
|
|
29
|
+
|
|
30
|
+
const conversation =
|
|
31
|
+
panel.querySelector('#conversation') ||
|
|
32
|
+
document.querySelector('#conversation');
|
|
33
|
+
const scrollArea = conversation?.querySelector('.overflow-y-auto');
|
|
34
|
+
const msgList = scrollArea?.querySelector('.mx-auto');
|
|
35
|
+
const allTurns = msgList ? Array.from(msgList.children) : [];
|
|
36
|
+
const lastTurn = allTurns.length > 0 ? allTurns[allTurns.length - 1] : null;
|
|
37
|
+
|
|
38
|
+
if (!lastTurn) return '<p>No turns found</p>';
|
|
39
|
+
return (lastTurn as HTMLElement).innerHTML;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
raw: rawHTML,
|
|
44
|
+
parsed: parsedState,
|
|
45
|
+
meta: {
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
activeWindowIdx: ctx.activeWindowIdx,
|
|
48
|
+
turnCount: parsedState.turnCount,
|
|
49
|
+
toolCallCount: parsedState.toolCalls.length,
|
|
50
|
+
responseCount: parsedState.responses.length,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
} catch (e: any) {
|
|
54
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
await ensureCdpConnection();
|
|
9
|
+
return NextResponse.json({
|
|
10
|
+
status: 'ok',
|
|
11
|
+
connected: !!ctx.workbenchPage,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { startCdpServer } from '@/lib/cdp/process-manager';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/v1/windows/cdp-start — Start the Antigravity CDP server.
|
|
8
|
+
*
|
|
9
|
+
* Body (optional):
|
|
10
|
+
* { "projectDir": "/path/to/project", "killExisting": false }
|
|
11
|
+
*
|
|
12
|
+
* If projectDir is not provided, defaults to ".".
|
|
13
|
+
* If killExisting is true, all existing Antigravity instances are killed first.
|
|
14
|
+
*/
|
|
15
|
+
export async function POST(request: NextRequest) {
|
|
16
|
+
try {
|
|
17
|
+
const body = await request.json().catch(() => ({}));
|
|
18
|
+
const projectDir = body.projectDir || '.';
|
|
19
|
+
const killExisting = body.killExisting === true;
|
|
20
|
+
|
|
21
|
+
const result = await startCdpServer(projectDir, killExisting);
|
|
22
|
+
|
|
23
|
+
return NextResponse.json(result, {
|
|
24
|
+
status: result.success ? 200 : 500,
|
|
25
|
+
});
|
|
26
|
+
} catch (e: any) {
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ success: false, message: e.message },
|
|
29
|
+
{ status: 500 },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isCdpServerActive, getWindowTargets } from '@/lib/cdp/process-manager';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/v1/windows/cdp-status — Check if the CDP server is active.
|
|
8
|
+
* Returns the status and a list of open workbench windows.
|
|
9
|
+
*/
|
|
10
|
+
export async function GET() {
|
|
11
|
+
try {
|
|
12
|
+
const status = await isCdpServerActive();
|
|
13
|
+
let targets: { id: string; title: string; url: string }[] = [];
|
|
14
|
+
|
|
15
|
+
if (status.active) {
|
|
16
|
+
const result = await getWindowTargets();
|
|
17
|
+
targets = result.targets;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return NextResponse.json({
|
|
21
|
+
active: status.active,
|
|
22
|
+
windowCount: status.windowCount,
|
|
23
|
+
targets,
|
|
24
|
+
error: status.error || null,
|
|
25
|
+
});
|
|
26
|
+
} catch (e: any) {
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ active: false, windowCount: 0, targets: [], error: e.message },
|
|
29
|
+
{ status: 500 },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { closeWindow, getWindowTargets } from '@/lib/cdp/process-manager';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { discoverWorkbenches } from '@/lib/cdp/connection';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/v1/windows/close — Close a specific Antigravity window.
|
|
10
|
+
*
|
|
11
|
+
* Body:
|
|
12
|
+
* { "targetId": "CDP-target-id" }
|
|
13
|
+
* OR
|
|
14
|
+
* { "index": 0 } — closes by window index
|
|
15
|
+
*/
|
|
16
|
+
export async function POST(request: NextRequest) {
|
|
17
|
+
const body = await request.json().catch(() => ({}));
|
|
18
|
+
let { targetId, index } = body;
|
|
19
|
+
|
|
20
|
+
// If index is provided instead of targetId, resolve it
|
|
21
|
+
if (!targetId && index !== undefined && index !== null) {
|
|
22
|
+
const { targets } = await getWindowTargets();
|
|
23
|
+
if (index < 0 || index >= targets.length) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ success: false, message: `Invalid window index ${index}. Available: 0-${targets.length - 1}` },
|
|
26
|
+
{ status: 400 },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
targetId = targets[index].id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!targetId) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ success: false, message: 'Either targetId or index is required.' },
|
|
35
|
+
{ status: 400 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await closeWindow(targetId);
|
|
41
|
+
|
|
42
|
+
// After closing, re-discover workbenches
|
|
43
|
+
if (result.success) {
|
|
44
|
+
// Wait a moment for the window to fully close
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
46
|
+
try {
|
|
47
|
+
await discoverWorkbenches(ctx);
|
|
48
|
+
// If the active window was closed, reset to first available
|
|
49
|
+
if (ctx.allWorkbenches.length > 0 && ctx.activeWindowIdx >= ctx.allWorkbenches.length) {
|
|
50
|
+
ctx.activeWindowIdx = 0;
|
|
51
|
+
ctx.workbenchPage = ctx.allWorkbenches[0].page;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Non-fatal
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return NextResponse.json(result, {
|
|
59
|
+
status: result.success ? 200 : 500,
|
|
60
|
+
});
|
|
61
|
+
} catch (e: any) {
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{ success: false, message: e.message },
|
|
64
|
+
{ status: 500 },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { openNewWindow } from '@/lib/cdp/process-manager';
|
|
3
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
4
|
+
import ctx from '@/lib/context';
|
|
5
|
+
import { discoverWorkbenches } from '@/lib/cdp/connection';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/v1/windows/open — Open a new Antigravity window.
|
|
11
|
+
*
|
|
12
|
+
* Body:
|
|
13
|
+
* { "projectDir": "/path/to/project" }
|
|
14
|
+
*/
|
|
15
|
+
export async function POST(request: NextRequest) {
|
|
16
|
+
const body = await request.json().catch(() => ({}));
|
|
17
|
+
const { projectDir } = body;
|
|
18
|
+
|
|
19
|
+
if (!projectDir || typeof projectDir !== 'string' || projectDir.trim() === '') {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ success: false, message: 'projectDir is required and must be a non-empty string.' },
|
|
22
|
+
{ status: 400 },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await openNewWindow(projectDir.trim());
|
|
28
|
+
|
|
29
|
+
// After opening, re-discover workbenches so the proxy knows about the new window
|
|
30
|
+
if (result.success) {
|
|
31
|
+
try {
|
|
32
|
+
// Try to reconnect/rediscover
|
|
33
|
+
await ensureCdpConnection();
|
|
34
|
+
await discoverWorkbenches(ctx);
|
|
35
|
+
} catch {
|
|
36
|
+
// Non-fatal — the window list will refresh on next poll
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return NextResponse.json(result, {
|
|
41
|
+
status: result.success ? 200 : 500,
|
|
42
|
+
});
|
|
43
|
+
} catch (e: any) {
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ success: false, message: e.message },
|
|
46
|
+
{ status: 500 },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getRecentProjects } from '@/lib/cdp/recent-projects';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/v1/windows/recent — List recently opened Antigravity projects.
|
|
8
|
+
*
|
|
9
|
+
* Query params:
|
|
10
|
+
* ?limit=10 (default: 15)
|
|
11
|
+
*/
|
|
12
|
+
export async function GET(request: NextRequest) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(request.url);
|
|
15
|
+
const limit = parseInt(url.searchParams.get('limit') || '15', 10);
|
|
16
|
+
const recentProjects = getRecentProjects(limit);
|
|
17
|
+
|
|
18
|
+
return NextResponse.json({ recentProjects });
|
|
19
|
+
} catch (e: any) {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ recentProjects: [], error: e.message },
|
|
22
|
+
{ status: 500 },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { discoverWorkbenches, selectWindow } from '@/lib/cdp/connection';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/v1/windows — list all available Antigravity workbench windows.
|
|
10
|
+
*/
|
|
11
|
+
export async function GET() {
|
|
12
|
+
await ensureCdpConnection();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const workbenches = await discoverWorkbenches(ctx);
|
|
16
|
+
return NextResponse.json({
|
|
17
|
+
windows: workbenches.map((w, idx) => ({
|
|
18
|
+
index: idx,
|
|
19
|
+
title: w.title,
|
|
20
|
+
url: w.url,
|
|
21
|
+
active: idx === ctx.activeWindowIdx,
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
} catch (e: any) {
|
|
25
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { selectWindow } from '@/lib/cdp/connection';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/v1/windows/select — switch to a different workbench window.
|
|
10
|
+
*/
|
|
11
|
+
export async function POST(request: NextRequest) {
|
|
12
|
+
await ensureCdpConnection();
|
|
13
|
+
|
|
14
|
+
const body = await request.json();
|
|
15
|
+
const { index } = body;
|
|
16
|
+
if (index === undefined || index === null) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: 'index is required' },
|
|
19
|
+
{ status: 400 }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const info = selectWindow(ctx, index);
|
|
25
|
+
return NextResponse.json({
|
|
26
|
+
success: true,
|
|
27
|
+
window: { index, title: info.title, url: info.url },
|
|
28
|
+
});
|
|
29
|
+
} catch (e: any) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: e.message },
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|