diff --git a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts index bf3cc636cc..b0bffa50b6 100644 --- a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts @@ -1,8 +1,8 @@ import { ipcMain, BrowserWindow, shell } from 'electron'; import { IPC_CHANNELS, AUTO_BUILD_PATHS } from '../../../shared/constants'; -import type { IPCResult, WorktreeStatus, WorktreeDiff, WorktreeDiffFile, WorktreeMergeResult, WorktreeDiscardResult, WorktreeListResult, WorktreeListItem, SupportedIDE, SupportedTerminal } from '../../../shared/types'; +import type { IPCResult, WorktreeStatus, WorktreeDiff, WorktreeDiffFile, WorktreeMergeResult, WorktreeDiscardResult, WorktreeListResult, WorktreeListItem, SupportedIDE, SupportedTerminal, CreatePullRequestResult } from '../../../shared/types'; import path from 'path'; -import { existsSync, readdirSync, statSync, readFileSync } from 'fs'; +import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from 'fs'; import { execSync, execFileSync, spawn, spawnSync, exec, execFile } from 'child_process'; import { projectStore } from '../../project-store'; import { getConfiguredPythonPath, PythonEnvManager, pythonEnvManager as pythonEnvManagerSingleton } from '../../python-env-manager'; @@ -2211,4 +2211,152 @@ export function registerWorktreeHandlers( } } ); + + /** + * Create a pull request from the task's worktree branch + * Auto-pushes the branch to remote if not already pushed + */ + ipcMain.handle( + IPC_CHANNELS.TASK_CREATE_PR, + async (_, taskId: string, targetBranch: string): Promise> => { + try { + const { task, project } = findTaskAndProject(taskId); + if (!task || !project) { + return { success: false, error: 'Task not found' }; + } + + const worktreePath = path.join(project.path, '.worktrees', task.specId); + + if (!existsSync(worktreePath)) { + return { success: false, error: 'No worktree found for this task' }; + } + + // Get current branch in worktree + const gitPath = getToolPath('git'); + if (!gitPath) { + return { success: false, error: 'Git not found' }; + } + + const branch = execFileSync(gitPath, ['rev-parse', '--abbrev-ref', 'HEAD'], { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + + // Check if branch has remote tracking + let wasPushed = false; + try { + execFileSync(gitPath, ['rev-parse', '--abbrev-ref', `${branch}@{upstream}`], { + cwd: worktreePath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + } catch { + // No upstream - push the branch + wasPushed = true; + try { + execFileSync(gitPath, ['push', '-u', 'origin', branch], { + cwd: worktreePath, + encoding: 'utf-8' + }); + } catch (pushError) { + return { + success: false, + error: `Failed to push branch: ${pushError instanceof Error ? pushError.message : 'Unknown error'}` + }; + } + } + + // Check if gh CLI is available + const ghPath = getToolPath('gh'); + if (!ghPath) { + return { + success: false, + error: 'GitHub CLI (gh) not found. Install it from https://cli.github.com/' + }; + } + + // Create PR using gh CLI + try { + const prBody = `## Summary + +${task.description || 'Auto-generated from Auto Claude task.'} + +--- +Generated by Auto Claude`; + + const prOutput = execFileSync(ghPath, [ + 'pr', 'create', + '--base', targetBranch, + '--head', branch, + '--title', task.title, + '--body', prBody + ], { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + + // Extract PR URL from output (gh pr create prints the URL) + const prUrl = prOutput.match(/https:\/\/github\.com\/[^\s]+/)?.[0] || prOutput; + const prNumberMatch = prUrl.match(/\/pull\/(\d+)/); + const prNumber = prNumberMatch ? parseInt(prNumberMatch[1], 10) : undefined; + + // Store PR info in implementation_plan.json (without changing task status) + const specDir = path.join(project.path, project.autoBuildPath || '.auto-claude', 'specs', task.specId); + const planPath = path.join(specDir, 'implementation_plan.json'); + + if (existsSync(planPath)) { + try { + const plan = JSON.parse(readFileSync(planPath, 'utf-8')); + plan.prUrl = prUrl; + plan.prCreatedAt = new Date().toISOString(); + plan.updated_at = new Date().toISOString(); + writeFileSync(planPath, JSON.stringify(plan, null, 2)); + } catch (planError) { + console.error('Failed to update implementation plan:', planError); + // Don't fail the PR creation just because plan update failed + } + } + + return { + success: true, + data: { + success: true, + message: 'Pull request created successfully', + prUrl, + prNumber, + wasPushed + } + }; + } catch (prError) { + const errorMsg = prError instanceof Error ? prError.message : String(prError); + const errorOutput = (prError as { stderr?: string })?.stderr || errorMsg; + + // Check for common errors + if (errorOutput.includes('already exists')) { + return { + success: false, + error: 'A pull request already exists for this branch' + }; + } + if (errorOutput.includes('no commits')) { + return { + success: false, + error: 'No commits to create a pull request from' + }; + } + + return { + success: false, + error: `Failed to create pull request: ${errorOutput}` + }; + } + } catch (error) { + console.error('Failed to create pull request:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create pull request' + }; + } + } + ); } diff --git a/apps/frontend/src/main/project-store.ts b/apps/frontend/src/main/project-store.ts index 420e97ab1c..f7b529b3a1 100644 --- a/apps/frontend/src/main/project-store.ts +++ b/apps/frontend/src/main/project-store.ts @@ -422,10 +422,12 @@ export class ProjectStore { })); }) || []; - // Extract staged status from plan (set when changes are merged with --no-commit) - const planWithStaged = plan as unknown as { stagedInMainProject?: boolean; stagedAt?: string } | null; - const stagedInMainProject = planWithStaged?.stagedInMainProject; - const stagedAt = planWithStaged?.stagedAt; + // Extract staged status and PR info from plan + const planWithExtras = plan as unknown as { stagedInMainProject?: boolean; stagedAt?: string; prUrl?: string; prCreatedAt?: string } | null; + const stagedInMainProject = planWithExtras?.stagedInMainProject; + const stagedAt = planWithExtras?.stagedAt; + const prUrl = planWithExtras?.prUrl; + const prCreatedAt = planWithExtras?.prCreatedAt; // Determine title - check if feature looks like a spec ID (e.g., "054-something-something") let title = plan?.feature || plan?.title || dir.name; @@ -459,6 +461,8 @@ export class ProjectStore { metadata, stagedInMainProject, stagedAt, + prUrl, + prCreatedAt, location, // Add location metadata (main vs worktree) specsPath: specPath, // Add full path to specs directory createdAt: new Date(plan?.created_at || Date.now()), diff --git a/apps/frontend/src/preload/api/task-api.ts b/apps/frontend/src/preload/api/task-api.ts index 6049f85b75..c8d105d6ae 100644 --- a/apps/frontend/src/preload/api/task-api.ts +++ b/apps/frontend/src/preload/api/task-api.ts @@ -51,6 +51,7 @@ export interface TaskAPI { mergeWorktree: (taskId: string, options?: { noCommit?: boolean }) => Promise>; mergeWorktreePreview: (taskId: string) => Promise>; discardWorktree: (taskId: string) => Promise>; + createPullRequest: (taskId: string, targetBranch: string) => Promise>; listWorktrees: (projectId: string) => Promise>; worktreeOpenInIDE: (worktreePath: string, ide: SupportedIDE, customPath?: string) => Promise>; worktreeOpenInTerminal: (worktreePath: string, terminal: SupportedTerminal, customPath?: string) => Promise>; @@ -141,6 +142,9 @@ export const createTaskAPI = (): TaskAPI => ({ discardWorktree: (taskId: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DISCARD, taskId), + createPullRequest: (taskId: string, targetBranch: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_CREATE_PR, taskId, targetBranch), + listWorktrees: (projectId: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.TASK_LIST_WORKTREES, projectId), diff --git a/apps/frontend/src/renderer/components/TaskCard.tsx b/apps/frontend/src/renderer/components/TaskCard.tsx index 6256d8bae8..b6d92a4059 100644 --- a/apps/frontend/src/renderer/components/TaskCard.tsx +++ b/apps/frontend/src/renderer/components/TaskCard.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive } from 'lucide-react'; +import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive, GitPullRequest } from 'lucide-react'; import { Card, CardContent } from './ui/card'; import { Badge } from './ui/badge'; import { Button } from './ui/button'; @@ -254,6 +254,16 @@ export function TaskCard({ task, onClick }: TaskCardProps) { {reviewReasonInfo.label} )} + {/* PR Created badge - shown when a PR has been created for this task */} + {task.prUrl && ( + + + {t('labels.prCreated')} + + )} diff --git a/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx b/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx index 6113454dd2..216f39f2b3 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx @@ -25,7 +25,8 @@ import { Loader2, AlertTriangle, Pencil, - X + X, + GitPullRequest } from 'lucide-react'; import { cn } from '../../lib/utils'; import { calculateProgress } from '../../lib/utils'; @@ -151,6 +152,31 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, state.setIsDiscarding(false); }; + const handleCreatePR = async () => { + if (!state.prTargetBranch) { + state.setWorkspaceError('Please select a target branch'); + return; + } + state.setIsCreatingPR(true); + state.setWorkspaceError(null); + try { + const result = await window.electronAPI.createPullRequest(task.id, state.prTargetBranch); + if (result.success && result.data?.success) { + state.setPrSuccess({ + prUrl: result.data.prUrl || '', + message: result.data.message + }); + state.setShowPRBranchSelector(false); + } else { + state.setWorkspaceError(result.data?.message || result.error || 'Failed to create pull request'); + } + } catch (error) { + state.setWorkspaceError(error instanceof Error ? error.message : 'Unknown error creating PR'); + } finally { + state.setIsCreatingPR(false); + } + }; + const handleClose = () => { onOpenChange(false); }; @@ -291,6 +317,15 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, task.reviewReason === 'plan_review' ? 'Approve Plan' : 'QA Issues'} )} + {task.prUrl && ( + + + PR Created + + )} )} {/* Compact progress indicator */} @@ -414,6 +449,15 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, onClose={handleClose} onSwitchToTerminals={onSwitchToTerminals} onOpenInbuiltTerminal={onOpenInbuiltTerminal} + // PR creation props + isCreatingPR={state.isCreatingPR} + showPRBranchSelector={state.showPRBranchSelector} + prTargetBranch={state.prTargetBranch} + availableBranches={state.availableBranches} + prSuccess={state.prSuccess} + onShowPRBranchSelector={state.setShowPRBranchSelector} + onPrTargetBranchChange={state.setPrTargetBranch} + onCreatePR={handleCreatePR} /> )} diff --git a/apps/frontend/src/renderer/components/task-detail/TaskDetailPanel.tsx b/apps/frontend/src/renderer/components/task-detail/TaskDetailPanel.tsx index cb9e6237cc..239fb37b00 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskDetailPanel.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskDetailPanel.tsx @@ -115,6 +115,31 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) { state.setIsDiscarding(false); }; + const handleCreatePR = async () => { + if (!state.prTargetBranch) { + state.setWorkspaceError('Please select a target branch'); + return; + } + state.setIsCreatingPR(true); + state.setWorkspaceError(null); + try { + const result = await window.electronAPI.createPullRequest(task.id, state.prTargetBranch); + if (result.success && result.data?.success) { + state.setPrSuccess({ + prUrl: result.data.prUrl || '', + message: result.data.message + }); + state.setShowPRBranchSelector(false); + } else { + state.setWorkspaceError(result.data?.message || result.error || 'Failed to create pull request'); + } + } catch (error) { + state.setWorkspaceError(error instanceof Error ? error.message : 'Unknown error creating PR'); + } finally { + state.setIsCreatingPR(false); + } + }; + return (
@@ -210,6 +235,16 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) { onStageOnlyChange={state.setStageOnly} onShowConflictDialog={state.setShowConflictDialog} onLoadMergePreview={state.loadMergePreview} + onClose={onClose} + // PR creation props + isCreatingPR={state.isCreatingPR} + showPRBranchSelector={state.showPRBranchSelector} + prTargetBranch={state.prTargetBranch} + availableBranches={state.availableBranches} + prSuccess={state.prSuccess} + onShowPRBranchSelector={state.setShowPRBranchSelector} + onPrTargetBranchChange={state.setPrTargetBranch} + onCreatePR={handleCreatePR} /> )}
diff --git a/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx b/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx index 7dd2d1ca0e..402aa0139c 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx @@ -42,6 +42,15 @@ interface TaskReviewProps { onClose?: () => void; onSwitchToTerminals?: () => void; onOpenInbuiltTerminal?: (id: string, cwd: string) => void; + // PR creation props + isCreatingPR?: boolean; + showPRBranchSelector?: boolean; + prTargetBranch?: string; + availableBranches?: string[]; + prSuccess?: { prUrl: string; message: string } | null; + onShowPRBranchSelector?: (show: boolean) => void; + onPrTargetBranchChange?: (branch: string) => void; + onCreatePR?: () => void; } /** @@ -83,7 +92,16 @@ export function TaskReview({ onLoadMergePreview, onClose, onSwitchToTerminals, - onOpenInbuiltTerminal + onOpenInbuiltTerminal, + // PR creation props + isCreatingPR, + showPRBranchSelector, + prTargetBranch, + availableBranches, + prSuccess, + onShowPRBranchSelector, + onPrTargetBranchChange, + onCreatePR }: TaskReviewProps) { return (
@@ -119,6 +137,15 @@ export function TaskReview({ onClose={onClose} onSwitchToTerminals={onSwitchToTerminals} onOpenInbuiltTerminal={onOpenInbuiltTerminal} + // PR creation props + isCreatingPR={isCreatingPR} + showPRBranchSelector={showPRBranchSelector} + prTargetBranch={prTargetBranch} + availableBranches={availableBranches} + prSuccess={prSuccess} + onShowPRBranchSelector={onShowPRBranchSelector} + onPrTargetBranchChange={onPrTargetBranchChange} + onCreatePR={onCreatePR} /> ) : task.stagedInMainProject && !stagedSuccess ? ( (''); + const [availableBranches, setAvailableBranches] = useState([]); + const [prSuccess, setPrSuccess] = useState<{ prUrl: string; message: string } | null>(null); + const selectedProject = useProjectStore((state) => state.getSelectedProject()); const isRunning = task.status === 'in_progress'; // isActiveTask includes ai_review for stuck detection (CHANGELOG documents this feature) @@ -128,6 +135,39 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { } }, [task.id, needsReview]); + // Load available branches for PR creation when task is in human_review + useEffect(() => { + if (needsReview && selectedProject) { + // Get branches + window.electronAPI.getGitBranches(selectedProject.path).then((result) => { + if (result.success && result.data) { + setAvailableBranches(result.data); + + // Set default target branch: + // 1. Task's baseBranch if set + // 2. Project's defaultBranch + // 3. Detected main branch (main/master/develop) + const taskBaseBranch = task.metadata?.baseBranch; + if (taskBaseBranch && result.data.includes(taskBaseBranch)) { + setPrTargetBranch(taskBaseBranch); + } else { + window.electronAPI.getProjectEnv(selectedProject.id).then((envResult) => { + if (envResult.success && envResult.data?.defaultBranch && result.data?.includes(envResult.data.defaultBranch)) { + setPrTargetBranch(envResult.data.defaultBranch); + } else { + // Use main/master/develop as fallback + const mainBranch = result.data?.find(b => ['main', 'master', 'develop'].includes(b)); + if (mainBranch) { + setPrTargetBranch(mainBranch); + } + } + }); + } + } + }); + } + }, [needsReview, selectedProject, task.metadata?.baseBranch]); + // Load and watch phase logs useEffect(() => { if (!selectedProject) return; @@ -291,6 +331,11 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { mergePreview, isLoadingPreview, showConflictDialog, + isCreatingPR, + showPRBranchSelector, + prTargetBranch, + availableBranches, + prSuccess, // Setters setFeedback, @@ -322,6 +367,11 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { setMergePreview, setIsLoadingPreview, setShowConflictDialog, + setIsCreatingPR, + setShowPRBranchSelector, + setPrTargetBranch, + setAvailableBranches, + setPrSuccess, // Handlers handleLogsScroll, diff --git a/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceStatus.tsx b/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceStatus.tsx index ad286d1019..5e15607e51 100644 --- a/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceStatus.tsx +++ b/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceStatus.tsx @@ -12,13 +12,17 @@ import { CheckCircle, GitCommit, Code, - Terminal + Terminal, + GitPullRequest, + ExternalLink } from 'lucide-react'; import { Button } from '../../ui/button'; import { Checkbox } from '../../ui/checkbox'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select'; import { cn } from '../../../lib/utils'; import type { WorktreeStatus, MergeConflict, MergeStats, GitConflictInfo, SupportedIDE, SupportedTerminal } from '../../../../shared/types'; import { useSettingsStore } from '../../../stores/settings-store'; +import { useTranslation } from 'react-i18next'; interface WorkspaceStatusProps { worktreeStatus: WorktreeStatus; @@ -37,6 +41,15 @@ interface WorkspaceStatusProps { onClose?: () => void; onSwitchToTerminals?: () => void; onOpenInbuiltTerminal?: (id: string, cwd: string) => void; + // PR creation props + isCreatingPR?: boolean; + showPRBranchSelector?: boolean; + prTargetBranch?: string; + availableBranches?: string[]; + prSuccess?: { prUrl: string; message: string } | null; + onShowPRBranchSelector?: (show: boolean) => void; + onPrTargetBranchChange?: (branch: string) => void; + onCreatePR?: () => void; } /** @@ -92,8 +105,18 @@ export function WorkspaceStatus({ onMerge, onClose, onSwitchToTerminals, - onOpenInbuiltTerminal + onOpenInbuiltTerminal, + // PR creation props + isCreatingPR = false, + showPRBranchSelector = false, + prTargetBranch = '', + availableBranches = [], + prSuccess = null, + onShowPRBranchSelector, + onPrTargetBranchChange, + onCreatePR }: WorkspaceStatusProps) { + const { t } = useTranslation(['tasks', 'common']); const { settings } = useSettingsStore(); const preferredIDE = settings.preferredIDE || 'vscode'; const preferredTerminal = settings.preferredTerminal || 'system'; @@ -385,7 +408,7 @@ export function WorkspaceStatus({ + + {/* Create PR Button */} + {onShowPRBranchSelector && ( + + )} +
+ + {/* PR Branch Selector */} + {showPRBranchSelector && onPrTargetBranchChange && onCreatePR && onShowPRBranchSelector && ( +
+
+ + {t('tasks:review.createPRTitle')} +
+ +
+ {t('tasks:review.targetBranch')}: + +
+ +
+ + +
+
+ )} + + {/* PR Success Message */} + {prSuccess && ( +
+
+
+ + {prSuccess.message} +
+ +
+
+ )} ); diff --git a/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts b/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts index 5db2f07a1b..5554d86ed8 100644 --- a/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts @@ -89,5 +89,16 @@ export const workspaceMock = { { id: 'system', name: 'System Terminal', path: '', installed: true } ] } + }), + + createPullRequest: async (_taskId: string, _targetBranch: string) => ({ + success: true, + data: { + success: true, + message: 'Pull request created successfully', + prUrl: 'https://github.com/mock/repo/pull/1', + prNumber: 1, + wasPushed: true + } }) }; diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index e92d404b4e..db1955af76 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -39,6 +39,7 @@ export const IPC_CHANNELS = { TASK_WORKTREE_OPEN_IN_TERMINAL: 'task:worktreeOpenInTerminal', TASK_WORKTREE_DETECT_TOOLS: 'task:worktreeDetectTools', // Detect installed IDEs/terminals TASK_LIST_WORKTREES: 'task:listWorktrees', + TASK_CREATE_PR: 'task:createPullRequest', // Create a PR from worktree branch TASK_ARCHIVE: 'task:archive', TASK_UNARCHIVE: 'task:unarchive', diff --git a/apps/frontend/src/shared/i18n/locales/en/tasks.json b/apps/frontend/src/shared/i18n/locales/en/tasks.json index c5b7c88788..74640351be 100644 --- a/apps/frontend/src/shared/i18n/locales/en/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/en/tasks.json @@ -25,7 +25,8 @@ "incomplete": "Incomplete", "recovering": "Recovering...", "needsRecovery": "Needs Recovery", - "needsResume": "Needs Resume" + "needsResume": "Needs Resume", + "prCreated": "PR Created" }, "reviewReason": { "completed": "Completed", @@ -82,5 +83,18 @@ "code": "Code", "qa": "QA" } + }, + "review": { + "createPR": "Create PR", + "createPRTitle": "Create Pull Request", + "targetBranch": "Target branch", + "selectBranch": "Select branch", + "creatingPR": "Creating PR...", + "viewPR": "View PR", + "prCreated": "Pull request created successfully", + "prAlreadyExists": "A pull request already exists for this branch", + "ghNotInstalled": "GitHub CLI (gh) not found. Install it from https://cli.github.com/", + "pushFailed": "Failed to push branch to remote", + "noCommits": "No commits to create a pull request from" } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/tasks.json b/apps/frontend/src/shared/i18n/locales/fr/tasks.json index 4a819a32b2..2d152bb210 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/fr/tasks.json @@ -25,7 +25,8 @@ "incomplete": "Incomplet", "recovering": "Récupération...", "needsRecovery": "Récupération requise", - "needsResume": "Reprise requise" + "needsResume": "Reprise requise", + "prCreated": "PR créée" }, "reviewReason": { "completed": "Terminé", @@ -82,5 +83,18 @@ "code": "Code", "qa": "QA" } + }, + "review": { + "createPR": "Créer PR", + "createPRTitle": "Créer une Pull Request", + "targetBranch": "Branche cible", + "selectBranch": "Sélectionner une branche", + "creatingPR": "Création de la PR...", + "viewPR": "Voir la PR", + "prCreated": "Pull request créée avec succès", + "prAlreadyExists": "Une pull request existe déjà pour cette branche", + "ghNotInstalled": "GitHub CLI (gh) introuvable. Installez-le depuis https://cli.github.com/", + "pushFailed": "Échec du push de la branche vers le remote", + "noCommits": "Aucun commit pour créer une pull request" } } diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index 835af2e460..6ec7f67196 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -37,7 +37,8 @@ import type { TaskRecoveryOptions, TaskMetadata, TaskLogs, - TaskLogStreamChunk + TaskLogStreamChunk, + CreatePullRequestResult } from './task'; import type { TerminalCreateOptions, @@ -149,6 +150,7 @@ export interface ElectronAPI { mergeWorktree: (taskId: string, options?: { noCommit?: boolean }) => Promise>; mergeWorktreePreview: (taskId: string) => Promise>; discardWorktree: (taskId: string) => Promise>; + createPullRequest: (taskId: string, targetBranch: string) => Promise>; listWorktrees: (projectId: string) => Promise>; worktreeOpenInIDE: (worktreePath: string, ide: SupportedIDE, customPath?: string) => Promise>; worktreeOpenInTerminal: (worktreePath: string, terminal: SupportedTerminal, customPath?: string) => Promise>; diff --git a/apps/frontend/src/shared/types/task.ts b/apps/frontend/src/shared/types/task.ts index 42c9e61a61..c776f13a96 100644 --- a/apps/frontend/src/shared/types/task.ts +++ b/apps/frontend/src/shared/types/task.ts @@ -250,6 +250,8 @@ export interface Task { stagedAt?: string; // ISO timestamp when changes were staged location?: 'main' | 'worktree'; // Where task was loaded from (main project or worktree) specsPath?: string; // Full path to specs directory for this task + prUrl?: string; // URL of the pull request if one was created + prCreatedAt?: string; // ISO timestamp when PR was created createdAt: Date; updatedAt: Date; } @@ -401,6 +403,17 @@ export interface WorktreeDiscardResult { message: string; } +/** + * Result of creating a pull request from a worktree branch + */ +export interface CreatePullRequestResult { + success: boolean; + message: string; + prUrl?: string; + prNumber?: number; + wasPushed?: boolean; // true if branch was pushed as part of this operation +} + /** * Information about a single spec worktree * Per-spec architecture: Each spec has its own worktree at .worktrees/{spec-name}/