Skip to content

Commit 9b2575d

Browse files
feat: optimize pr ux
1 parent 9e77a18 commit 9b2575d

File tree

6 files changed

+150
-35
lines changed

6 files changed

+150
-35
lines changed

async-code-web/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33
import { AuthProvider } from "@/contexts/auth-context";
4+
import { Toaster } from "@/components/ui/sonner";
45
import "./globals.css";
56

67
const geistSans = Geist({
@@ -30,6 +31,7 @@ export default function RootLayout({
3031
>
3132
<AuthProvider>
3233
{children}
34+
<Toaster />
3335
</AuthProvider>
3436
</body>
3537
</html>

async-code-web/app/tasks/[id]/page.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useEffect } from "react";
44
import { useParams } from "next/navigation";
5-
import { ArrowLeft, Github, Clock, CheckCircle, XCircle, AlertCircle, GitCommit, FileText, ExternalLink, MessageSquare, Plus, Copy } from "lucide-react";
5+
import { ArrowLeft, Github, Clock, CheckCircle, XCircle, AlertCircle, GitCommit, FileText, ExternalLink, MessageSquare, Plus, Copy, Loader2 } from "lucide-react";
66
import Link from "next/link";
77
import { Button } from "@/components/ui/button";
88
import { Input } from "@/components/ui/input";
@@ -16,6 +16,7 @@ import { ApiService } from "@/lib/api-service";
1616
import { Task, Project, ChatMessage } from "@/types";
1717
import { formatDiff, parseDiffStats } from "@/lib/utils";
1818
import { DiffViewer } from "@/components/diff-viewer";
19+
import { toast } from "sonner";
1920

2021
interface TaskWithProject extends Task {
2122
project?: Project
@@ -32,6 +33,7 @@ export default function TaskDetailPage() {
3233
const [diffStats, setDiffStats] = useState({ additions: 0, deletions: 0, files: 0 });
3334
const [newMessage, setNewMessage] = useState("");
3435
const [githubToken, setGithubToken] = useState("");
36+
const [creatingPR, setCreatingPR] = useState(false);
3537

3638
useEffect(() => {
3739
if (typeof window !== 'undefined') {
@@ -111,30 +113,44 @@ export default function TaskDetailPage() {
111113
content: newMessage.trim()
112114
});
113115
setNewMessage("");
116+
toast.success("Message added successfully");
114117
loadTask(); // Reload to get updated messages
115118
} catch (error) {
116119
console.error('Error adding message:', error);
117-
alert('Error adding message');
120+
toast.error('Failed to add message');
118121
}
119122
};
120123

121124
const handleCreatePR = async () => {
122125
if (!task || task.status !== "completed" || !user?.id) return;
123126

127+
setCreatingPR(true);
128+
124129
try {
125130
const prompt = (task.chat_messages as unknown as ChatMessage[])?.[0]?.content || '';
126131
const modelName = task.agent === 'codex' ? 'Codex' : 'Claude Code';
127132

133+
toast.loading("Creating pull request...");
134+
128135
const response = await ApiService.createPullRequest(user.id, task.id, {
129136
title: `${modelName}: ${prompt.substring(0, 50)}...`,
130137
body: `Automated changes generated by ${modelName}.\n\nPrompt: ${prompt}`,
131138
github_token: githubToken
132139
});
133140

134-
alert(`Pull request created successfully! #${response.pr_number}`);
141+
toast.dismiss();
142+
toast.success(`Pull request #${response.pr_number} created successfully!`);
143+
144+
// Refresh task data to show the new PR info
145+
await loadTask();
146+
147+
// Open the PR in a new tab
135148
window.open(response.pr_url, '_blank');
136149
} catch (error) {
137-
alert(`Error creating PR: ${error}`);
150+
toast.dismiss();
151+
toast.error(`Failed to create PR: ${error}`);
152+
} finally {
153+
setCreatingPR(false);
138154
}
139155
};
140156

@@ -216,10 +232,23 @@ export default function TaskDetailPage() {
216232
</div>
217233
</div>
218234
{task.status === "completed" && (
219-
<Button onClick={handleCreatePR} className="gap-2">
220-
<ExternalLink className="w-4 h-4" />
221-
Create PR
222-
</Button>
235+
task.pr_url ? (
236+
<Button asChild variant="outline" className="gap-2">
237+
<a href={task.pr_url} target="_blank" rel="noopener noreferrer">
238+
<ExternalLink className="w-4 h-4" />
239+
View PR #{task.pr_number}
240+
</a>
241+
</Button>
242+
) : (
243+
<Button onClick={handleCreatePR} disabled={creatingPR} className="gap-2">
244+
{creatingPR ? (
245+
<Loader2 className="w-4 h-4 animate-spin" />
246+
) : (
247+
<ExternalLink className="w-4 h-4" />
248+
)}
249+
{creatingPR ? "Creating PR..." : "Create PR"}
250+
</Button>
251+
)
223252
)}
224253
</div>
225254
</div>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client"
2+
3+
import { useTheme } from "next-themes"
4+
import { Toaster as Sonner, ToasterProps } from "sonner"
5+
6+
const Toaster = ({ ...props }: ToasterProps) => {
7+
const { theme = "system" } = useTheme()
8+
9+
return (
10+
<Sonner
11+
theme={theme as ToasterProps["theme"]}
12+
className="toaster group"
13+
style={
14+
{
15+
"--normal-bg": "var(--popover)",
16+
"--normal-text": "var(--popover-foreground)",
17+
"--normal-border": "var(--border)",
18+
} as React.CSSProperties
19+
}
20+
{...props}
21+
/>
22+
)
23+
}
24+
25+
export { Toaster }

async-code-web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
"codemirror": "^6.0.1",
3535
"lucide-react": "^0.513.0",
3636
"next": "15.3.3",
37+
"next-themes": "^0.4.6",
3738
"react": "^19.0.0",
3839
"react-dom": "^19.0.0",
40+
"sonner": "^2.0.5",
3941
"tailwind-merge": "^3.3.0"
4042
},
4143
"devDependencies": {

async-code-web/yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2887,6 +2887,11 @@ natural-compare@^1.4.0:
28872887
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
28882888
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
28892889

2890+
next-themes@^0.4.6:
2891+
version "0.4.6"
2892+
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
2893+
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
2894+
28902895
28912896
version "15.3.3"
28922897
resolved "https://registry.npmjs.org/next/-/next-15.3.3.tgz"
@@ -3391,6 +3396,11 @@ simple-swizzle@^0.2.2:
33913396
dependencies:
33923397
is-arrayish "^0.3.1"
33933398

3399+
sonner@^2.0.5:
3400+
version "2.0.5"
3401+
resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.5.tgz#ffb70a6ffe3207c4302cffd3ee46a25242953477"
3402+
integrity sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ==
3403+
33943404
source-map-js@^1.0.2, source-map-js@^1.2.1:
33953405
version "1.2.1"
33963406
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"

server/tasks.py

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,11 @@ def apply_patch_to_github_repo(repo, branch, patch_content, task):
567567
i = j
568568
i += 1
569569

570-
# Now update all the files via GitHub API
570+
# Create a single commit with all file changes using GitHub's Tree API
571+
if not files_to_update:
572+
logger.warning("⚠️ No files to update")
573+
return []
574+
571575
updated_files = []
572576
commit_message = f"Claude Code: {task.get('prompt', 'Automated changes')[:100]}"
573577

@@ -578,34 +582,77 @@ def apply_patch_to_github_repo(repo, branch, patch_content, task):
578582
commit_message = f"Claude Code: {msg.get('content', '')[:100]}"
579583
break
580584

581-
for file_path, new_content in files_to_update.items():
582-
try:
583-
# Check if file exists
584-
try:
585-
file_obj = repo.get_contents(file_path, ref=branch)
586-
# Update existing file
587-
repo.update_file(
588-
path=file_path,
589-
message=commit_message,
590-
content=new_content,
591-
sha=file_obj.sha,
592-
branch=branch
593-
)
594-
logger.info(f"📝 Updated existing file: {file_path}")
595-
except:
596-
# Create new file
597-
repo.create_file(
598-
path=file_path,
599-
message=commit_message,
600-
content=new_content,
601-
branch=branch
602-
)
603-
logger.info(f"🆕 Created new file: {file_path}")
585+
try:
586+
# Get the current commit to build upon
587+
current_commit = repo.get_commit(branch)
588+
589+
# Create tree elements for all changed files
590+
tree_elements = []
591+
592+
for file_path, new_content in files_to_update.items():
593+
# Create a blob for the file content
594+
blob = repo.create_git_blob(new_content, "utf-8")
604595

605-
updated_files.append(file_path)
596+
# Add to tree elements
597+
tree_elements.append({
598+
"path": file_path,
599+
"mode": "100644", # Normal file mode
600+
"type": "blob",
601+
"sha": blob.sha
602+
})
606603

607-
except Exception as file_error:
608-
logger.error(f"❌ Failed to update {file_path}: {file_error}")
604+
logger.info(f"📝 Prepared blob for {file_path}")
605+
updated_files.append(file_path)
606+
607+
# Create a new tree with all the changes
608+
new_tree = repo.create_git_tree(tree_elements, base_tree=current_commit.commit.tree)
609+
610+
# Create a single commit with all the changes
611+
new_commit = repo.create_git_commit(
612+
message=commit_message,
613+
tree=new_tree,
614+
parents=[current_commit.commit]
615+
)
616+
617+
# Update the branch to point to the new commit
618+
ref = repo.get_git_ref(f"heads/{branch}")
619+
ref.edit(new_commit.sha)
620+
621+
logger.info(f"✅ Created single commit {new_commit.sha[:8]} with {len(updated_files)} files")
622+
623+
except Exception as commit_error:
624+
logger.error(f"❌ Failed to create single commit: {commit_error}")
625+
# Fallback to individual file updates if tree method fails
626+
logger.info("🔄 Falling back to individual file updates...")
627+
628+
for file_path, new_content in files_to_update.items():
629+
try:
630+
# Check if file exists
631+
try:
632+
file_obj = repo.get_contents(file_path, ref=branch)
633+
# Update existing file
634+
repo.update_file(
635+
path=file_path,
636+
message=commit_message,
637+
content=new_content,
638+
sha=file_obj.sha,
639+
branch=branch
640+
)
641+
logger.info(f"📝 Updated existing file: {file_path}")
642+
except:
643+
# Create new file
644+
repo.create_file(
645+
path=file_path,
646+
message=commit_message,
647+
content=new_content,
648+
branch=branch
649+
)
650+
logger.info(f"🆕 Created new file: {file_path}")
651+
652+
updated_files.append(file_path)
653+
654+
except Exception as file_error:
655+
logger.error(f"❌ Failed to update {file_path}: {file_error}")
609656

610657
return updated_files
611658

0 commit comments

Comments
 (0)