All checks were successful
CI / build (push) Successful in 2m41s
Split monolithic app.go into focused modules (dashboard, chat, workflow, config, agents, terminal, commands, handlers). Add proper error handling for installer commands, proxy pipes, and MCP config parsing. Fix daemon channel buffer, cap orchestrator history, compile think regex once, and set HTTP timeouts on preview server. Improve CI with Go module caching, dependency download step, and test stage with race detection. 😘 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
214 lines
6.6 KiB
Go
214 lines
6.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/muyue/muyue/internal/workflow"
|
|
)
|
|
|
|
func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
if m.orch == nil || m.orch.Workflow == nil {
|
|
return m, nil
|
|
}
|
|
|
|
wf := m.orch.Workflow
|
|
|
|
switch msg.String() {
|
|
case "a":
|
|
if wf.Phase == workflow.PhaseReviewing {
|
|
m.chatLog = append(m.chatLog, userMsgStyle.Render("you: [Plan approved]"))
|
|
m.chatLoading = true
|
|
m.viewport.SetContent(m.renderContent())
|
|
return m, reviewPlanCmd(m.orch, true, "")
|
|
}
|
|
case "r":
|
|
if wf.Phase == workflow.PhaseReviewing {
|
|
m.chatInput = ""
|
|
m.chatLog = append(m.chatLog, itemWarnStyle.Render("Type your rejection feedback below:"))
|
|
m.viewport.SetContent(m.renderContent())
|
|
}
|
|
case "g":
|
|
if wf.Phase == workflow.PhaseGathering && len(wf.Plan.Questions) > 0 && len(wf.Plan.Answers) >= len(wf.Plan.Questions) {
|
|
m.chatLog = append(m.chatLog, userMsgStyle.Render("you: [Generate plan]"))
|
|
m.chatLoading = true
|
|
m.viewport.SetContent(m.renderContent())
|
|
return m, generatePlanCmd(m.orch)
|
|
}
|
|
case "n":
|
|
if wf.Phase == workflow.PhaseExecuting {
|
|
current := wf.CurrentStep()
|
|
if current != nil {
|
|
m.chatLoading = true
|
|
m.viewport.SetContent(m.renderContent())
|
|
return m, continueWorkflowCmd(m.orch, "proceeding")
|
|
}
|
|
}
|
|
case "x":
|
|
wf.Reset()
|
|
m.chatLog = append(m.chatLog, itemWarnStyle.Render("Workflow reset."))
|
|
m.viewport.SetContent(m.renderContent())
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) renderWorkflow() string {
|
|
var b strings.Builder
|
|
|
|
if m.orch == nil || m.orch.Workflow == nil {
|
|
b.WriteString("Workflow engine not available.")
|
|
return b.String()
|
|
}
|
|
|
|
wf := m.orch.Workflow
|
|
|
|
b.WriteString(sectionStyle.Render("Workflow"))
|
|
b.WriteString(" ")
|
|
|
|
phaseColors := map[workflow.Phase]lipgloss.Style{
|
|
workflow.PhaseIdle: lipgloss.NewStyle().Foreground(mutedColor),
|
|
workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warningColor).Bold(true),
|
|
workflow.PhasePlanning: lipgloss.NewStyle().Foreground(accentColor).Bold(true),
|
|
workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(aiColor).Bold(true),
|
|
workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(baseColor).Bold(true),
|
|
workflow.PhaseDone: lipgloss.NewStyle().Foreground(successColor).Bold(true),
|
|
workflow.PhaseError: lipgloss.NewStyle().Foreground(errorColor).Bold(true),
|
|
}
|
|
|
|
if style, ok := phaseColors[wf.Phase]; ok {
|
|
b.WriteString(style.Render(string(wf.Phase)))
|
|
}
|
|
b.WriteString("\n\n")
|
|
|
|
if wf.Plan.Goal != "" {
|
|
b.WriteString(fmt.Sprintf("Goal: %s\n\n", wf.Plan.Goal))
|
|
}
|
|
|
|
switch wf.Phase {
|
|
case workflow.PhaseIdle:
|
|
b.WriteString("No active workflow.\n")
|
|
b.WriteString("Type /plan <goal> to start a structured workflow.\n")
|
|
b.WriteString("Example: /plan Create a REST API in Go\n")
|
|
|
|
case workflow.PhaseGathering:
|
|
b.WriteString(sectionStyle.Render("Gathering Requirements"))
|
|
b.WriteString("\n")
|
|
for i, q := range wf.Plan.Questions {
|
|
icon := itemPendingStyle.Render(" ")
|
|
if i < len(wf.Plan.Answers) {
|
|
icon = itemOKStyle.Render(" ")
|
|
b.WriteString(fmt.Sprintf(" %s Q: %s\n", icon, q))
|
|
b.WriteString(fmt.Sprintf(" A: %s\n", wf.Plan.Answers[i]))
|
|
} else {
|
|
b.WriteString(fmt.Sprintf(" %s Q: %s\n", icon, q))
|
|
}
|
|
}
|
|
if len(wf.Plan.Answers) >= len(wf.Plan.Questions) && len(wf.Plan.Questions) > 0 {
|
|
b.WriteString("\n ")
|
|
b.WriteString(itemOKStyle.Render("[g] Generate plan"))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
case workflow.PhasePlanning:
|
|
b.WriteString(m.spinner.View())
|
|
b.WriteString(" ")
|
|
b.WriteString(itemWarnStyle.Render("Generating plan..."))
|
|
b.WriteString("\n")
|
|
|
|
case workflow.PhaseReviewing:
|
|
b.WriteString(sectionStyle.Render("Plan (review before execution)"))
|
|
b.WriteString("\n\n")
|
|
for i, s := range wf.Plan.Steps {
|
|
numStyle := lipgloss.NewStyle().Foreground(accentColor).Bold(true)
|
|
icon := stepPendingStyle.Render(" ")
|
|
b.WriteString(fmt.Sprintf(" %s %s %s\n", icon, numStyle.Render("#"+s.ID+":"), s.Title))
|
|
b.WriteString(fmt.Sprintf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Render(s.Description)))
|
|
agentStyle := lipgloss.NewStyle().Foreground(aiColor).Render(s.Agent)
|
|
b.WriteString(fmt.Sprintf(" Agent: %s\n", agentStyle))
|
|
if i < len(wf.Plan.Steps)-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
b.WriteString("\n ")
|
|
b.WriteString(itemOKStyle.Render("[a] Approve plan"))
|
|
b.WriteString(" ")
|
|
b.WriteString(itemMissingStyle.Render("[r] Reject with feedback"))
|
|
b.WriteString("\n")
|
|
|
|
if len(wf.Plan.PreviewFiles) > 0 {
|
|
b.WriteString("\n ")
|
|
b.WriteString(itemWarnStyle.Render("Preview files available (opened in browser)"))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
case workflow.PhaseExecuting:
|
|
b.WriteString(sectionStyle.Render("Executing Plan"))
|
|
b.WriteString("\n\n")
|
|
done, total := wf.Progress()
|
|
|
|
m.progressBar.SetPercent(float64(done) / float64(max(total, 1)))
|
|
fmt.Fprintf(&b, " %s %d/%d\n\n", m.progressBar.View(), done, total)
|
|
|
|
for _, s := range wf.Plan.Steps {
|
|
var icon string
|
|
switch s.Status {
|
|
case "done":
|
|
icon = stepDoneStyle.Render(" ")
|
|
case "error":
|
|
icon = stepErrorStyle.Render(" ")
|
|
default:
|
|
if wf.Plan.Steps[wf.Plan.StepIndex].ID == s.ID {
|
|
icon = stepCurrentStyle.Render(">")
|
|
} else {
|
|
icon = stepPendingStyle.Render(" ")
|
|
}
|
|
}
|
|
b.WriteString(fmt.Sprintf(" %s %s: %s\n", icon, s.ID, s.Title))
|
|
if s.Output != "" {
|
|
output := s.Output
|
|
if len(output) > 80 {
|
|
output = output[:80] + "..."
|
|
}
|
|
b.WriteString(fmt.Sprintf(" %s\n", output))
|
|
}
|
|
}
|
|
b.WriteString("\n ")
|
|
b.WriteString(itemOKStyle.Render("[n] Next step"))
|
|
b.WriteString(" ")
|
|
b.WriteString(itemMissingStyle.Render("[x] Cancel workflow"))
|
|
b.WriteString("\n")
|
|
|
|
case workflow.PhaseDone:
|
|
b.WriteString(itemOKStyle.Render("Workflow completed!"))
|
|
b.WriteString("\n\n")
|
|
for _, s := range wf.Plan.Steps {
|
|
icon := stepDoneStyle.Render(" ")
|
|
b.WriteString(fmt.Sprintf(" %s %s: %s\n", icon, s.ID, s.Title))
|
|
}
|
|
b.WriteString("\n [x] Reset workflow\n")
|
|
|
|
case workflow.PhaseError:
|
|
b.WriteString(itemMissingStyle.Render("Workflow encountered an error."))
|
|
b.WriteString("\n [x] Reset workflow\n")
|
|
}
|
|
|
|
b.WriteString("\n\n")
|
|
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("─", max(m.width-4, 10))))
|
|
b.WriteString("\n")
|
|
b.WriteString(sectionStyle.Render("Chat"))
|
|
b.WriteString("\n")
|
|
for _, msg := range m.chatLog {
|
|
lines := strings.Split(msg, "\n")
|
|
for _, line := range lines {
|
|
if len(line) > m.width-4 {
|
|
line = line[:m.width-7] + "..."
|
|
}
|
|
b.WriteString(" " + line + "\n")
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|