From 9c70858b21a1087a61e4f9f4ed1925d6b69fda69 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 8 Dec 2025 16:53:37 +0000 Subject: [PATCH] Make it possible to create draft PRs (vibe-kanban) (#1460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * All checks pass. Let me summarize what was implemented: ## Summary I've successfully implemented the draft PR feature. Here's what was changed: ### Backend Changes 1. **`crates/server/src/routes/task_attempts.rs`** (lines 70-76, 724-731): - Added `draft: Option` field to `CreateGitHubPrRequest` struct - Pass the draft field to the `CreatePrRequest` when creating PRs 2. **`crates/services/src/services/github.rs`** (lines 146-153): - Added `draft: Option` field to `CreatePrRequest` struct 3. **`crates/services/src/services/github/cli.rs`** (lines 158-160): - Added logic to pass `--draft` flag to `gh pr create` when draft is true ### Frontend Changes 4. **`frontend/src/components/dialogs/tasks/CreatePRDialog.tsx`**: - Added `Checkbox` import - Added `isDraft` state (default: false) - Added checkbox UI with label from translations - Pass `draft: isDraft` in the API call - Reset `isDraft` on success and cancel ### Type Generation 5. **`shared/types.ts`** (auto-generated): - `CreateGitHubPrRequest` now includes `draft: boolean | null` ### Translations 6. Added `"draftLabel"` translation key to all locale files: - `frontend/src/i18n/locales/en/tasks.json`: "Create as draft" - `frontend/src/i18n/locales/es/tasks.json`: "Crear como borrador" - `frontend/src/i18n/locales/ja/tasks.json`: "下書きとして作成" - `frontend/src/i18n/locales/ko/tasks.json`: "초안으로 만들기" - `frontend/src/i18n/locales/zh-Hans/tasks.json`: "创建为草稿" * Done. I've updated the checkbox to use `h-5 w-5` (20px) and added `text-sm` to the label for consistent sizing. --- crates/server/src/routes/task_attempts.rs | 2 ++ crates/services/src/services/github.rs | 1 + crates/services/src/services/github/cli.rs | 4 ++++ .../components/dialogs/tasks/CreatePRDialog.tsx | 17 +++++++++++++++++ frontend/src/i18n/locales/en/tasks.json | 1 + frontend/src/i18n/locales/es/tasks.json | 1 + frontend/src/i18n/locales/ja/tasks.json | 1 + frontend/src/i18n/locales/ko/tasks.json | 1 + frontend/src/i18n/locales/zh-Hans/tasks.json | 1 + shared/types.ts | 2 +- 10 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/server/src/routes/task_attempts.rs b/crates/server/src/routes/task_attempts.rs index 0e27bf41..806edd86 100644 --- a/crates/server/src/routes/task_attempts.rs +++ b/crates/server/src/routes/task_attempts.rs @@ -72,6 +72,7 @@ pub struct CreateGitHubPrRequest { pub title: String, pub body: Option, pub target_branch: Option, + pub draft: Option, } #[derive(Debug, Deserialize)] @@ -726,6 +727,7 @@ pub async fn create_github_pr( body: request.body.clone(), head_branch: task_attempt.branch.clone(), base_branch: norm_target_branch_name.clone(), + draft: request.draft, }; // Use GitService to get the remote URL, then create GitHubRepoInfo let repo_info = deployment diff --git a/crates/services/src/services/github.rs b/crates/services/src/services/github.rs index 9de6ab0c..6019266f 100644 --- a/crates/services/src/services/github.rs +++ b/crates/services/src/services/github.rs @@ -149,6 +149,7 @@ pub struct CreatePrRequest { pub body: Option, pub head_branch: String, pub base_branch: String, + pub draft: Option, } #[derive(Debug, Clone)] diff --git a/crates/services/src/services/github/cli.rs b/crates/services/src/services/github/cli.rs index e6c7c1de..d1468338 100644 --- a/crates/services/src/services/github/cli.rs +++ b/crates/services/src/services/github/cli.rs @@ -155,6 +155,10 @@ impl GhCli { args.push(OsString::from("--body")); args.push(OsString::from(body)); + if request.draft.unwrap_or(false) { + args.push(OsString::from("--draft")); + } + let raw = self.run(args)?; Self::parse_pr_create_text(&raw) } diff --git a/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx b/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx index 9e2926a4..8a30d95a 100644 --- a/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx +++ b/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx @@ -10,6 +10,7 @@ import { Label } from '@radix-ui/react-label'; import { Textarea } from '@/components/ui/textarea.tsx'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Checkbox } from '@/components/ui/checkbox'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import BranchSelector from '@/components/tasks/BranchSelector'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -56,6 +57,7 @@ const CreatePRDialogImpl = NiceModal.create( ); const [branches, setBranches] = useState([]); const [branchesLoading, setBranchesLoading] = useState(false); + const [isDraft, setIsDraft] = useState(false); const getGhCliHelpTitle = (variant: GhCliSupportVariant) => variant === 'homebrew' @@ -136,12 +138,14 @@ const CreatePRDialogImpl = NiceModal.create( title: prTitle, body: prBody || null, target_branch: prBaseBranch || null, + draft: isDraft, }); if (result.success) { setPrTitle(''); setPrBody(''); setPrBaseBranch(''); + setIsDraft(false); setCreatingPR(false); modal.hide(); return; @@ -213,6 +217,7 @@ const CreatePRDialogImpl = NiceModal.create( prBaseBranch, prBody, prTitle, + isDraft, modal, isMacEnvironment, t, @@ -224,6 +229,7 @@ const CreatePRDialogImpl = NiceModal.create( setPrTitle(''); setPrBody(''); setPrBaseBranch(''); + setIsDraft(false); }, [modal]); return ( @@ -286,6 +292,17 @@ const CreatePRDialogImpl = NiceModal.create( } /> +
+ + +
{ghCliHelp?.variant && ( diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index f79f7222..74d8b1d3 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -355,6 +355,7 @@ "baseBranchLabel": "Base Branch", "loadingBranches": "Loading branches...", "selectBaseBranch": "Select base branch", + "draftLabel": "Create as draft", "creating": "Creating...", "createButton": "Create PR", "errors": { diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index 16cf7606..d6dd5f18 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -112,6 +112,7 @@ "baseBranchLabel": "Rama Base", "loadingBranches": "Cargando ramas...", "selectBaseBranch": "Seleccionar rama base", + "draftLabel": "Crear como borrador", "creating": "Creando...", "createButton": "Crear PR", "errors": { diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index 946fab99..e0cee247 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -112,6 +112,7 @@ "baseBranchLabel": "ベースブランチ", "loadingBranches": "ブランチを読み込み中...", "selectBaseBranch": "ベースブランチを選択", + "draftLabel": "下書きとして作成", "creating": "作成中...", "createButton": "PRを作成", "errors": { diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index 78554bd7..6320d9be 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -112,6 +112,7 @@ "baseBranchLabel": "기본 브랜치", "loadingBranches": "브랜치 로딩 중...", "selectBaseBranch": "기본 브랜치 선택", + "draftLabel": "초안으로 만들기", "creating": "생성 중...", "createButton": "PR 생성", "errors": { diff --git a/frontend/src/i18n/locales/zh-Hans/tasks.json b/frontend/src/i18n/locales/zh-Hans/tasks.json index 06703a08..f2eab558 100644 --- a/frontend/src/i18n/locales/zh-Hans/tasks.json +++ b/frontend/src/i18n/locales/zh-Hans/tasks.json @@ -355,6 +355,7 @@ "baseBranchLabel": "基础分支", "loadingBranches": "加载分支中...", "selectBaseBranch": "选择基础分支", + "draftLabel": "创建为草稿", "creating": "创建中...", "createButton": "创建 PR", "errors": { diff --git a/shared/types.ts b/shared/types.ts index e82e35cb..c4324a34 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -216,7 +216,7 @@ export type ShareTaskResponse = { shared_task_id: string, }; export type CreateAndStartTaskRequest = { task: CreateTask, executor_profile_id: ExecutorProfileId, base_branch: string, }; -export type CreateGitHubPrRequest = { title: string, body: string | null, target_branch: string | null, }; +export type CreateGitHubPrRequest = { title: string, body: string | null, target_branch: string | null, draft: boolean | null, }; export type ImageResponse = { id: string, file_path: string, original_name: string, mime_type: string | null, size_bytes: bigint, hash: string, created_at: string, updated_at: string, };