package controller import ( "context" "fmt" "strings" "github.com/kandev/kandev/internal/workflow/models" "github.com/kandev/kandev/internal/workflow/service" ) // Controller handles workflow-related requests type Controller struct { svc *service.Service } // NewController creates a new workflow controller func NewController(svc *service.Service) *Controller { return &Controller{svc: svc} } // Template responses type ListTemplatesResponse struct { Templates []*models.WorkflowTemplate `json:"templates"` } type GetTemplateResponse struct { Template *models.WorkflowTemplate `json:"template"` } func (c *Controller) ListTemplates(ctx context.Context) (*ListTemplatesResponse, error) { templates, err := c.svc.ListTemplates(ctx) if err != nil { return nil, err } return &ListTemplatesResponse{Templates: templates}, nil } func (c *Controller) GetTemplate(ctx context.Context, id string) (*GetTemplateResponse, error) { template, err := c.svc.GetTemplate(ctx, id) if err != nil { return nil, err } return &GetTemplateResponse{Template: template}, nil } // Step responses type ListStepsRequest struct { WorkflowID string `json:"workflow_id"` } type ListStepsResponse struct { Steps []*models.WorkflowStep `json:"steps"` } type GetStepResponse struct { Step *models.WorkflowStep `json:"step"` DemotedStartSteps []*models.WorkflowStep `json:"demoted_start_steps,omitempty"` } type CreateStepsFromTemplateRequest struct { WorkflowID string `json:"workflow_id"` TemplateID string `json:"template_id"` } func (c *Controller) ListStepsByWorkflow(ctx context.Context, req ListStepsRequest) (*ListStepsResponse, error) { steps, err := c.svc.ListStepsByWorkflow(ctx, req.WorkflowID) if err != nil { return nil, err } return &ListStepsResponse{Steps: steps}, nil } // ListStepsByWorkspace returns all workflow steps for all workflows in a workspace. func (c *Controller) ListStepsByWorkspace(ctx context.Context, workspaceID string) (*ListStepsResponse, error) { steps, err := c.svc.ListStepsByWorkspaceID(ctx, workspaceID) if err != nil { return nil, err } return &ListStepsResponse{Steps: steps}, nil } func (c *Controller) GetStep(ctx context.Context, id string) (*GetStepResponse, error) { step, err := c.svc.GetStep(ctx, id) if err != nil { return nil, err } return &GetStepResponse{Step: step}, nil } func (c *Controller) CreateStepsFromTemplate(ctx context.Context, req CreateStepsFromTemplateRequest) error { if err := c.svc.EnsureWorkflowMutable(ctx, req.WorkflowID); err != nil { return err } return c.svc.CreateStepsFromTemplate(ctx, req.WorkflowID, req.TemplateID) } // CreateStepRequest is the request for creating a single workflow step. type CreateStepRequest struct { WorkflowID string `json:"workflow_id"` Name string `json:"name"` Position int `json:"position"` Color string `json:"color"` Prompt string `json:"prompt,omitempty"` Events *models.StepEvents `json:"events,omitempty"` AllowManualMove bool `json:"allow_manual_move"` IsStartStep *bool `json:"is_start_step,omitempty"` ShowInCommandPanel *bool `json:"show_in_command_panel,omitempty"` AutoAdvanceRequiresSignal *bool `json:"auto_advance_requires_signal,omitempty"` CancelTriggersTurnComplete *bool `json:"cancel_triggers_turn_complete,omitempty"` WIPLimit *int `json:"wip_limit,omitempty"` PullFromStepID *string `json:"pull_from_step_id,omitempty"` } // CreateStep creates a new workflow step. func (c *Controller) CreateStep(ctx context.Context, req CreateStepRequest) (*GetStepResponse, error) { if err := c.svc.EnsureWorkflowMutable(ctx, req.WorkflowID); err != nil { return nil, err } step := &models.WorkflowStep{ WorkflowID: req.WorkflowID, Name: req.Name, Position: req.Position, Color: req.Color, Prompt: req.Prompt, AllowManualMove: req.AllowManualMove, } if req.Events != nil { step.Events = *req.Events } if req.IsStartStep != nil { step.IsStartStep = *req.IsStartStep } if req.ShowInCommandPanel != nil { step.ShowInCommandPanel = *req.ShowInCommandPanel } else { step.ShowInCommandPanel = true // default to visible } if req.AutoAdvanceRequiresSignal != nil { step.AutoAdvanceRequiresSignal = *req.AutoAdvanceRequiresSignal } if req.CancelTriggersTurnComplete != nil { step.CancelTriggersTurnComplete = *req.CancelTriggersTurnComplete } if req.WIPLimit != nil { if *req.WIPLimit < 0 { return nil, fmt.Errorf("wip_limit must be non-negative") } step.WIPLimit = *req.WIPLimit } if req.PullFromStepID != nil { step.PullFromStepID = strings.TrimSpace(*req.PullFromStepID) } if err := c.validatePullFromStep(ctx, step); err != nil { return nil, err } demotedStartSteps, err := c.svc.CreateStepWithStartStepUpdates(ctx, step) if err != nil { return nil, err } return &GetStepResponse{Step: step, DemotedStartSteps: demotedStartSteps}, nil } // UpdateStepRequest is the request for updating a workflow step. type UpdateStepRequest struct { ID string `json:"id"` Name *string `json:"name,omitempty"` Position *int `json:"position,omitempty"` Color *string `json:"color,omitempty"` Prompt *string `json:"prompt,omitempty"` Events *models.StepEvents `json:"events,omitempty"` AllowManualMove *bool `json:"allow_manual_move,omitempty"` IsStartStep *bool `json:"is_start_step,omitempty"` ShowInCommandPanel *bool `json:"show_in_command_panel,omitempty"` AutoArchiveAfterHours *int `json:"auto_archive_after_hours,omitempty"` AgentProfileID *string `json:"agent_profile_id,omitempty"` AutoAdvanceRequiresSignal *bool `json:"auto_advance_requires_signal,omitempty"` CancelTriggersTurnComplete *bool `json:"cancel_triggers_turn_complete,omitempty"` WIPLimit *int `json:"wip_limit,omitempty"` PullFromStepID *string `json:"pull_from_step_id,omitempty"` } // UpdateStep updates an existing workflow step. func (c *Controller) UpdateStep(ctx context.Context, req UpdateStepRequest) (*GetStepResponse, error) { step, err := c.svc.GetStep(ctx, req.ID) if err != nil { return nil, err } if err := c.svc.EnsureWorkflowMutable(ctx, step.WorkflowID); err != nil { return nil, err } if req.Name != nil { step.Name = *req.Name } if req.Position != nil { step.Position = *req.Position } if req.Color != nil { step.Color = *req.Color } if req.Prompt != nil { step.Prompt = *req.Prompt } if req.Events != nil { step.Events = *req.Events } if req.AllowManualMove != nil { step.AllowManualMove = *req.AllowManualMove } if req.IsStartStep != nil { step.IsStartStep = *req.IsStartStep } if req.ShowInCommandPanel != nil { step.ShowInCommandPanel = *req.ShowInCommandPanel } if req.AutoArchiveAfterHours != nil { step.AutoArchiveAfterHours = *req.AutoArchiveAfterHours } if req.AgentProfileID != nil { step.AgentProfileID = strings.TrimSpace(*req.AgentProfileID) } if req.AutoAdvanceRequiresSignal != nil { step.AutoAdvanceRequiresSignal = *req.AutoAdvanceRequiresSignal } if req.CancelTriggersTurnComplete != nil { step.CancelTriggersTurnComplete = *req.CancelTriggersTurnComplete } if req.WIPLimit != nil { if *req.WIPLimit < 0 { return nil, fmt.Errorf("wip_limit must be non-negative") } step.WIPLimit = *req.WIPLimit } if req.PullFromStepID != nil { step.PullFromStepID = strings.TrimSpace(*req.PullFromStepID) } if err := c.validatePullFromStep(ctx, step); err != nil { return nil, err } demotedStartSteps, err := c.svc.UpdateStepWithStartStepUpdates(ctx, step) if err != nil { return nil, err } return &GetStepResponse{Step: step, DemotedStartSteps: demotedStartSteps}, nil } func (c *Controller) validatePullFromStep(ctx context.Context, step *models.WorkflowStep) error { if step.PullFromStepID == "" { return nil } if step.ID != "" && step.PullFromStepID == step.ID { return fmt.Errorf("pull_from_step_id cannot reference the same step") } source, err := c.svc.GetStep(ctx, step.PullFromStepID) if err != nil { return fmt.Errorf("pull_from_step_id is invalid: %w", err) } if source.WorkflowID != step.WorkflowID { return fmt.Errorf("pull_from_step_id must reference a step in the same workflow") } if err := c.validatePullFromStepAcyclic(ctx, step.ID, source); err != nil { return err } return nil } func (c *Controller) validatePullFromStepAcyclic(ctx context.Context, stepID string, source *models.WorkflowStep) error { if stepID == "" { return nil } seen := map[string]struct{}{} for current := source; current != nil && current.PullFromStepID != ""; { if current.PullFromStepID == stepID { return fmt.Errorf("pull_from_step_id cannot create a pull cycle") } if _, ok := seen[current.ID]; ok { return fmt.Errorf("pull_from_step_id cannot create a pull cycle") } seen[current.ID] = struct{}{} next, err := c.svc.GetStep(ctx, current.PullFromStepID) if err != nil { return fmt.Errorf("pull_from_step_id is invalid: %w", err) } if next.WorkflowID != source.WorkflowID { return fmt.Errorf("pull_from_step_id must reference a step in the same workflow") } current = next } return nil } // DeleteStep deletes a workflow step. func (c *Controller) DeleteStep(ctx context.Context, id string) error { step, err := c.svc.GetStep(ctx, id) if err != nil { return err } if err := c.svc.EnsureWorkflowMutable(ctx, step.WorkflowID); err != nil { return err } return c.svc.DeleteStep(ctx, id) } // ReorderStepsRequest is the request for reordering workflow steps. type ReorderStepsRequest struct { WorkflowID string `json:"workflow_id"` StepIDs []string `json:"step_ids"` } // ReorderSteps reorders workflow steps for a workflow. func (c *Controller) ReorderSteps(ctx context.Context, req ReorderStepsRequest) error { if err := c.svc.EnsureWorkflowMutable(ctx, req.WorkflowID); err != nil { return err } return c.svc.ReorderSteps(ctx, req.WorkflowID, req.StepIDs) } // History responses type ListHistoryRequest struct { SessionID string `json:"session_id"` } type ListHistoryResponse struct { History []*models.SessionStepHistory `json:"history"` } func (c *Controller) ListHistoryBySession(ctx context.Context, req ListHistoryRequest) (*ListHistoryResponse, error) { history, err := c.svc.ListHistoryBySession(ctx, req.SessionID) if err != nil { return nil, err } return &ListHistoryResponse{History: history}, nil } // Export/Import types and methods // ImportWorkflowsRequest carries import data. type ImportWorkflowsRequest struct { WorkspaceID string `json:"workspace_id"` Data *models.WorkflowExport `json:"data"` } // ExportWorkflow exports a single workflow. func (c *Controller) ExportWorkflow(ctx context.Context, workflowID string) (*models.WorkflowExport, error) { return c.svc.ExportWorkflow(ctx, workflowID) } // ExportWorkflows exports workflows for a workspace. A nil workflowIDs exports // every workflow; a non-nil slice restricts the export to that set of IDs. func (c *Controller) ExportWorkflows(ctx context.Context, workspaceID string, workflowIDs []string) (*models.WorkflowExport, error) { return c.svc.ExportWorkflows(ctx, workspaceID, workflowIDs) } // ImportWorkflows imports workflows into a workspace. func (c *Controller) ImportWorkflows(ctx context.Context, req ImportWorkflowsRequest) (*service.ImportResult, error) { return c.svc.ImportWorkflows(ctx, req.WorkspaceID, req.Data) }