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,30 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { clickActionButton } from '@/lib/actions/hitl';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function POST(request: NextRequest) {
|
|
9
|
+
await ensureCdpConnection();
|
|
10
|
+
if (!ctx.workbenchPage) {
|
|
11
|
+
return NextResponse.json({ error: 'Not connected' }, { status: 503 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const body = await request.json();
|
|
15
|
+
const { toolId, buttonText } = body;
|
|
16
|
+
if (!buttonText) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: 'buttonText is required' },
|
|
19
|
+
{ status: 400 }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = await clickActionButton(ctx, toolId || null, buttonText);
|
|
25
|
+
ctx.lastActionTimestamp = Date.now();
|
|
26
|
+
return NextResponse.json(result, { status: result.success ? 200 : 404 });
|
|
27
|
+
} catch (e: any) {
|
|
28
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { clickApproveButton } from '@/lib/actions/hitl';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function POST() {
|
|
9
|
+
await ensureCdpConnection();
|
|
10
|
+
if (!ctx.workbenchPage) {
|
|
11
|
+
return NextResponse.json({ error: 'Not connected' }, { status: 503 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const result = await clickApproveButton(ctx);
|
|
16
|
+
ctx.lastActionTimestamp = Date.now();
|
|
17
|
+
return NextResponse.json(result, { status: result.success ? 200 : 404 });
|
|
18
|
+
} catch (e: any) {
|
|
19
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { getChatHistory } from '@/lib/scraper/chat-history';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
await ensureCdpConnection();
|
|
10
|
+
if (!ctx.workbenchPage) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: 'Not connected to Antigravity' },
|
|
13
|
+
{ status: 503 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const history = await getChatHistory(ctx);
|
|
19
|
+
return NextResponse.json(history);
|
|
20
|
+
} catch (e: any) {
|
|
21
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { getAgentMode, setAgentMode } from '@/lib/scraper/agent-mode';
|
|
5
|
+
import type { AgentMode } from '@/lib/scraper/agent-mode';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/v1/chat/mode — Read the current conversation mode.
|
|
11
|
+
*/
|
|
12
|
+
export async function GET() {
|
|
13
|
+
await ensureCdpConnection();
|
|
14
|
+
if (!ctx.workbenchPage) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: 'Not connected to Antigravity' },
|
|
17
|
+
{ status: 503 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const mode = await getAgentMode(ctx);
|
|
23
|
+
return NextResponse.json({ mode });
|
|
24
|
+
} catch (e: any) {
|
|
25
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* POST /api/v1/chat/mode — Switch the conversation mode.
|
|
31
|
+
* Body: { mode: 'planning' | 'fast' }
|
|
32
|
+
*/
|
|
33
|
+
export async function POST(req: NextRequest) {
|
|
34
|
+
await ensureCdpConnection();
|
|
35
|
+
if (!ctx.workbenchPage) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: 'Not connected to Antigravity' },
|
|
38
|
+
{ status: 503 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const body = await req.json();
|
|
44
|
+
const targetMode = body.mode as AgentMode;
|
|
45
|
+
|
|
46
|
+
if (targetMode !== 'planning' && targetMode !== 'fast') {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: 'Invalid mode. Must be "planning" or "fast".' },
|
|
49
|
+
{ status: 400 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await setAgentMode(ctx, targetMode);
|
|
54
|
+
const currentMode = await getAgentMode(ctx);
|
|
55
|
+
return NextResponse.json({ success: true, mode: currentMode });
|
|
56
|
+
} catch (e: any) {
|
|
57
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { startNewChat } from '@/lib/actions/new-chat';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function POST() {
|
|
9
|
+
await ensureCdpConnection();
|
|
10
|
+
if (!ctx.workbenchPage) {
|
|
11
|
+
return NextResponse.json({ error: 'Not connected' }, { status: 503 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const result = await startNewChat(ctx);
|
|
16
|
+
ctx.lastActionTimestamp = Date.now();
|
|
17
|
+
return NextResponse.json(result, { status: result.success ? 200 : 404 });
|
|
18
|
+
} catch (e: any) {
|
|
19
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { clickRejectButton } from '@/lib/actions/hitl';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function POST() {
|
|
9
|
+
await ensureCdpConnection();
|
|
10
|
+
if (!ctx.workbenchPage) {
|
|
11
|
+
return NextResponse.json({ error: 'Not connected' }, { status: 503 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const result = await clickRejectButton(ctx);
|
|
16
|
+
ctx.lastActionTimestamp = Date.now();
|
|
17
|
+
return NextResponse.json(result, { status: result.success ? 200 : 404 });
|
|
18
|
+
} catch (e: any) {
|
|
19
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { sendMessage } from '@/lib/actions/send-message';
|
|
5
|
+
import { getFullAgentState } from '@/lib/scraper/agent-state';
|
|
6
|
+
import { sleep } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
export const dynamic = 'force-dynamic';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* POST /api/v1/chat — Blocking chat endpoint.
|
|
12
|
+
* Sends a message and polls until the agent completes, then returns the response.
|
|
13
|
+
*/
|
|
14
|
+
export async function POST(request: NextRequest) {
|
|
15
|
+
await ensureCdpConnection();
|
|
16
|
+
if (!ctx.workbenchPage) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: 'Not connected to Antigravity' },
|
|
19
|
+
{ status: 503 }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const body = await request.json();
|
|
24
|
+
const { message } = body;
|
|
25
|
+
if (!message) {
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: 'message is required' },
|
|
28
|
+
{ status: 400 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await sendMessage(ctx, message);
|
|
34
|
+
|
|
35
|
+
const startTime = Date.now();
|
|
36
|
+
const timeoutMs = 180000;
|
|
37
|
+
let doneCount = 0;
|
|
38
|
+
let started = false;
|
|
39
|
+
const initialState = await getFullAgentState(ctx);
|
|
40
|
+
const initialBlockCount = initialState.responses.length + initialState.notifications.length;
|
|
41
|
+
|
|
42
|
+
// Phase 1: Wait for agent to start
|
|
43
|
+
for (let i = 0; i < 40; i++) {
|
|
44
|
+
await sleep(300);
|
|
45
|
+
const state = await getFullAgentState(ctx);
|
|
46
|
+
if (state.isRunning) {
|
|
47
|
+
started = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const blocks = state.responses.length + state.notifications.length;
|
|
51
|
+
if (blocks > initialBlockCount) {
|
|
52
|
+
started = true;
|
|
53
|
+
await sleep(500);
|
|
54
|
+
const check = await getFullAgentState(ctx);
|
|
55
|
+
if (!check.isRunning) {
|
|
56
|
+
const response = check.notifications.length > 0
|
|
57
|
+
? check.notifications[check.notifications.length - 1]
|
|
58
|
+
: check.responses.length > 0
|
|
59
|
+
? check.responses[check.responses.length - 1]
|
|
60
|
+
: '';
|
|
61
|
+
return NextResponse.json({ response });
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!started) {
|
|
68
|
+
const state = await getFullAgentState(ctx);
|
|
69
|
+
const response = state.responses.length > 0
|
|
70
|
+
? state.responses[state.responses.length - 1]
|
|
71
|
+
: '[Agent did not respond]';
|
|
72
|
+
return NextResponse.json({ response });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Phase 2: Wait for completion
|
|
76
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
77
|
+
const state = await getFullAgentState(ctx);
|
|
78
|
+
if (state.error) {
|
|
79
|
+
return NextResponse.json({ response: state.error });
|
|
80
|
+
}
|
|
81
|
+
if (!state.isRunning) {
|
|
82
|
+
doneCount++;
|
|
83
|
+
if (doneCount >= 3) {
|
|
84
|
+
const response = state.notifications.length > 0
|
|
85
|
+
? state.notifications[state.notifications.length - 1]
|
|
86
|
+
: state.responses.length > 0
|
|
87
|
+
? state.responses[state.responses.length - 1]
|
|
88
|
+
: '[Agent did not produce a response]';
|
|
89
|
+
return NextResponse.json({ response });
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
doneCount = 0;
|
|
93
|
+
}
|
|
94
|
+
await sleep(500);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const finalState = await getFullAgentState(ctx);
|
|
98
|
+
const response = finalState.responses.length > 0
|
|
99
|
+
? finalState.responses[finalState.responses.length - 1]
|
|
100
|
+
: '[Timeout: No response received]';
|
|
101
|
+
return NextResponse.json({ response });
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { getFullAgentState } from '@/lib/scraper/agent-state';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
await ensureCdpConnection();
|
|
10
|
+
if (!ctx.workbenchPage) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: 'Not connected to Antigravity' },
|
|
13
|
+
{ status: 503 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const state = await getFullAgentState(ctx);
|
|
19
|
+
return NextResponse.json(state);
|
|
20
|
+
} catch (e: any) {
|
|
21
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { ensureCdpConnection } from '@/lib/init';
|
|
3
|
+
import ctx from '@/lib/context';
|
|
4
|
+
import { sendMessage } from '@/lib/actions/send-message';
|
|
5
|
+
import { getFullAgentState } from '@/lib/scraper/agent-state';
|
|
6
|
+
import { diffStates } from '@/lib/sse/diff-states';
|
|
7
|
+
import type { AgentState, ToolCall } from '@/lib/types';
|
|
8
|
+
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* POST /api/v1/chat/stream — SSE streaming endpoint.
|
|
13
|
+
* Sends a message and streams agent state diffs as Server-Sent Events.
|
|
14
|
+
*/
|
|
15
|
+
export async function POST(request: NextRequest) {
|
|
16
|
+
await ensureCdpConnection();
|
|
17
|
+
if (!ctx.workbenchPage) {
|
|
18
|
+
return new Response(
|
|
19
|
+
JSON.stringify({ error: 'Not connected to Antigravity' }),
|
|
20
|
+
{ status: 503, headers: { 'Content-Type': 'application/json' } }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const body = await request.json();
|
|
25
|
+
const { message } = body;
|
|
26
|
+
if (!message) {
|
|
27
|
+
return new Response(
|
|
28
|
+
JSON.stringify({ error: 'message is required' }),
|
|
29
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const encoder = new TextEncoder();
|
|
34
|
+
|
|
35
|
+
const stream = new ReadableStream({
|
|
36
|
+
async start(controller) {
|
|
37
|
+
const writeEvent = (type: string, data: Record<string, unknown>) => {
|
|
38
|
+
try {
|
|
39
|
+
const payload = JSON.stringify({ ...data, type });
|
|
40
|
+
controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
|
|
41
|
+
} catch {
|
|
42
|
+
// Controller may already be closed
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let closed = false;
|
|
47
|
+
const closeStream = () => {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
closed = true;
|
|
50
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
writeEvent('status', { isRunning: true, phase: 'sending' });
|
|
55
|
+
|
|
56
|
+
// Capture initial state before sending
|
|
57
|
+
let prevState = await getFullAgentState(ctx);
|
|
58
|
+
const sessionToolCalls = new Map<string, ToolCall>();
|
|
59
|
+
let sessionResponses: string[] = [];
|
|
60
|
+
|
|
61
|
+
await sendMessage(ctx, message);
|
|
62
|
+
|
|
63
|
+
writeEvent('status', { isRunning: true, phase: 'waiting' });
|
|
64
|
+
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
let doneCount = 0;
|
|
67
|
+
let started = false;
|
|
68
|
+
let lastStableHTML = '';
|
|
69
|
+
const initialTurnCount = prevState.turnCount;
|
|
70
|
+
let pollErrorCount = 0;
|
|
71
|
+
|
|
72
|
+
const interval = setInterval(async () => {
|
|
73
|
+
if (closed) { clearInterval(interval); return; }
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const currState = await getFullAgentState(ctx);
|
|
77
|
+
pollErrorCount = 0; // Reset on success
|
|
78
|
+
|
|
79
|
+
// Track tools by ID to survive virtualization
|
|
80
|
+
if (currState.turnCount > prevState.turnCount) {
|
|
81
|
+
sessionToolCalls.clear();
|
|
82
|
+
prevState = {
|
|
83
|
+
...prevState,
|
|
84
|
+
toolCalls: [],
|
|
85
|
+
responses: [],
|
|
86
|
+
thinking: [],
|
|
87
|
+
notifications: [],
|
|
88
|
+
fileChanges: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
for (const t of currState.toolCalls) {
|
|
92
|
+
sessionToolCalls.set(t.id, t);
|
|
93
|
+
}
|
|
94
|
+
currState.toolCalls = Array.from(sessionToolCalls.values());
|
|
95
|
+
|
|
96
|
+
// Accumulate responses to survive DOM virtualization
|
|
97
|
+
if (currState.responses.length > sessionResponses.length) {
|
|
98
|
+
sessionResponses = [...currState.responses];
|
|
99
|
+
} else if (
|
|
100
|
+
currState.responses.length < sessionResponses.length &&
|
|
101
|
+
currState.responses.length > 0
|
|
102
|
+
) {
|
|
103
|
+
const lastIdx = currState.responses.length - 1;
|
|
104
|
+
sessionResponses[sessionResponses.length - 1] =
|
|
105
|
+
currState.responses[lastIdx];
|
|
106
|
+
} else if (
|
|
107
|
+
currState.responses.length === sessionResponses.length &&
|
|
108
|
+
currState.responses.length > 0
|
|
109
|
+
) {
|
|
110
|
+
sessionResponses[sessionResponses.length - 1] =
|
|
111
|
+
currState.responses[currState.responses.length - 1];
|
|
112
|
+
}
|
|
113
|
+
currState.responses = [...sessionResponses];
|
|
114
|
+
|
|
115
|
+
// Detect start
|
|
116
|
+
if (!started) {
|
|
117
|
+
if (
|
|
118
|
+
currState.isRunning ||
|
|
119
|
+
currState.turnCount > initialTurnCount ||
|
|
120
|
+
currState.toolCalls.length > prevState.toolCalls.length ||
|
|
121
|
+
currState.responses.length > prevState.responses.length ||
|
|
122
|
+
currState.thinking.length > prevState.thinking.length
|
|
123
|
+
) {
|
|
124
|
+
started = true;
|
|
125
|
+
writeEvent('status', { isRunning: true, phase: 'processing' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for unresolved tools
|
|
130
|
+
const hasUnresolvedTools = Array.from(
|
|
131
|
+
sessionToolCalls.values()
|
|
132
|
+
).some((t) => t.hasCancelBtn && !t.exitCode);
|
|
133
|
+
|
|
134
|
+
// Compute and emit diffs
|
|
135
|
+
const events = diffStates(prevState, currState);
|
|
136
|
+
for (const evt of events) {
|
|
137
|
+
writeEvent(evt.type, evt.data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check for completion
|
|
141
|
+
if (started && !currState.isRunning && !currState.error && !hasUnresolvedTools) {
|
|
142
|
+
const contentChanged =
|
|
143
|
+
currState.toolCalls.length !== prevState.toolCalls.length ||
|
|
144
|
+
currState.responses.length !== prevState.responses.length ||
|
|
145
|
+
currState.thinking.length !== prevState.thinking.length ||
|
|
146
|
+
currState.notifications.length !== prevState.notifications.length ||
|
|
147
|
+
currState.fileChanges.length !== prevState.fileChanges.length ||
|
|
148
|
+
currState.stepGroupCount !== prevState.stepGroupCount ||
|
|
149
|
+
(currState.responses.length > 0 &&
|
|
150
|
+
prevState.responses.length > 0 &&
|
|
151
|
+
currState.responses[currState.responses.length - 1] !==
|
|
152
|
+
prevState.responses[prevState.responses.length - 1]) ||
|
|
153
|
+
currState.lastTurnResponseHTML !== prevState.lastTurnResponseHTML;
|
|
154
|
+
|
|
155
|
+
if (contentChanged) {
|
|
156
|
+
doneCount = 0;
|
|
157
|
+
lastStableHTML = '';
|
|
158
|
+
} else if (Date.now() - ctx.lastActionTimestamp < 15000) {
|
|
159
|
+
doneCount = 0;
|
|
160
|
+
lastStableHTML = '';
|
|
161
|
+
} else {
|
|
162
|
+
doneCount++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const currentHTML = currState.lastTurnResponseHTML || '';
|
|
166
|
+
if (doneCount >= 2 && currentHTML && currentHTML !== lastStableHTML) {
|
|
167
|
+
doneCount = 1;
|
|
168
|
+
}
|
|
169
|
+
lastStableHTML = currentHTML;
|
|
170
|
+
|
|
171
|
+
const hasSubagentTools = currState.toolCalls.some(
|
|
172
|
+
(t) =>
|
|
173
|
+
t.type === 'browser' ||
|
|
174
|
+
(t.status || '').toLowerCase().includes('subagent') ||
|
|
175
|
+
(t.status || '').toLowerCase().includes('navigat')
|
|
176
|
+
);
|
|
177
|
+
const requiredDoneCount = hasSubagentTools ? 20 : 10;
|
|
178
|
+
if (doneCount >= requiredDoneCount) {
|
|
179
|
+
const finalResponse =
|
|
180
|
+
currState.notifications.length > 0
|
|
181
|
+
? currState.notifications[currState.notifications.length - 1]
|
|
182
|
+
: currState.responses.length > 0
|
|
183
|
+
? currState.responses[currState.responses.length - 1]
|
|
184
|
+
: '';
|
|
185
|
+
|
|
186
|
+
writeEvent('done', {
|
|
187
|
+
finalResponse,
|
|
188
|
+
isHTML: true,
|
|
189
|
+
thinking: currState.thinking,
|
|
190
|
+
toolCalls: currState.toolCalls,
|
|
191
|
+
});
|
|
192
|
+
clearInterval(interval);
|
|
193
|
+
closeStream();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
doneCount = 0;
|
|
198
|
+
lastStableHTML = '';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Error
|
|
202
|
+
if (currState.error) {
|
|
203
|
+
writeEvent('error', { message: currState.error });
|
|
204
|
+
writeEvent('done', { error: currState.error });
|
|
205
|
+
clearInterval(interval);
|
|
206
|
+
closeStream();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Timeout (10 min)
|
|
211
|
+
if (Date.now() - startTime > 600000) {
|
|
212
|
+
const finalResponse =
|
|
213
|
+
currState.responses.length > 0
|
|
214
|
+
? currState.responses[currState.responses.length - 1]
|
|
215
|
+
: '[Timeout]';
|
|
216
|
+
writeEvent('done', { finalResponse, timeout: true });
|
|
217
|
+
clearInterval(interval);
|
|
218
|
+
closeStream();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
prevState = currState;
|
|
223
|
+
} catch (e: any) {
|
|
224
|
+
// Transient errors: don't kill the stream, just log and retry
|
|
225
|
+
pollErrorCount++;
|
|
226
|
+
console.error(`[Stream] Poll error (${pollErrorCount}):`, e.message);
|
|
227
|
+
|
|
228
|
+
// Only kill after many consecutive failures
|
|
229
|
+
if (pollErrorCount >= 20) {
|
|
230
|
+
writeEvent('error', { message: `Too many poll errors: ${e.message}` });
|
|
231
|
+
writeEvent('done', { error: e.message });
|
|
232
|
+
clearInterval(interval);
|
|
233
|
+
closeStream();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, 500);
|
|
237
|
+
|
|
238
|
+
// Handle client disconnect
|
|
239
|
+
request.signal.addEventListener('abort', () => {
|
|
240
|
+
clearInterval(interval);
|
|
241
|
+
closeStream();
|
|
242
|
+
});
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
writeEvent('error', { message: e.message });
|
|
245
|
+
closeStream();
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return new Response(stream, {
|
|
251
|
+
headers: {
|
|
252
|
+
'Content-Type': 'text/event-stream',
|
|
253
|
+
'Cache-Control': 'no-cache',
|
|
254
|
+
Connection: 'keep-alive',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import ctx from '@/lib/context';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
const BRAIN_DIR = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
|
|
10
|
+
|
|
11
|
+
function extractTitle(convDir: string): string | null {
|
|
12
|
+
const taskFile = path.join(convDir, 'task.md');
|
|
13
|
+
try {
|
|
14
|
+
if (fs.existsSync(taskFile)) {
|
|
15
|
+
const content = fs.readFileSync(taskFile, 'utf-8');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (trimmed.startsWith('# ')) {
|
|
20
|
+
return trimmed.slice(2).trim();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
/* ignore */
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getConversationFiles(convDir: string) {
|
|
31
|
+
try {
|
|
32
|
+
const results: any[] = [];
|
|
33
|
+
const entries = fs.readdirSync(convDir, { recursive: true, withFileTypes: true });
|
|
34
|
+
for (const d of entries) {
|
|
35
|
+
if (!d.isFile() || !d.name.endsWith('.md')) continue;
|
|
36
|
+
|
|
37
|
+
// Node 20+ uses parentPath, older Node might use path. Fallback to convDir just in case
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
const parentDir = d.parentPath || d.path || convDir;
|
|
40
|
+
const fullPath = path.join(parentDir, d.name);
|
|
41
|
+
const relPath = path.relative(convDir, fullPath).replace(/\\/g, '/');
|
|
42
|
+
|
|
43
|
+
if (relPath.split('/').some(p => p.startsWith('.'))) continue;
|
|
44
|
+
|
|
45
|
+
const stat = fs.statSync(fullPath);
|
|
46
|
+
results.push({ name: relPath, size: stat.size, mtime: stat.mtime.toISOString() });
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Auto-detect the most recently modified conversation directory.
|
|
56
|
+
* This ensures the currently opened conversation is pre-selected on first load.
|
|
57
|
+
*/
|
|
58
|
+
function autoDetectActiveConversation(): string | null {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(BRAIN_DIR)) return null;
|
|
61
|
+
const entries = fs.readdirSync(BRAIN_DIR, { withFileTypes: true });
|
|
62
|
+
let latest: { id: string; mtime: number } | null = null;
|
|
63
|
+
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
66
|
+
const dirPath = path.join(BRAIN_DIR, entry.name);
|
|
67
|
+
const stat = fs.statSync(dirPath);
|
|
68
|
+
// Use the most recently modified directory
|
|
69
|
+
if (!latest || stat.mtimeMs > latest.mtime) {
|
|
70
|
+
latest = { id: entry.name, mtime: stat.mtimeMs };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return latest?.id ?? null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function GET() {
|
|
80
|
+
// Auto-detect on first load if no conversation has been selected yet
|
|
81
|
+
if (!ctx.activeConversationId && !ctx.activeTitle) {
|
|
82
|
+
ctx.activeConversationId = autoDetectActiveConversation();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!ctx.activeConversationId && ctx.activeTitle) {
|
|
86
|
+
return NextResponse.json({
|
|
87
|
+
active: true,
|
|
88
|
+
id: null,
|
|
89
|
+
title: ctx.activeTitle,
|
|
90
|
+
files: [],
|
|
91
|
+
mtime: new Date().toISOString(),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!ctx.activeConversationId) {
|
|
96
|
+
return NextResponse.json({ active: false });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const convDir = path.join(BRAIN_DIR, ctx.activeConversationId);
|
|
100
|
+
if (!fs.existsSync(convDir)) {
|
|
101
|
+
ctx.activeConversationId = null;
|
|
102
|
+
return NextResponse.json({ active: false });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const files = getConversationFiles(convDir);
|
|
106
|
+
const title = extractTitle(convDir);
|
|
107
|
+
const stat = fs.statSync(convDir);
|
|
108
|
+
|
|
109
|
+
return NextResponse.json({
|
|
110
|
+
active: true,
|
|
111
|
+
id: ctx.activeConversationId,
|
|
112
|
+
title,
|
|
113
|
+
files,
|
|
114
|
+
mtime: stat.mtime.toISOString(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|