antigravity-mobile-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 +698 -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,278 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
import { useConversations } from './use-conversations';
|
|
5
|
+
import { useArtifacts } from './use-artifacts';
|
|
6
|
+
import type { ChatMessage, SSEStep } from '@/lib/types';
|
|
7
|
+
|
|
8
|
+
const API_BASE = '/api/v1';
|
|
9
|
+
|
|
10
|
+
export function useChat() {
|
|
11
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
12
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
13
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
14
|
+
const [statusText, setStatusText] = useState('Agent');
|
|
15
|
+
const [statusState, setStatusState] = useState('connected');
|
|
16
|
+
const [showWelcome, setShowWelcome] = useState(true);
|
|
17
|
+
const [currentSteps, setCurrentSteps] = useState<SSEStep[]>([]);
|
|
18
|
+
const [currentResponse, setCurrentResponse] = useState('');
|
|
19
|
+
const [currentMode, setCurrentMode] = useState<'planning' | 'fast'>('planning');
|
|
20
|
+
|
|
21
|
+
const controllerRef = useRef<AbortController | null>(null);
|
|
22
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const currentResponseRef = useRef('');
|
|
24
|
+
const currentStepsRef = useRef<SSEStep[]>([]);
|
|
25
|
+
|
|
26
|
+
const scrollToBottom = useCallback(() => {
|
|
27
|
+
requestAnimationFrame(() => {
|
|
28
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
29
|
+
});
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const setStatus = useCallback((state: string, text: string) => {
|
|
33
|
+
setStatusState(state);
|
|
34
|
+
setStatusText(text);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const fetchHistory = useCallback(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${API_BASE}/chat/history`);
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
if (data.turns && data.turns.length > 0) {
|
|
42
|
+
setShowWelcome(false);
|
|
43
|
+
setMessages(data.turns.map((t: any) => ({ role: t.role, content: t.content })));
|
|
44
|
+
}
|
|
45
|
+
} catch { /* ignore */ }
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
artifactFiles,
|
|
50
|
+
artifactPanelOpen,
|
|
51
|
+
toggleArtifactPanel,
|
|
52
|
+
openArtifactPanel,
|
|
53
|
+
loadArtifacts
|
|
54
|
+
} = useArtifacts();
|
|
55
|
+
|
|
56
|
+
// Auto-open artifact panel and refresh files when a conversation switch completes
|
|
57
|
+
const handleConversationSwitched = useCallback(() => {
|
|
58
|
+
openArtifactPanel();
|
|
59
|
+
loadArtifacts();
|
|
60
|
+
}, [openArtifactPanel, loadArtifacts]);
|
|
61
|
+
|
|
62
|
+
const {
|
|
63
|
+
windows,
|
|
64
|
+
conversations,
|
|
65
|
+
activeConversation,
|
|
66
|
+
cdpStatus,
|
|
67
|
+
recentProjects,
|
|
68
|
+
loadWindows,
|
|
69
|
+
selectWindow,
|
|
70
|
+
loadConversations,
|
|
71
|
+
selectConversation,
|
|
72
|
+
checkCdpStatus,
|
|
73
|
+
startCdpServer,
|
|
74
|
+
openNewWindow,
|
|
75
|
+
closeWindowByIndex,
|
|
76
|
+
loadRecentProjects,
|
|
77
|
+
} = useConversations(fetchHistory, setShowWelcome, handleConversationSwitched);
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
const fetchMode = useCallback(async () => {
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(`${API_BASE}/chat/mode`);
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
if (data.mode) setCurrentMode(data.mode);
|
|
85
|
+
} catch { /* ignore */ }
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const toggleMode = useCallback(async () => {
|
|
89
|
+
const newMode = currentMode === 'planning' ? 'fast' : 'planning';
|
|
90
|
+
setCurrentMode(newMode); // Optimistic update
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`${API_BASE}/chat/mode`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({ mode: newMode }),
|
|
96
|
+
});
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
if (data.mode) setCurrentMode(data.mode);
|
|
99
|
+
} catch {
|
|
100
|
+
setCurrentMode(currentMode); // Rollback on error
|
|
101
|
+
}
|
|
102
|
+
}, [currentMode]);
|
|
103
|
+
|
|
104
|
+
const checkHealth = useCallback(async () => {
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(`${API_BASE}/health`);
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
setIsConnected(data.connected);
|
|
109
|
+
setStatus(data.connected ? 'connected' : 'disconnected', data.connected ? 'Agent' : 'Disconnected');
|
|
110
|
+
} catch {
|
|
111
|
+
setIsConnected(false);
|
|
112
|
+
setStatus('disconnected', 'Offline');
|
|
113
|
+
}
|
|
114
|
+
}, [setStatus]);
|
|
115
|
+
|
|
116
|
+
const handleSSEvent = useCallback((payload: any) => {
|
|
117
|
+
const { type, ...data } = payload;
|
|
118
|
+
|
|
119
|
+
switch (type) {
|
|
120
|
+
case 'thinking':
|
|
121
|
+
case 'tool_call':
|
|
122
|
+
case 'hitl':
|
|
123
|
+
case 'file_change':
|
|
124
|
+
case 'error':
|
|
125
|
+
case 'notification':
|
|
126
|
+
setCurrentSteps(prev => {
|
|
127
|
+
const updated = [...prev, { type, data }];
|
|
128
|
+
currentStepsRef.current = updated;
|
|
129
|
+
return updated;
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'response':
|
|
134
|
+
setCurrentResponse(data.content || '');
|
|
135
|
+
currentResponseRef.current = data.content || '';
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case 'status':
|
|
139
|
+
setStatus(data.isRunning ? 'streaming' : 'connected', data.isRunning ? 'Agent working...' : 'Agent');
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case 'done':
|
|
143
|
+
if (data.finalResponse) {
|
|
144
|
+
setCurrentResponse(data.finalResponse);
|
|
145
|
+
currentResponseRef.current = data.finalResponse;
|
|
146
|
+
}
|
|
147
|
+
setStatus('connected', 'Agent');
|
|
148
|
+
setIsStreaming(false);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
scrollToBottom();
|
|
152
|
+
}, [setStatus, scrollToBottom]);
|
|
153
|
+
|
|
154
|
+
const sendMessage = useCallback(async (text: string) => {
|
|
155
|
+
if (isStreaming || !text.trim()) return;
|
|
156
|
+
|
|
157
|
+
setShowWelcome(false);
|
|
158
|
+
const trimmed = text.trim();
|
|
159
|
+
|
|
160
|
+
setMessages(prev => [...prev, { role: 'user', content: trimmed }]);
|
|
161
|
+
setIsStreaming(true);
|
|
162
|
+
setCurrentSteps([]);
|
|
163
|
+
setCurrentResponse('');
|
|
164
|
+
currentResponseRef.current = '';
|
|
165
|
+
currentStepsRef.current = [];
|
|
166
|
+
setStatus('streaming', 'Agent typing...');
|
|
167
|
+
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
controllerRef.current = controller;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`${API_BASE}/chat/stream`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'Content-Type': 'application/json' },
|
|
175
|
+
body: JSON.stringify({ message: trimmed }),
|
|
176
|
+
signal: controller.signal,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const reader = res.body!.getReader();
|
|
180
|
+
const decoder = new TextDecoder();
|
|
181
|
+
let buffer = '';
|
|
182
|
+
|
|
183
|
+
while (true) {
|
|
184
|
+
const { done, value } = await reader.read();
|
|
185
|
+
if (done) break;
|
|
186
|
+
|
|
187
|
+
buffer += decoder.decode(value, { stream: true });
|
|
188
|
+
const lines = buffer.split('\n');
|
|
189
|
+
buffer = lines.pop()!;
|
|
190
|
+
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
if (!line.startsWith('data: ')) continue;
|
|
193
|
+
try {
|
|
194
|
+
const payload = JSON.parse(line.slice(6));
|
|
195
|
+
handleSSEvent(payload);
|
|
196
|
+
} catch { /* skip */ }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (e: any) {
|
|
200
|
+
if (e.name !== 'AbortError') {
|
|
201
|
+
setCurrentSteps(prev => [...prev, { type: 'error', data: { message: e.message } }]);
|
|
202
|
+
setStatus('error', 'Error');
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
const finalResponse = currentResponseRef.current;
|
|
206
|
+
const finalSteps = [...currentStepsRef.current];
|
|
207
|
+
|
|
208
|
+
setIsStreaming(false);
|
|
209
|
+
controllerRef.current = null;
|
|
210
|
+
|
|
211
|
+
if (finalResponse || finalSteps.length > 0) {
|
|
212
|
+
setMessages(prev => [
|
|
213
|
+
...prev,
|
|
214
|
+
{ role: 'agent', content: finalResponse, steps: finalSteps },
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
setStatus('connected', 'Agent');
|
|
218
|
+
}
|
|
219
|
+
}, [isStreaming, handleSSEvent, setStatus]);
|
|
220
|
+
|
|
221
|
+
const startNewChat = useCallback(async () => {
|
|
222
|
+
if (controllerRef.current) controllerRef.current.abort();
|
|
223
|
+
setMessages([]);
|
|
224
|
+
setCurrentSteps([]);
|
|
225
|
+
setCurrentResponse('');
|
|
226
|
+
setShowWelcome(true);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const res = await fetch(`${API_BASE}/chat/new`, { method: 'POST' });
|
|
230
|
+
const data = await res.json();
|
|
231
|
+
if (data.success) {
|
|
232
|
+
setStatus('connected', 'New Chat');
|
|
233
|
+
}
|
|
234
|
+
} catch { /* ignore */ }
|
|
235
|
+
}, [setStatus]);
|
|
236
|
+
|
|
237
|
+
const approve = useCallback(async () => {
|
|
238
|
+
try {
|
|
239
|
+
await fetch(`${API_BASE}/chat/approve`, { method: 'POST' });
|
|
240
|
+
setStatus('streaming', 'Agent');
|
|
241
|
+
} catch { /* ignore */ }
|
|
242
|
+
}, [setStatus]);
|
|
243
|
+
|
|
244
|
+
const reject = useCallback(async () => {
|
|
245
|
+
try {
|
|
246
|
+
await fetch(`${API_BASE}/chat/reject`, { method: 'POST' });
|
|
247
|
+
} catch { /* ignore */ }
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
checkHealth();
|
|
252
|
+
loadWindows();
|
|
253
|
+
fetchHistory();
|
|
254
|
+
loadConversations();
|
|
255
|
+
loadArtifacts();
|
|
256
|
+
fetchMode();
|
|
257
|
+
checkCdpStatus();
|
|
258
|
+
loadRecentProjects();
|
|
259
|
+
|
|
260
|
+
// Passive polling for health and CDP status
|
|
261
|
+
const healthTimer = setInterval(checkHealth, 30000);
|
|
262
|
+
const cdpTimer = setInterval(checkCdpStatus, 15000);
|
|
263
|
+
return () => { clearInterval(healthTimer); clearInterval(cdpTimer); };
|
|
264
|
+
}, [checkHealth, loadWindows, fetchHistory, loadConversations, loadArtifacts, fetchMode, checkCdpStatus, loadRecentProjects]);
|
|
265
|
+
|
|
266
|
+
useEffect(scrollToBottom, [messages, currentSteps, currentResponse, scrollToBottom]);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
messages, isStreaming, isConnected, statusText, statusState,
|
|
270
|
+
showWelcome, currentSteps, currentResponse, windows,
|
|
271
|
+
conversations, activeConversation, artifactFiles, artifactPanelOpen,
|
|
272
|
+
currentMode, cdpStatus, recentProjects,
|
|
273
|
+
sendMessage, startNewChat, approve, reject,
|
|
274
|
+
selectWindow, selectConversation, toggleArtifactPanel, openArtifactPanel,
|
|
275
|
+
toggleMode, startCdpServer, openNewWindow, closeWindowByIndex,
|
|
276
|
+
messagesEndRef, setShowWelcome,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import type { WindowInfo, ConversationInfo } from '@/lib/types';
|
|
3
|
+
|
|
4
|
+
const API_BASE = '/api/v1';
|
|
5
|
+
|
|
6
|
+
export interface CdpStatus {
|
|
7
|
+
active: boolean;
|
|
8
|
+
windowCount: number;
|
|
9
|
+
error?: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RecentProject {
|
|
13
|
+
path: string;
|
|
14
|
+
name: string;
|
|
15
|
+
lastOpened: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useConversations(
|
|
19
|
+
fetchHistory: () => Promise<void>,
|
|
20
|
+
setShowWelcome: (s: boolean) => void,
|
|
21
|
+
onConversationSwitched?: () => void
|
|
22
|
+
) {
|
|
23
|
+
const [windows, setWindows] = useState<WindowInfo[]>([]);
|
|
24
|
+
const [conversations, setConversations] = useState<ConversationInfo[]>([]);
|
|
25
|
+
const [activeConversation, setActiveConversation] = useState<ConversationInfo | null>(null);
|
|
26
|
+
const [cdpStatus, setCdpStatus] = useState<CdpStatus>({ active: false, windowCount: 0 });
|
|
27
|
+
const [recentProjects, setRecentProjects] = useState<RecentProject[]>([]);
|
|
28
|
+
|
|
29
|
+
const loadWindows = useCallback(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`${API_BASE}/windows`);
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
setWindows(data.windows || []);
|
|
34
|
+
} catch { /* ignore */ }
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const checkCdpStatus = useCallback(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${API_BASE}/windows/cdp-status`);
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
setCdpStatus({
|
|
42
|
+
active: data.active,
|
|
43
|
+
windowCount: data.windowCount,
|
|
44
|
+
error: data.error,
|
|
45
|
+
});
|
|
46
|
+
return data.active;
|
|
47
|
+
} catch {
|
|
48
|
+
setCdpStatus({ active: false, windowCount: 0, error: 'Failed to check' });
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const startCdpServer = useCallback(async (projectDir?: string, killExisting?: boolean) => {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${API_BASE}/windows/cdp-start`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ projectDir: projectDir || '.', killExisting: killExisting || false }),
|
|
59
|
+
});
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
if (data.success) {
|
|
62
|
+
await checkCdpStatus();
|
|
63
|
+
await loadWindows();
|
|
64
|
+
}
|
|
65
|
+
return data;
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
return { success: false, message: e.message || 'Failed to start CDP server' };
|
|
68
|
+
}
|
|
69
|
+
}, [checkCdpStatus, loadWindows]);
|
|
70
|
+
|
|
71
|
+
const openNewWindow = useCallback(async (projectDir: string) => {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`${API_BASE}/windows/open`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ projectDir }),
|
|
77
|
+
});
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
if (data.success) {
|
|
80
|
+
// Refresh window list after opening
|
|
81
|
+
await loadWindows();
|
|
82
|
+
await checkCdpStatus();
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
} catch (e: any) {
|
|
86
|
+
return { success: false, message: e.message || 'Failed to open window' };
|
|
87
|
+
}
|
|
88
|
+
}, [loadWindows, checkCdpStatus]);
|
|
89
|
+
|
|
90
|
+
const closeWindowByIndex = useCallback(async (index: number) => {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`${API_BASE}/windows/close`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({ index }),
|
|
96
|
+
});
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
if (data.success) {
|
|
99
|
+
await loadWindows();
|
|
100
|
+
await checkCdpStatus();
|
|
101
|
+
}
|
|
102
|
+
return data;
|
|
103
|
+
} catch (e: any) {
|
|
104
|
+
return { success: false, message: e.message || 'Failed to close window' };
|
|
105
|
+
}
|
|
106
|
+
}, [loadWindows, checkCdpStatus]);
|
|
107
|
+
|
|
108
|
+
const selectWindow = useCallback(async (idx: number) => {
|
|
109
|
+
try {
|
|
110
|
+
await fetch(`${API_BASE}/windows/select`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ index: idx }),
|
|
114
|
+
});
|
|
115
|
+
await loadWindows();
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
}, [loadWindows]);
|
|
118
|
+
|
|
119
|
+
const loadConversations = useCallback(async () => {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`${API_BASE}/conversations`);
|
|
122
|
+
const data = await res.json();
|
|
123
|
+
const convs: ConversationInfo[] = data.conversations || [];
|
|
124
|
+
setConversations(convs);
|
|
125
|
+
const active = convs.find((c) => c.active);
|
|
126
|
+
if (active) {
|
|
127
|
+
setActiveConversation(active);
|
|
128
|
+
}
|
|
129
|
+
} catch { /* ignore */ }
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const selectConversation = useCallback(async (title: string) => {
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch(`${API_BASE}/conversations/select`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({ title }),
|
|
138
|
+
});
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
if (data.success) {
|
|
141
|
+
setShowWelcome(false);
|
|
142
|
+
let attempts = 0;
|
|
143
|
+
const poll = async () => {
|
|
144
|
+
attempts++;
|
|
145
|
+
try {
|
|
146
|
+
const resList = await fetch(`${API_BASE}/conversations`);
|
|
147
|
+
const dList = await resList.json();
|
|
148
|
+
const convs = dList.conversations || [];
|
|
149
|
+
const active = convs.find((c: any) => c.active);
|
|
150
|
+
if ((active && active.title === title) || attempts > 10) {
|
|
151
|
+
setConversations(convs);
|
|
152
|
+
if (active) setActiveConversation(active);
|
|
153
|
+
await fetchHistory();
|
|
154
|
+
// Notify parent that the switch is complete (for artifact sync)
|
|
155
|
+
onConversationSwitched?.();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
} catch { /* ignore */ }
|
|
159
|
+
setTimeout(poll, 500);
|
|
160
|
+
};
|
|
161
|
+
poll();
|
|
162
|
+
}
|
|
163
|
+
} catch { /* ignore */ }
|
|
164
|
+
}, [fetchHistory, setShowWelcome, onConversationSwitched]);
|
|
165
|
+
|
|
166
|
+
const loadRecentProjects = useCallback(async () => {
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(`${API_BASE}/windows/recent`);
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
setRecentProjects(data.recentProjects || []);
|
|
171
|
+
} catch { /* ignore */ }
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
windows,
|
|
176
|
+
conversations,
|
|
177
|
+
activeConversation,
|
|
178
|
+
cdpStatus,
|
|
179
|
+
recentProjects,
|
|
180
|
+
loadWindows,
|
|
181
|
+
selectWindow,
|
|
182
|
+
loadConversations,
|
|
183
|
+
selectConversation,
|
|
184
|
+
checkCdpStatus,
|
|
185
|
+
startCdpServer,
|
|
186
|
+
openNewWindow,
|
|
187
|
+
closeWindowByIndex,
|
|
188
|
+
loadRecentProjects,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL (Human-in-the-Loop) button interactions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ProxyContext } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Click the approve/run/allow button in the HITL panel.
|
|
9
|
+
*/
|
|
10
|
+
export async function clickApproveButton(ctx: ProxyContext) {
|
|
11
|
+
if (!ctx.workbenchPage) return { success: false, error: 'Not connected' };
|
|
12
|
+
|
|
13
|
+
return ctx.workbenchPage.evaluate(() => {
|
|
14
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
15
|
+
if (!panel) return { success: false, error: 'No panel found' };
|
|
16
|
+
|
|
17
|
+
const buttons = Array.from(panel.querySelectorAll('button'));
|
|
18
|
+
|
|
19
|
+
for (const btn of buttons) {
|
|
20
|
+
const text = btn.textContent?.trim().toLowerCase() || '';
|
|
21
|
+
if (
|
|
22
|
+
(text === 'run' ||
|
|
23
|
+
text === 'approve' ||
|
|
24
|
+
text === 'allow' ||
|
|
25
|
+
text === 'yes') &&
|
|
26
|
+
!btn.disabled
|
|
27
|
+
) {
|
|
28
|
+
btn.click();
|
|
29
|
+
return { success: true, clicked: btn.textContent?.trim() };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const footers = panel.querySelectorAll('.rounded-b.border-t');
|
|
34
|
+
for (const footer of footers) {
|
|
35
|
+
const actionBtns = footer.querySelectorAll('button');
|
|
36
|
+
for (const btn of actionBtns) {
|
|
37
|
+
const text = btn.textContent?.trim().toLowerCase() || '';
|
|
38
|
+
if (text !== 'cancel' && !btn.disabled) {
|
|
39
|
+
btn.click();
|
|
40
|
+
return { success: true, clicked: btn.textContent?.trim() };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { success: false, error: 'No approve button found' };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Click the reject/cancel/deny button in the HITL panel.
|
|
51
|
+
*/
|
|
52
|
+
export async function clickRejectButton(ctx: ProxyContext) {
|
|
53
|
+
if (!ctx.workbenchPage) return { success: false, error: 'Not connected' };
|
|
54
|
+
|
|
55
|
+
return ctx.workbenchPage.evaluate(() => {
|
|
56
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
57
|
+
if (!panel) return { success: false, error: 'No panel found' };
|
|
58
|
+
|
|
59
|
+
const buttons = Array.from(panel.querySelectorAll('button'));
|
|
60
|
+
for (const btn of buttons) {
|
|
61
|
+
const text = btn.textContent?.trim().toLowerCase() || '';
|
|
62
|
+
if (
|
|
63
|
+
(text === 'cancel' || text === 'reject' || text === 'deny') &&
|
|
64
|
+
!btn.disabled
|
|
65
|
+
) {
|
|
66
|
+
btn.click();
|
|
67
|
+
return { success: true, clicked: btn.textContent?.trim() };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { success: false, error: 'No reject/cancel button found' };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Click any footer button by toolId + buttonText.
|
|
77
|
+
*/
|
|
78
|
+
export async function clickActionButton(
|
|
79
|
+
ctx: ProxyContext,
|
|
80
|
+
toolId: string | null,
|
|
81
|
+
buttonText: string
|
|
82
|
+
) {
|
|
83
|
+
if (!ctx.workbenchPage) return { success: false, error: 'Not connected' };
|
|
84
|
+
|
|
85
|
+
return ctx.workbenchPage.evaluate(
|
|
86
|
+
(tid: string | null, btext: string) => {
|
|
87
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
88
|
+
if (!panel) return { success: false, error: 'No panel found' };
|
|
89
|
+
|
|
90
|
+
let searchRoot: Element = panel;
|
|
91
|
+
if (tid) {
|
|
92
|
+
const scoped = panel.querySelector(
|
|
93
|
+
`[data-proxy-tool-id="${tid}"]`
|
|
94
|
+
);
|
|
95
|
+
if (scoped) searchRoot = scoped;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const buttons = Array.from(searchRoot.querySelectorAll('button'));
|
|
99
|
+
const target = buttons.find((b) => {
|
|
100
|
+
const t = b.textContent?.trim() || '';
|
|
101
|
+
return t.toLowerCase() === btext.toLowerCase() && !b.disabled;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (target) {
|
|
105
|
+
target.click();
|
|
106
|
+
return { success: true, clicked: target.textContent?.trim() };
|
|
107
|
+
}
|
|
108
|
+
return { success: false, error: `Button "${btext}" not found` };
|
|
109
|
+
},
|
|
110
|
+
toolId,
|
|
111
|
+
buttonText
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start a new chat in the Antigravity IDE by clicking the new-chat button.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ProxyContext } from '../types';
|
|
6
|
+
|
|
7
|
+
export async function startNewChat(ctx: ProxyContext) {
|
|
8
|
+
if (!ctx.workbenchPage)
|
|
9
|
+
return { success: false, error: 'Not connected' };
|
|
10
|
+
|
|
11
|
+
const btnResult = await ctx.workbenchPage.evaluate(() => {
|
|
12
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
13
|
+
if (!panel)
|
|
14
|
+
return { success: false, error: 'No panel found' } as any;
|
|
15
|
+
|
|
16
|
+
const allButtons = Array.from(panel.querySelectorAll('button'));
|
|
17
|
+
|
|
18
|
+
const getCoords = (btn: Element, method: string) => {
|
|
19
|
+
const rect = btn.getBoundingClientRect();
|
|
20
|
+
return {
|
|
21
|
+
success: true,
|
|
22
|
+
method,
|
|
23
|
+
clicked:
|
|
24
|
+
(btn as HTMLElement).textContent?.trim() ||
|
|
25
|
+
btn.getAttribute('aria-label') ||
|
|
26
|
+
'+',
|
|
27
|
+
x: rect.left + rect.width / 2,
|
|
28
|
+
y: rect.top + rect.height / 2,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Strategy 0: Exact match using known tooltip id
|
|
33
|
+
const exactBtn = panel.querySelector(
|
|
34
|
+
'a[data-tooltip-id="new-conversation-tooltip"]'
|
|
35
|
+
);
|
|
36
|
+
if (exactBtn) {
|
|
37
|
+
if (
|
|
38
|
+
exactBtn.classList.contains('cursor-not-allowed') ||
|
|
39
|
+
exactBtn.classList.contains('disabled') ||
|
|
40
|
+
getComputedStyle(exactBtn).opacity === '0.5'
|
|
41
|
+
) {
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
method: 'tooltip-id-exact-disabled',
|
|
45
|
+
clicked: 'Already in a new chat',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return getCoords(exactBtn, 'tooltip-id-exact');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Strategy 0.5: Structural match
|
|
52
|
+
const header = panel.querySelector(
|
|
53
|
+
'.title-actions, .actions-container, [class*="header"], [class*="titlebar"]'
|
|
54
|
+
);
|
|
55
|
+
if (header) {
|
|
56
|
+
const headerBtns = Array.from(
|
|
57
|
+
header.querySelectorAll('button, a.action-label')
|
|
58
|
+
);
|
|
59
|
+
if (headerBtns.length >= 4) {
|
|
60
|
+
const target = headerBtns[headerBtns.length - 4];
|
|
61
|
+
return getCoords(target, 'header-4th-from-right');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Strategy 1: aria-label or title
|
|
66
|
+
for (const btn of allButtons) {
|
|
67
|
+
const aria = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
68
|
+
const title = (btn.getAttribute('title') || '').toLowerCase();
|
|
69
|
+
const combined = aria + ' ' + title;
|
|
70
|
+
if (combined.includes('new') || combined.includes('start') || combined.includes('create')) {
|
|
71
|
+
if (
|
|
72
|
+
combined.includes('chat') ||
|
|
73
|
+
combined.includes('conversation') ||
|
|
74
|
+
combined.includes('session') ||
|
|
75
|
+
aria.includes('new') ||
|
|
76
|
+
title.includes('new')
|
|
77
|
+
) {
|
|
78
|
+
return getCoords(btn, 'aria/title');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: 'No new-chat button found',
|
|
88
|
+
buttonCount: allButtons.length,
|
|
89
|
+
} as any;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (btnResult.success && btnResult.x && btnResult.y) {
|
|
93
|
+
await ctx.workbenchPage.mouse.click(btnResult.x, btnResult.y);
|
|
94
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
95
|
+
return btnResult;
|
|
96
|
+
} else if (
|
|
97
|
+
btnResult.success &&
|
|
98
|
+
btnResult.method === 'tooltip-id-exact-disabled'
|
|
99
|
+
) {
|
|
100
|
+
return btnResult;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Strategy 5: Keyboard shortcut fallback
|
|
104
|
+
try {
|
|
105
|
+
await ctx.workbenchPage.keyboard.down('Control');
|
|
106
|
+
await ctx.workbenchPage.keyboard.press('l');
|
|
107
|
+
await ctx.workbenchPage.keyboard.up('Control');
|
|
108
|
+
return { success: true, method: 'keyboard-shortcut', clicked: 'Ctrl+L' };
|
|
109
|
+
} catch (e: any) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error:
|
|
113
|
+
'All strategies failed: ' + (btnResult.error || '') + ' | keyboard: ' + e.message,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|