feat: draft pr support (#165)

This commit is contained in:
Carlos Florêncio
2026-02-03 04:37:49 +00:00
committed by GitHub
parent b22e66750c
commit 7f883deb8f
7 changed files with 43 additions and 12 deletions
@@ -104,6 +104,7 @@ type GitCreatePRRequest struct {
Title string `json:"title"`
Body string `json:"body"`
BaseBranch string `json:"base_branch"`
Draft bool `json:"draft"`
}
// GitShowCommitRequest for session.commit_diff action
@@ -363,7 +364,7 @@ func (h *GitHandlers) wsCreatePR(ctx context.Context, msg *ws.Message) (*ws.Mess
return nil, err
}
result, err := client.GitCreatePR(ctx, req.Title, req.Body, req.BaseBranch)
result, err := client.GitCreatePR(ctx, req.Title, req.Body, req.BaseBranch, req.Draft)
if err != nil {
return nil, fmt.Errorf("create PR failed: %w", err)
}
+3 -1
View File
@@ -132,15 +132,17 @@ func (c *Client) GitDiscard(ctx context.Context, paths []string) (*GitOperationR
}
// GitCreatePR creates a pull request using the gh CLI.
func (c *Client) GitCreatePR(ctx context.Context, title, body, baseBranch string) (*PRCreateResult, error) {
func (c *Client) GitCreatePR(ctx context.Context, title, body, baseBranch string, draft bool) (*PRCreateResult, error) {
payload := struct {
Title string `json:"title"`
Body string `json:"body"`
BaseBranch string `json:"base_branch"`
Draft bool `json:"draft"`
}{
Title: title,
Body: body,
BaseBranch: baseBranch,
Draft: draft,
}
reqBody, err := json.Marshal(payload)
@@ -66,6 +66,7 @@ type GitCreatePRRequest struct {
Title string `json:"title"`
Body string `json:"body"`
BaseBranch string `json:"base_branch"`
Draft bool `json:"draft"`
}
// handleGitPull handles POST /api/v1/git/pull
@@ -331,7 +332,7 @@ func (s *Server) handleGitCreatePR(c *gin.Context) {
}
gitOp := s.procMgr.GitOperator()
result, err := gitOp.CreatePR(c.Request.Context(), req.Title, req.Body, req.BaseBranch)
result, err := gitOp.CreatePR(c.Request.Context(), req.Title, req.Body, req.BaseBranch, req.Draft)
if err != nil {
if errors.Is(err, process.ErrOperationInProgress) {
c.JSON(http.StatusConflict, process.PRCreateResult{
@@ -760,7 +760,7 @@ type PRCreateResult struct {
// CreatePR creates a pull request using the gh CLI.
// It first pushes the current branch to the remote, then creates the PR.
func (g *GitOperator) CreatePR(ctx context.Context, title, body, baseBranch string) (*PRCreateResult, error) {
func (g *GitOperator) CreatePR(ctx context.Context, title, body, baseBranch string, draft bool) (*PRCreateResult, error) {
if !g.tryLock("create-pr") {
return nil, ErrOperationInProgress
}
@@ -795,6 +795,10 @@ func (g *GitOperator) CreatePR(ctx context.Context, title, body, baseBranch stri
args = append(args, "--base", cleanBaseBranch)
}
if draft {
args = append(args, "--draft")
}
cmd := exec.CommandContext(ctx, "gh", args...)
cmd.Dir = g.workDir
@@ -67,6 +67,7 @@ export const SessionMobileTopBar = memo(function SessionMobileTopBar({
const [prDialogOpen, setPrDialogOpen] = useState(false);
const [prTitle, setPrTitle] = useState('');
const [prBody, setPrBody] = useState('');
const [prDraft, setPrDraft] = useState(true);
const { toast } = useToast();
const gitStatus = useSessionGitStatus(sessionId ?? null);
@@ -160,10 +161,10 @@ export const SessionMobileTopBar = memo(function SessionMobileTopBar({
if (!prTitle.trim()) return;
setPrDialogOpen(false);
try {
const result = await createPR(prTitle.trim(), prBody.trim(), baseBranch);
const result = await createPR(prTitle.trim(), prBody.trim(), baseBranch, prDraft);
if (result.success) {
toast({
title: 'Pull Request created',
title: prDraft ? 'Draft Pull Request created' : 'Pull Request created',
description: result.pr_url || 'PR created successfully',
variant: 'success',
});
@@ -186,7 +187,7 @@ export const SessionMobileTopBar = memo(function SessionMobileTopBar({
}
setPrTitle('');
setPrBody('');
}, [prTitle, prBody, baseBranch, createPR, toast]);
}, [prTitle, prBody, baseBranch, prDraft, createPR, toast]);
return (
<header className="flex items-center justify-between px-2 py-2 bg-background">
@@ -415,6 +416,16 @@ export const SessionMobileTopBar = memo(function SessionMobileTopBar({
className="resize-none"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="pr-draft-mobile"
checked={prDraft}
onCheckedChange={(checked) => setPrDraft(checked === true)}
/>
<Label htmlFor="pr-draft-mobile" className="text-sm cursor-pointer">
Create as draft
</Label>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
+14 -3
View File
@@ -91,6 +91,7 @@ const TaskTopBar = memo(function TaskTopBar({
const [prDialogOpen, setPrDialogOpen] = useState(false);
const [prTitle, setPrTitle] = useState('');
const [prBody, setPrBody] = useState('');
const [prDraft, setPrDraft] = useState(true);
const { toast } = useToast();
const gitStatus = useSessionGitStatus(activeSessionId ?? null);
@@ -198,10 +199,10 @@ const TaskTopBar = memo(function TaskTopBar({
if (!prTitle.trim()) return;
setPrDialogOpen(false);
try {
const result = await createPR(prTitle.trim(), prBody.trim(), baseBranch);
const result = await createPR(prTitle.trim(), prBody.trim(), baseBranch, prDraft);
if (result.success) {
toast({
title: 'Pull Request created',
title: prDraft ? 'Draft Pull Request created' : 'Pull Request created',
description: result.pr_url || 'PR created successfully',
variant: 'success',
});
@@ -225,7 +226,7 @@ const TaskTopBar = memo(function TaskTopBar({
}
setPrTitle('');
setPrBody('');
}, [prTitle, prBody, baseBranch, createPR, toast]);
}, [prTitle, prBody, baseBranch, prDraft, createPR, toast]);
const handleBranchClick = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -667,6 +668,16 @@ const TaskTopBar = memo(function TaskTopBar({
className="resize-none"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="pr-draft"
checked={prDraft}
onCheckedChange={(checked) => setPrDraft(checked === true)}
/>
<Label htmlFor="pr-draft" className="text-sm cursor-pointer">
Create as draft
</Label>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
+3 -2
View File
@@ -31,7 +31,7 @@ interface UseGitOperationsReturn {
stage: (paths?: string[]) => Promise<GitOperationResult>;
unstage: (paths?: string[]) => Promise<GitOperationResult>;
discard: (paths?: string[]) => Promise<GitOperationResult>;
createPR: (title: string, body: string, baseBranch?: string) => Promise<PRCreateResult>;
createPR: (title: string, body: string, baseBranch?: string, draft?: boolean) => Promise<PRCreateResult>;
// State
isLoading: boolean;
@@ -119,7 +119,7 @@ export function useGitOperations(sessionId: string | null): UseGitOperationsRetu
return executeOperation<GitOperationResult>('worktree.discard', { paths: paths ?? [] });
}, [executeOperation]);
const createPR = useCallback(async (title: string, body: string, baseBranch?: string): Promise<PRCreateResult> => {
const createPR = useCallback(async (title: string, body: string, baseBranch?: string, draft?: boolean): Promise<PRCreateResult> => {
if (!sessionId) {
throw new Error('No session ID provided');
}
@@ -138,6 +138,7 @@ export function useGitOperations(sessionId: string | null): UseGitOperationsRetu
title,
body,
base_branch: baseBranch ?? '',
draft: draft ?? true,
}, 120000); // 2 min timeout for PR creation (gh cli can be slow)
if (!result.success && result.error) {