diff --git a/apps/backend/cmd/kandev/main.go b/apps/backend/cmd/kandev/main.go index 6c0beb1c31..52da8076e5 100644 --- a/apps/backend/cmd/kandev/main.go +++ b/apps/backend/cmd/kandev/main.go @@ -33,6 +33,7 @@ import ( agenthandlers "github.com/kandev/kandev/internal/agent/handlers" "github.com/kandev/kandev/internal/agent/lifecycle" "github.com/kandev/kandev/internal/agent/registry" + "github.com/kandev/kandev/internal/agent/worktree" // Orchestrator packages "github.com/kandev/kandev/internal/orchestrator" @@ -178,6 +179,37 @@ func main() { log.Info("Agent Manager disabled (no Docker)") } + // ============================================ + // WORKTREE MANAGER + // ============================================ + log.Info("Initializing Worktree Manager...") + + // Create worktree store (uses same database as tasks) + // Type assert to get the *sql.DB from the SQLite repository + var worktreeMgr *worktree.Manager + if sqliteRepo, ok := taskRepo.(*repository.SQLiteRepository); ok { + worktreeStore, err := worktree.NewSQLiteStore(sqliteRepo.DB()) + if err != nil { + log.Fatal("Failed to initialize worktree store", zap.Error(err)) + } + + // Create worktree manager with config from common config + worktreeMgr, err = worktree.NewManager(worktree.Config{ + Enabled: cfg.Worktree.Enabled, + BasePath: cfg.Worktree.BasePath, + MaxPerRepo: cfg.Worktree.MaxPerRepo, + BranchPrefix: "kandev/", // Default branch prefix + }, worktreeStore, log) + if err != nil { + log.Fatal("Failed to initialize worktree manager", zap.Error(err)) + } + log.Info("Worktree Manager initialized", + zap.Bool("enabled", cfg.Worktree.Enabled), + zap.String("base_path", cfg.Worktree.BasePath)) + } else { + log.Warn("Worktree Manager disabled (requires SQLite repository)") + } + // ============================================ // ORCHESTRATOR // ============================================ @@ -212,6 +244,9 @@ func main() { workspaceController := taskcontroller.NewWorkspaceController(taskSvc) boardController := taskcontroller.NewBoardController(taskSvc) taskController := taskcontroller.NewTaskController(taskSvc) + if worktreeMgr != nil { + taskController.SetWorktreeLookup(worktreeMgr) // Enable worktree info in task responses + } repositoryController := taskcontroller.NewRepositoryController(taskSvc) // Create orchestrator controller and handlers (Pattern A) diff --git a/apps/backend/internal/agent/worktree/manager.go b/apps/backend/internal/agent/worktree/manager.go index 630e763824..baa8d09a2d 100644 --- a/apps/backend/internal/agent/worktree/manager.go +++ b/apps/backend/internal/agent/worktree/manager.go @@ -90,6 +90,17 @@ func (m *Manager) IsEnabled() bool { return m.config.Enabled } +// GetWorktreeInfo returns worktree path and branch for a task. +// Returns nil values if no worktree exists for the task. +// This method implements the WorktreeLookup interface for task enrichment. +func (m *Manager) GetWorktreeInfo(ctx context.Context, taskID string) (path, branch *string) { + wt, err := m.GetByTaskID(ctx, taskID) + if err != nil || wt == nil { + return nil, nil + } + return &wt.Path, &wt.Branch +} + // Create creates a new worktree for a task. // If a worktree already exists for the task, it returns the existing one. func (m *Manager) Create(ctx context.Context, req CreateRequest) (*Worktree, error) { diff --git a/apps/backend/internal/task/controller/task.go b/apps/backend/internal/task/controller/task.go index 2edbd4e42b..869c5e042c 100644 --- a/apps/backend/internal/task/controller/task.go +++ b/apps/backend/internal/task/controller/task.go @@ -7,14 +7,36 @@ import ( "github.com/kandev/kandev/internal/task/service" ) +// WorktreeLookup provides worktree information for tasks. +type WorktreeLookup interface { + // GetWorktreeInfo returns worktree path and branch for a task, or nil if no worktree exists. + GetWorktreeInfo(ctx context.Context, taskID string) (path, branch *string) +} + type TaskController struct { - service *service.Service + service *service.Service + worktreeLookup WorktreeLookup } func NewTaskController(svc *service.Service) *TaskController { return &TaskController{service: svc} } +// SetWorktreeLookup sets the worktree lookup for enriching task responses. +func (c *TaskController) SetWorktreeLookup(lookup WorktreeLookup) { + c.worktreeLookup = lookup +} + +// enrichWithWorktree adds worktree info to a TaskDTO if available. +func (c *TaskController) enrichWithWorktree(ctx context.Context, task *dto.TaskDTO) { + if c.worktreeLookup == nil { + return + } + path, branch := c.worktreeLookup.GetWorktreeInfo(ctx, task.ID) + task.WorktreePath = path + task.WorktreeBranch = branch +} + func (c *TaskController) ListTasks(ctx context.Context, req dto.ListTasksRequest) (dto.ListTasksResponse, error) { tasks, err := c.service.ListTasks(ctx, req.BoardID) if err != nil { @@ -25,7 +47,9 @@ func (c *TaskController) ListTasks(ctx context.Context, req dto.ListTasksRequest Total: len(tasks), } for _, task := range tasks { - resp.Tasks = append(resp.Tasks, dto.FromTask(task)) + taskDTO := dto.FromTask(task) + c.enrichWithWorktree(ctx, &taskDTO) + resp.Tasks = append(resp.Tasks, taskDTO) } return resp, nil } @@ -35,7 +59,9 @@ func (c *TaskController) GetTask(ctx context.Context, req dto.GetTaskRequest) (d if err != nil { return dto.TaskDTO{}, err } - return dto.FromTask(task), nil + taskDTO := dto.FromTask(task) + c.enrichWithWorktree(ctx, &taskDTO) + return taskDTO, nil } func (c *TaskController) CreateTask(ctx context.Context, req dto.CreateTaskRequest) (dto.TaskDTO, error) { diff --git a/apps/backend/internal/task/dto/dto.go b/apps/backend/internal/task/dto/dto.go index 82e1922792..6f66366388 100644 --- a/apps/backend/internal/task/dto/dto.go +++ b/apps/backend/internal/task/dto/dto.go @@ -81,6 +81,9 @@ type TaskDTO struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Metadata map[string]interface{} `json:"metadata,omitempty"` + // Worktree information (populated if worktree exists for this task) + WorktreePath *string `json:"worktree_path,omitempty"` + WorktreeBranch *string `json:"worktree_branch,omitempty"` } type BoardSnapshotDTO struct { diff --git a/apps/web/app/task/[id]/page-client.tsx b/apps/web/app/task/[id]/page-client.tsx index d1b5fed37a..fa6faee0b8 100644 --- a/apps/web/app/task/[id]/page-client.tsx +++ b/apps/web/app/task/[id]/page-client.tsx @@ -205,6 +205,8 @@ export default function TaskPage({ task }: TaskPageClientProps) { onStopAgent={handleStopAgent} isAgentRunning={isAgentRunning} isAgentLoading={isAgentLoading} + worktreePath={task?.worktree_path} + worktreeBranch={task?.worktree_branch} />