antigravity-chat-proxy 0.1.0

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