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,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full agent state scraper.
|
|
3
|
+
* Scrapes the Antigravity agent side panel DOM to extract comprehensive state.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { SELECTORS } from '../cdp/selectors';
|
|
9
|
+
import type { ProxyContext, AgentState } from '../types';
|
|
10
|
+
|
|
11
|
+
const DEBUG_FILE = path.join('/tmp', 'proxy-debug-state.json');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get a comprehensive snapshot of the entire agent panel state.
|
|
15
|
+
* Includes turn-based scoping to isolate the current conversation turn.
|
|
16
|
+
*/
|
|
17
|
+
export async function getFullAgentState(ctx: ProxyContext): Promise<AgentState> {
|
|
18
|
+
if (!ctx.workbenchPage) {
|
|
19
|
+
return {
|
|
20
|
+
isRunning: false,
|
|
21
|
+
turnCount: 0,
|
|
22
|
+
stepGroupCount: 0,
|
|
23
|
+
thinking: [],
|
|
24
|
+
toolCalls: [],
|
|
25
|
+
responses: [],
|
|
26
|
+
notifications: [],
|
|
27
|
+
error: null,
|
|
28
|
+
fileChanges: [],
|
|
29
|
+
lastTurnResponseHTML: '',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const state = await ctx.workbenchPage.evaluate((spinnerSel: string) => {
|
|
34
|
+
const getClass = (el: Element | null) =>
|
|
35
|
+
(el?.getAttribute ? el.getAttribute('class') : '') || '';
|
|
36
|
+
|
|
37
|
+
interface BrowserToolCall {
|
|
38
|
+
id: string;
|
|
39
|
+
status: string;
|
|
40
|
+
type: string;
|
|
41
|
+
path: string;
|
|
42
|
+
command: string | null;
|
|
43
|
+
exitCode: string | null;
|
|
44
|
+
hasCancelBtn: boolean;
|
|
45
|
+
footerButtons: string[];
|
|
46
|
+
hasTerminal: boolean;
|
|
47
|
+
terminalOutput: string | null;
|
|
48
|
+
additions?: string | null;
|
|
49
|
+
deletions?: string | null;
|
|
50
|
+
lineRange?: string | null;
|
|
51
|
+
mcpToolName?: string | null;
|
|
52
|
+
mcpArgs?: string | null;
|
|
53
|
+
mcpOutput?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
57
|
+
if (!panel)
|
|
58
|
+
return {
|
|
59
|
+
isRunning: false,
|
|
60
|
+
turnCount: 0,
|
|
61
|
+
stepGroupCount: 0,
|
|
62
|
+
thinking: [] as { time: string }[],
|
|
63
|
+
toolCalls: [] as BrowserToolCall[],
|
|
64
|
+
responses: [] as string[],
|
|
65
|
+
notifications: [] as string[],
|
|
66
|
+
error: null as string | null,
|
|
67
|
+
fileChanges: [] as { fileName: string; type: string }[],
|
|
68
|
+
lastTurnResponseHTML: '',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ── 1. Running state (multi-signal) ──
|
|
72
|
+
let isRunning = false;
|
|
73
|
+
let buttonStateDefinitive = false;
|
|
74
|
+
|
|
75
|
+
// Check Chat Input Send/Stop button first (Most reliable indicator)
|
|
76
|
+
let inputArea = document.querySelector('#antigravity\\.agentSidePanelInputBox');
|
|
77
|
+
if (!inputArea) inputArea = panel.querySelector('[id*="InputBox"]');
|
|
78
|
+
|
|
79
|
+
if (inputArea) {
|
|
80
|
+
const wrapper = inputArea.closest('.flex') || inputArea.parentElement?.parentElement || inputArea.parentElement;
|
|
81
|
+
if (wrapper) {
|
|
82
|
+
(window as any).__proxyInputBoxHTML = wrapper.outerHTML;
|
|
83
|
+
const inputBtns = wrapper.querySelectorAll('button');
|
|
84
|
+
|
|
85
|
+
let hasStop = false;
|
|
86
|
+
let hasSend = false;
|
|
87
|
+
|
|
88
|
+
for (const btn of inputBtns) {
|
|
89
|
+
const html = btn.innerHTML || '';
|
|
90
|
+
const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
91
|
+
const text = (btn.textContent || '').trim().toLowerCase();
|
|
92
|
+
const tooltipId = (btn.getAttribute('data-tooltip-id') || '').toLowerCase();
|
|
93
|
+
|
|
94
|
+
// The send/stop button changes its tooltip and contents.
|
|
95
|
+
// We must be careful not to match 'lucide-square-slash' or other derived
|
|
96
|
+
// square icons by ensuring 'lucide-square' is followed by a non-word character.
|
|
97
|
+
const isStopIcon =
|
|
98
|
+
html.match(/lucide-square(?:[^a-z0-9-]|$)/i) ||
|
|
99
|
+
html.includes('lucide-circle-stop') ||
|
|
100
|
+
html.includes('lucide-octagon');
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
isStopIcon ||
|
|
104
|
+
ariaLabel.includes('stop') ||
|
|
105
|
+
ariaLabel.includes('cancel') ||
|
|
106
|
+
text === 'stop' ||
|
|
107
|
+
tooltipId.includes('stop')
|
|
108
|
+
) {
|
|
109
|
+
hasStop = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
html.includes('lucide-send') ||
|
|
114
|
+
html.includes('lucide-arrow-up') ||
|
|
115
|
+
html.includes('lucide-arrow-right') ||
|
|
116
|
+
html.includes('codicon-send') ||
|
|
117
|
+
html.includes('lucide-corner-down-left') ||
|
|
118
|
+
ariaLabel.includes('send') ||
|
|
119
|
+
ariaLabel.includes('submit') ||
|
|
120
|
+
text === 'send' ||
|
|
121
|
+
tooltipId.includes('send')
|
|
122
|
+
) {
|
|
123
|
+
hasSend = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Let the stop state win if both mistakenly present, otherwise send state wins
|
|
128
|
+
if (hasStop) {
|
|
129
|
+
isRunning = true;
|
|
130
|
+
buttonStateDefinitive = true;
|
|
131
|
+
} else if (hasSend) {
|
|
132
|
+
isRunning = false;
|
|
133
|
+
buttonStateDefinitive = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Fallback to other signals only if the input button state was ambiguous
|
|
139
|
+
if (!buttonStateDefinitive) {
|
|
140
|
+
// Signal A: Visible spinner
|
|
141
|
+
const spinners = panel.querySelectorAll(spinnerSel);
|
|
142
|
+
for (const spinner of spinners) {
|
|
143
|
+
let el: Element | null = spinner;
|
|
144
|
+
let hidden = false;
|
|
145
|
+
while (el) {
|
|
146
|
+
const cls = getClass(el);
|
|
147
|
+
if (cls.includes('invisible') || cls.includes('opacity-0')) {
|
|
148
|
+
hidden = true;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
el = el.parentElement;
|
|
152
|
+
}
|
|
153
|
+
if (!hidden) {
|
|
154
|
+
isRunning = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Signal B: General Stop/abort button visible elsewhere in the panel
|
|
160
|
+
if (!isRunning) {
|
|
161
|
+
const allBtns = panel.querySelectorAll('button');
|
|
162
|
+
for (const btn of allBtns) {
|
|
163
|
+
const label = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
164
|
+
const text = (btn.textContent?.trim() || '').toLowerCase();
|
|
165
|
+
if (
|
|
166
|
+
(text === 'stop' ||
|
|
167
|
+
text === 'abort' ||
|
|
168
|
+
label.includes('stop') ||
|
|
169
|
+
label.includes('abort') ||
|
|
170
|
+
label.includes('interrupt')) &&
|
|
171
|
+
getComputedStyle(btn).display !== 'none' &&
|
|
172
|
+
getComputedStyle(btn).visibility !== 'hidden'
|
|
173
|
+
) {
|
|
174
|
+
isRunning = true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 2. Turn & Step Group structure ──
|
|
182
|
+
const conversation =
|
|
183
|
+
panel.querySelector('#conversation') ||
|
|
184
|
+
document.querySelector('#conversation');
|
|
185
|
+
const scrollArea = conversation?.querySelector('.overflow-y-auto');
|
|
186
|
+
const msgList = scrollArea?.querySelector('.mx-auto');
|
|
187
|
+
const allTurns = msgList ? Array.from(msgList.children) : [];
|
|
188
|
+
const turnCount = allTurns.length;
|
|
189
|
+
const lastTurn =
|
|
190
|
+
allTurns.length > 0 ? allTurns[allTurns.length - 1] : null;
|
|
191
|
+
|
|
192
|
+
const contentDiv =
|
|
193
|
+
lastTurn?.querySelector('.relative.flex.flex-col.gap-y-3') || lastTurn;
|
|
194
|
+
const stepGroups = contentDiv ? Array.from(contentDiv.children) : [];
|
|
195
|
+
const stepGroupCount = stepGroups.length;
|
|
196
|
+
|
|
197
|
+
const scopeEl = lastTurn || panel;
|
|
198
|
+
|
|
199
|
+
// HITL button detection helper
|
|
200
|
+
const HITL_WORDS = [
|
|
201
|
+
'run', 'proceed', 'approve', 'allow', 'yes', 'accept',
|
|
202
|
+
'continue', 'save', 'confirm', 'deny', 'reject', 'cancel', 'no',
|
|
203
|
+
'allow once', 'allow this conversation', 'ask every time', 'relocate',
|
|
204
|
+
];
|
|
205
|
+
const isHitlAction = (text: string) => {
|
|
206
|
+
if (!text) return false;
|
|
207
|
+
const lower = text.trim().toLowerCase();
|
|
208
|
+
return HITL_WORDS.some((w) => lower === w || lower.startsWith(w));
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ── 3. Thinking blocks ──
|
|
212
|
+
const thinking: { time: string }[] = [];
|
|
213
|
+
const thinkingBtns = Array.from(scopeEl.querySelectorAll('button')).filter(
|
|
214
|
+
(b) => b.textContent?.trim().startsWith('Thought for')
|
|
215
|
+
);
|
|
216
|
+
for (const btn of thinkingBtns) {
|
|
217
|
+
thinking.push({ time: btn.textContent!.trim() });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 4. Tool call steps ──
|
|
221
|
+
const toolCalls: BrowserToolCall[] = [];
|
|
222
|
+
const toolContainers = scopeEl.querySelectorAll(
|
|
223
|
+
'.flex.flex-col.gap-2.border.rounded-lg.my-1'
|
|
224
|
+
);
|
|
225
|
+
let toolCounter = (window as any).__proxyToolCounter || 0;
|
|
226
|
+
|
|
227
|
+
for (const container of toolContainers) {
|
|
228
|
+
const el = container as HTMLElement;
|
|
229
|
+
if (!el.dataset.proxyToolId) {
|
|
230
|
+
el.dataset.proxyToolId = String(toolCounter++);
|
|
231
|
+
}
|
|
232
|
+
const proxyToolId = el.dataset.proxyToolId;
|
|
233
|
+
|
|
234
|
+
const header = el.querySelector('.mb-1.px-2.py-1.text-sm');
|
|
235
|
+
const statusSpan = header?.querySelector('span.opacity-60');
|
|
236
|
+
const status = statusSpan?.textContent?.trim() || '';
|
|
237
|
+
|
|
238
|
+
const pathSpan = el.querySelector('span.font-mono.text-sm');
|
|
239
|
+
const filePath = pathSpan?.textContent?.trim() || '';
|
|
240
|
+
|
|
241
|
+
let command = '';
|
|
242
|
+
const pre = el.querySelector('pre.whitespace-pre-wrap');
|
|
243
|
+
if (pre) {
|
|
244
|
+
const preText = pre.textContent?.trim() || '';
|
|
245
|
+
const dollarIdx = preText.indexOf('$');
|
|
246
|
+
if (dollarIdx !== -1) {
|
|
247
|
+
command = preText.substring(dollarIdx + 1).trim();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let exitCode: string | null = null;
|
|
252
|
+
const allEls = el.querySelectorAll('span, div');
|
|
253
|
+
for (const e of allEls) {
|
|
254
|
+
const t = e.textContent?.trim() || '';
|
|
255
|
+
if (t.startsWith('Exit code')) {
|
|
256
|
+
exitCode = t;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const allBtns = Array.from(el.querySelectorAll('button'));
|
|
262
|
+
const hasCancelBtn = allBtns.some(
|
|
263
|
+
(b) => b.textContent?.trim() === 'Cancel'
|
|
264
|
+
);
|
|
265
|
+
const footerButtons = allBtns
|
|
266
|
+
.map((b) => b.textContent?.trim() || '')
|
|
267
|
+
.filter(isHitlAction);
|
|
268
|
+
|
|
269
|
+
let type = 'unknown';
|
|
270
|
+
const sl = status.toLowerCase();
|
|
271
|
+
if (sl.includes('command')) type = 'command';
|
|
272
|
+
else if (
|
|
273
|
+
sl.includes('file') ||
|
|
274
|
+
sl.includes('edit') ||
|
|
275
|
+
sl.includes('creat') ||
|
|
276
|
+
sl.includes('writ')
|
|
277
|
+
)
|
|
278
|
+
type = 'file';
|
|
279
|
+
else if (sl.includes('search') || sl.includes('grep')) type = 'search';
|
|
280
|
+
else if (sl.includes('read') || sl.includes('view')) type = 'read';
|
|
281
|
+
else if (sl.includes('brows')) type = 'browser';
|
|
282
|
+
|
|
283
|
+
const terminal = el.querySelector('.component-shared-terminal');
|
|
284
|
+
let terminalOutput = '';
|
|
285
|
+
if (terminal) {
|
|
286
|
+
const rows =
|
|
287
|
+
terminal.querySelector('.xterm-rows') ||
|
|
288
|
+
terminal.querySelector('.xterm-screen') ||
|
|
289
|
+
terminal.querySelector('[class*="xterm"]');
|
|
290
|
+
if (rows) terminalOutput = rows.textContent?.substring(0, 500) || '';
|
|
291
|
+
if (!terminalOutput)
|
|
292
|
+
terminalOutput = terminal.textContent?.substring(0, 500) || '';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
toolCalls.push({
|
|
296
|
+
id: proxyToolId,
|
|
297
|
+
status,
|
|
298
|
+
type,
|
|
299
|
+
path: filePath,
|
|
300
|
+
command: command || null,
|
|
301
|
+
exitCode,
|
|
302
|
+
hasCancelBtn,
|
|
303
|
+
footerButtons,
|
|
304
|
+
hasTerminal: !!terminal,
|
|
305
|
+
terminalOutput: terminalOutput || null,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
(window as any).__proxyToolCounter = toolCounter;
|
|
309
|
+
|
|
310
|
+
// ── 4b. Inline file-system tools ──
|
|
311
|
+
const fileToolRows = scopeEl.querySelectorAll(
|
|
312
|
+
'.flex.flex-col.space-y-2 > .flex.flex-row:not(.my-2)'
|
|
313
|
+
);
|
|
314
|
+
const statusPattern =
|
|
315
|
+
/^(Edited|Created|Analyzed|Read|Viewed|Wrote|Replaced|Searching|Deleted|Moved|Renamed|MCP Tool)/i;
|
|
316
|
+
for (const row of fileToolRows) {
|
|
317
|
+
try {
|
|
318
|
+
const rowEl = row as HTMLElement;
|
|
319
|
+
const rowText = rowEl.textContent?.trim() || '';
|
|
320
|
+
const match = rowText.match(statusPattern);
|
|
321
|
+
if (!match) continue;
|
|
322
|
+
const statusText = match[1];
|
|
323
|
+
|
|
324
|
+
if (!rowEl.dataset.proxyToolId) {
|
|
325
|
+
rowEl.dataset.proxyToolId = String(toolCounter++);
|
|
326
|
+
}
|
|
327
|
+
const proxyToolId = rowEl.dataset.proxyToolId;
|
|
328
|
+
|
|
329
|
+
const allSpans = Array.from(rowEl.querySelectorAll('span'));
|
|
330
|
+
let fileName = '';
|
|
331
|
+
let additions: string | null = null;
|
|
332
|
+
let deletions: string | null = null;
|
|
333
|
+
let lineRange: string | null = null;
|
|
334
|
+
let mcpArgs: string | null = null;
|
|
335
|
+
let mcpOutput: string | null = null;
|
|
336
|
+
let mcpToolName: string | null = null;
|
|
337
|
+
|
|
338
|
+
if (statusText.startsWith('MCP')) {
|
|
339
|
+
const nameDiv = rowEl.querySelector(
|
|
340
|
+
'.flex.flex-row.items-center.gap-1.overflow-hidden'
|
|
341
|
+
);
|
|
342
|
+
if (nameDiv) {
|
|
343
|
+
const directTexts: string[] = [];
|
|
344
|
+
for (const child of nameDiv.childNodes) {
|
|
345
|
+
if (child.nodeType === 3)
|
|
346
|
+
directTexts.push((child as Text).textContent!.trim());
|
|
347
|
+
}
|
|
348
|
+
mcpToolName = directTexts.join('').trim() || null;
|
|
349
|
+
}
|
|
350
|
+
if (!mcpToolName) {
|
|
351
|
+
const colonIdx = rowText.indexOf(':');
|
|
352
|
+
if (colonIdx > -1) {
|
|
353
|
+
const afterColon = rowText.substring(colonIdx + 1).trim();
|
|
354
|
+
const cutoff = afterColon.search(/\n|Show|Ran/);
|
|
355
|
+
mcpToolName =
|
|
356
|
+
cutoff > -1
|
|
357
|
+
? afterColon.substring(0, cutoff).trim()
|
|
358
|
+
: afterColon.substring(0, 60).trim();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
fileName = mcpToolName || '';
|
|
362
|
+
|
|
363
|
+
const argSpans = allSpans.filter((s) =>
|
|
364
|
+
(s.className || '').startsWith('mtk')
|
|
365
|
+
);
|
|
366
|
+
if (argSpans.length > 0) {
|
|
367
|
+
mcpArgs = argSpans.map((s) => s.textContent).join('').trim();
|
|
368
|
+
if (mcpArgs.length > 500) mcpArgs = mcpArgs.substring(0, 500) + '…';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const outputLabel = allSpans.find(
|
|
372
|
+
(s) => s.textContent?.trim() === 'Output'
|
|
373
|
+
);
|
|
374
|
+
if (outputLabel) {
|
|
375
|
+
const outputParent =
|
|
376
|
+
outputLabel.closest('.flex.flex-col') ||
|
|
377
|
+
outputLabel.parentElement;
|
|
378
|
+
if (outputParent) {
|
|
379
|
+
const fullText = outputParent.textContent || '';
|
|
380
|
+
const outputIdx = fullText.indexOf('Output');
|
|
381
|
+
if (outputIdx > -1) {
|
|
382
|
+
mcpOutput = fullText.substring(outputIdx + 6).trim();
|
|
383
|
+
if (mcpOutput.length > 500)
|
|
384
|
+
mcpOutput = mcpOutput.substring(0, 500) + '…';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
const fileSpan = allSpans.find((s) => {
|
|
390
|
+
const cls = s.className || '';
|
|
391
|
+
return cls.includes('inline-flex') && cls.includes('items-center');
|
|
392
|
+
});
|
|
393
|
+
fileName = fileSpan?.textContent?.trim() || '';
|
|
394
|
+
|
|
395
|
+
const addSpan = allSpans.find((s) =>
|
|
396
|
+
(s.className || '').includes('text-green')
|
|
397
|
+
);
|
|
398
|
+
additions = addSpan?.textContent?.trim() || null;
|
|
399
|
+
|
|
400
|
+
const delSpan = allSpans.find((s) =>
|
|
401
|
+
(s.className || '').includes('text-red')
|
|
402
|
+
);
|
|
403
|
+
deletions = delSpan?.textContent?.trim() || null;
|
|
404
|
+
|
|
405
|
+
const lineSpan = allSpans.find((s) =>
|
|
406
|
+
/^#L\d/.test(s.textContent?.trim() || '')
|
|
407
|
+
);
|
|
408
|
+
lineRange = lineSpan?.textContent?.trim() || null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let type = 'file';
|
|
412
|
+
const sl2 = statusText.toLowerCase();
|
|
413
|
+
if (sl2.includes('search') || sl2.includes('grep')) type = 'search';
|
|
414
|
+
else if (
|
|
415
|
+
sl2.includes('read') ||
|
|
416
|
+
sl2.includes('view') ||
|
|
417
|
+
sl2.includes('analyz')
|
|
418
|
+
)
|
|
419
|
+
type = 'read';
|
|
420
|
+
else if (sl2.startsWith('mcp')) type = 'mcp';
|
|
421
|
+
|
|
422
|
+
let allRowBtns = Array.from(rowEl.querySelectorAll('button'));
|
|
423
|
+
|
|
424
|
+
let ancestor: HTMLElement | null = rowEl.parentElement;
|
|
425
|
+
let depth = 0;
|
|
426
|
+
const foundPermBtns: HTMLButtonElement[] = [];
|
|
427
|
+
while (ancestor && depth < 5) {
|
|
428
|
+
const siblingBtns = Array.from(
|
|
429
|
+
ancestor.querySelectorAll('button')
|
|
430
|
+
) as HTMLButtonElement[];
|
|
431
|
+
for (const btn of siblingBtns) {
|
|
432
|
+
const t = (btn.textContent || '').trim().toLowerCase();
|
|
433
|
+
if (isHitlAction(t) && !foundPermBtns.includes(btn)) {
|
|
434
|
+
foundPermBtns.push(btn);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (foundPermBtns.length > 0 && foundPermBtns.length < 5) {
|
|
438
|
+
allRowBtns = [...allRowBtns, ...foundPermBtns];
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
ancestor = ancestor.parentElement;
|
|
442
|
+
depth++;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
allRowBtns = [...new Set(allRowBtns)];
|
|
446
|
+
const footerButtons = allRowBtns
|
|
447
|
+
.map((b) => b.textContent?.trim() || '')
|
|
448
|
+
.filter(isHitlAction);
|
|
449
|
+
const hasCancelBtn = footerButtons.some(
|
|
450
|
+
(t) => t.toLowerCase() === 'cancel'
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
toolCalls.push({
|
|
454
|
+
id: proxyToolId,
|
|
455
|
+
status: statusText,
|
|
456
|
+
type,
|
|
457
|
+
path: fileName,
|
|
458
|
+
command: null,
|
|
459
|
+
exitCode: null,
|
|
460
|
+
hasCancelBtn,
|
|
461
|
+
footerButtons,
|
|
462
|
+
hasTerminal: false,
|
|
463
|
+
terminalOutput: null,
|
|
464
|
+
additions,
|
|
465
|
+
deletions,
|
|
466
|
+
lineRange,
|
|
467
|
+
mcpToolName,
|
|
468
|
+
mcpArgs,
|
|
469
|
+
mcpOutput,
|
|
470
|
+
});
|
|
471
|
+
} catch {
|
|
472
|
+
// Silent skip for resilience
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
(window as any).__proxyToolCounter = toolCounter;
|
|
476
|
+
|
|
477
|
+
// ── 4c. Permission dialogs ──
|
|
478
|
+
try {
|
|
479
|
+
const allPanelRows = panel.querySelectorAll(
|
|
480
|
+
'.flex.flex-col.space-y-2 > .flex.flex-row:not(.my-2)'
|
|
481
|
+
);
|
|
482
|
+
for (const permRow of allPanelRows) {
|
|
483
|
+
const permRowEl = permRow as HTMLElement;
|
|
484
|
+
const permBtns = Array.from(permRowEl.querySelectorAll('button'));
|
|
485
|
+
const permBtnTexts = permBtns
|
|
486
|
+
.map((b) => b.textContent?.trim() || '')
|
|
487
|
+
.filter(Boolean);
|
|
488
|
+
const hasPermButtons = permBtnTexts.some((t) =>
|
|
489
|
+
/^(allow|deny|allow once|allow this conversation)$/i.test(t)
|
|
490
|
+
);
|
|
491
|
+
if (!hasPermButtons) continue;
|
|
492
|
+
|
|
493
|
+
const alreadyCaptured =
|
|
494
|
+
permRowEl.dataset?.proxyToolId &&
|
|
495
|
+
toolCalls.some(
|
|
496
|
+
(tc: BrowserToolCall) =>
|
|
497
|
+
tc.id === permRowEl.dataset.proxyToolId &&
|
|
498
|
+
tc.footerButtons.length > 0
|
|
499
|
+
);
|
|
500
|
+
if (alreadyCaptured) continue;
|
|
501
|
+
|
|
502
|
+
const actionButtons = permBtnTexts.filter(isHitlAction);
|
|
503
|
+
if (actionButtons.length === 0) continue;
|
|
504
|
+
|
|
505
|
+
const lastAnalyzed = [...toolCalls]
|
|
506
|
+
.reverse()
|
|
507
|
+
.find((tc: BrowserToolCall) => /^(Analyzed|Read|Viewed)/i.test(tc.status));
|
|
508
|
+
|
|
509
|
+
if (lastAnalyzed && lastAnalyzed.footerButtons.length === 0) {
|
|
510
|
+
lastAnalyzed.footerButtons = actionButtons;
|
|
511
|
+
lastAnalyzed.hasCancelBtn = actionButtons.some(
|
|
512
|
+
(t: string) =>
|
|
513
|
+
t.toLowerCase() === 'deny' || t.toLowerCase() === 'cancel'
|
|
514
|
+
);
|
|
515
|
+
} else {
|
|
516
|
+
if (!permRowEl.dataset.proxyToolId) {
|
|
517
|
+
permRowEl.dataset.proxyToolId = String(
|
|
518
|
+
(window as any).__proxyToolCounter++
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const permText = permRowEl.textContent || '';
|
|
522
|
+
const pathMatch = permText.match(/access to\s+(.+?)(?:\?|$)/i);
|
|
523
|
+
const permPath = pathMatch ? pathMatch[1].trim() : '';
|
|
524
|
+
|
|
525
|
+
toolCalls.push({
|
|
526
|
+
id: permRowEl.dataset.proxyToolId,
|
|
527
|
+
status: 'Permission Required',
|
|
528
|
+
type: 'read',
|
|
529
|
+
path: permPath,
|
|
530
|
+
command: null,
|
|
531
|
+
exitCode: null,
|
|
532
|
+
hasCancelBtn: true,
|
|
533
|
+
footerButtons: actionButtons,
|
|
534
|
+
hasTerminal: false,
|
|
535
|
+
terminalOutput: null,
|
|
536
|
+
additions: null,
|
|
537
|
+
deletions: null,
|
|
538
|
+
lineRange: null,
|
|
539
|
+
mcpToolName: null,
|
|
540
|
+
mcpArgs: null,
|
|
541
|
+
mcpOutput: null,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// Silent skip for resilience
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Signal C: any tool still executing
|
|
550
|
+
if (!isRunning && toolCalls.some((t: BrowserToolCall) => t.hasCancelBtn && !t.exitCode)) {
|
|
551
|
+
isRunning = true;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Signal D: Active task boundary / subagent execution
|
|
555
|
+
if (!isRunning) {
|
|
556
|
+
const lastStepGroup = stepGroups[stepGroups.length - 1];
|
|
557
|
+
if (lastStepGroup) {
|
|
558
|
+
const stepSpinners = lastStepGroup.querySelectorAll('.animate-spin');
|
|
559
|
+
for (const spinner of stepSpinners) {
|
|
560
|
+
if (
|
|
561
|
+
spinner.classList.contains('w-4') &&
|
|
562
|
+
spinner.classList.contains('h-4')
|
|
563
|
+
)
|
|
564
|
+
continue;
|
|
565
|
+
let el: Element | null = spinner;
|
|
566
|
+
let hidden = false;
|
|
567
|
+
while (el && el !== lastStepGroup) {
|
|
568
|
+
const cls = getClass(el);
|
|
569
|
+
if (
|
|
570
|
+
cls.includes('invisible') ||
|
|
571
|
+
cls.includes('opacity-0') ||
|
|
572
|
+
cls.includes('hidden')
|
|
573
|
+
) {
|
|
574
|
+
hidden = true;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
el = el.parentElement;
|
|
578
|
+
}
|
|
579
|
+
if (!hidden) {
|
|
580
|
+
isRunning = true;
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!isRunning) {
|
|
586
|
+
const indicators = lastStepGroup.querySelectorAll(
|
|
587
|
+
'.animate-pulse, .in-progress-checkbox, .typing-indicator'
|
|
588
|
+
);
|
|
589
|
+
for (const ind of indicators) {
|
|
590
|
+
let el: Element | null = ind;
|
|
591
|
+
let hidden = false;
|
|
592
|
+
while (el && el !== lastStepGroup) {
|
|
593
|
+
const cls = getClass(el);
|
|
594
|
+
if (
|
|
595
|
+
cls.includes('invisible') ||
|
|
596
|
+
cls.includes('opacity-0') ||
|
|
597
|
+
cls.includes('hidden')
|
|
598
|
+
) {
|
|
599
|
+
hidden = true;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
el = el.parentElement;
|
|
603
|
+
}
|
|
604
|
+
if (!hidden) {
|
|
605
|
+
isRunning = true;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (!isRunning) {
|
|
612
|
+
const statusTexts = lastStepGroup.querySelectorAll(
|
|
613
|
+
'[class*="text-sm"][class*="opacity"]'
|
|
614
|
+
);
|
|
615
|
+
for (const st of statusTexts) {
|
|
616
|
+
if (getClass(st).includes('invisible')) continue;
|
|
617
|
+
const txt = (st.textContent || '').toLowerCase();
|
|
618
|
+
if (
|
|
619
|
+
txt.includes('running') ||
|
|
620
|
+
txt.includes('progress') ||
|
|
621
|
+
txt.includes('navigat') ||
|
|
622
|
+
txt.includes('executing') ||
|
|
623
|
+
txt.includes('analyzing') ||
|
|
624
|
+
txt.includes('processing') ||
|
|
625
|
+
txt.includes('subagent') ||
|
|
626
|
+
txt.includes('browser')
|
|
627
|
+
) {
|
|
628
|
+
isRunning = true;
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ── 5. Notify user containers ──
|
|
637
|
+
const notifications: string[] = [];
|
|
638
|
+
const notifyBlocks = scopeEl.querySelectorAll('.notify-user-container');
|
|
639
|
+
for (const block of notifyBlocks) {
|
|
640
|
+
const clone = block.cloneNode(true) as Element;
|
|
641
|
+
clone.querySelectorAll('style, script').forEach((el) => el.remove());
|
|
642
|
+
const html = (clone as HTMLElement).innerHTML?.trim();
|
|
643
|
+
if (html) notifications.push(html);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ── 6. Final response blocks ──
|
|
647
|
+
const responses: string[] = [];
|
|
648
|
+
let lastTurnResponseHTML = '';
|
|
649
|
+
const textBlocks = Array.from(
|
|
650
|
+
scopeEl.querySelectorAll('.leading-relaxed.select-text')
|
|
651
|
+
);
|
|
652
|
+
const finalBlocks = textBlocks.filter((el) => {
|
|
653
|
+
let ancestor = el.parentElement;
|
|
654
|
+
let depth = 0;
|
|
655
|
+
while (ancestor && ancestor !== scopeEl && depth < 10) {
|
|
656
|
+
const cls = getClass(ancestor);
|
|
657
|
+
if (cls.includes('max-h-0')) return false;
|
|
658
|
+
ancestor = ancestor.parentElement;
|
|
659
|
+
depth++;
|
|
660
|
+
}
|
|
661
|
+
const text = el.textContent?.trim() || '';
|
|
662
|
+
return !!text;
|
|
663
|
+
});
|
|
664
|
+
for (const block of finalBlocks) {
|
|
665
|
+
const clone = block.cloneNode(true) as Element;
|
|
666
|
+
clone.querySelectorAll('style, script').forEach((el) => el.remove());
|
|
667
|
+
const html = (clone as HTMLElement).innerHTML?.trim();
|
|
668
|
+
if (html) responses.push(html);
|
|
669
|
+
}
|
|
670
|
+
if (finalBlocks.length > 0) {
|
|
671
|
+
lastTurnResponseHTML =
|
|
672
|
+
(finalBlocks[finalBlocks.length - 1] as HTMLElement).innerHTML || '';
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ── 7. Error detection ──
|
|
676
|
+
let error: string | null = null;
|
|
677
|
+
const panelText = panel.textContent || '';
|
|
678
|
+
const errorPatterns = [
|
|
679
|
+
'Agent terminated due to error',
|
|
680
|
+
'error persists',
|
|
681
|
+
'start a new conversation',
|
|
682
|
+
];
|
|
683
|
+
for (const pattern of errorPatterns) {
|
|
684
|
+
if (panelText.includes(pattern)) {
|
|
685
|
+
const walker = document.createTreeWalker(
|
|
686
|
+
panel,
|
|
687
|
+
NodeFilter.SHOW_TEXT,
|
|
688
|
+
null
|
|
689
|
+
);
|
|
690
|
+
let n;
|
|
691
|
+
while ((n = walker.nextNode())) {
|
|
692
|
+
if (n.textContent!.includes('Agent terminated')) {
|
|
693
|
+
error = n.textContent!.trim();
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (!error) error = '[Agent terminated due to error]';
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ── 8. File change cards ──
|
|
703
|
+
const fileChanges: { fileName: string; type: string }[] = [];
|
|
704
|
+
const fileDiffIcons = panel.querySelectorAll('svg.lucide-file-diff');
|
|
705
|
+
for (const icon of fileDiffIcons) {
|
|
706
|
+
const parent = icon.closest('.flex.items-center');
|
|
707
|
+
if (parent) {
|
|
708
|
+
const nameSpan = parent.querySelector('span');
|
|
709
|
+
if (nameSpan) {
|
|
710
|
+
fileChanges.push({
|
|
711
|
+
fileName: nameSpan.textContent?.trim() || '',
|
|
712
|
+
type: 'diff',
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
isRunning,
|
|
720
|
+
turnCount,
|
|
721
|
+
stepGroupCount,
|
|
722
|
+
thinking,
|
|
723
|
+
toolCalls,
|
|
724
|
+
responses,
|
|
725
|
+
notifications,
|
|
726
|
+
error,
|
|
727
|
+
fileChanges,
|
|
728
|
+
lastTurnResponseHTML,
|
|
729
|
+
inputBoxHTML: (window as any).__proxyInputBoxHTML || '',
|
|
730
|
+
};
|
|
731
|
+
}, SELECTORS.spinner);
|
|
732
|
+
|
|
733
|
+
// Write debug state to file for inspection
|
|
734
|
+
try {
|
|
735
|
+
const debug = {
|
|
736
|
+
timestamp: new Date().toISOString(),
|
|
737
|
+
isRunning: state.isRunning,
|
|
738
|
+
turnCount: state.turnCount,
|
|
739
|
+
toolCallsCount: state.toolCalls.length,
|
|
740
|
+
responsesCount: state.responses.length,
|
|
741
|
+
rawLastTurnResponseHTML: state.lastTurnResponseHTML,
|
|
742
|
+
extractedResponses: state.responses,
|
|
743
|
+
toolCalls: state.toolCalls,
|
|
744
|
+
thinking: state.thinking,
|
|
745
|
+
notifications: state.notifications,
|
|
746
|
+
error: state.error,
|
|
747
|
+
inputBoxHTML: (state as any).inputBoxHTML || '',
|
|
748
|
+
};
|
|
749
|
+
fs.writeFileSync(DEBUG_FILE, JSON.stringify(debug, null, 2));
|
|
750
|
+
} catch (err) {
|
|
751
|
+
console.error('Failed to write debug file', err);
|
|
752
|
+
// Silent — debug file writing should never break scraping
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return state as AgentState;
|
|
756
|
+
}
|