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,302 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import type { WindowInfo, ConversationInfo } from '@/lib/types';
|
|
5
|
+
import type { CdpStatus, RecentProject } from '@/hooks/use-conversations';
|
|
6
|
+
import ConversationSelector from './conversation-selector';
|
|
7
|
+
|
|
8
|
+
interface HeaderProps {
|
|
9
|
+
statusState: string;
|
|
10
|
+
statusText: string;
|
|
11
|
+
windows: WindowInfo[];
|
|
12
|
+
conversations: ConversationInfo[];
|
|
13
|
+
activeConversation: ConversationInfo | null;
|
|
14
|
+
cdpStatus: CdpStatus;
|
|
15
|
+
recentProjects: RecentProject[];
|
|
16
|
+
onSelectWindow: (idx: number) => void;
|
|
17
|
+
onSelectConversation: (id: string) => void;
|
|
18
|
+
onNewChat: () => void;
|
|
19
|
+
onToggleArtifacts: () => void;
|
|
20
|
+
onStartCdp: (projectDir?: string, killExisting?: boolean) => Promise<any>;
|
|
21
|
+
onOpenWindow: (projectDir: string) => Promise<any>;
|
|
22
|
+
onCloseWindow: (index: number) => Promise<any>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function Header({
|
|
26
|
+
statusState, statusText, windows, conversations, activeConversation,
|
|
27
|
+
cdpStatus, recentProjects, onSelectWindow, onSelectConversation, onNewChat, onToggleArtifacts,
|
|
28
|
+
onStartCdp, onOpenWindow, onCloseWindow,
|
|
29
|
+
}: HeaderProps) {
|
|
30
|
+
const [windowOpen, setWindowOpen] = useState(false);
|
|
31
|
+
const [newDirPath, setNewDirPath] = useState('');
|
|
32
|
+
const [isOpening, setIsOpening] = useState(false);
|
|
33
|
+
const [isStartingCdp, setIsStartingCdp] = useState(false);
|
|
34
|
+
const [actionMessage, setActionMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
|
35
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const handler = (e: MouseEvent) => {
|
|
40
|
+
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
|
41
|
+
setWindowOpen(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
document.addEventListener('mousedown', handler);
|
|
45
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Clear action message after 4 seconds
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (actionMessage) {
|
|
51
|
+
const t = setTimeout(() => setActionMessage(null), 4000);
|
|
52
|
+
return () => clearTimeout(t);
|
|
53
|
+
}
|
|
54
|
+
}, [actionMessage]);
|
|
55
|
+
|
|
56
|
+
const handleStartCdp = async () => {
|
|
57
|
+
setIsStartingCdp(true);
|
|
58
|
+
setActionMessage(null);
|
|
59
|
+
try {
|
|
60
|
+
const result = await onStartCdp('.', false);
|
|
61
|
+
setActionMessage({
|
|
62
|
+
text: result.message || (result.success ? 'CDP started!' : 'Failed to start CDP'),
|
|
63
|
+
type: result.success ? 'success' : 'error',
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
setActionMessage({ text: 'Failed to start CDP server', type: 'error' });
|
|
67
|
+
} finally {
|
|
68
|
+
setIsStartingCdp(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleOpenWindow = async (e: React.FormEvent) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const trimmed = newDirPath.trim();
|
|
75
|
+
if (!trimmed || isOpening) return;
|
|
76
|
+
|
|
77
|
+
setIsOpening(true);
|
|
78
|
+
setActionMessage(null);
|
|
79
|
+
try {
|
|
80
|
+
const result = await onOpenWindow(trimmed);
|
|
81
|
+
setActionMessage({
|
|
82
|
+
text: result.message || (result.success ? 'Window opened!' : 'Failed to open'),
|
|
83
|
+
type: result.success ? 'success' : 'error',
|
|
84
|
+
});
|
|
85
|
+
if (result.success) setNewDirPath('');
|
|
86
|
+
} catch {
|
|
87
|
+
setActionMessage({ text: 'Failed to open window', type: 'error' });
|
|
88
|
+
} finally {
|
|
89
|
+
setIsOpening(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleCloseWindow = async (idx: number, e: React.MouseEvent) => {
|
|
94
|
+
e.stopPropagation();
|
|
95
|
+
const confirmed = window.confirm(`Close window "${windows[idx]?.title || idx}"?`);
|
|
96
|
+
if (!confirmed) return;
|
|
97
|
+
|
|
98
|
+
const result = await onCloseWindow(idx);
|
|
99
|
+
setActionMessage({
|
|
100
|
+
text: result.message || (result.success ? 'Closed!' : 'Failed to close'),
|
|
101
|
+
type: result.success ? 'success' : 'error',
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<header className="header">
|
|
107
|
+
<div className="header-left">
|
|
108
|
+
<div className="logo">
|
|
109
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="url(#header-gradient)" strokeWidth="1.5">
|
|
110
|
+
<defs>
|
|
111
|
+
<linearGradient id="header-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
112
|
+
<stop offset="0%" style={{ stopColor: '#6366f1' }} />
|
|
113
|
+
<stop offset="100%" style={{ stopColor: '#a855f7' }} />
|
|
114
|
+
</linearGradient>
|
|
115
|
+
</defs>
|
|
116
|
+
<circle cx="12" cy="12" r="10" />
|
|
117
|
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
|
118
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
119
|
+
</svg>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="header-right">
|
|
123
|
+
{/* Conversation Selector */}
|
|
124
|
+
<ConversationSelector
|
|
125
|
+
conversations={conversations}
|
|
126
|
+
activeConversation={activeConversation}
|
|
127
|
+
onSelect={onSelectConversation}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Window Selector */}
|
|
131
|
+
<div ref={wrapperRef} className={`window-selector-wrapper ${windowOpen ? 'open' : ''}`}>
|
|
132
|
+
<button className="window-selector-btn" onClick={() => setWindowOpen(!windowOpen)} title="Manage Antigravity windows">
|
|
133
|
+
{/* CDP status indicator */}
|
|
134
|
+
<span className={`cdp-indicator ${cdpStatus.active ? 'active' : 'inactive'}`} />
|
|
135
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
136
|
+
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
|
137
|
+
<line x1="8" y1="21" x2="16" y2="21" />
|
|
138
|
+
<line x1="12" y1="17" x2="12" y2="21" />
|
|
139
|
+
</svg>
|
|
140
|
+
<span style={{ maxWidth: '120px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'inline-block', verticalAlign: 'bottom' }}>
|
|
141
|
+
{windows.find(w => w.active)?.title || 'Windows'}
|
|
142
|
+
</span>
|
|
143
|
+
<svg className="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
144
|
+
<polyline points="6 9 12 15 18 9" />
|
|
145
|
+
</svg>
|
|
146
|
+
</button>
|
|
147
|
+
<div className={`window-dropdown ${windowOpen ? 'open' : ''}`}>
|
|
148
|
+
{/* CDP Status Bar */}
|
|
149
|
+
<div className="wm-cdp-status">
|
|
150
|
+
<div className="wm-cdp-info">
|
|
151
|
+
<span className={`wm-cdp-dot ${cdpStatus.active ? 'active' : 'inactive'}`} />
|
|
152
|
+
<span>{cdpStatus.active ? `CDP Active · ${cdpStatus.windowCount} window${cdpStatus.windowCount !== 1 ? 's' : ''}` : 'CDP Inactive'}</span>
|
|
153
|
+
</div>
|
|
154
|
+
{!cdpStatus.active && (
|
|
155
|
+
<button
|
|
156
|
+
className="wm-cdp-start-btn"
|
|
157
|
+
onClick={handleStartCdp}
|
|
158
|
+
disabled={isStartingCdp}
|
|
159
|
+
title="Start Antigravity with CDP"
|
|
160
|
+
>
|
|
161
|
+
{isStartingCdp ? (
|
|
162
|
+
<span className="wm-spinner" />
|
|
163
|
+
) : (
|
|
164
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
165
|
+
<polygon points="5 3 19 12 5 21 5 3" />
|
|
166
|
+
</svg>
|
|
167
|
+
)}
|
|
168
|
+
{isStartingCdp ? 'Starting...' : 'Start CDP'}
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Window List */}
|
|
174
|
+
<div className="window-dropdown-header">
|
|
175
|
+
Open Windows
|
|
176
|
+
</div>
|
|
177
|
+
{windows.map(w => (
|
|
178
|
+
<div key={w.index} className={`window-item ${w.active ? 'active' : ''}`}>
|
|
179
|
+
<button
|
|
180
|
+
className="window-item-select"
|
|
181
|
+
onClick={() => { onSelectWindow(w.index); setWindowOpen(false); }}
|
|
182
|
+
>
|
|
183
|
+
<span className="window-dot" />
|
|
184
|
+
<span className="window-item-title">{w.title}</span>
|
|
185
|
+
</button>
|
|
186
|
+
<button
|
|
187
|
+
className="window-item-close"
|
|
188
|
+
onClick={(e) => handleCloseWindow(w.index, e)}
|
|
189
|
+
title={`Close "${w.title}"`}
|
|
190
|
+
>
|
|
191
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
192
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
193
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
194
|
+
</svg>
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
{windows.length === 0 && (
|
|
199
|
+
<div style={{ padding: '12px', color: 'var(--text-muted)', fontSize: '12px' }}>
|
|
200
|
+
No windows detected
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
{/* Recent Projects */}
|
|
204
|
+
{recentProjects.length > 0 && (
|
|
205
|
+
<div className="wm-recent-section">
|
|
206
|
+
<div className="wm-recent-header">Recent Projects</div>
|
|
207
|
+
{recentProjects.map(p => (
|
|
208
|
+
<button
|
|
209
|
+
key={p.path}
|
|
210
|
+
className="wm-recent-item"
|
|
211
|
+
onClick={async () => {
|
|
212
|
+
setIsOpening(true);
|
|
213
|
+
setActionMessage(null);
|
|
214
|
+
try {
|
|
215
|
+
const result = await onOpenWindow(p.path);
|
|
216
|
+
setActionMessage({
|
|
217
|
+
text: result.message || (result.success ? 'Opened!' : 'Failed'),
|
|
218
|
+
type: result.success ? 'success' : 'error',
|
|
219
|
+
});
|
|
220
|
+
} catch {
|
|
221
|
+
setActionMessage({ text: 'Failed to open', type: 'error' });
|
|
222
|
+
} finally {
|
|
223
|
+
setIsOpening(false);
|
|
224
|
+
}
|
|
225
|
+
}}
|
|
226
|
+
disabled={isOpening}
|
|
227
|
+
title={p.path}
|
|
228
|
+
>
|
|
229
|
+
<svg className="wm-recent-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
230
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
231
|
+
</svg>
|
|
232
|
+
<div className="wm-recent-info">
|
|
233
|
+
<span className="wm-recent-name">{p.name}</span>
|
|
234
|
+
<span className="wm-recent-path">{p.path}</span>
|
|
235
|
+
</div>
|
|
236
|
+
</button>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
{/* Open New Window */}
|
|
242
|
+
<div className="wm-open-section">
|
|
243
|
+
<div className="wm-open-label">Open New Window</div>
|
|
244
|
+
<form className="wm-open-form" onSubmit={handleOpenWindow}>
|
|
245
|
+
<input
|
|
246
|
+
ref={inputRef}
|
|
247
|
+
type="text"
|
|
248
|
+
className="wm-open-input"
|
|
249
|
+
value={newDirPath}
|
|
250
|
+
onChange={(e) => setNewDirPath(e.target.value)}
|
|
251
|
+
placeholder="/path/to/project"
|
|
252
|
+
disabled={isOpening}
|
|
253
|
+
/>
|
|
254
|
+
<button
|
|
255
|
+
type="submit"
|
|
256
|
+
className="wm-open-btn"
|
|
257
|
+
disabled={isOpening || !newDirPath.trim()}
|
|
258
|
+
title="Open directory in Antigravity"
|
|
259
|
+
>
|
|
260
|
+
{isOpening ? (
|
|
261
|
+
<span className="wm-spinner" />
|
|
262
|
+
) : (
|
|
263
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
264
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
265
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
266
|
+
</svg>
|
|
267
|
+
)}
|
|
268
|
+
</button>
|
|
269
|
+
</form>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Action Message Toast */}
|
|
273
|
+
{actionMessage && (
|
|
274
|
+
<div className={`wm-action-message ${actionMessage.type}`}>
|
|
275
|
+
{actionMessage.text}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Artifacts Button */}
|
|
282
|
+
<button className="icon-btn" onClick={onToggleArtifacts} title="Artifacts" aria-label="Artifacts">
|
|
283
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
284
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
285
|
+
<polyline points="14 2 14 8 20 8" />
|
|
286
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
287
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
288
|
+
<polyline points="10 9 9 9 8 9" />
|
|
289
|
+
</svg>
|
|
290
|
+
</button>
|
|
291
|
+
|
|
292
|
+
{/* New Chat Button */}
|
|
293
|
+
<button className="icon-btn" onClick={onNewChat} title="New Chat" aria-label="New Chat">
|
|
294
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
295
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
296
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
297
|
+
</svg>
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
</header>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface HITLDialogProps {
|
|
4
|
+
onApprove: () => void;
|
|
5
|
+
onReject: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function HITLDialog({ onApprove, onReject }: HITLDialogProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="hitl-dialog">
|
|
11
|
+
<div className="hitl-label">⚠️ Needs Approval</div>
|
|
12
|
+
<div className="hitl-context">The agent needs your permission to proceed.</div>
|
|
13
|
+
<div className="hitl-actions">
|
|
14
|
+
<button className="hitl-approve-btn" onClick={onApprove}>
|
|
15
|
+
✓ Approve
|
|
16
|
+
</button>
|
|
17
|
+
<button className="hitl-reject-btn" onClick={onReject}>
|
|
18
|
+
✕ Reject
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ChatMessage, SSEStep } from '@/lib/types';
|
|
4
|
+
import UserMessage from './user-message';
|
|
5
|
+
import AgentMessage from './agent-message';
|
|
6
|
+
|
|
7
|
+
interface MessageListProps {
|
|
8
|
+
messages: ChatMessage[];
|
|
9
|
+
currentSteps: SSEStep[];
|
|
10
|
+
currentResponse: string;
|
|
11
|
+
isStreaming: boolean;
|
|
12
|
+
onApprove: () => void;
|
|
13
|
+
onReject: () => void;
|
|
14
|
+
onRetry?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function MessageList({ messages, currentSteps, currentResponse, isStreaming, onApprove, onReject, onRetry }: MessageListProps) {
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{messages.map((msg, i) => (
|
|
21
|
+
msg.role === 'user' ? (
|
|
22
|
+
<UserMessage key={`msg-${i}`} content={msg.content} />
|
|
23
|
+
) : (
|
|
24
|
+
<AgentMessage key={`msg-${i}`} content={msg.content} steps={msg.steps || []} isStreaming={false} onApprove={onApprove} onReject={onReject} onRetry={onRetry} />
|
|
25
|
+
)
|
|
26
|
+
))}
|
|
27
|
+
|
|
28
|
+
{/* Active streaming message */}
|
|
29
|
+
{isStreaming && (
|
|
30
|
+
<AgentMessage
|
|
31
|
+
content={currentResponse}
|
|
32
|
+
steps={currentSteps}
|
|
33
|
+
isStreaming={true}
|
|
34
|
+
onApprove={onApprove}
|
|
35
|
+
onReject={onReject}
|
|
36
|
+
onRetry={onRetry}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface ThinkingBlockProps {
|
|
4
|
+
time: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function ThinkingBlock({ time }: ThinkingBlockProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="thinking-block">
|
|
10
|
+
<span className="thinking-icon">💭</span>
|
|
11
|
+
<span>{time}</span>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
4
|
+
command: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>',
|
|
5
|
+
file: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
|
|
6
|
+
search: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
7
|
+
read: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
8
|
+
browser: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
|
9
|
+
mcp: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v6M12 22v-6M4.93 4.93l4.24 4.24M14.83 14.83l4.24 4.24M2 12h6M22 12h-6M4.93 19.07l4.24-4.24M14.83 9.17l4.24-4.24"/><circle cx="12" cy="12" r="4"/></svg>',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getStatusClass(status: string): string {
|
|
13
|
+
const s = (status || '').toLowerCase();
|
|
14
|
+
if (s.startsWith('running') || s.startsWith('editing') || s.startsWith('creating') || s.startsWith('search')) return 'running';
|
|
15
|
+
if (s.startsWith('ran') || s.startsWith('edited') || s.startsWith('created') || s.startsWith('read') || s.startsWith('viewed') || s.startsWith('analyzed') || s.startsWith('wrote') || s.startsWith('replaced') || s.startsWith('deleted')) return 'done';
|
|
16
|
+
if (s.includes('error') || s.includes('fail')) return 'error';
|
|
17
|
+
if (s.startsWith('mcp')) return 'mcp';
|
|
18
|
+
return 'running';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ToolCallCardProps {
|
|
22
|
+
data: Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function ToolCallCard({ data }: ToolCallCardProps) {
|
|
26
|
+
const statusClass = getStatusClass(data.status);
|
|
27
|
+
const iconHtml = TOOL_ICONS[data.type] || TOOL_ICONS.file;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={`tool-call-card ${statusClass}`} data-tool-index={data.index}>
|
|
31
|
+
<div className="tool-header">
|
|
32
|
+
<span className="tool-icon" dangerouslySetInnerHTML={{ __html: iconHtml }} />
|
|
33
|
+
<span className="tool-status-text">{data.status}</span>
|
|
34
|
+
{data.path && <span className="tool-path" title={data.path}>{data.path}</span>}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{data.command && (
|
|
38
|
+
<div className="tool-command"><code>{data.command}</code></div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{(data.additions || data.deletions) && (
|
|
42
|
+
<div className="tool-file-changes">
|
|
43
|
+
{data.additions && <span className="tool-additions">{data.additions}</span>}
|
|
44
|
+
{data.deletions && <span className="tool-deletions">{data.deletions}</span>}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{data.exitCode && <div className="tool-exit-code">{data.exitCode}</div>}
|
|
49
|
+
|
|
50
|
+
{data.terminalOutput && (
|
|
51
|
+
<div className="tool-terminal">{data.terminalOutput}</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{data.footerButtons && data.footerButtons.length > 0 && (
|
|
55
|
+
<div className="tool-footer-actions">
|
|
56
|
+
{data.footerButtons.map((btn: string, i: number) => (
|
|
57
|
+
<button key={i} className="tool-footer-btn"
|
|
58
|
+
onClick={async () => {
|
|
59
|
+
try {
|
|
60
|
+
await fetch('/api/v1/chat/action', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ toolId: data.id, buttonText: btn }),
|
|
64
|
+
});
|
|
65
|
+
} catch { /* ignore */ }
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{btn}
|
|
69
|
+
</button>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface UserMessageProps {
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function UserMessage({ content }: UserMessageProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="user-message">
|
|
10
|
+
<div className="message-content">{content}</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface WelcomeScreenProps {
|
|
4
|
+
onQuickPrompt: (text: string) => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function WelcomeScreen({ onQuickPrompt }: WelcomeScreenProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="welcome-screen">
|
|
10
|
+
<div className="welcome-icon">
|
|
11
|
+
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="url(#gradient)" strokeWidth="1.5">
|
|
12
|
+
<defs>
|
|
13
|
+
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
14
|
+
<stop offset="0%" style={{ stopColor: '#6366f1' }} />
|
|
15
|
+
<stop offset="100%" style={{ stopColor: '#a855f7' }} />
|
|
16
|
+
</linearGradient>
|
|
17
|
+
</defs>
|
|
18
|
+
<circle cx="12" cy="12" r="10" />
|
|
19
|
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
|
20
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
21
|
+
</svg>
|
|
22
|
+
</div>
|
|
23
|
+
<h2>Antigravity Agent</h2>
|
|
24
|
+
<p>Ask anything. The agent has access to your IDE — files, terminal, search, and more.</p>
|
|
25
|
+
<div className="quick-prompts">
|
|
26
|
+
<button className="quick-prompt" onClick={() => onQuickPrompt('What files are in the current workspace?')}>
|
|
27
|
+
📁 List workspace files
|
|
28
|
+
</button>
|
|
29
|
+
<button className="quick-prompt" onClick={() => onQuickPrompt('Explain the architecture of this project')}>
|
|
30
|
+
🏗️ Explain architecture
|
|
31
|
+
</button>
|
|
32
|
+
<button className="quick-prompt" onClick={() => onQuickPrompt('Help me debug the most recent error')}>
|
|
33
|
+
🐛 Debug last error
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import type { ArtifactFile } from '@/lib/types';
|
|
3
|
+
|
|
4
|
+
const API_BASE = '/api/v1';
|
|
5
|
+
const POLL_INTERVAL_MS = 3000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Computes a simple hash string from file metadata to detect changes
|
|
9
|
+
* without unnecessary re-renders (mirrors old app's approach).
|
|
10
|
+
*/
|
|
11
|
+
function computeFileHash(files: ArtifactFile[]): string {
|
|
12
|
+
return JSON.stringify(files.map(f => f.name + f.size + f.mtime));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useArtifacts(activeConversationId?: string | null) {
|
|
16
|
+
const [artifactFiles, setArtifactFiles] = useState<ArtifactFile[]>([]);
|
|
17
|
+
const [artifactPanelOpen, setArtifactPanelOpen] = useState(false);
|
|
18
|
+
|
|
19
|
+
const lastHashRef = useRef('');
|
|
20
|
+
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
21
|
+
|
|
22
|
+
const loadArtifacts = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${API_BASE}/artifacts/active`);
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
const files: ArtifactFile[] = data.files || [];
|
|
27
|
+
|
|
28
|
+
// Hash-based change detection: only update state if files actually changed
|
|
29
|
+
const newHash = computeFileHash(files);
|
|
30
|
+
if (newHash !== lastHashRef.current) {
|
|
31
|
+
lastHashRef.current = newHash;
|
|
32
|
+
setArtifactFiles(files);
|
|
33
|
+
}
|
|
34
|
+
} catch { /* ignore */ }
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const toggleArtifactPanel = useCallback(() => {
|
|
38
|
+
setArtifactPanelOpen(prev => !prev);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const openArtifactPanel = useCallback(() => {
|
|
42
|
+
setArtifactPanelOpen(true);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Start/stop polling based on panel visibility
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (artifactPanelOpen) {
|
|
48
|
+
// Fetch immediately when panel opens
|
|
49
|
+
loadArtifacts();
|
|
50
|
+
|
|
51
|
+
// Start polling every 3 seconds
|
|
52
|
+
pollingRef.current = setInterval(loadArtifacts, POLL_INTERVAL_MS);
|
|
53
|
+
} else {
|
|
54
|
+
// Stop polling when panel is closed
|
|
55
|
+
if (pollingRef.current) {
|
|
56
|
+
clearInterval(pollingRef.current);
|
|
57
|
+
pollingRef.current = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
if (pollingRef.current) {
|
|
63
|
+
clearInterval(pollingRef.current);
|
|
64
|
+
pollingRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}, [artifactPanelOpen, loadArtifacts]);
|
|
68
|
+
|
|
69
|
+
// Re-fetch when the active conversation changes
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (activeConversationId) {
|
|
72
|
+
// Reset hash so we always show fresh data for the new conversation
|
|
73
|
+
lastHashRef.current = '';
|
|
74
|
+
loadArtifacts();
|
|
75
|
+
}
|
|
76
|
+
}, [activeConversationId, loadArtifacts]);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
artifactFiles,
|
|
80
|
+
artifactPanelOpen,
|
|
81
|
+
toggleArtifactPanel,
|
|
82
|
+
openArtifactPanel,
|
|
83
|
+
loadArtifacts
|
|
84
|
+
};
|
|
85
|
+
}
|