feat: expose worktree path and branch in task API and UI
- Add WorktreePath and WorktreeBranch fields to TaskDTO - Add WorktreeLookup interface to TaskController for enriching task responses - Add GetWorktreeInfo method to worktree.Manager implementing WorktreeLookup - Wire worktree manager to task controller in main.go - Add worktree_path and worktree_branch to frontend Task type - Display worktree path with copy button in TaskTopBar when worktree exists - Show worktree branch in tooltip for easy reference
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -205,6 +205,8 @@ export default function TaskPage({ task }: TaskPageClientProps) {
|
||||
onStopAgent={handleStopAgent}
|
||||
isAgentRunning={isAgentRunning}
|
||||
isAgentLoading={isAgentLoading}
|
||||
worktreePath={task?.worktree_path}
|
||||
worktreeBranch={task?.worktree_branch}
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-h-0 px-4 pb-4">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
IconChevronRight,
|
||||
IconCopy,
|
||||
IconEye,
|
||||
IconFolder,
|
||||
IconGitBranch,
|
||||
IconGitFork,
|
||||
IconGitMerge,
|
||||
@@ -34,6 +35,8 @@ type TaskTopBarProps = {
|
||||
onStopAgent?: () => void;
|
||||
isAgentRunning?: boolean;
|
||||
isAgentLoading?: boolean;
|
||||
worktreePath?: string | null;
|
||||
worktreeBranch?: string | null;
|
||||
};
|
||||
|
||||
const TaskTopBar = memo(function TaskTopBar({
|
||||
@@ -45,6 +48,8 @@ const TaskTopBar = memo(function TaskTopBar({
|
||||
onStopAgent,
|
||||
isAgentRunning = false,
|
||||
isAgentLoading = false,
|
||||
worktreePath,
|
||||
worktreeBranch,
|
||||
}: TaskTopBarProps) {
|
||||
const [branchName, setBranchName] = useState('feature/agent-ui');
|
||||
const [isEditingBranch, setIsEditingBranch] = useState(false);
|
||||
@@ -165,6 +170,42 @@ const TaskTopBar = memo(function TaskTopBar({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Lines changed</TooltipContent>
|
||||
</Tooltip>
|
||||
{/* Worktree Info */}
|
||||
{worktreePath && (
|
||||
<div className="flex items-center gap-2 ml-2 pl-2 border-l border-border/50">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="group flex items-center gap-1.5 rounded-md px-2 h-7 bg-muted/40 hover:bg-muted/60 cursor-pointer">
|
||||
<IconFolder className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground max-w-[200px] truncate">
|
||||
{worktreePath}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard?.writeText(worktreePath);
|
||||
}}
|
||||
>
|
||||
<IconCopy className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[400px]">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">Worktree Path</div>
|
||||
<div className="text-xs text-muted-foreground break-all">{worktreePath}</div>
|
||||
{worktreeBranch && (
|
||||
<div className="text-xs">
|
||||
Branch: <span className="font-mono">{worktreeBranch}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Start/Stop Agent Button */}
|
||||
|
||||
@@ -83,6 +83,9 @@ export type Task = {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
// Worktree information (present if agent has created a worktree for this task)
|
||||
worktree_path?: string | null;
|
||||
worktree_branch?: string | null;
|
||||
};
|
||||
|
||||
export type BoardSnapshot = {
|
||||
|
||||
Reference in New Issue
Block a user