feat: improve onboarding and task setup (#21)
This commit is contained in:
@@ -229,6 +229,10 @@ func main() {
|
||||
)
|
||||
log.Info("Task Service initialized")
|
||||
|
||||
if err := runInitialAgentSetup(ctx, userSvc, agentSettingsController, log); err != nil {
|
||||
log.Warn("Failed to run initial agent setup", zap.Error(err))
|
||||
}
|
||||
|
||||
// ACP messages are now stored as comments in the task_comments table
|
||||
// The comment system provides unified storage for all task-related communication
|
||||
log.Info("ACP messages will be stored as comments")
|
||||
@@ -829,6 +833,32 @@ func main() {
|
||||
log.Info("Kandev stopped")
|
||||
}
|
||||
|
||||
func runInitialAgentSetup(
|
||||
ctx context.Context,
|
||||
userSvc *userservice.Service,
|
||||
agentSettingsController *agentsettingscontroller.Controller,
|
||||
log *logger.Logger,
|
||||
) error {
|
||||
settings, err := userSvc.GetUserSettings(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if settings.InitialSetupComplete {
|
||||
return nil
|
||||
}
|
||||
if err := agentSettingsController.EnsureInitialAgentProfiles(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
complete := true
|
||||
if _, err := userSvc.UpdateUserSettings(ctx, &userservice.UpdateUserSettingsRequest{
|
||||
InitialSetupComplete: &complete,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Initial agent setup complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// taskRepositoryAdapter adapts the task repository for the orchestrator's scheduler
|
||||
type taskRepositoryAdapter struct {
|
||||
repo repository.Repository
|
||||
|
||||
@@ -21,6 +21,8 @@ var (
|
||||
ErrAgentProfileInUse = errors.New("agent profile is used by an active agent session")
|
||||
)
|
||||
|
||||
const defaultAgentProfileName = "default"
|
||||
|
||||
type Controller struct {
|
||||
repo store.Repository
|
||||
discovery *discovery.Registry
|
||||
@@ -76,6 +78,49 @@ func (c *Controller) ListAgents(ctx context.Context) (*dto.ListAgentsResponse, e
|
||||
return &dto.ListAgentsResponse{Agents: payload, Total: len(payload)}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) EnsureInitialAgentProfiles(ctx context.Context) error {
|
||||
results, err := c.discovery.Detect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, result := range results {
|
||||
if !result.Available {
|
||||
continue
|
||||
}
|
||||
agent, err := c.repo.GetAgentByName(ctx, result.Name)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if errors.Is(err, sql.ErrNoRows) || agent == nil {
|
||||
agent = &models.Agent{
|
||||
Name: result.Name,
|
||||
SupportsMCP: result.SupportsMCP,
|
||||
MCPConfigPath: result.MCPConfigPath,
|
||||
}
|
||||
if err := c.repo.CreateAgent(ctx, agent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
profiles, err := c.repo.ListAgentProfiles(ctx, agent.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(profiles) > 0 {
|
||||
continue
|
||||
}
|
||||
defaultProfile := &models.AgentProfile{
|
||||
AgentID: agent.ID,
|
||||
Name: defaultAgentProfileName,
|
||||
Model: "",
|
||||
Plan: "",
|
||||
}
|
||||
if err := c.repo.CreateAgentProfile(ctx, defaultProfile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) GetAgent(ctx context.Context, id string) (*dto.AgentDTO, error) {
|
||||
agent, err := c.repo.GetAgent(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
@@ -163,6 +163,25 @@ func (c *RepositoryController) ListRepositoryBranches(ctx context.Context, req d
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *RepositoryController) ListLocalRepositoryBranches(ctx context.Context, req dto.ListLocalRepositoryBranchesRequest) (dto.RepositoryBranchesResponse, error) {
|
||||
branches, err := c.service.ListLocalRepositoryBranches(ctx, req.Path)
|
||||
if err != nil {
|
||||
return dto.RepositoryBranchesResponse{}, err
|
||||
}
|
||||
dtoBranches := make([]dto.BranchDTO, len(branches))
|
||||
for i, branch := range branches {
|
||||
dtoBranches[i] = dto.BranchDTO{
|
||||
Name: branch.Name,
|
||||
Type: branch.Type,
|
||||
Remote: branch.Remote,
|
||||
}
|
||||
}
|
||||
return dto.RepositoryBranchesResponse{
|
||||
Branches: dtoBranches,
|
||||
Total: len(dtoBranches),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *RepositoryController) DiscoverRepositories(ctx context.Context, req dto.DiscoverRepositoriesRequest) (dto.RepositoryDiscoveryResponse, error) {
|
||||
result, err := c.service.DiscoverLocalRepositories(ctx, req.Root)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,8 +14,8 @@ type WorktreeLookup interface {
|
||||
}
|
||||
|
||||
type TaskController struct {
|
||||
service *service.Service
|
||||
worktreeLookup WorktreeLookup
|
||||
service *service.Service
|
||||
worktreeLookup WorktreeLookup
|
||||
}
|
||||
|
||||
func NewTaskController(svc *service.Service) *TaskController {
|
||||
@@ -84,23 +84,26 @@ func (c *TaskController) CreateTask(ctx context.Context, req dto.CreateTaskReque
|
||||
var repos []service.TaskRepositoryInput
|
||||
for _, r := range req.Repositories {
|
||||
repos = append(repos, service.TaskRepositoryInput{
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
LocalPath: r.LocalPath,
|
||||
Name: r.Name,
|
||||
DefaultBranch: r.DefaultBranch,
|
||||
})
|
||||
}
|
||||
|
||||
task, err := c.service.CreateTask(ctx, &service.CreateTaskRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
ColumnID: req.ColumnID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
State: req.State,
|
||||
Repositories: repos,
|
||||
AssignedTo: req.AssignedTo,
|
||||
Position: req.Position,
|
||||
Metadata: req.Metadata,
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
ColumnID: req.ColumnID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
State: req.State,
|
||||
Repositories: repos,
|
||||
AssignedTo: req.AssignedTo,
|
||||
Position: req.Position,
|
||||
Metadata: req.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.TaskDTO{}, err
|
||||
@@ -113,8 +116,11 @@ func (c *TaskController) UpdateTask(ctx context.Context, req dto.UpdateTaskReque
|
||||
var repos []service.TaskRepositoryInput
|
||||
for _, r := range req.Repositories {
|
||||
repos = append(repos, service.TaskRepositoryInput{
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
LocalPath: r.LocalPath,
|
||||
Name: r.Name,
|
||||
DefaultBranch: r.DefaultBranch,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,11 @@ type ListRepositoryBranchesRequest struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type ListLocalRepositoryBranchesRequest struct {
|
||||
WorkspaceID string
|
||||
Path string
|
||||
}
|
||||
|
||||
type DiscoverRepositoriesRequest struct {
|
||||
WorkspaceID string
|
||||
Root string
|
||||
@@ -238,22 +243,25 @@ type GetTaskRequest struct {
|
||||
}
|
||||
|
||||
type TaskRepositoryInput struct {
|
||||
RepositoryID string
|
||||
BaseBranch string
|
||||
RepositoryID string
|
||||
BaseBranch string
|
||||
LocalPath string
|
||||
Name string
|
||||
DefaultBranch string
|
||||
}
|
||||
|
||||
type CreateTaskRequest struct {
|
||||
WorkspaceID string
|
||||
BoardID string
|
||||
ColumnID string
|
||||
Title string
|
||||
Description string
|
||||
Priority int
|
||||
State *v1.TaskState
|
||||
Repositories []TaskRepositoryInput
|
||||
AssignedTo string
|
||||
Position int
|
||||
Metadata map[string]interface{}
|
||||
WorkspaceID string
|
||||
BoardID string
|
||||
ColumnID string
|
||||
Title string
|
||||
Description string
|
||||
Priority int
|
||||
State *v1.TaskState
|
||||
Repositories []TaskRepositoryInput
|
||||
AssignedTo string
|
||||
Position int
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
type UpdateTaskRequest struct {
|
||||
@@ -274,10 +282,10 @@ type DeleteTaskRequest struct {
|
||||
|
||||
type ListMessagesRequest struct {
|
||||
TaskSessionID string
|
||||
Limit int
|
||||
Before string
|
||||
After string
|
||||
Sort string
|
||||
Limit int
|
||||
Before string
|
||||
After string
|
||||
Sort string
|
||||
}
|
||||
|
||||
type CreateMessageRequest struct {
|
||||
|
||||
@@ -38,6 +38,7 @@ func (h *RepositoryHandlers) registerHTTP(router *gin.Engine) {
|
||||
api.GET("/workspaces/:id/repositories", h.httpListRepositories)
|
||||
api.POST("/workspaces/:id/repositories", h.httpCreateRepository)
|
||||
api.GET("/workspaces/:id/repositories/discover", h.httpDiscoverRepositories)
|
||||
api.GET("/workspaces/:id/repositories/branches", h.httpListLocalRepositoryBranches)
|
||||
api.GET("/workspaces/:id/repositories/validate", h.httpValidateRepositoryPath)
|
||||
api.GET("/repositories/:id", h.httpGetRepository)
|
||||
api.GET("/repositories/:id/branches", h.httpListRepositoryBranches)
|
||||
@@ -111,6 +112,28 @@ func (h *RepositoryHandlers) httpValidateRepositoryPath(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *RepositoryHandlers) httpListLocalRepositoryBranches(c *gin.Context) {
|
||||
path := c.Query("path")
|
||||
if path == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "path is required"})
|
||||
return
|
||||
}
|
||||
resp, err := h.controller.ListLocalRepositoryBranches(c.Request.Context(), dto.ListLocalRepositoryBranchesRequest{
|
||||
WorkspaceID: c.Param("id"),
|
||||
Path: path,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrPathNotAllowed) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "path is not within allowed roots"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("failed to list local repository branches", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list local repository branches"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type httpCreateRepositoryRequest struct {
|
||||
Name string `json:"name"`
|
||||
SourceType string `json:"source_type"`
|
||||
|
||||
@@ -84,22 +84,25 @@ func (h *TaskHandlers) httpListTaskSessions(c *gin.Context) {
|
||||
}
|
||||
|
||||
type httpTaskRepositoryInput struct {
|
||||
RepositoryID string `json:"repository_id"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
RepositoryID string `json:"repository_id"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
LocalPath string `json:"local_path"`
|
||||
Name string `json:"name"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
}
|
||||
|
||||
type httpCreateTaskRequest struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
Repositories []httpTaskRepositoryInput `json:"repositories,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
Repositories []httpTaskRepositoryInput `json:"repositories,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (h *TaskHandlers) httpCreateTask(c *gin.Context) {
|
||||
@@ -116,9 +119,16 @@ func (h *TaskHandlers) httpCreateTask(c *gin.Context) {
|
||||
// Convert repositories
|
||||
var repos []dto.TaskRepositoryInput
|
||||
for _, r := range body.Repositories {
|
||||
if r.RepositoryID == "" && r.LocalPath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "repository_id or local_path is required"})
|
||||
return
|
||||
}
|
||||
repos = append(repos, dto.TaskRepositoryInput{
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
LocalPath: r.LocalPath,
|
||||
Name: r.Name,
|
||||
DefaultBranch: r.DefaultBranch,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,8 +175,11 @@ func (h *TaskHandlers) httpUpdateTask(c *gin.Context) {
|
||||
if body.Repositories != nil {
|
||||
for _, r := range body.Repositories {
|
||||
repos = append(repos, dto.TaskRepositoryInput{
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
LocalPath: r.LocalPath,
|
||||
Name: r.Name,
|
||||
DefaultBranch: r.DefaultBranch,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -272,17 +285,17 @@ func (h *TaskHandlers) wsListTasks(ctx context.Context, msg *ws.Message) (*ws.Me
|
||||
}
|
||||
|
||||
type wsCreateTaskRequest struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
Repositories []httpTaskRepositoryInput `json:"repositories,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
Repositories []httpTaskRepositoryInput `json:"repositories,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (h *TaskHandlers) wsCreateTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
@@ -306,9 +319,15 @@ func (h *TaskHandlers) wsCreateTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
// Convert repositories
|
||||
var repos []dto.TaskRepositoryInput
|
||||
for _, r := range req.Repositories {
|
||||
if r.RepositoryID == "" && r.LocalPath == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "repository_id or local_path is required", nil)
|
||||
}
|
||||
repos = append(repos, dto.TaskRepositoryInput{
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
LocalPath: r.LocalPath,
|
||||
Name: r.Name,
|
||||
DefaultBranch: r.DefaultBranch,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -378,8 +397,11 @@ func (h *TaskHandlers) wsUpdateTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
if req.Repositories != nil {
|
||||
for _, r := range req.Repositories {
|
||||
repos = append(repos, dto.TaskRepositoryInput{
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
RepositoryID: r.RepositoryID,
|
||||
BaseBranch: r.BaseBranch,
|
||||
LocalPath: r.LocalPath,
|
||||
Name: r.Name,
|
||||
DefaultBranch: r.DefaultBranch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,27 @@ func (s *Service) ListRepositoryBranches(ctx context.Context, repoID string) ([]
|
||||
return listGitBranches(absPath)
|
||||
}
|
||||
|
||||
func (s *Service) ListLocalRepositoryBranches(ctx context.Context, path string) ([]Branch, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("repository path is required")
|
||||
}
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid repository path: %w", err)
|
||||
}
|
||||
if !isPathAllowed(absPath, s.discoveryRoots()) {
|
||||
return nil, ErrPathNotAllowed
|
||||
}
|
||||
info, err := os.Stat(absPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, fmt.Errorf("repository path is not a directory")
|
||||
}
|
||||
return listGitBranches(absPath)
|
||||
}
|
||||
|
||||
func (s *Service) discoveryRoots() []string {
|
||||
if len(s.discoveryConfig.Roots) > 0 {
|
||||
return normalizeRoots(s.discoveryConfig.Roots)
|
||||
@@ -344,15 +365,32 @@ func resolveGitDir(repoPath string) (string, error) {
|
||||
return filepath.Clean(filepath.Join(repoPath, gitDir)), nil
|
||||
}
|
||||
|
||||
func resolveCommonGitDir(gitDir string) string {
|
||||
commonFile := filepath.Join(gitDir, "commondir")
|
||||
content, err := os.ReadFile(commonFile)
|
||||
if err != nil {
|
||||
return gitDir
|
||||
}
|
||||
commonDir := strings.TrimSpace(string(content))
|
||||
if commonDir == "" {
|
||||
return gitDir
|
||||
}
|
||||
if filepath.IsAbs(commonDir) {
|
||||
return filepath.Clean(commonDir)
|
||||
}
|
||||
return filepath.Clean(filepath.Join(gitDir, commonDir))
|
||||
}
|
||||
|
||||
func listGitBranches(repoPath string) ([]Branch, error) {
|
||||
gitDir, err := resolveGitDir(repoPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refsRoot := resolveCommonGitDir(gitDir)
|
||||
branchMap := make(map[string]Branch)
|
||||
|
||||
// List local branches from refs/heads
|
||||
localRefsRoot := filepath.Join(gitDir, "refs", "heads")
|
||||
localRefsRoot := filepath.Join(refsRoot, "refs", "heads")
|
||||
_ = filepath.WalkDir(localRefsRoot, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -376,7 +414,7 @@ func listGitBranches(repoPath string) ([]Branch, error) {
|
||||
})
|
||||
|
||||
// List remote branches from refs/remotes
|
||||
remoteRefsRoot := filepath.Join(gitDir, "refs", "remotes")
|
||||
remoteRefsRoot := filepath.Join(refsRoot, "refs", "remotes")
|
||||
_ = filepath.WalkDir(remoteRefsRoot, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -412,7 +450,7 @@ func listGitBranches(repoPath string) ([]Branch, error) {
|
||||
})
|
||||
|
||||
// Read packed-refs file for additional branches
|
||||
packedRefs := filepath.Join(gitDir, "packed-refs")
|
||||
packedRefs := filepath.Join(refsRoot, "packed-refs")
|
||||
if content, err := os.ReadFile(packedRefs); err == nil {
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -65,8 +66,11 @@ func (s *Service) SetExecutionStopper(stopper TaskExecutionStopper) {
|
||||
|
||||
// TaskRepositoryInput for creating/updating task repositories
|
||||
type TaskRepositoryInput struct {
|
||||
RepositoryID string `json:"repository_id"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
RepositoryID string `json:"repository_id"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
LocalPath string `json:"local_path,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DefaultBranch string `json:"default_branch,omitempty"`
|
||||
}
|
||||
|
||||
// CreateTaskRequest contains the data for creating a new task
|
||||
@@ -213,10 +217,10 @@ type UpdateEnvironmentRequest struct {
|
||||
|
||||
type ListMessagesRequest struct {
|
||||
TaskSessionID string
|
||||
Limit int
|
||||
Before string
|
||||
After string
|
||||
Sort string
|
||||
Limit int
|
||||
Before string
|
||||
After string
|
||||
Sort string
|
||||
}
|
||||
|
||||
// CreateRepositoryScriptRequest contains the data for creating a repository script
|
||||
@@ -262,11 +266,64 @@ func (s *Service) CreateTask(ctx context.Context, req *CreateTaskRequest) (*mode
|
||||
}
|
||||
|
||||
// Create task-repository associations
|
||||
var repoByPath map[string]*models.Repository
|
||||
for _, repoInput := range req.Repositories {
|
||||
if repoInput.RepositoryID == "" && repoInput.LocalPath != "" {
|
||||
repos, err := s.repo.ListRepositories(ctx, req.WorkspaceID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to list repositories", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
repoByPath = make(map[string]*models.Repository, len(repos))
|
||||
for _, repo := range repos {
|
||||
if repo.LocalPath == "" {
|
||||
continue
|
||||
}
|
||||
repoByPath[repo.LocalPath] = repo
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i, repoInput := range req.Repositories {
|
||||
repositoryID := repoInput.RepositoryID
|
||||
baseBranch := repoInput.BaseBranch
|
||||
if repositoryID == "" && repoInput.LocalPath != "" {
|
||||
repo := repoByPath[repoInput.LocalPath]
|
||||
if repo == nil {
|
||||
name := strings.TrimSpace(repoInput.Name)
|
||||
if name == "" {
|
||||
name = filepath.Base(repoInput.LocalPath)
|
||||
}
|
||||
defaultBranch := repoInput.DefaultBranch
|
||||
if defaultBranch == "" {
|
||||
defaultBranch = repoInput.BaseBranch
|
||||
}
|
||||
created, err := s.CreateRepository(ctx, &CreateRepositoryRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
Name: name,
|
||||
SourceType: "local",
|
||||
LocalPath: repoInput.LocalPath,
|
||||
DefaultBranch: defaultBranch,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo = created
|
||||
repoByPath[repoInput.LocalPath] = repo
|
||||
}
|
||||
repositoryID = repo.ID
|
||||
if baseBranch == "" {
|
||||
baseBranch = repo.DefaultBranch
|
||||
}
|
||||
}
|
||||
if repositoryID == "" {
|
||||
return nil, fmt.Errorf("repository_id is required")
|
||||
}
|
||||
taskRepo := &models.TaskRepository{
|
||||
TaskID: task.ID,
|
||||
RepositoryID: repoInput.RepositoryID,
|
||||
BaseBranch: repoInput.BaseBranch,
|
||||
RepositoryID: repositoryID,
|
||||
BaseBranch: baseBranch,
|
||||
Position: i,
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
@@ -1352,13 +1409,13 @@ func (s *Service) publishEnvironmentEvent(ctx context.Context, eventType string,
|
||||
// CreateMessageRequest contains the data for creating a new message
|
||||
type CreateMessageRequest struct {
|
||||
TaskSessionID string `json:"task_session_id"`
|
||||
TaskID string `json:"task_id,omitempty"`
|
||||
Content string `json:"content"`
|
||||
AuthorType string `json:"author_type,omitempty"` // "user" or "agent", defaults to "user"
|
||||
AuthorID string `json:"author_id,omitempty"`
|
||||
RequestsInput bool `json:"requests_input,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
TaskID string `json:"task_id,omitempty"`
|
||||
Content string `json:"content"`
|
||||
AuthorType string `json:"author_type,omitempty"` // "user" or "agent", defaults to "user"
|
||||
AuthorID string `json:"author_id,omitempty"`
|
||||
RequestsInput bool `json:"requests_input,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// CreateMessage creates a new message on an agent session
|
||||
@@ -1384,16 +1441,16 @@ func (s *Service) CreateMessage(ctx context.Context, req *CreateMessageRequest)
|
||||
}
|
||||
|
||||
message := &models.Message{
|
||||
ID: uuid.New().String(),
|
||||
ID: uuid.New().String(),
|
||||
TaskSessionID: req.TaskSessionID,
|
||||
TaskID: taskID,
|
||||
AuthorType: authorType,
|
||||
AuthorID: req.AuthorID,
|
||||
Content: req.Content,
|
||||
Type: messageType,
|
||||
Metadata: req.Metadata,
|
||||
RequestsInput: req.RequestsInput,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
TaskID: taskID,
|
||||
AuthorType: authorType,
|
||||
AuthorID: req.AuthorID,
|
||||
Content: req.Content,
|
||||
Type: messageType,
|
||||
Metadata: req.Metadata,
|
||||
RequestsInput: req.RequestsInput,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateMessage(ctx, message); err != nil {
|
||||
@@ -1501,15 +1558,15 @@ func (s *Service) publishMessageEvent(ctx context.Context, eventType string, mes
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"message_id": message.ID,
|
||||
"message_id": message.ID,
|
||||
"task_session_id": message.TaskSessionID,
|
||||
"task_id": message.TaskID,
|
||||
"author_type": string(message.AuthorType),
|
||||
"author_id": message.AuthorID,
|
||||
"content": message.Content,
|
||||
"type": messageType,
|
||||
"requests_input": message.RequestsInput,
|
||||
"created_at": message.CreatedAt.Format(time.RFC3339),
|
||||
"task_id": message.TaskID,
|
||||
"author_type": string(message.AuthorType),
|
||||
"author_id": message.AuthorID,
|
||||
"content": message.Content,
|
||||
"type": messageType,
|
||||
"requests_input": message.RequestsInput,
|
||||
"created_at": message.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if message.Metadata != nil {
|
||||
|
||||
@@ -40,9 +40,10 @@ func (c *Controller) GetUserSettings(ctx context.Context) (dto.UserSettingsRespo
|
||||
|
||||
func (c *Controller) UpdateUserSettings(ctx context.Context, req dto.UpdateUserSettingsRequest) (dto.UserSettingsResponse, error) {
|
||||
settings, err := c.svc.UpdateUserSettings(ctx, &service.UpdateUserSettingsRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
RepositoryIDs: req.RepositoryIDs,
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
RepositoryIDs: req.RepositoryIDs,
|
||||
InitialSetupComplete: req.InitialSetupComplete,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.UserSettingsResponse{}, err
|
||||
|
||||
@@ -14,15 +14,16 @@ type UserDTO struct {
|
||||
}
|
||||
|
||||
type UserSettingsDTO struct {
|
||||
UserID string `json:"user_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
RepositoryIDs []string `json:"repository_ids"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
UserID string `json:"user_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
RepositoryIDs []string `json:"repository_ids"`
|
||||
InitialSetupComplete bool `json:"initial_setup_complete"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
User UserDTO `json:"user"`
|
||||
User UserDTO `json:"user"`
|
||||
Settings UserSettingsDTO `json:"settings"`
|
||||
}
|
||||
|
||||
@@ -31,9 +32,10 @@ type UserSettingsResponse struct {
|
||||
}
|
||||
|
||||
type UpdateUserSettingsRequest struct {
|
||||
WorkspaceID *string `json:"workspace_id,omitempty"`
|
||||
BoardID *string `json:"board_id,omitempty"`
|
||||
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
|
||||
WorkspaceID *string `json:"workspace_id,omitempty"`
|
||||
BoardID *string `json:"board_id,omitempty"`
|
||||
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
|
||||
InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"`
|
||||
}
|
||||
|
||||
func FromUser(user *models.User) UserDTO {
|
||||
@@ -47,10 +49,11 @@ func FromUser(user *models.User) UserDTO {
|
||||
|
||||
func FromUserSettings(settings *models.UserSettings) UserSettingsDTO {
|
||||
return UserSettingsDTO{
|
||||
UserID: settings.UserID,
|
||||
WorkspaceID: settings.WorkspaceID,
|
||||
BoardID: settings.BoardID,
|
||||
RepositoryIDs: settings.RepositoryIDs,
|
||||
UpdatedAt: settings.UpdatedAt.Format(time.RFC3339),
|
||||
UserID: settings.UserID,
|
||||
WorkspaceID: settings.WorkspaceID,
|
||||
BoardID: settings.BoardID,
|
||||
RepositoryIDs: settings.RepositoryIDs,
|
||||
InitialSetupComplete: settings.InitialSetupComplete,
|
||||
UpdatedAt: settings.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,10 @@ func (h *Handlers) httpGetUserSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
type httpUpdateUserSettingsRequest struct {
|
||||
WorkspaceID *string `json:"workspace_id,omitempty"`
|
||||
BoardID *string `json:"board_id,omitempty"`
|
||||
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
|
||||
WorkspaceID *string `json:"workspace_id,omitempty"`
|
||||
BoardID *string `json:"board_id,omitempty"`
|
||||
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
|
||||
InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handlers) httpUpdateUserSettings(c *gin.Context) {
|
||||
@@ -76,9 +77,10 @@ func (h *Handlers) httpUpdateUserSettings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
resp, err := h.controller.UpdateUserSettings(c.Request.Context(), dto.UpdateUserSettingsRequest{
|
||||
WorkspaceID: body.WorkspaceID,
|
||||
BoardID: body.BoardID,
|
||||
RepositoryIDs: body.RepositoryIDs,
|
||||
WorkspaceID: body.WorkspaceID,
|
||||
BoardID: body.BoardID,
|
||||
RepositoryIDs: body.RepositoryIDs,
|
||||
InitialSetupComplete: body.InitialSetupComplete,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to update user settings", zap.Error(err))
|
||||
@@ -97,9 +99,10 @@ func (h *Handlers) wsGetUser(ctx context.Context, msg *ws.Message) (*ws.Message,
|
||||
}
|
||||
|
||||
type wsUpdateUserSettingsRequest struct {
|
||||
WorkspaceID *string `json:"workspace_id,omitempty"`
|
||||
BoardID *string `json:"board_id,omitempty"`
|
||||
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
|
||||
WorkspaceID *string `json:"workspace_id,omitempty"`
|
||||
BoardID *string `json:"board_id,omitempty"`
|
||||
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
|
||||
InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handlers) wsUpdateUserSettings(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
@@ -108,9 +111,10 @@ func (h *Handlers) wsUpdateUserSettings(ctx context.Context, msg *ws.Message) (*
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
||||
}
|
||||
resp, err := h.controller.UpdateUserSettings(ctx, dto.UpdateUserSettingsRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
RepositoryIDs: req.RepositoryIDs,
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
RepositoryIDs: req.RepositoryIDs,
|
||||
InitialSetupComplete: req.InitialSetupComplete,
|
||||
})
|
||||
if err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update user settings", nil)
|
||||
|
||||
@@ -10,10 +10,11 @@ type User struct {
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
UserID string `json:"user_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
RepositoryIDs []string `json:"repository_ids"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UserID string `json:"user_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
RepositoryIDs []string `json:"repository_ids"`
|
||||
InitialSetupComplete bool `json:"initial_setup_complete"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ type Service struct {
|
||||
}
|
||||
|
||||
type UpdateUserSettingsRequest struct {
|
||||
WorkspaceID *string
|
||||
BoardID *string
|
||||
RepositoryIDs *[]string
|
||||
WorkspaceID *string
|
||||
BoardID *string
|
||||
RepositoryIDs *[]string
|
||||
InitialSetupComplete *bool
|
||||
}
|
||||
|
||||
func NewService(repo store.Repository, eventBus bus.EventBus, log *logger.Logger) *Service {
|
||||
@@ -67,6 +68,9 @@ func (s *Service) UpdateUserSettings(ctx context.Context, req *UpdateUserSetting
|
||||
if req.RepositoryIDs != nil {
|
||||
settings.RepositoryIDs = *req.RepositoryIDs
|
||||
}
|
||||
if req.InitialSetupComplete != nil {
|
||||
settings.InitialSetupComplete = *req.InitialSetupComplete
|
||||
}
|
||||
settings.UpdatedAt = time.Now().UTC()
|
||||
if err := s.repo.UpsertUserSettings(ctx, settings); err != nil {
|
||||
return nil, err
|
||||
@@ -80,11 +84,12 @@ func (s *Service) publishUserSettingsEvent(ctx context.Context, settings *models
|
||||
return
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"user_id": settings.UserID,
|
||||
"workspace_id": settings.WorkspaceID,
|
||||
"board_id": settings.BoardID,
|
||||
"repository_ids": settings.RepositoryIDs,
|
||||
"updated_at": settings.UpdatedAt.Format(time.RFC3339),
|
||||
"user_id": settings.UserID,
|
||||
"workspace_id": settings.WorkspaceID,
|
||||
"board_id": settings.BoardID,
|
||||
"repository_ids": settings.RepositoryIDs,
|
||||
"initial_setup_complete": settings.InitialSetupComplete,
|
||||
"updated_at": settings.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if err := s.eventBus.Publish(ctx, events.UserSettingsUpdated, bus.NewEvent(events.UserSettingsUpdated, "user-service", data)); err != nil {
|
||||
s.logger.Error("failed to publish user settings event", zap.Error(err))
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/kandev/kandev/internal/user/models"
|
||||
)
|
||||
@@ -187,9 +187,10 @@ func (r *SQLiteRepository) UpsertUserSettings(ctx context.Context, settings *mod
|
||||
settings.CreatedAt = settings.UpdatedAt
|
||||
}
|
||||
settingsPayload, err := json.Marshal(map[string]interface{}{
|
||||
"workspace_id": settings.WorkspaceID,
|
||||
"board_id": settings.BoardID,
|
||||
"repository_ids": settings.RepositoryIDs,
|
||||
"workspace_id": settings.WorkspaceID,
|
||||
"board_id": settings.BoardID,
|
||||
"repository_ids": settings.RepositoryIDs,
|
||||
"initial_setup_complete": settings.InitialSetupComplete,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -228,9 +229,10 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin
|
||||
return settings, nil
|
||||
}
|
||||
var payload struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
RepositoryIDs []string `json:"repository_ids"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
RepositoryIDs []string `json:"repository_ids"`
|
||||
InitialSetupComplete bool `json:"initial_setup_complete"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(settingsRaw), &payload); err != nil {
|
||||
return nil, err
|
||||
@@ -238,5 +240,6 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin
|
||||
settings.WorkspaceID = payload.WorkspaceID
|
||||
settings.BoardID = payload.BoardID
|
||||
settings.RepositoryIDs = payload.RepositoryIDs
|
||||
settings.InitialSetupComplete = payload.InitialSetupComplete
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ListColumnsResponse,
|
||||
RepositoryDiscoveryResponse,
|
||||
ListRepositoriesResponse,
|
||||
RepositoryBranchesResponse,
|
||||
ListRepositoryScriptsResponse,
|
||||
ListWorkspacesResponse,
|
||||
RepositoryPathValidationResponse,
|
||||
@@ -142,6 +143,16 @@ export async function listRepositoriesAction(workspaceId: string): Promise<ListR
|
||||
return fetchJson<ListRepositoriesResponse>(`${apiBaseUrl}/api/v1/workspaces/${workspaceId}/repositories`);
|
||||
}
|
||||
|
||||
export async function listLocalRepositoryBranchesAction(
|
||||
workspaceId: string,
|
||||
path: string
|
||||
): Promise<RepositoryBranchesResponse> {
|
||||
const params = `?path=${encodeURIComponent(path)}`;
|
||||
return fetchJson<RepositoryBranchesResponse>(
|
||||
`${apiBaseUrl}/api/v1/workspaces/${workspaceId}/repositories/branches${params}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function discoverRepositoriesAction(
|
||||
workspaceId: string,
|
||||
root?: string
|
||||
|
||||
@@ -90,15 +90,18 @@ export function Combobox({
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={(currentValue) => {
|
||||
onValueChange(currentValue === value ? "" : currentValue)
|
||||
onSelect={() => {
|
||||
onValueChange(option.value === value ? "" : option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="relative pr-7"
|
||||
>
|
||||
{option.renderLabel ? option.renderLabel() : option.label}
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
{option.renderLabel ? option.renderLabel() : option.label}
|
||||
</div>
|
||||
<IconCheck
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
"absolute right-2 h-4 w-4",
|
||||
value === option.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -18,16 +18,14 @@ import { Button } from '@kandev/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kandev/ui/select';
|
||||
import { Combobox } from './combobox';
|
||||
import { Badge } from '@kandev/ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@kandev/ui/tooltip';
|
||||
import type { Task } from '@/lib/types/http';
|
||||
import type { LocalRepository, Task } from '@/lib/types/http';
|
||||
import {
|
||||
DEFAULT_LOCAL_ENVIRONMENT_KIND,
|
||||
DEFAULT_LOCAL_EXECUTOR_TYPE,
|
||||
@@ -44,6 +42,8 @@ import { SHORTCUTS } from '@/lib/keyboard/constants';
|
||||
import { useKeyboardShortcutHandler } from '@/hooks/use-keyboard-shortcut';
|
||||
import { KeyboardShortcutTooltip } from '@/components/keyboard-shortcut-tooltip';
|
||||
import { getWebSocketClient } from '@/lib/ws/connection';
|
||||
import { useToast } from '@/components/toast-provider';
|
||||
import { discoverRepositoriesAction, listLocalRepositoryBranchesAction } from '@/app/actions/workspaces';
|
||||
|
||||
interface TaskCreateDialogProps {
|
||||
open: boolean;
|
||||
@@ -71,6 +71,119 @@ interface TaskCreateDialogProps {
|
||||
submitLabel?: string;
|
||||
}
|
||||
|
||||
type RepositoryOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
renderLabel: () => React.ReactNode;
|
||||
};
|
||||
|
||||
type RepositorySelectorProps = {
|
||||
options: RepositoryOption[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
disabled: boolean;
|
||||
placeholder: string;
|
||||
searchPlaceholder: string;
|
||||
emptyMessage: string;
|
||||
};
|
||||
|
||||
function RepositorySelector({
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
searchPlaceholder,
|
||||
emptyMessage,
|
||||
}: RepositorySelectorProps) {
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
placeholder={placeholder}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
emptyMessage={emptyMessage}
|
||||
disabled={disabled}
|
||||
dropdownLabel="Repository"
|
||||
className={disabled ? undefined : 'cursor-pointer'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type BranchOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
renderLabel: () => React.ReactNode;
|
||||
};
|
||||
|
||||
type BranchSelectorProps = {
|
||||
options: BranchOption[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
disabled: boolean;
|
||||
placeholder: string;
|
||||
searchPlaceholder: string;
|
||||
emptyMessage: string;
|
||||
};
|
||||
|
||||
function BranchSelector({
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
searchPlaceholder,
|
||||
emptyMessage,
|
||||
}: BranchSelectorProps) {
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
placeholder={placeholder}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
emptyMessage={emptyMessage}
|
||||
disabled={disabled}
|
||||
dropdownLabel="Base Branch"
|
||||
className={disabled ? undefined : 'cursor-pointer'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type AgentSelectorProps = {
|
||||
options: Array<{ value: string; label: string; renderLabel: () => React.ReactNode }>;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
disabled: boolean;
|
||||
placeholder: string;
|
||||
triggerClassName?: string;
|
||||
};
|
||||
|
||||
function AgentSelector({
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
triggerClassName,
|
||||
}: AgentSelectorProps) {
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
placeholder={placeholder}
|
||||
searchPlaceholder="Search agents..."
|
||||
emptyMessage="No agent found."
|
||||
disabled={disabled}
|
||||
dropdownLabel="Agent profile"
|
||||
className={disabled ? undefined : 'cursor-pointer'}
|
||||
triggerClassName={triggerClassName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TaskCreateDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -96,12 +209,20 @@ export function TaskCreateDialog({
|
||||
const [environmentId, setEnvironmentId] = useState('');
|
||||
const [executorId, setExecutorId] = useState('');
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [discoveredRepositories, setDiscoveredRepositories] = useState<LocalRepository[]>([]);
|
||||
const [discoveredRepoPath, setDiscoveredRepoPath] = useState('');
|
||||
const [selectedLocalRepo, setSelectedLocalRepo] = useState<LocalRepository | null>(null);
|
||||
const [localBranches, setLocalBranches] = useState<typeof branches>([]);
|
||||
const [localBranchesLoading, setLocalBranchesLoading] = useState(false);
|
||||
const [discoverReposLoading, setDiscoverReposLoading] = useState(false);
|
||||
const [discoverReposLoaded, setDiscoverReposLoaded] = useState(false);
|
||||
const workspaces = useAppStore((state) => state.workspaces.items);
|
||||
const agentProfiles = useAppStore((state) => state.agentProfiles.items);
|
||||
const executors = useAppStore((state) => state.executors.items);
|
||||
const environments = useAppStore((state) => state.environments.items);
|
||||
const settingsData = useAppStore((state) => state.settingsData);
|
||||
const descriptionRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const { toast } = useToast();
|
||||
useSettingsData(open);
|
||||
const { repositories, isLoading: repositoriesLoading } = useRepositories(workspaceId, open);
|
||||
const { branches, isLoading: branchesLoading } = useRepositoryBranches(
|
||||
@@ -131,16 +252,56 @@ export function TaskCreateDialog({
|
||||
open,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setDiscoveredRepositories([]);
|
||||
setDiscoveredRepoPath('');
|
||||
setSelectedLocalRepo(null);
|
||||
setLocalBranches([]);
|
||||
setDiscoverReposLoaded(false);
|
||||
}, [open, workspaceId]);
|
||||
|
||||
const workspaceDefaults = workspaceId
|
||||
? workspaces.find((workspace) => workspace.id === workspaceId)
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !workspaceId) return;
|
||||
if (!repositoryId && repositories.length === 1) {
|
||||
if (!repositoryId && !selectedLocalRepo && repositories.length === 1) {
|
||||
setRepositoryId(repositories[0].id);
|
||||
}
|
||||
}, [open, repositories, repositoryId, workspaceId]);
|
||||
}, [open, repositories, repositoryId, selectedLocalRepo, workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !workspaceId) return;
|
||||
if (repositoriesLoading) return;
|
||||
if (discoverReposLoaded || discoverReposLoading) return;
|
||||
setDiscoverReposLoading(true);
|
||||
discoverRepositoriesAction(workspaceId)
|
||||
.then((response) => {
|
||||
setDiscoveredRepositories(response.repositories);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast({
|
||||
title: 'Failed to discover repositories',
|
||||
description: error instanceof Error ? error.message : 'Request failed',
|
||||
variant: 'error',
|
||||
});
|
||||
setDiscoveredRepositories([]);
|
||||
})
|
||||
.finally(() => {
|
||||
setDiscoverReposLoading(false);
|
||||
setDiscoverReposLoaded(true);
|
||||
});
|
||||
}, [
|
||||
discoverReposLoaded,
|
||||
discoverReposLoading,
|
||||
open,
|
||||
repositories.length,
|
||||
repositoriesLoading,
|
||||
toast,
|
||||
workspaceId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!repositoryId) return;
|
||||
@@ -152,6 +313,89 @@ export function TaskCreateDialog({
|
||||
}
|
||||
}, [branch, branches, repositoryId]);
|
||||
|
||||
const handleSelectLocalRepository = (path: string) => {
|
||||
const selected = discoveredRepositories.find((repo) => repo.path === path) ?? null;
|
||||
setDiscoveredRepoPath(path);
|
||||
setSelectedLocalRepo(selected);
|
||||
setRepositoryId('');
|
||||
setBranch('');
|
||||
setLocalBranches([]);
|
||||
};
|
||||
|
||||
const hasRepositorySelection = Boolean(repositoryId || selectedLocalRepo);
|
||||
const branchOptions = repositoryId
|
||||
? branches
|
||||
: localBranches;
|
||||
const normalizeRepoPath = (path: string) => path.replace(/\\/g, '/').replace(/\/+$/g, '');
|
||||
const workspaceRepoPaths = new Set(
|
||||
repositories
|
||||
.map((repo) => repo.local_path)
|
||||
.filter(Boolean)
|
||||
.map((path) => normalizeRepoPath(path))
|
||||
);
|
||||
const localRepoOptions = discoveredRepositories.filter(
|
||||
(repo) => !workspaceRepoPaths.has(normalizeRepoPath(repo.path))
|
||||
);
|
||||
const repositoryOptions: RepositoryOption[] = [
|
||||
...repositories.map((repo) => ({
|
||||
value: repo.id,
|
||||
label: repo.name,
|
||||
renderLabel: () => (
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="shrink-0">{repo.name}</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs text-muted-foreground max-w-[220px] min-w-0 truncate ml-auto"
|
||||
title={formatUserHomePath(repo.local_path)}
|
||||
>
|
||||
{truncateRepoPath(repo.local_path)}
|
||||
</Badge>
|
||||
</span>
|
||||
),
|
||||
})),
|
||||
...localRepoOptions.map((repo) => ({
|
||||
value: repo.path,
|
||||
label: formatUserHomePath(repo.path),
|
||||
renderLabel: () => (
|
||||
<span
|
||||
className="flex min-w-0 flex-1 items-center truncate"
|
||||
title={formatUserHomePath(repo.path)}
|
||||
>
|
||||
{truncateRepoPath(repo.path)}
|
||||
</span>
|
||||
),
|
||||
})),
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !workspaceId || !selectedLocalRepo) return;
|
||||
setLocalBranchesLoading(true);
|
||||
listLocalRepositoryBranchesAction(workspaceId, selectedLocalRepo.path)
|
||||
.then((response) => {
|
||||
setLocalBranches(response.branches);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast({
|
||||
title: 'Failed to load branches',
|
||||
description: error instanceof Error ? error.message : 'Request failed',
|
||||
variant: 'error',
|
||||
});
|
||||
setLocalBranches([]);
|
||||
})
|
||||
.finally(() => {
|
||||
setLocalBranchesLoading(false);
|
||||
});
|
||||
}, [open, selectedLocalRepo, toast, workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (repositoryId || localBranches.length === 0) return;
|
||||
if (branch) return;
|
||||
const preferredBranch = selectPreferredBranch(localBranches);
|
||||
if (preferredBranch) {
|
||||
setBranch(preferredBranch);
|
||||
}
|
||||
}, [branch, localBranches, repositoryId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || agentProfileId || agentProfiles.length === 0) return;
|
||||
if (!isSessionMode && !startAgent) return;
|
||||
@@ -254,7 +498,7 @@ export function TaskCreateDialog({
|
||||
return;
|
||||
}
|
||||
if (!workspaceId || !boardId) return;
|
||||
if (!repositoryId) return;
|
||||
if (!repositoryId && !selectedLocalRepo) return;
|
||||
const columnId = editingTask?.columnId ?? defaultColumnId;
|
||||
if (!columnId) return;
|
||||
let targetColumnId = columnId;
|
||||
@@ -273,10 +517,23 @@ export function TaskCreateDialog({
|
||||
column_id: targetColumnId,
|
||||
title: trimmedTitle,
|
||||
description: description.trim(),
|
||||
repositories: repositoryId ? [{
|
||||
repository_id: repositoryId,
|
||||
base_branch: branch || undefined,
|
||||
}] : [],
|
||||
repositories: repositoryId
|
||||
? [
|
||||
{
|
||||
repository_id: repositoryId,
|
||||
base_branch: branch || undefined,
|
||||
},
|
||||
]
|
||||
: selectedLocalRepo
|
||||
? [
|
||||
{
|
||||
repository_id: '',
|
||||
base_branch: branch || undefined,
|
||||
local_path: selectedLocalRepo.path,
|
||||
default_branch: selectedLocalRepo.default_branch || undefined,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
state: targetState,
|
||||
});
|
||||
console.log('[TaskCreateDialog] task created', {
|
||||
@@ -308,10 +565,10 @@ export function TaskCreateDialog({
|
||||
setDescription('');
|
||||
setRepositoryId('');
|
||||
setBranch('');
|
||||
setStartAgent(!isEditMode);
|
||||
setAgentProfileId('');
|
||||
setEnvironmentId('');
|
||||
setExecutorId('');
|
||||
setStartAgent(!isEditMode);
|
||||
setAgentProfileId('');
|
||||
setEnvironmentId('');
|
||||
setExecutorId('');
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
@@ -379,60 +636,55 @@ export function TaskCreateDialog({
|
||||
{!isSessionMode && (
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
{repositories.length === 0 && !repositoriesLoading ? (
|
||||
<div className="flex items-center justify-center h-10 px-3 py-2 text-sm border border-input rounded-md bg-background">
|
||||
<span className="text-muted-foreground mr-2">No repositories found.</span>
|
||||
<Link href="/settings/workspace" className="text-primary hover:underline">
|
||||
Add repository
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<Combobox
|
||||
options={repositories.map((repo) => ({
|
||||
value: repo.id,
|
||||
label: repo.name,
|
||||
renderLabel: () => (
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="shrink-0">{repo.name}</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs text-muted-foreground max-w-[220px] min-w-0 truncate ml-auto"
|
||||
title={formatUserHomePath(repo.local_path)}
|
||||
>
|
||||
{truncateRepoPath(repo.local_path)}
|
||||
</Badge>
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
value={repositoryId}
|
||||
<RepositorySelector
|
||||
options={repositoryOptions}
|
||||
value={repositoryId || discoveredRepoPath}
|
||||
onValueChange={(value) => {
|
||||
setRepositoryId(value);
|
||||
setBranch('');
|
||||
const workspaceRepo = repositories.find((repo) => repo.id === value);
|
||||
if (workspaceRepo) {
|
||||
setRepositoryId(value);
|
||||
setDiscoveredRepoPath('');
|
||||
setSelectedLocalRepo(null);
|
||||
setLocalBranches([]);
|
||||
setBranch('');
|
||||
return;
|
||||
}
|
||||
handleSelectLocalRepository(value);
|
||||
}}
|
||||
placeholder={repositoriesLoading ? 'Loading repositories...' : 'Select repository'}
|
||||
placeholder={
|
||||
!workspaceId
|
||||
? 'Select workspace first'
|
||||
: repositoriesLoading || discoverReposLoading
|
||||
? 'Loading repositories...'
|
||||
: 'Select repository'
|
||||
}
|
||||
searchPlaceholder="Search repositories..."
|
||||
emptyMessage="No repository found."
|
||||
disabled={isEditMode || repositoriesLoading}
|
||||
dropdownLabel="Repository"
|
||||
className={isEditMode || repositoriesLoading ? undefined : 'cursor-pointer'}
|
||||
emptyMessage={
|
||||
repositoriesLoading || discoverReposLoading
|
||||
? 'Loading repositories...'
|
||||
: 'No repositories found.'
|
||||
}
|
||||
disabled={!workspaceId || repositoriesLoading || discoverReposLoading || isEditMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Combobox
|
||||
options={branches.map((branchObj) => {
|
||||
const displayName = branchObj.type === 'remote' && branchObj.remote
|
||||
? `${branchObj.remote}/${branchObj.name}`
|
||||
: branchObj.name;
|
||||
<BranchSelector
|
||||
options={branchOptions.map((branchObj) => {
|
||||
const displayName =
|
||||
branchObj.type === 'remote' && branchObj.remote
|
||||
? `${branchObj.remote}/${branchObj.name}`
|
||||
: branchObj.name;
|
||||
// Use display name as unique value since it includes remote prefix
|
||||
return {
|
||||
value: displayName,
|
||||
label: displayName,
|
||||
renderLabel: () => (
|
||||
<span className="flex min-w-0 flex-1 items-center justify-between gap-2">
|
||||
<span className="truncate">{displayName}</span>
|
||||
<span className="truncate" title={displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
<Badge variant={branchObj.type === 'local' ? 'default' : 'secondary'} className="text-xs">
|
||||
{branchObj.type}
|
||||
{branchObj.type === 'local' ? 'local' : branchObj.remote || 'remote'}
|
||||
</Badge>
|
||||
</span>
|
||||
),
|
||||
@@ -444,33 +696,57 @@ export function TaskCreateDialog({
|
||||
setBranch(displayName);
|
||||
}}
|
||||
placeholder={
|
||||
!repositoryId
|
||||
!hasRepositorySelection
|
||||
? 'Select repository first'
|
||||
: branchesLoading
|
||||
? 'Loading branches...'
|
||||
: 'Select branch'
|
||||
: repositoryId
|
||||
? branchesLoading
|
||||
? 'Loading branches...'
|
||||
: 'Select branch'
|
||||
: localBranchesLoading
|
||||
? 'Loading branches...'
|
||||
: branchOptions.length > 0
|
||||
? 'Select branch'
|
||||
: 'No branches found'
|
||||
}
|
||||
searchPlaceholder="Search branches..."
|
||||
emptyMessage="No branch found."
|
||||
disabled={isEditMode || !repositoryId || branchesLoading}
|
||||
dropdownLabel="Base Branch"
|
||||
className={isEditMode || !repositoryId || branchesLoading ? undefined : 'cursor-pointer'}
|
||||
disabled={
|
||||
isEditMode ||
|
||||
!hasRepositorySelection ||
|
||||
(repositoryId && branchesLoading) ||
|
||||
(!repositoryId && (localBranchesLoading || branchOptions.length === 0))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{startAgent && (
|
||||
<div>
|
||||
{agentProfiles.length === 0 && !agentProfilesLoading ? (
|
||||
<div className="flex h-7 items-center justify-center gap-2 rounded-sm border border-input px-3 text-xs text-muted-foreground">
|
||||
<span>No agents found.</span>
|
||||
<Link href="/settings/agents" className="text-primary hover:underline">
|
||||
Add agent
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<Combobox
|
||||
{startAgent && (
|
||||
<div>
|
||||
{agentProfiles.length === 0 && !agentProfilesLoading ? (
|
||||
<div className="flex h-7 items-center justify-center gap-2 rounded-sm border border-input px-3 text-xs text-muted-foreground">
|
||||
<span>No agents found.</span>
|
||||
<Link href="/settings/agents" className="text-primary hover:underline">
|
||||
Add agent
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<AgentSelector
|
||||
options={agentProfiles.map((profile) => ({
|
||||
value: profile.id,
|
||||
label: profile.label,
|
||||
renderLabel: () => {
|
||||
const parts = profile.label.split(' • ');
|
||||
const agentLabel = parts[0] ?? profile.label;
|
||||
const profileLabel = parts[1] ?? '';
|
||||
return (
|
||||
<span className="flex min-w-0 flex-1 items-center justify-between gap-2">
|
||||
<span className="truncate">{agentLabel}</span>
|
||||
{profileLabel ? (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{profileLabel}
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
}))}
|
||||
value={agentProfileId}
|
||||
onValueChange={setAgentProfileId}
|
||||
@@ -481,36 +757,44 @@ export function TaskCreateDialog({
|
||||
? 'No agents available'
|
||||
: 'Select agent'
|
||||
}
|
||||
searchPlaceholder="Search agents..."
|
||||
emptyMessage="No agent found."
|
||||
dropdownLabel="Agent profile"
|
||||
className={agentProfilesLoading ? undefined : 'cursor-pointer'}
|
||||
disabled={agentProfilesLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Profile - shown in session mode */}
|
||||
{isSessionMode && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="agent-profile" className="text-sm whitespace-nowrap">
|
||||
Agent Profile
|
||||
</Label>
|
||||
<Select value={agentProfileId} onValueChange={setAgentProfileId} required>
|
||||
<SelectTrigger id="agent-profile" className="w-[280px]">
|
||||
<SelectValue placeholder={agentProfilesLoading ? 'Loading agent profiles...' : 'Select agent profile'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{agentProfiles.map((profile) => (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
{profile.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<AgentSelector
|
||||
options={agentProfiles.map((profile) => ({
|
||||
value: profile.id,
|
||||
label: profile.label,
|
||||
renderLabel: () => {
|
||||
const parts = profile.label.split(' • ');
|
||||
const agentLabel = parts[0] ?? profile.label;
|
||||
const profileLabel = parts[1] ?? '';
|
||||
return (
|
||||
<span className="flex min-w-0 flex-1 items-center justify-between gap-2">
|
||||
<span className="truncate">{agentLabel}</span>
|
||||
{profileLabel ? (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{profileLabel}
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
}))}
|
||||
value={agentProfileId}
|
||||
onValueChange={setAgentProfileId}
|
||||
placeholder={agentProfilesLoading ? 'Loading agent profiles...' : 'Select agent profile'}
|
||||
disabled={agentProfilesLoading}
|
||||
triggerClassName="w-[280px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* More Options Toggle */}
|
||||
@@ -541,34 +825,13 @@ export function TaskCreateDialog({
|
||||
)}
|
||||
{showAdvancedSettings && isSessionMode && (
|
||||
<div className="pt-2 border-t">
|
||||
{/* Executor and Environment in same row */}
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Executor */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="executor" className="text-sm whitespace-nowrap">
|
||||
Executor
|
||||
</Label>
|
||||
<Select value={executorId} onValueChange={setExecutorId}>
|
||||
<SelectTrigger id="executor" className="w-[200px]">
|
||||
<SelectValue placeholder={executorsLoading ? 'Loading executors...' : 'Select executor'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{executors.map((executor) => (
|
||||
<SelectItem key={executor.id} value={executor.id}>
|
||||
{executor.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Environment */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="environment" className="text-sm whitespace-nowrap">
|
||||
Environment
|
||||
</Label>
|
||||
<Select value={environmentId} onValueChange={setEnvironmentId}>
|
||||
<SelectTrigger id="environment" className="w-[200px]">
|
||||
<SelectTrigger id="environment" className="w-full">
|
||||
<SelectValue placeholder={environmentsLoading ? 'Loading environments...' : 'Select environment'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -580,12 +843,30 @@ export function TaskCreateDialog({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="executor" className="text-sm whitespace-nowrap">
|
||||
Executor
|
||||
</Label>
|
||||
<Select value={executorId} onValueChange={setExecutorId}>
|
||||
<SelectTrigger id="executor" className="w-full">
|
||||
<SelectValue placeholder={executorsLoading ? 'Loading executors...' : 'Select executor'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{executors.map((executor) => (
|
||||
<SelectItem key={executor.id} value={executor.id}>
|
||||
{executor.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showAdvancedSettings && !isSessionMode && startAgent && (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm whitespace-nowrap">Environment</Label>
|
||||
<Select value={environmentId} onValueChange={setEnvironmentId} disabled={isEditMode}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
@@ -599,18 +880,16 @@ export function TaskCreateDialog({
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Environment</SelectLabel>
|
||||
{environments.map((environment) => (
|
||||
<SelectItem key={environment.id} value={environment.id}>
|
||||
{environment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
{environments.map((environment) => (
|
||||
<SelectItem key={environment.id} value={environment.id}>
|
||||
{environment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm whitespace-nowrap">Executor</Label>
|
||||
<Select value={executorId} onValueChange={setExecutorId} disabled={isEditMode}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
@@ -624,14 +903,11 @@ export function TaskCreateDialog({
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Executor</SelectLabel>
|
||||
{executors.map((executor) => (
|
||||
<SelectItem key={executor.id} value={executor.id}>
|
||||
{executor.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
{executors.map((executor) => (
|
||||
<SelectItem key={executor.id} value={executor.id}>
|
||||
{executor.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -663,7 +939,7 @@ export function TaskCreateDialog({
|
||||
</div>
|
||||
{startAgent && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
A git worktree will be created from the above branch.
|
||||
A git worktree will be created from the base branch.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -680,8 +956,9 @@ export function TaskCreateDialog({
|
||||
isSessionMode
|
||||
? !description.trim() || !agentProfileId
|
||||
: !title.trim() ||
|
||||
(!isEditMode && !repositoryId) ||
|
||||
(startAgent && !agentProfileId)
|
||||
!description.trim() ||
|
||||
(!isEditMode && !repositoryId && !selectedLocalRepo) ||
|
||||
(startAgent && !agentProfileId)
|
||||
}
|
||||
>
|
||||
{isSessionMode ? 'Create Session' : submitLabel}
|
||||
|
||||
@@ -96,6 +96,9 @@ export async function createTask(
|
||||
repositories?: Array<{
|
||||
repository_id: string;
|
||||
base_branch?: string;
|
||||
local_path?: string;
|
||||
name?: string;
|
||||
default_branch?: string;
|
||||
}>;
|
||||
state?: Task['state'];
|
||||
},
|
||||
|
||||
@@ -221,6 +221,7 @@ export type UserSettingsUpdatedPayload = {
|
||||
workspace_id: string;
|
||||
board_id: string;
|
||||
repository_ids: string[];
|
||||
initial_setup_complete?: boolean;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ export type UserSettings = {
|
||||
workspace_id: string;
|
||||
board_id: string;
|
||||
repository_ids: string[];
|
||||
initial_setup_complete?: boolean;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export function formatUserHomePath(path: string): string {
|
||||
/**
|
||||
* Truncate a repository path for display, favoring the last segments.
|
||||
*/
|
||||
export function truncateRepoPath(path: string, maxLength = 28): string {
|
||||
export function truncateRepoPath(path: string, maxLength = 34): string {
|
||||
const displayPath = formatUserHomePath(path);
|
||||
if (displayPath.length <= maxLength) return displayPath;
|
||||
const normalizedPath = displayPath.replace(/\\/g, '/');
|
||||
@@ -99,8 +99,11 @@ export function truncateRepoPath(path: string, maxLength = 28): string {
|
||||
const prefix = hasHomePrefix ? '~/' : hasRootPrefix ? '/' : '';
|
||||
const parts = normalizedPath.replace(/^~\//, '').replace(/^\//, '').split('/').filter(Boolean);
|
||||
if (parts.length === 0) return displayPath;
|
||||
const lastThree = parts.slice(-3).join('/');
|
||||
let result = `${prefix}.../${lastThree}`;
|
||||
if (result.length <= maxLength) return result;
|
||||
const lastTwo = parts.slice(-2).join('/');
|
||||
let result = `${prefix}.../${lastTwo}`;
|
||||
result = `${prefix}.../${lastTwo}`;
|
||||
if (result.length <= maxLength) return result;
|
||||
const lastOne = parts.slice(-1).join('/');
|
||||
result = `${prefix}.../${lastOne}`;
|
||||
|
||||
Reference in New Issue
Block a user