From 7f883deb8f83549cd9aeecb9155ebb9b2f416825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Tue, 3 Feb 2026 04:37:49 +0000 Subject: [PATCH] feat: draft pr support (#165) --- .../internal/agent/handlers/git_handlers.go | 3 ++- apps/backend/internal/agentctl/client/git.go | 4 +++- .../backend/internal/agentctl/server/api/git.go | 3 ++- .../internal/agentctl/server/process/git.go | 6 +++++- .../task/mobile/session-mobile-top-bar.tsx | 17 ++++++++++++++--- apps/web/components/task/task-top-bar.tsx | 17 ++++++++++++++--- apps/web/hooks/use-git-operations.ts | 5 +++-- 7 files changed, 43 insertions(+), 12 deletions(-) diff --git a/apps/backend/internal/agent/handlers/git_handlers.go b/apps/backend/internal/agent/handlers/git_handlers.go index 75e412401c..a39c99a1b5 100644 --- a/apps/backend/internal/agent/handlers/git_handlers.go +++ b/apps/backend/internal/agent/handlers/git_handlers.go @@ -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) } diff --git a/apps/backend/internal/agentctl/client/git.go b/apps/backend/internal/agentctl/client/git.go index 4f6c65fe9c..9adbd9a9f1 100644 --- a/apps/backend/internal/agentctl/client/git.go +++ b/apps/backend/internal/agentctl/client/git.go @@ -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) diff --git a/apps/backend/internal/agentctl/server/api/git.go b/apps/backend/internal/agentctl/server/api/git.go index 80ed2103ae..3b0653a1af 100644 --- a/apps/backend/internal/agentctl/server/api/git.go +++ b/apps/backend/internal/agentctl/server/api/git.go @@ -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{ diff --git a/apps/backend/internal/agentctl/server/process/git.go b/apps/backend/internal/agentctl/server/process/git.go index 5844400edc..86fff88fce 100644 --- a/apps/backend/internal/agentctl/server/process/git.go +++ b/apps/backend/internal/agentctl/server/process/git.go @@ -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 diff --git a/apps/web/components/task/mobile/session-mobile-top-bar.tsx b/apps/web/components/task/mobile/session-mobile-top-bar.tsx index d6d2630c9b..6a304f164c 100644 --- a/apps/web/components/task/mobile/session-mobile-top-bar.tsx +++ b/apps/web/components/task/mobile/session-mobile-top-bar.tsx @@ -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 (
@@ -415,6 +416,16 @@ export const SessionMobileTopBar = memo(function SessionMobileTopBar({ className="resize-none" /> +
+ setPrDraft(checked === true)} + /> + +
diff --git a/apps/web/components/task/task-top-bar.tsx b/apps/web/components/task/task-top-bar.tsx index 1ac579dee3..6bb598aaec 100644 --- a/apps/web/components/task/task-top-bar.tsx +++ b/apps/web/components/task/task-top-bar.tsx @@ -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" /> +
+ setPrDraft(checked === true)} + /> + +
diff --git a/apps/web/hooks/use-git-operations.ts b/apps/web/hooks/use-git-operations.ts index 7338937131..b4f58a6c4e 100644 --- a/apps/web/hooks/use-git-operations.ts +++ b/apps/web/hooks/use-git-operations.ts @@ -31,7 +31,7 @@ interface UseGitOperationsReturn { stage: (paths?: string[]) => Promise; unstage: (paths?: string[]) => Promise; discard: (paths?: string[]) => Promise; - createPR: (title: string, body: string, baseBranch?: string) => Promise; + createPR: (title: string, body: string, baseBranch?: string, draft?: boolean) => Promise; // State isLoading: boolean; @@ -119,7 +119,7 @@ export function useGitOperations(sessionId: string | null): UseGitOperationsRetu return executeOperation('worktree.discard', { paths: paths ?? [] }); }, [executeOperation]); - const createPR = useCallback(async (title: string, body: string, baseBranch?: string): Promise => { + const createPR = useCallback(async (title: string, body: string, baseBranch?: string, draft?: boolean): Promise => { 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) {