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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat history scraper.
|
|
3
|
+
* Scrolls the Antigravity conversation view to de-virtualize all content,
|
|
4
|
+
* then walks the DOM to extract user/agent messages in order.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ProxyContext, ChatHistory } from '../types';
|
|
8
|
+
|
|
9
|
+
export async function getChatHistory(ctx: ProxyContext): Promise<ChatHistory> {
|
|
10
|
+
if (!ctx.workbenchPage) {
|
|
11
|
+
return { isRunning: false, turnCount: 0, turns: [] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return await ctx.workbenchPage.evaluate(async () => {
|
|
15
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
16
|
+
if (!panel) return { isRunning: false, turnCount: 0, turns: [] };
|
|
17
|
+
|
|
18
|
+
const conversation =
|
|
19
|
+
panel.querySelector('#conversation') ||
|
|
20
|
+
document.querySelector('#conversation');
|
|
21
|
+
const scrollArea = conversation?.querySelector('.overflow-y-auto');
|
|
22
|
+
if (!scrollArea)
|
|
23
|
+
return { isRunning: false, turnCount: 0, turns: [] };
|
|
24
|
+
|
|
25
|
+
// Step 1: Scroll to top to force older content to render
|
|
26
|
+
scrollArea.scrollTop = 0;
|
|
27
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
28
|
+
|
|
29
|
+
// Step 2: Incrementally scroll down to de-virtualize all content
|
|
30
|
+
const scrollHeight = scrollArea.scrollHeight;
|
|
31
|
+
const viewportHeight = scrollArea.clientHeight;
|
|
32
|
+
const scrollStep = viewportHeight * 0.8;
|
|
33
|
+
let pos = 0;
|
|
34
|
+
while (pos < scrollHeight) {
|
|
35
|
+
scrollArea.scrollTop = pos;
|
|
36
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
37
|
+
pos += scrollStep;
|
|
38
|
+
}
|
|
39
|
+
scrollArea.scrollTop = scrollArea.scrollHeight;
|
|
40
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
41
|
+
|
|
42
|
+
// Step 3: Walk the DOM to find messages in document order
|
|
43
|
+
const turns: { role: 'user' | 'agent'; content: string }[] = [];
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
|
|
46
|
+
const msgList =
|
|
47
|
+
(scrollArea as Element).querySelector('.mx-auto') || scrollArea;
|
|
48
|
+
|
|
49
|
+
const candidates: {
|
|
50
|
+
el: Element;
|
|
51
|
+
role: 'user' | 'agent';
|
|
52
|
+
content: string;
|
|
53
|
+
}[] = [];
|
|
54
|
+
|
|
55
|
+
// Find user messages
|
|
56
|
+
const allWhitespace = msgList.querySelectorAll('.whitespace-pre-wrap');
|
|
57
|
+
for (const el of allWhitespace) {
|
|
58
|
+
const text = el.textContent?.trim();
|
|
59
|
+
if (!text) continue;
|
|
60
|
+
|
|
61
|
+
let isInsideAgentResponse = false;
|
|
62
|
+
let parent = el.parentElement;
|
|
63
|
+
while (parent && parent !== msgList) {
|
|
64
|
+
const cls = parent.getAttribute('class') || '';
|
|
65
|
+
if (
|
|
66
|
+
cls.includes('leading-relaxed') &&
|
|
67
|
+
cls.includes('select-text')
|
|
68
|
+
) {
|
|
69
|
+
isInsideAgentResponse = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
parent = parent.parentElement;
|
|
73
|
+
}
|
|
74
|
+
if (isInsideAgentResponse) continue;
|
|
75
|
+
|
|
76
|
+
if (el.closest('[data-lexical-editor]')) continue;
|
|
77
|
+
if (el.closest('#antigravity\\.agentSidePanelInputBox')) continue;
|
|
78
|
+
|
|
79
|
+
const key = 'user:' + text.substring(0, 200);
|
|
80
|
+
if (seen.has(key)) continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
|
|
83
|
+
candidates.push({ el, role: 'user', content: text });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find agent response blocks
|
|
87
|
+
const allResponses = msgList.querySelectorAll(
|
|
88
|
+
'.leading-relaxed.select-text'
|
|
89
|
+
);
|
|
90
|
+
for (const el of allResponses) {
|
|
91
|
+
let hidden = false;
|
|
92
|
+
let ancestor = el.parentElement;
|
|
93
|
+
let depth = 0;
|
|
94
|
+
while (ancestor && ancestor !== msgList && depth < 15) {
|
|
95
|
+
const cls = ancestor.getAttribute('class') || '';
|
|
96
|
+
if (cls.includes('max-h-0') || cls.includes('hidden')) {
|
|
97
|
+
hidden = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
ancestor = ancestor.parentElement;
|
|
101
|
+
depth++;
|
|
102
|
+
}
|
|
103
|
+
if (hidden) continue;
|
|
104
|
+
|
|
105
|
+
const clone = el.cloneNode(true) as Element;
|
|
106
|
+
clone.querySelectorAll('style, script').forEach((n) => n.remove());
|
|
107
|
+
const html = (clone as HTMLElement).innerHTML?.trim();
|
|
108
|
+
if (!html) continue;
|
|
109
|
+
|
|
110
|
+
const key = 'agent:' + el.textContent?.trim().substring(0, 200);
|
|
111
|
+
if (seen.has(key)) continue;
|
|
112
|
+
seen.add(key);
|
|
113
|
+
|
|
114
|
+
candidates.push({ el, role: 'agent', content: html });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Sort by document position
|
|
118
|
+
candidates.sort((a, b) => {
|
|
119
|
+
const pos = a.el.compareDocumentPosition(b.el);
|
|
120
|
+
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
121
|
+
if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
122
|
+
return 0;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
for (const c of candidates) {
|
|
126
|
+
turns.push({ role: c.role, content: c.content });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Scroll back to bottom
|
|
130
|
+
scrollArea.scrollTop = scrollArea.scrollHeight;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
isRunning: false,
|
|
134
|
+
turnCount: turns.length,
|
|
135
|
+
turns,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ProxyContext, Conversation } from '../types';
|
|
2
|
+
import { logger } from '../logger';
|
|
3
|
+
|
|
4
|
+
export interface IdeConversation {
|
|
5
|
+
title: string;
|
|
6
|
+
active: boolean;
|
|
7
|
+
index: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gets the list of available conversations directly from the IDE's UI.
|
|
12
|
+
* This ensures we only see conversations for the current window context.
|
|
13
|
+
*/
|
|
14
|
+
export async function getIdeConversations(ctx: ProxyContext): Promise<IdeConversation[]> {
|
|
15
|
+
try {
|
|
16
|
+
logger.info('[Scraper] Fetching conversations strictly from IDE UI...');
|
|
17
|
+
|
|
18
|
+
if (!ctx.workbenchPage) {
|
|
19
|
+
logger.info('[Scraper] No active workbench page.');
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = await ctx.workbenchPage.evaluate(async () => {
|
|
24
|
+
try {
|
|
25
|
+
// 1. Get the CURRENT active conversation title from the chat panel header
|
|
26
|
+
const activeHeaderEl = document.querySelector('span.font-semibold.text-ide-text-color');
|
|
27
|
+
const activeTitle = activeHeaderEl && activeHeaderEl.textContent ? activeHeaderEl.textContent.trim() : null;
|
|
28
|
+
|
|
29
|
+
// 2. Find the history button
|
|
30
|
+
const historyBtn = document.querySelector('a[data-past-conversations-toggle="true"]');
|
|
31
|
+
|
|
32
|
+
if (!historyBtn) {
|
|
33
|
+
return { error: 'History button not found (a[data-past-conversations-toggle="true"])' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let isAlreadyOpen = !!document.querySelector('.text-quickinput-foreground.opacity-50');
|
|
37
|
+
|
|
38
|
+
if (!isAlreadyOpen) {
|
|
39
|
+
historyBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
|
|
40
|
+
historyBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
|
|
41
|
+
historyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < 20; i++) {
|
|
44
|
+
await new Promise(r => setTimeout(r, 100));
|
|
45
|
+
if (document.querySelector('.text-quickinput-foreground.opacity-50')) {
|
|
46
|
+
isAlreadyOpen = true;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isAlreadyOpen) {
|
|
53
|
+
return { error: 'History dropdown did not appear after clicking' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Extract the conversations
|
|
57
|
+
const rowSelector = '.cursor-pointer.flex.items-center.justify-between.rounded-md.text-quickinput-foreground';
|
|
58
|
+
const rowElements = Array.from(document.querySelectorAll(rowSelector));
|
|
59
|
+
|
|
60
|
+
const rows = rowElements.map((row, index) => {
|
|
61
|
+
const titleEl = row.querySelector('.truncate span');
|
|
62
|
+
const title = titleEl ? titleEl.textContent?.trim() || '' : row.textContent?.trim() || '';
|
|
63
|
+
|
|
64
|
+
let isActive = row.className.includes('bg-gray-500/10') || !!row.querySelector('svg.lucide-circle');
|
|
65
|
+
if (activeTitle && title === activeTitle) {
|
|
66
|
+
isActive = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { title, active: isActive, index };
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 4. Close the dropdown
|
|
73
|
+
historyBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
|
|
74
|
+
historyBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
|
|
75
|
+
historyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
76
|
+
|
|
77
|
+
return { rows, activeTitle };
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
return { error: e.message };
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (result?.error) {
|
|
84
|
+
throw new Error(`Failed to scrape IDE conversations: ${result.error}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!result?.rows) {
|
|
88
|
+
logger.info('[Scraper] No rows returned from conversation scraper snippet.');
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logger.info(`[Scraper] Successfully scraped ${result.rows.length} conversations. Active title: ${result.activeTitle}`);
|
|
93
|
+
|
|
94
|
+
let foundActive = false;
|
|
95
|
+
const conversations: IdeConversation[] = result.rows.map((r: any) => {
|
|
96
|
+
let active = r.active;
|
|
97
|
+
|
|
98
|
+
if (result.activeTitle && r.title === result.activeTitle) {
|
|
99
|
+
active = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (active) foundActive = true;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
title: r.title,
|
|
106
|
+
active,
|
|
107
|
+
index: r.index
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!foundActive && result.activeTitle) {
|
|
112
|
+
conversations.unshift({
|
|
113
|
+
title: result.activeTitle,
|
|
114
|
+
active: true,
|
|
115
|
+
index: -1
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return conversations;
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
logger.error(`[Scraper] Error fetching IDE conversations: ${err.message}`);
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State diffing for SSE stream.
|
|
3
|
+
* Compares two agent states and returns typed events for any changes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AgentState, SSEStep } from '@/lib/types';
|
|
7
|
+
import { logger } from '@/lib/logger';
|
|
8
|
+
|
|
9
|
+
export function diffStates(prev: AgentState, curr: AgentState): SSEStep[] {
|
|
10
|
+
const events: SSEStep[] = [];
|
|
11
|
+
|
|
12
|
+
// New thinking blocks
|
|
13
|
+
if (curr.thinking.length > prev.thinking.length) {
|
|
14
|
+
for (let i = prev.thinking.length; i < curr.thinking.length; i++) {
|
|
15
|
+
events.push({ type: 'thinking', data: curr.thinking[i] as any });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// New or updated tool calls
|
|
20
|
+
if (curr.toolCalls.length > prev.toolCalls.length) {
|
|
21
|
+
for (let i = prev.toolCalls.length; i < curr.toolCalls.length; i++) {
|
|
22
|
+
logger.debug(
|
|
23
|
+
`[Diff] Detected tool_call change at index ${i}: id=${curr.toolCalls[i]?.id}, status=${curr.toolCalls[i]?.status}`
|
|
24
|
+
);
|
|
25
|
+
events.push({
|
|
26
|
+
type: 'tool_call',
|
|
27
|
+
data: { ...curr.toolCalls[i], index: i, isNew: true } as any,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Updated existing tool calls
|
|
33
|
+
const sharedLen = Math.min(prev.toolCalls.length, curr.toolCalls.length);
|
|
34
|
+
for (let i = 0; i < sharedLen; i++) {
|
|
35
|
+
const p = prev.toolCalls[i];
|
|
36
|
+
const c = curr.toolCalls[i];
|
|
37
|
+
const footerChanged =
|
|
38
|
+
JSON.stringify(p.footerButtons) !== JSON.stringify(c.footerButtons);
|
|
39
|
+
if (
|
|
40
|
+
p.status !== c.status ||
|
|
41
|
+
p.exitCode !== c.exitCode ||
|
|
42
|
+
p.hasCancelBtn !== c.hasCancelBtn ||
|
|
43
|
+
footerChanged
|
|
44
|
+
) {
|
|
45
|
+
console.log(
|
|
46
|
+
`[diffStates] UPDATED tool_call at index ${i}: status ${p.status}->${c.status}, exitCode ${p.exitCode}->${c.exitCode}, footerChanged=${footerChanged}`
|
|
47
|
+
);
|
|
48
|
+
events.push({
|
|
49
|
+
type: 'tool_call',
|
|
50
|
+
data: { ...c, index: i, isNew: false } as any,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// HITL state changes
|
|
56
|
+
const prevHITL = prev.toolCalls.some((t) => t.hasCancelBtn);
|
|
57
|
+
const currHITL = curr.toolCalls.some((t) => t.hasCancelBtn);
|
|
58
|
+
if (currHITL && !prevHITL) {
|
|
59
|
+
const hitlTool = curr.toolCalls.find((t) => t.hasCancelBtn);
|
|
60
|
+
events.push({
|
|
61
|
+
type: 'hitl',
|
|
62
|
+
data: { action: 'approval_required', tool: hitlTool } as any,
|
|
63
|
+
});
|
|
64
|
+
} else if (!currHITL && prevHITL) {
|
|
65
|
+
events.push({
|
|
66
|
+
type: 'hitl',
|
|
67
|
+
data: { action: 'resolved' } as any,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// New response blocks
|
|
72
|
+
if (curr.responses.length > prev.responses.length) {
|
|
73
|
+
for (let i = prev.responses.length; i < curr.responses.length; i++) {
|
|
74
|
+
events.push({
|
|
75
|
+
type: 'response',
|
|
76
|
+
data: {
|
|
77
|
+
content: curr.responses[i],
|
|
78
|
+
index: i,
|
|
79
|
+
partial: curr.isRunning,
|
|
80
|
+
} as any,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Updated last response
|
|
85
|
+
if (
|
|
86
|
+
curr.responses.length > 0 &&
|
|
87
|
+
prev.responses.length > 0 &&
|
|
88
|
+
curr.responses.length === prev.responses.length
|
|
89
|
+
) {
|
|
90
|
+
const lastIdx = curr.responses.length - 1;
|
|
91
|
+
if (curr.responses[lastIdx] !== prev.responses[lastIdx]) {
|
|
92
|
+
events.push({
|
|
93
|
+
type: 'response',
|
|
94
|
+
data: {
|
|
95
|
+
content: curr.responses[lastIdx],
|
|
96
|
+
index: lastIdx,
|
|
97
|
+
partial: curr.isRunning,
|
|
98
|
+
} as any,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Notification blocks
|
|
104
|
+
if (curr.notifications.length > prev.notifications.length) {
|
|
105
|
+
for (let i = prev.notifications.length; i < curr.notifications.length; i++) {
|
|
106
|
+
events.push({
|
|
107
|
+
type: 'notification',
|
|
108
|
+
data: { content: curr.notifications[i], index: i } as any,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// File changes
|
|
114
|
+
if (
|
|
115
|
+
curr.fileChanges &&
|
|
116
|
+
prev.fileChanges &&
|
|
117
|
+
curr.fileChanges.length > prev.fileChanges.length
|
|
118
|
+
) {
|
|
119
|
+
for (let i = prev.fileChanges.length; i < curr.fileChanges.length; i++) {
|
|
120
|
+
events.push({ type: 'file_change', data: curr.fileChanges[i] as any });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Status change
|
|
125
|
+
if (prev.isRunning !== curr.isRunning) {
|
|
126
|
+
events.push({
|
|
127
|
+
type: 'status',
|
|
128
|
+
data: { isRunning: curr.isRunning } as any,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Error
|
|
133
|
+
if (curr.error && !prev.error) {
|
|
134
|
+
events.push({
|
|
135
|
+
type: 'error',
|
|
136
|
+
data: { message: curr.error } as any,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return events;
|
|
141
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TypeScript types for the Antigravity Chat Proxy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Page, Browser } from 'puppeteer-core';
|
|
6
|
+
|
|
7
|
+
// ── Context (shared server state) ──
|
|
8
|
+
|
|
9
|
+
export interface ProxyContext {
|
|
10
|
+
workbenchPage: Page | null;
|
|
11
|
+
browser: Browser | null;
|
|
12
|
+
allWorkbenches: WorkbenchInfo[];
|
|
13
|
+
activeWindowIdx: number;
|
|
14
|
+
activeConversationId: string | null;
|
|
15
|
+
activeTitle?: string | null;
|
|
16
|
+
lastActionTimestamp: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkbenchInfo {
|
|
20
|
+
page: Page;
|
|
21
|
+
title: string;
|
|
22
|
+
url: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Agent State (from scraper) ──
|
|
26
|
+
|
|
27
|
+
export interface ToolCall {
|
|
28
|
+
id: string;
|
|
29
|
+
status: string;
|
|
30
|
+
type: string;
|
|
31
|
+
path: string;
|
|
32
|
+
command: string | null;
|
|
33
|
+
exitCode: string | null;
|
|
34
|
+
hasCancelBtn: boolean;
|
|
35
|
+
footerButtons: string[];
|
|
36
|
+
hasTerminal: boolean;
|
|
37
|
+
terminalOutput: string | null;
|
|
38
|
+
additions?: string | null;
|
|
39
|
+
deletions?: string | null;
|
|
40
|
+
lineRange?: string | null;
|
|
41
|
+
mcpToolName?: string | null;
|
|
42
|
+
mcpArgs?: string | null;
|
|
43
|
+
mcpOutput?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ThinkingBlock {
|
|
47
|
+
time: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FileChange {
|
|
51
|
+
fileName: string;
|
|
52
|
+
type: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AgentState {
|
|
56
|
+
isRunning: boolean;
|
|
57
|
+
turnCount: number;
|
|
58
|
+
stepGroupCount: number;
|
|
59
|
+
thinking: ThinkingBlock[];
|
|
60
|
+
toolCalls: ToolCall[];
|
|
61
|
+
responses: string[];
|
|
62
|
+
notifications: string[];
|
|
63
|
+
error: string | null;
|
|
64
|
+
fileChanges: FileChange[];
|
|
65
|
+
lastTurnResponseHTML: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ChatTurn {
|
|
69
|
+
role: 'user' | 'agent';
|
|
70
|
+
content: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ChatHistory {
|
|
74
|
+
isRunning: boolean;
|
|
75
|
+
turnCount: number;
|
|
76
|
+
turns: ChatTurn[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── SSE Events ──
|
|
80
|
+
|
|
81
|
+
export type SSEEventType =
|
|
82
|
+
| 'thinking'
|
|
83
|
+
| 'tool_call'
|
|
84
|
+
| 'hitl'
|
|
85
|
+
| 'response'
|
|
86
|
+
| 'notification'
|
|
87
|
+
| 'file_change'
|
|
88
|
+
| 'status'
|
|
89
|
+
| 'done'
|
|
90
|
+
| 'error';
|
|
91
|
+
|
|
92
|
+
export interface SSEEvent {
|
|
93
|
+
type: SSEEventType;
|
|
94
|
+
data: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Conversation & Artifacts ──
|
|
98
|
+
|
|
99
|
+
export interface ConversationFile {
|
|
100
|
+
name: string;
|
|
101
|
+
size: number;
|
|
102
|
+
mtime: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface Conversation {
|
|
106
|
+
id: string;
|
|
107
|
+
title: string | null;
|
|
108
|
+
files: ConversationFile[];
|
|
109
|
+
mtime: string;
|
|
110
|
+
active: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Frontend Types ──
|
|
114
|
+
|
|
115
|
+
export interface WindowInfo {
|
|
116
|
+
index: number;
|
|
117
|
+
title: string;
|
|
118
|
+
url: string;
|
|
119
|
+
active: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ConversationInfo {
|
|
123
|
+
id: string; // The backend brain UUID, or "-1" for unknown
|
|
124
|
+
title: string;
|
|
125
|
+
active: boolean;
|
|
126
|
+
index: number;
|
|
127
|
+
mtime?: string;
|
|
128
|
+
files?: ArtifactFile[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ArtifactFile {
|
|
132
|
+
name: string;
|
|
133
|
+
size: number;
|
|
134
|
+
mtime: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ChatMessage {
|
|
138
|
+
role: 'user' | 'agent';
|
|
139
|
+
content: string;
|
|
140
|
+
steps?: SSEStep[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SSEStep {
|
|
144
|
+
type: string;
|
|
145
|
+
data: Record<string, any>;
|
|
146
|
+
}
|
package/lib/utils.ts
ADDED
package/next.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "antigravity-chat-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A web chat proxy for Antigravity IDE with ngrok OAuth tunnel support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"antigravity",
|
|
7
|
+
"ide",
|
|
8
|
+
"chat",
|
|
9
|
+
"proxy",
|
|
10
|
+
"ngrok"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"bin": {
|
|
14
|
+
"antigravity-chat-proxy": "bin/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"app/",
|
|
19
|
+
"components/",
|
|
20
|
+
"hooks/",
|
|
21
|
+
"lib/",
|
|
22
|
+
"public/",
|
|
23
|
+
"next.config.ts",
|
|
24
|
+
"tsconfig.json",
|
|
25
|
+
"package.json"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "next dev -p 5555",
|
|
29
|
+
"build": "next build",
|
|
30
|
+
"start": "next start -p 5555",
|
|
31
|
+
"tunnel": "node bin/cli.js",
|
|
32
|
+
"lint": "next lint",
|
|
33
|
+
"type-check": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@ngrok/ngrok": "^1.7.0",
|
|
37
|
+
"next": "16.1.6",
|
|
38
|
+
"puppeteer-core": "^24.39.0",
|
|
39
|
+
"react": "19.2.3",
|
|
40
|
+
"react-dom": "19.2.3"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20",
|
|
44
|
+
"@types/react": "^19",
|
|
45
|
+
"@types/react-dom": "^19",
|
|
46
|
+
"eslint": "^9.39.4",
|
|
47
|
+
"eslint-config-next": "^16.1.6",
|
|
48
|
+
"typescript": "^5"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/public/file.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
package/public/globe.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|