Skip to content

Commit 9904d13

Browse files
committed
First pass
1 parent f0eec34 commit 9904d13

15 files changed

Lines changed: 5284 additions & 153 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Trigger.dev Configuration
2+
# Get these from https://cloud.trigger.dev
3+
TRIGGER_PROJECT_REF=your_project_ref
4+
TRIGGER_SECRET_KEY=your_secret_key
5+
6+
# Claude API Key
7+
# Get from https://console.anthropic.com
8+
ANTHROPIC_API_KEY=your_anthropic_api_key
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { runs } from "@trigger.dev/sdk/v3";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
export async function POST(request: NextRequest) {
5+
try {
6+
const { runId } = await request.json();
7+
8+
// Validate input
9+
if (!runId || typeof runId !== "string") {
10+
return NextResponse.json(
11+
{ error: "Run ID is required" },
12+
{ status: 400 }
13+
);
14+
}
15+
16+
// Cancel the running task
17+
// This will trigger the AbortController in the task, which propagates to the Claude agent
18+
await runs.cancel(runId);
19+
20+
return NextResponse.json({ success: true });
21+
22+
} catch (error: any) {
23+
console.error("Failed to abort task:", error);
24+
return NextResponse.json(
25+
{ error: error.message || "Failed to abort task" },
26+
{ status: 500 }
27+
);
28+
}
29+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { tasks } from "@trigger.dev/sdk/v3";
2+
import { cloneRepo } from "@/trigger/clone-repo";
3+
import { NextRequest, NextResponse } from "next/server";
4+
5+
export async function POST(request: NextRequest) {
6+
try {
7+
const { githubUrl } = await request.json();
8+
9+
// Validate inputs
10+
if (!githubUrl || typeof githubUrl !== "string") {
11+
return NextResponse.json(
12+
{ error: "GitHub URL is required" },
13+
{ status: 400 }
14+
);
15+
}
16+
17+
// Basic GitHub URL validation
18+
const githubUrlPattern = /^https?:\/\/(www\.)?github\.com\/[\w-]+\/[\w.-]+/;
19+
if (!githubUrlPattern.test(githubUrl)) {
20+
return NextResponse.json(
21+
{ error: "Invalid GitHub URL format" },
22+
{ status: 400 }
23+
);
24+
}
25+
26+
// Trigger the clone task
27+
const handle = await tasks.trigger<typeof cloneRepo>(
28+
"clone-repo",
29+
{ githubUrl }
30+
);
31+
32+
// Return the run ID to track clone progress
33+
return NextResponse.json({
34+
cloneRunId: handle.id,
35+
});
36+
37+
} catch (error: any) {
38+
console.error("Failed to trigger clone-repo task:", error);
39+
return NextResponse.json(
40+
{ error: error.message || "Failed to start cloning" },
41+
{ status: 500 }
42+
);
43+
}
44+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { tasks, runs } from "@trigger.dev/sdk/v3";
2+
import { chatWithRepo } from "@/trigger/chat-with-repo";
3+
import { cloneRepo } from "@/trigger/clone-repo";
4+
import { NextRequest, NextResponse } from "next/server";
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const { cloneRunId, query } = await request.json();
9+
10+
// Validate inputs
11+
if (!cloneRunId || typeof cloneRunId !== "string") {
12+
return NextResponse.json(
13+
{ error: "Clone run ID is required" },
14+
{ status: 400 }
15+
);
16+
}
17+
18+
if (!query || typeof query !== "string") {
19+
return NextResponse.json(
20+
{ error: "Query is required" },
21+
{ status: 400 }
22+
);
23+
}
24+
25+
// Fetch the clone task result to get tempDir
26+
const cloneRun = await runs.retrieve<typeof cloneRepo>(cloneRunId);
27+
28+
if (!cloneRun.output) {
29+
return NextResponse.json(
30+
{ error: "Clone task has not completed yet or failed" },
31+
{ status: 400 }
32+
);
33+
}
34+
35+
const { tempDir, repoName } = cloneRun.output;
36+
37+
// Trigger the chat task
38+
const handle = await tasks.trigger<typeof chatWithRepo>(
39+
"chat-with-repo",
40+
{ tempDir, query, repoName }
41+
);
42+
43+
// Generate public access token for stream
44+
const publicAccessToken = await handle.publicAccessToken();
45+
46+
// Return the chat run ID and access token
47+
return NextResponse.json({
48+
chatRunId: handle.id,
49+
accessToken: publicAccessToken,
50+
repoName,
51+
});
52+
53+
} catch (error: any) {
54+
console.error("Failed to trigger chat task:", error);
55+
return NextResponse.json(
56+
{ error: error.message || "Failed to start chat" },
57+
{ status: 500 }
58+
);
59+
}
60+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { runs } from "@trigger.dev/sdk/v3";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
export async function GET(
5+
request: NextRequest,
6+
{ params }: { params: { runId: string } }
7+
) {
8+
try {
9+
const { runId } = params;
10+
11+
if (!runId || typeof runId !== "string") {
12+
return NextResponse.json(
13+
{ error: "Run ID is required" },
14+
{ status: 400 }
15+
);
16+
}
17+
18+
// Fetch the run status and output
19+
const run = await runs.retrieve(runId);
20+
21+
return NextResponse.json({
22+
status: run.status,
23+
output: run.output,
24+
error: run.error,
25+
});
26+
27+
} catch (error: any) {
28+
console.error("Failed to fetch run status:", error);
29+
return NextResponse.json(
30+
{ error: error.message || "Failed to fetch run status" },
31+
{ status: 500 }
32+
);
33+
}
34+
}

claude-agent-github-wiki/app/chat/[runId]/page.tsx

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { Send, StopCircle, Github, MessageSquare } from "lucide-react";
1010
import { UserMessage } from "@/components/chat/user-message";
1111
import { AiMessage } from "@/components/chat/ai-message";
1212
import { ToolCard } from "@/components/chat/tool-card";
13+
import { useRealtimeStream } from "@trigger.dev/react-hooks";
14+
import { agentStream } from "@/trigger/chat-with-repo";
15+
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
1316

1417
type Message = {
1518
id: string;
@@ -21,6 +24,31 @@ type Message = {
2124
timestamp: Date;
2225
};
2326

27+
function transformSDKMessage(sdkMsg: SDKMessage, index: number): Message | null {
28+
if (sdkMsg.type === 'assistant') {
29+
for (const block of sdkMsg.message.content) {
30+
if (block.type === 'text') {
31+
return {
32+
id: `ai-${index}`,
33+
type: 'ai',
34+
content: block.text,
35+
timestamp: new Date(),
36+
};
37+
} else if (block.type === 'tool_use') {
38+
return {
39+
id: `tool-${index}`,
40+
type: 'tool',
41+
content: '',
42+
toolName: block.name,
43+
toolInput: block.input,
44+
timestamp: new Date(),
45+
};
46+
}
47+
}
48+
}
49+
return null;
50+
}
51+
2452
const mockMessages: Message[] = [
2553
{
2654
id: "1",
@@ -91,46 +119,80 @@ The codebase shows extensive server-side rendering capabilities and a robust bui
91119
},
92120
];
93121

94-
export default function ChatPage() {
95-
const searchParams = useSearchParams();
96-
const repo = searchParams.get("repo") || "";
97-
const [messages, setMessages] = useState<Message[]>(mockMessages);
122+
export default function ChatPage({ params }: { params: { runId: string } }) {
123+
const cloneRunId = params.runId;
124+
const [messages, setMessages] = useState<Message[]>([]);
98125
const [input, setInput] = useState("");
99126
const [isRunning, setIsRunning] = useState(false);
127+
const [repoName, setRepoName] = useState<string>("");
128+
const [error, setError] = useState<string>("");
129+
const [chatRunId, setChatRunId] = useState<string | null>(null);
130+
const [accessToken, setAccessToken] = useState<string | null>(null);
100131
const scrollRef = useRef<HTMLDivElement>(null);
101132

102-
const repoName = repo.split("/").slice(-2).join("/").replace(".git", "");
133+
// Subscribe to realtime stream
134+
const { parts, error: streamError } = useRealtimeStream(
135+
agentStream,
136+
chatRunId ?? '',
137+
{
138+
accessToken: accessToken ?? undefined,
139+
enabled: !!chatRunId && !!accessToken,
140+
timeoutInSeconds: 600,
141+
throttleInMs: 50,
142+
}
143+
);
144+
145+
// Transform stream parts directly - NO useEffect
146+
const streamMessages = parts
147+
.map((msg, idx) => transformSDKMessage(msg, idx))
148+
.filter((msg): msg is Message => msg !== null);
149+
150+
// Combine with user messages
151+
const allMessages = [...messages, ...streamMessages];
103152

104153
useEffect(() => {
105154
if (scrollRef.current) {
106155
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
107156
}
108157
}, [messages]);
109158

110-
const handleSend = () => {
159+
const handleSend = async () => {
111160
if (!input.trim() || isRunning) return;
112161

113-
const newMessage: Message = {
162+
const userMessage: Message = {
114163
id: Date.now().toString(),
115164
type: "user",
116165
content: input,
117166
timestamp: new Date(),
118167
};
119168

120-
setMessages((prev) => [...prev, newMessage]);
169+
setMessages((prev) => [...prev, userMessage]);
170+
const query = input;
121171
setInput("");
122172
setIsRunning(true);
123173

124-
setTimeout(() => {
125-
const aiResponse: Message = {
126-
id: (Date.now() + 1).toString(),
127-
type: "ai",
128-
content: "This is a demo. In a real implementation, the AI would analyze the repository and provide insights based on the codebase.",
129-
timestamp: new Date(),
130-
};
131-
setMessages((prev) => [...prev, aiResponse]);
174+
try {
175+
const response = await fetch("/api/chat", {
176+
method: "POST",
177+
headers: { "Content-Type": "application/json" },
178+
body: JSON.stringify({ cloneRunId, query }),
179+
});
180+
181+
if (!response.ok) {
182+
const errorData = await response.json();
183+
throw new Error(errorData.error || "Failed to send message");
184+
}
185+
186+
const { chatRunId, accessToken, repoName: repo } = await response.json();
187+
if (repo && !repoName) setRepoName(repo);
188+
189+
// Store for streaming
190+
setChatRunId(chatRunId);
191+
setAccessToken(accessToken);
192+
} catch (err: any) {
193+
setError(err.message || "Failed to send message");
132194
setIsRunning(false);
133-
}, 2000);
195+
}
134196
};
135197

136198
const handleAbort = () => {
@@ -143,30 +205,23 @@ export default function ChatPage() {
143205
<Github className="w-5 h-5" />
144206
<div className="flex items-center gap-2 flex-1">
145207
<span className="font-semibold">{repoName || "Repository"}</span>
146-
{repo && (
147-
<Badge variant="secondary" className="font-normal">
148-
<a
149-
href={repo}
150-
target="_blank"
151-
rel="noopener noreferrer"
152-
className="hover:underline"
153-
>
154-
{repo}
155-
</a>
208+
{cloneRunId && (
209+
<Badge variant="secondary" className="font-normal text-xs">
210+
Clone ID: {cloneRunId.substring(0, 8)}...
156211
</Badge>
157212
)}
158213
</div>
159214
</header>
160215

161216
<ScrollArea className="flex-1 px-6" ref={scrollRef}>
162217
<div className="max-w-4xl mx-auto py-6 space-y-6">
163-
{messages.length === 0 ? (
218+
{allMessages.length === 0 ? (
164219
<div className="text-center text-muted-foreground py-12">
165220
<MessageSquare className="w-12 h-12 mx-auto mb-4 opacity-50" />
166221
<p className="text-lg">Ask a question about this repository</p>
167222
</div>
168223
) : (
169-
messages.map((message) => {
224+
allMessages.map((message) => {
170225
if (message.type === "user") {
171226
return <UserMessage key={message.id} content={message.content} />;
172227
} else if (message.type === "ai") {

0 commit comments

Comments
 (0)