Files
kandev/apps/backend/internal/editors/controller/controller.go
T
João Salavisa ea84b913b4 feat: choose worktree when opening IDE on multi-repo tasks (#1743)
* feat: choose worktree when opening IDE on multi-repo tasks

On multi-repo tasks the open-in-IDE button always opened the first
worktree. The editors menu now shows a worktree picker (repo + branch)
when a session has multiple worktrees, and each editor in the dropdown
expands into a per-worktree submenu.

- open-editor/open-folder accept an optional worktree_id; empty keeps
  the first-worktree fallback so existing clients are unaffected
- session DTOs now serialize the full worktrees list instead of only
  the flattened first-worktree fields
- useSessionWorktrees unions API worktrees with the WS-populated list
- new Playwright spec covers the picker on a real multi-repo session

* fix: address review feedback on worktree picker

- fall back on empty-string worktree fields, not only null/undefined
- bind optional open-folder body via EOF check instead of ContentLength
  so chunked requests keep their worktree_id
- wrap disabled editor buttons in a focusable span so tooltips work
- add hook test asserting worktreeId is forwarded as worktree_id

* fix: map editor service errors to HTTP status, drop e2e sleep

- open-editor/open-folder return 404/400/409 for known service errors
  (unknown worktree or editor, invalid config, unavailable editor)
  instead of a blanket 500
- replace the picker spec's 750ms sleep with an explicit composer click
  so the autofocus race is deterministic

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-16 22:43:06 +01:00

111 lines
3.0 KiB
Go

package controller
import (
"context"
"sort"
"github.com/kandev/kandev/internal/editors/discovery"
"github.com/kandev/kandev/internal/editors/dto"
"github.com/kandev/kandev/internal/editors/service"
)
type Controller struct {
service *service.Service
}
func NewController(svc *service.Service) *Controller {
return &Controller{service: svc}
}
func (c *Controller) ListEditors(ctx context.Context) (dto.EditorsResponse, error) {
editors, err := c.service.ListEditors(ctx)
if err != nil {
return dto.EditorsResponse{}, err
}
result := make([]dto.EditorDTO, 0, len(editors))
for _, editor := range editors {
if editor == nil || !editor.Enabled {
continue
}
result = append(result, dto.FromEditor(editor))
}
if order, err := discovery.LoadDefaults(); err == nil {
orderIndex := make(map[string]int, len(order))
for index, editor := range order {
if editor.Type == "" {
continue
}
orderIndex[editor.Type] = index
}
sort.SliceStable(result, func(i, j int) bool {
left, leftOk := orderIndex[result[i].Type]
right, rightOk := orderIndex[result[j].Type]
if leftOk && rightOk {
return left < right
}
if leftOk {
return true
}
if rightOk {
return false
}
return result[i].Name < result[j].Name
})
}
return dto.EditorsResponse{Editors: result}, nil
}
func (c *Controller) OpenSessionEditor(ctx context.Context, sessionID string, req dto.OpenEditorRequest) (dto.OpenEditorResponse, error) {
url, err := c.service.OpenEditor(ctx, service.OpenEditorInput{
SessionID: sessionID,
EditorID: req.EditorID,
EditorType: req.EditorType,
FilePath: req.FilePath,
Line: req.Line,
Column: req.Column,
WorktreeID: req.WorktreeID,
})
if err != nil {
return dto.OpenEditorResponse{}, err
}
return dto.OpenEditorResponse{URL: url}, nil
}
func (c *Controller) OpenFolder(ctx context.Context, sessionID string, req dto.OpenFolderRequest) (dto.OpenFolderResponse, error) {
if err := c.service.OpenFolder(ctx, sessionID, req.WorktreeID); err != nil {
return dto.OpenFolderResponse{Success: false}, err
}
return dto.OpenFolderResponse{Success: true}, nil
}
func (c *Controller) CreateEditor(ctx context.Context, req dto.CreateEditorRequest) (dto.EditorDTO, error) {
editor, err := c.service.CreateEditor(ctx, service.CreateEditorInput{
Name: req.Name,
Kind: req.Kind,
Config: req.Config,
Enabled: req.Enabled,
})
if err != nil {
return dto.EditorDTO{}, err
}
return dto.FromEditor(editor), nil
}
func (c *Controller) UpdateEditor(ctx context.Context, editorID string, req dto.UpdateEditorRequest) (dto.EditorDTO, error) {
editor, err := c.service.UpdateEditor(ctx, service.UpdateEditorInput{
EditorID: editorID,
Name: req.Name,
Kind: req.Kind,
Config: req.Config,
Enabled: req.Enabled,
})
if err != nil {
return dto.EditorDTO{}, err
}
return dto.FromEditor(editor), nil
}
func (c *Controller) DeleteEditor(ctx context.Context, editorID string) error {
return c.service.DeleteEditor(ctx, editorID)
}