diff --git a/internal/tui/agents.go b/internal/tui/agents.go deleted file mode 100644 index 90f913a..0000000 --- a/internal/tui/agents.go +++ /dev/null @@ -1,87 +0,0 @@ -package tui - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" - tea "github.com/charmbracelet/bubbletea" - "github.com/muyue/muyue/internal/proxy" -) - -func (m Model) renderAgents() string { - var b strings.Builder - - b.WriteString(sectionStyle.Render("Background Agents")) - b.WriteString("\n\n") - - agents := []struct { - name string - agentType proxy.AgentType - tool string - }{ - {"Crush", proxy.AgentCrush, "Z.AI GLM"}, - {"Claude Code", proxy.AgentClaude, "Anthropic Claude"}, - } - - for _, a := range agents { - status, logs := m.proxyMgr.Status(a.agentType) - available := m.proxyMgr.IsAvailable(a.agentType) - - var statusStr string - switch status { - case proxy.StatusRunning: - statusStr = itemWarnStyle.Render(" running") - case proxy.StatusStopped: - statusStr = itemMissingStyle.Render(" stopped") - case proxy.StatusError: - statusStr = itemMissingStyle.Render(" error") - default: - if available { - statusStr = itemOKStyle.Render(" available") - } else { - statusStr = itemMissingStyle.Render(" not installed") - } - } - - nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Bold(true) - b.WriteString(fmt.Sprintf(" %s %s %s\n", nameStyle.Render(a.name), statusStr, - lipgloss.NewStyle().Foreground(mutedColor).Render("("+a.tool+")"))) - - if logs != nil && len(logs) > 0 { - lastLogs := logs - if len(logs) > 5 { - lastLogs = logs[len(logs)-5:] - } - for _, l := range lastLogs { - b.WriteString(fmt.Sprintf(" %s %s\n", - lipgloss.NewStyle().Foreground(dimColor).Render(l.Timestamp.Format("15:04:05")), - l.Message)) - } - } - } - - b.WriteString("\n") - b.WriteString(sectionStyle.Render("Actions")) - b.WriteString("\n") - b.WriteString(fmt.Sprintf(" %s Start Crush\n", lipgloss.NewStyle().Foreground(baseColor).Bold(true).Render("[c]"))) - b.WriteString(fmt.Sprintf(" %s Start Claude Code\n", lipgloss.NewStyle().Foreground(baseColor).Bold(true).Render("[l]"))) - - return b.String() -} - -func (m Model) handleAgentsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.String() { - case "c": - if m.proxyMgr.IsAvailable(proxy.AgentCrush) { - m.proxyMgr.Start(proxy.AgentCrush) - } - m.viewport.SetContent(m.renderContent()) - case "l": - if m.proxyMgr.IsAvailable(proxy.AgentClaude) { - m.proxyMgr.Start(proxy.AgentClaude) - } - m.viewport.SetContent(m.renderContent()) - } - return m, nil -} diff --git a/internal/tui/chat.go b/internal/tui/chat.go deleted file mode 100644 index a45e74a..0000000 --- a/internal/tui/chat.go +++ /dev/null @@ -1,45 +0,0 @@ -package tui - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" -) - -func (m Model) renderChat() string { - var b strings.Builder - - header := sectionStyle.Render("Chat") - header += " " - header += lipgloss.NewStyle().Foreground(mutedColor).Render("(" + m.config.Profile.Preferences.DefaultAI + ")") - if m.chatLoading { - header += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warningColor).Render("thinking...") - } - b.WriteString(header) - b.WriteString("\n\n") - - separator := lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("โ”€", max(m.width-4, 10))) - b.WriteString(separator) - b.WriteString("\n\n") - - for _, msg := range m.chatLog { - b.WriteString(msg) - b.WriteString("\n\n") - } - - if m.previewURL != "" { - b.WriteString(itemOKStyle.Render(fmt.Sprintf("Preview: %s", m.previewURL))) - b.WriteString("\n\n") - } - - return b.String() -} - -func (m Model) renderChatInput() string { - if m.chatLoading { - return inputStyle.Render("> ") + m.spinner.View() + lipgloss.NewStyle().Foreground(mutedColor).Render(" waiting for response...") - } - cursor := lipgloss.NewStyle().Foreground(baseColor).Render("") - return inputStyle.Render("> ") + m.chatInput + cursor -} diff --git a/internal/tui/commands.go b/internal/tui/commands.go index 172955a..2a793bd 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -2,7 +2,6 @@ package tui import ( "fmt" - "strings" tea "github.com/charmbracelet/bubbletea" "github.com/muyue/muyue/internal/config" @@ -107,23 +106,3 @@ func continueWorkflowCmd(orch *orchestrator.Orchestrator, output string) tea.Cmd return aiResponseMsg{content: resp} }) } - -func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) { - input := m.chatInput - m.chatLog = append(m.chatLog, userMsgStyle.Render("you: "+input)) - m.chatInput = "" - m.chatLoading = true - m.viewport.SetContent(m.renderContent()) - m.viewport.GotoBottom() - - if strings.HasPrefix(input, "/plan ") { - goal := strings.TrimPrefix(input, "/plan ") - return m, startWorkflowCmd(m.orch, goal) - } - - if m.orch != nil && m.orch.Workflow != nil && m.orch.Workflow.Phase != workflow.PhaseIdle { - return m, workflowChatCmd(m.orch, input) - } - - return m, sendAIMessage(m.orch, input) -} diff --git a/internal/tui/config_tab.go b/internal/tui/config_tab.go index cf6c6ef..ef11323 100644 --- a/internal/tui/config_tab.go +++ b/internal/tui/config_tab.go @@ -15,10 +15,15 @@ func extractVersion(s string) string { } func (m Model) renderConfig() string { - var b strings.Builder + colWidth := m.width / 2 + if colWidth < 30 { + colWidth = 30 + } - b.WriteString(sectionStyle.Render("Profile")) - b.WriteString("\n") + var left, right strings.Builder + + left.WriteString(renderSectionWithIcon("Profile", "๐Ÿ‘ค")) + left.WriteString("\n") if m.config != nil { fields := []struct { label string @@ -33,73 +38,84 @@ func (m Model) renderConfig() string { {"Default AI", m.config.Profile.Preferences.DefaultAI}, } for _, f := range fields { - labelStyle := lipgloss.NewStyle().Foreground(mutedColor).Width(14) - valueStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")) - b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render(f.label+":"), valueStyle.Render(f.value))) + left.WriteString(fmt.Sprintf(" %s %s\n", + labelStyle.Render(f.label+":"), + valueStyle.Render(f.value))) } if len(m.config.Profile.Languages) > 0 { - labelStyle := lipgloss.NewStyle().Foreground(mutedColor).Width(14) - valueStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")) - b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Languages:"), valueStyle.Render(strings.Join(m.config.Profile.Languages, ", ")))) + left.WriteString(fmt.Sprintf(" %s %s\n", + labelStyle.Render("Languages:"), + valueStyle.Render(strings.Join(m.config.Profile.Languages, ", ")))) } } - b.WriteString("\n") + left.WriteString("\n") - b.WriteString(sectionStyle.Render("AI Providers")) - b.WriteString("\n") + left.WriteString(renderSectionWithIcon("AI Providers", "โ—†")) + left.WriteString("\n") if m.config != nil { for _, p := range m.config.AI.Providers { active := "" if p.Active { - active = itemOKStyle.Render(" active") + active = lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" โ—") } keyStatus := itemMissingStyle.Render("no key") if p.APIKey != "" { keyStatus = itemOKStyle.Render("configured") } - nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Bold(true) - b.WriteString(fmt.Sprintf(" %s model=%s key=%s%s\n", - nameStyle.Render(p.Name), p.Model, keyStatus, active)) + nameStyle := lipgloss.NewStyle().Foreground(textColor).Bold(true) + left.WriteString(fmt.Sprintf(" %s %s key=%s%s\n", + nameStyle.Render(p.Name), + lipgloss.NewStyle().Foreground(dimColor).Render("model="+p.Model), + keyStatus, active)) } } - b.WriteString("\n") + left.WriteString("\n") - b.WriteString(sectionStyle.Render("BMAD Method")) - b.WriteString("\n") + right.WriteString(renderSectionWithIcon("Terminal", "โ–ถ")) + right.WriteString("\n") + if m.config != nil { + right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Custom Prompt:"), valueStyle.Render(fmt.Sprintf("%v", m.config.Terminal.CustomPrompt)))) + right.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Prompt Theme:"), valueStyle.Render(m.config.Terminal.PromptTheme))) + right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Auto Update:"), valueStyle.Render(fmt.Sprintf("%v", m.config.Profile.Preferences.AutoUpdate)))) + right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Check on Start:"), valueStyle.Render(fmt.Sprintf("%v", m.config.Profile.Preferences.CheckOnStart)))) + } + right.WriteString("\n") + + right.WriteString(renderSectionWithIcon("BMAD Method", "โ—ˆ")) + right.WriteString("\n") if m.config != nil { installed := itemMissingStyle.Render("no") if m.config.BMAD.Installed { installed = itemOKStyle.Render("yes") } - b.WriteString(fmt.Sprintf(" Installed: %s\n", installed)) - b.WriteString(fmt.Sprintf(" Global: %v\n", m.config.BMAD.Global)) + right.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Installed:"), installed)) + right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Global:"), valueStyle.Render(fmt.Sprintf("%v", m.config.BMAD.Global)))) + if m.config.BMAD.Version != "" { + right.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Version:"), valueStyle.Render(m.config.BMAD.Version))) + } } - b.WriteString("\n") + right.WriteString("\n") - b.WriteString(sectionStyle.Render("Terminal")) - b.WriteString("\n") - if m.config != nil { - b.WriteString(fmt.Sprintf(" Custom Prompt: %v\n", m.config.Terminal.CustomPrompt)) - b.WriteString(fmt.Sprintf(" Prompt Theme: %s\n", m.config.Terminal.PromptTheme)) - } - b.WriteString("\n") - - b.WriteString(sectionStyle.Render(fmt.Sprintf("Skills (%d)", len(m.skillList)))) - b.WriteString("\n") + right.WriteString(renderSectionWithIcon(fmt.Sprintf("Skills (%d)", len(m.skillList)), "โšก")) + right.WriteString("\n") if len(m.skillList) > 0 { for _, s := range m.skillList { target := s.Target if target == "" { target = "both" } - b.WriteString(fmt.Sprintf(" %-20s %s %s\n", - lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Render(s.Name), - lipgloss.NewStyle().Foreground(aiColor).Render("["+target+"]"), - s.Description)) + right.WriteString(fmt.Sprintf(" %s %s %s\n", + lipgloss.NewStyle().Foreground(textColor).Render(s.Name), + lipgloss.NewStyle().Foreground(primaryColor).Render("["+target+"]"), + lipgloss.NewStyle().Foreground(dimColor).Render(s.Description))) } } else { - b.WriteString(" No skills. Run `muyue skills init` to install built-ins.\n") + right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(" No skills. Run `muyue skills init`.")) + right.WriteString("\n") } - return b.String() + leftCol := lipgloss.NewStyle().Width(colWidth).Render(left.String()) + rightCol := lipgloss.NewStyle().Width(colWidth).Render(right.String()) + + return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol) } diff --git a/internal/tui/dashboard.go b/internal/tui/dashboard.go index d6695f7..ed58fab 100644 --- a/internal/tui/dashboard.go +++ b/internal/tui/dashboard.go @@ -15,16 +15,16 @@ func (m Model) renderDashboard() string { var left, right strings.Builder - left.WriteString(sectionStyle.Render("System")) + left.WriteString(renderSectionWithIcon("System", "โ—‰")) left.WriteString("\n") if m.scanResult != nil { sysInfo := m.scanResult.System.String() left.WriteString(" ") - left.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Render(sysInfo)) + left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(sysInfo)) } left.WriteString("\n\n") - left.WriteString(sectionStyle.Render("Tools")) + left.WriteString(renderSectionWithIcon("Installed Tools", "โ—†")) left.WriteString("\n") if m.scanResult != nil { installed := 0 @@ -33,27 +33,32 @@ func (m Model) renderDashboard() string { if t.Installed { installed++ left.WriteString(" ") - left.WriteString(itemOKStyle.Render(" ")) - left.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, extractVersion(t.Version))) + left.WriteString(itemOKStyle.Render("โœ“ ")) + left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(t.Name)) + left.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s", extractVersion(t.Version)))) + left.WriteString("\n") } else { left.WriteString(" ") - left.WriteString(itemMissingStyle.Render(" ")) - left.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, itemPendingStyle.Render("(not installed)"))) + left.WriteString(itemMissingStyle.Render("โœ— ")) + left.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(t.Name)) + left.WriteString(itemPendingStyle.Render(" (missing)")) + left.WriteString("\n") } } + barWidth := 20 pct := 0 if total > 0 { pct = (installed * barWidth) / total } - bar := lipgloss.NewStyle().Foreground(successColor).Render(strings.Repeat("โ–ˆ", pct)) + + bar := lipgloss.NewStyle().Foreground(primaryColor).Render(strings.Repeat("โ–ˆ", pct)) + lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("โ–‘", barWidth-pct)) left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total)) } left.WriteString("\n") if m.installing { - left.WriteString(sectionStyle.Render("Installing...")) + left.WriteString(renderSectionWithIcon("Installing", "โณ")) left.WriteString("\n") progBar := m.progressBar.View() label := "" @@ -67,7 +72,7 @@ func (m Model) renderDashboard() string { } if len(m.installLog) > 0 { - left.WriteString(sectionStyle.Render("Install Log")) + left.WriteString(renderSectionWithIcon("Install Log", "๐Ÿ“‹")) left.WriteString("\n") for _, l := range m.installLog { left.WriteString(l + "\n") @@ -75,87 +80,102 @@ func (m Model) renderDashboard() string { left.WriteString("\n") } - right.WriteString(sectionStyle.Render("Quick Actions")) + right.WriteString(renderSectionWithIcon("Quick Actions", "โšก")) right.WriteString("\n") actions := []struct { - key string - desc string + key string + desc string + color lipgloss.Color }{ - {"i", "Install missing tools"}, - {"u", "Check for updates"}, - {"s", "Rescan system"}, - {"l", "Scan LSP servers"}, - {"m", "Configure MCP servers"}, + {"i", "Install missing tools", primaryColor}, + {"u", "Check for updates", warmColor}, + {"s", "Rescan system", roseColor}, + {"l", "Scan LSP servers", accentColor}, + {"m", "Configure MCP servers", roseLightColor}, } for _, a := range actions { right.WriteString(fmt.Sprintf(" %s %s\n", - lipgloss.NewStyle().Foreground(baseColor).Bold(true).Render("["+a.key+"]"), - a.desc)) + lipgloss.NewStyle().Foreground(a.color).Bold(true).Render("["+a.key+"]"), + lipgloss.NewStyle().Foreground(textColor).Render(a.desc))) + } + right.WriteString("\n") + + right.WriteString(renderSectionWithIcon("Active Agents", "โ—‰")) + right.WriteString("\n") + + agents := []struct { + name string + }{ + {"Crush"}, + {"Claude Code"}, + } + for _, a := range agents { + right.WriteString(" ") + right.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("โ— ")) + right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(a.name + " ")) + right.WriteString(itemPendingStyle.Render("stopped")) + right.WriteString("\n") } right.WriteString("\n") if len(m.updateStatus) > 0 { - right.WriteString(sectionStyle.Render("Updates")) + right.WriteString(renderSectionWithIcon("Updates", "โ†ป")) right.WriteString("\n") for _, s := range m.updateStatus { if s.NeedsUpdate { right.WriteString(" ") - right.WriteString(itemWarnStyle.Render(" ")) - right.WriteString(fmt.Sprintf(" %s: %s -> %s\n", s.Tool, s.Current, s.Latest)) + right.WriteString(itemWarnStyle.Render("โš  ")) + right.WriteString(fmt.Sprintf("%s: %s โ†’ %s\n", s.Tool, s.Current, s.Latest)) } else if s.Error == "" { right.WriteString(" ") - right.WriteString(itemOKStyle.Render(" ")) - right.WriteString(fmt.Sprintf(" %s: up to date\n", s.Tool)) + right.WriteString(itemOKStyle.Render("โœ“ ")) + right.WriteString(fmt.Sprintf("%s: up to date\n", s.Tool)) } } right.WriteString("\n") } if len(m.lspServers) > 0 { - right.WriteString(sectionStyle.Render("LSP Servers")) + right.WriteString(renderSectionWithIcon("LSP Servers", "ยง")) right.WriteString("\n") lspInstalled := 0 for _, s := range m.lspServers { if s.Installed { lspInstalled++ right.WriteString(" ") - right.WriteString(itemOKStyle.Render(" ")) - right.WriteString(fmt.Sprintf(" %-22s (%s)\n", s.Name, s.Language)) + right.WriteString(itemOKStyle.Render("โœ“ ")) + right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language)) } else { right.WriteString(" ") - right.WriteString(itemPendingStyle.Render(" ")) - right.WriteString(fmt.Sprintf(" %-22s (%s)\n", s.Name, s.Language)) + right.WriteString(itemPendingStyle.Render("โ—‹ ")) + right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language)) } } - right.WriteString(fmt.Sprintf("\n Installed: %d/%d\n", lspInstalled, len(m.lspServers))) + right.WriteString(fmt.Sprintf("\n %d/%d available\n", lspInstalled, len(m.lspServers))) right.WriteString("\n") } + mcpStatus := itemPendingStyle.Render("โ—‹ not configured") + if m.mcpConfigured { + mcpStatus = itemOKStyle.Render("โœ“ configured") + } + right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus)) + if m.daemon != nil { - right.WriteString(sectionStyle.Render("Daemon")) - right.WriteString("\n") + daemonStatus := itemPendingStyle.Render("โ—‹ stopped") if m.daemon.IsRunning() { - right.WriteString(" ") - right.WriteString(itemOKStyle.Render("running")) - lastCheck := m.daemon.LastCheck() - if !lastCheck.IsZero() { - right.WriteString(fmt.Sprintf(" last: %s", lastCheck.Format("15:04:05"))) - } - } else { - right.WriteString(" ") - right.WriteString(itemPendingStyle.Render("stopped")) + daemonStatus = itemOKStyle.Render("โœ“ running") } - right.WriteString("\n\n") + right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus)) } - mcpStatus := itemPendingStyle.Render("not configured") - if m.mcpConfigured { - mcpStatus = itemOKStyle.Render("configured") - } - right.WriteString(fmt.Sprintf("MCP: %s\n", mcpStatus)) - leftCol := lipgloss.NewStyle().Width(colWidth).Render(left.String()) rightCol := lipgloss.NewStyle().Width(colWidth).Render(right.String()) return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol) } + +func renderSectionWithIcon(title string, icon string) string { + return lipgloss.NewStyle().Foreground(primaryColor).Render(icon+" ") + + sectionStyle.Render(title) +} diff --git a/internal/tui/handlers.go b/internal/tui/handlers.go index 9ba3845..4810086 100644 --- a/internal/tui/handlers.go +++ b/internal/tui/handlers.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "strings" "time" tea "github.com/charmbracelet/bubbletea" @@ -12,6 +13,7 @@ import ( "github.com/muyue/muyue/internal/proxy" "github.com/muyue/muyue/internal/scanner" "github.com/muyue/muyue/internal/updater" + "github.com/muyue/muyue/internal/workflow" ) func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { @@ -22,8 +24,8 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.handleTabMenu(msg) } - if m.activeTab == tabTerminal { - return m.handleTerminalKey(msg) + if m.activeTab == tabShell { + return m.handleShellKey(msg) } switch msg.String() { @@ -43,17 +45,22 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.tabMenuCursor = int(m.activeTab) m.viewport.SetContent(m.renderContent()) return m, nil + case "ctrl+s": + if m.activeTab == tabStudio { + m.studioSidebarOpen = !m.studioSidebarOpen + m.viewport.SetContent(m.renderContent()) + } case "enter": - if (m.activeTab == tabChat || m.activeTab == tabWorkflow) && m.chatInput != "" && !m.chatLoading { + if m.activeTab == tabStudio && m.chatInput != "" && !m.chatLoading { return m.handleChatSubmit() } case "backspace": - if (m.activeTab == tabChat || m.activeTab == tabWorkflow) && len(m.chatInput) > 0 { + if m.activeTab == tabStudio && len(m.chatInput) > 0 { m.chatInput = m.chatInput[:len(m.chatInput)-1] m.viewport.SetContent(m.renderContent()) } default: - if (m.activeTab == tabChat || m.activeTab == tabWorkflow) && len(msg.String()) == 1 && !m.chatLoading { + if m.activeTab == tabStudio && len(msg.String()) == 1 && !m.chatLoading { m.chatInput += msg.String() m.viewport.SetContent(m.renderContent()) } @@ -62,11 +69,8 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if m.activeTab == tabDashboard { return m.handleDashboardKey(msg) } - if m.activeTab == tabAgents { - return m.handleAgentsKey(msg) - } - if m.activeTab == tabWorkflow { - return m.handleWorkflowKey(msg) + if m.activeTab == tabStudio { + return m.handleStudioKey(msg) } return m, nil @@ -170,13 +174,13 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } if len(missing) == 0 { - m.installLog = append(m.installLog, itemOKStyle.Render("All tools already installed!")) + m.installLog = append(m.installLog, itemOKStyle.Render("โœ“ All tools already installed!")) m.viewport.SetContent(m.renderContent()) return m, nil } needsSudo := checkNeedsSudo(m.scanResult) if needsSudo && !hasSudo() { - m.installLog = append(m.installLog, errMsgStyle.Render("Some tools require sudo. Run: sudo muyue install")) + m.installLog = append(m.installLog, errMsgStyle.Render("โœ— Some tools require sudo. Run: sudo muyue install")) m.viewport.SetContent(m.renderContent()) return m, nil } @@ -210,6 +214,94 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } +func (m Model) handleStudioKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + if !m.studioSidebarOpen { + return m, nil + } + + switch msg.String() { + case "1": + m.studioPanel = panelChat + m.viewport.SetContent(m.renderContent()) + case "2": + m.studioPanel = panelAgents + m.viewport.SetContent(m.renderContent()) + case "3": + m.studioPanel = panelWorkflows + m.viewport.SetContent(m.renderContent()) + } + + if m.studioPanel == panelAgents { + return m.handleAgentsKey(msg) + } + if m.studioPanel == panelWorkflows { + return m.handleWorkflowKey(msg) + } + + return m, nil +} + +func (m Model) handleAgentsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "c": + if m.proxyMgr.IsAvailable(proxy.AgentCrush) { + m.proxyMgr.Start(proxy.AgentCrush) + } + m.viewport.SetContent(m.renderContent()) + case "l": + if m.proxyMgr.IsAvailable(proxy.AgentClaude) { + m.proxyMgr.Start(proxy.AgentClaude) + } + m.viewport.SetContent(m.renderContent()) + } + return m, nil +} + +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("โŸฉ [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("โŸฉ [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 checkNeedsSudo(scan *scanner.ScanResult) bool { if scan == nil { return false @@ -237,3 +329,23 @@ func hasSudo() bool { } return false } + +func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) { + input := m.chatInput + m.chatLog = append(m.chatLog, userMsgStyle.Render("โŸฉ "+input)) + m.chatInput = "" + m.chatLoading = true + m.viewport.SetContent(m.renderContent()) + m.viewport.GotoBottom() + + if strings.HasPrefix(input, "/plan ") { + goal := strings.TrimPrefix(input, "/plan ") + return m, startWorkflowCmd(m.orch, goal) + } + + if m.orch != nil && m.orch.Workflow != nil && m.orch.Workflow.Phase != workflow.PhaseIdle { + return m, workflowChatCmd(m.orch, input) + } + + return m, sendAIMessage(m.orch, input) +} diff --git a/internal/tui/model.go b/internal/tui/model.go index 29cc413..45edfab 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -31,7 +31,6 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model { d := daemon.NewDaemon(cfg, 1*time.Hour) lspServers := lsp.ScanServers() - skillList, _ := skills.List() mcpConfigured := false @@ -45,19 +44,19 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model { sp := spinner.New() sp.Spinner = spinner.Dot - sp.Style = lipgloss.NewStyle().Foreground(baseColor) + sp.Style = lipgloss.NewStyle().Foreground(primaryColor) - prog := progress.New(progress.WithGradient("#FF6B9D", "#A0D2FF")) + prog := progress.New(progress.WithGradient("#E8364F", "#FF6B8A")) cwd, _ := os.Getwd() return Model{ - config: cfg, - scanResult: scan, - activeTab: tabDashboard, + config: cfg, + scanResult: scan, + activeTab: tabDashboard, chatLog: []string{ - aiMsgStyle.Render("muyue: Welcome! I'm your AI development environment assistant."), - aiMsgStyle.Render("muyue: Type /plan to start a structured workflow, or just chat."), + aiMsgStyle.Render(" Welcome to Studio! Chat with your AI assistant here."), + aiMsgStyle.Render(" Configure agents and workflows from the sidebar. Type /plan to start."), }, orch: orch, proxyMgr: proxyMgr, @@ -75,11 +74,26 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model { showingTabMenu: false, tabMenuCursor: 0, termCwd: cwd, + studioPanel: panelChat, + studioSidebarOpen: true, + termAIChat: []string{ + aiMsgStyle.Render(" I know your system inside out. Ask me anything."), + }, + termAIShow: true, + configSection: configProfile, + configField: 0, + animationFrame: 0, } } +func animTick() tea.Cmd { + return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { + return animTickMsg{time: t} + }) +} + func (m Model) Init() tea.Cmd { - return tea.Batch(spinner.Tick, tea.EnterAltScreen) + return tea.Batch(spinner.Tick, animTick(), tea.EnterAltScreen) } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -90,43 +104,60 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd + case animTickMsg: + m.animationFrame++ + return m, animTick() case progress.FrameMsg: pm, cmd := m.progressBar.Update(msg) m.progressBar = pm.(progress.Model) return m, cmd case termOutputMsg: m.termLog = append(m.termLog, msg.line) - if m.activeTab == tabTerminal { + if m.activeTab == tabShell { m.viewport.SetContent(m.renderContent()) m.viewport.GotoBottom() } return m, nil case termExitMsg: m.termRunning = false - m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(mutedColor).Render("(process exited)")) + m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render("(process exited)")) m.termCmd = nil - if m.activeTab == tabTerminal { + if m.activeTab == tabShell { m.viewport.SetContent(m.renderContent()) } return m, nil case aiResponseMsg: m.chatLoading = false + m.termAILoading = false content := msg.content - m.chatLog = append(m.chatLog, aiMsgStyle.Render("muyue: "+content)) - if m.orch != nil && m.orch.Workflow != nil { - previewFiles := parsePreviewFiles(content) - if len(previewFiles) > 0 { - m.handlePreview(previewFiles) + if m.activeTab == tabShell && m.termAIShow { + m.termAIChat = append(m.termAIChat, aiMsgStyle.Render(" "+content)) + if m.activeTab == tabShell { + m.viewport.SetContent(m.renderContent()) + m.viewport.GotoBottom() } + } else { + m.chatLog = append(m.chatLog, aiMsgStyle.Render(" "+content)) + if m.orch != nil && m.orch.Workflow != nil { + previewFiles := parsePreviewFiles(content) + if len(previewFiles) > 0 { + m.handlePreview(previewFiles) + } + } + m.viewport.SetContent(m.renderContent()) + m.viewport.GotoBottom() } - - m.viewport.SetContent(m.renderContent()) - m.viewport.GotoBottom() return m, nil case aiErrMsg: m.chatLoading = false - m.chatLog = append(m.chatLog, errMsgStyle.Render("error: "+msg.err.Error())) + m.termAILoading = false + errText := errMsgStyle.Render(" error: " + msg.err.Error()) + if m.activeTab == tabShell && m.termAIShow { + m.termAIChat = append(m.termAIChat, errText) + } else { + m.chatLog = append(m.chatLog, errText) + } m.viewport.SetContent(m.renderContent()) m.viewport.GotoBottom() return m, nil @@ -137,9 +168,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case installCompleteMsg: m.installing = false for _, r := range msg.results { - status := itemOKStyle.Render("[OK]") + status := itemOKStyle.Render("โœ“") if !r.Success { - status = itemMissingStyle.Render("[FAIL]") + status = itemMissingStyle.Render("โœ—") } m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message)) } @@ -148,7 +179,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.SetContent(m.renderContent()) return m, nil case installProgressMsg: - status := itemOKStyle.Render("[OK]") + status := itemOKStyle.Render("โœ“") m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool)) m.installCurrent = msg.current m.installTool = "" @@ -157,9 +188,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.SetContent(m.renderContent()) return m, nil case installBatchMsg: - status := itemOKStyle.Render("[OK]") + status := itemOKStyle.Render("โœ“") if !msg.result.Success { - status = itemMissingStyle.Render("[FAIL]") + status = itemMissingStyle.Render("โœ—") } m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message)) m.installCurrent = msg.index + 1 @@ -200,10 +231,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.width = msg.Width m.height = msg.Height m.helpModel.Width = msg.Width - headerH := 1 + headerH := 2 footerH := 2 inputH := 0 - if m.activeTab == tabChat || m.activeTab == tabWorkflow || m.activeTab == tabTerminal { + if m.activeTab == tabStudio || m.activeTab == tabShell { inputH = 2 } contentH := msg.Height - headerH - footerH - inputH @@ -223,7 +254,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) View() string { if !m.ready { - return "Loading..." + return lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Loading muyue...") } if m.showingQuit { @@ -238,13 +269,13 @@ func (m Model) View() string { b.WriteString(m.renderHeader()) b.WriteString("\n") b.WriteString(m.viewport.View()) - if m.activeTab == tabChat || m.activeTab == tabWorkflow { + if m.activeTab == tabStudio { b.WriteString("\n") - b.WriteString(m.renderChatInput()) + b.WriteString(m.renderStudioInput()) } - if m.activeTab == tabTerminal { + if m.activeTab == tabShell { b.WriteString("\n") - b.WriteString(m.renderTermInput()) + b.WriteString(m.renderShellInput()) } b.WriteString("\n") b.WriteString(m.renderFooter()) @@ -253,39 +284,52 @@ func (m Model) View() string { } func (m Model) renderHeader() string { - logoStyle := lipgloss.NewStyle().Foreground(baseColor).Bold(true) - badgeStyle := lipgloss.NewStyle(). - Background(baseColor). - Foreground(lipgloss.Color("#FFFFFF")). - Padding(0, 1) + var tabs []string + for i, name := range tabNames { + icon := tabIcons[i] + if tab(i) == m.activeTab { + tabStyle := lipgloss.NewStyle(). + Background(primaryColor). + Foreground(lipgloss.Color("#FFFFFF")). + Bold(true). + Padding(0, 2) + tabs = append(tabs, tabStyle.Render(icon+" "+name)) + } else { + tabStyle := lipgloss.NewStyle(). + Background(bgPanel). + Foreground(textDimColor). + Padding(0, 2) + tabs = append(tabs, tabStyle.Render(icon+" "+name)) + } + } - logo := logoStyle.Render("muyue") - badge := badgeStyle.Render("v" + version.Version) + tabLine := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...)) - activeTabName := lipgloss.NewStyle().Foreground(accentColor).Bold(true).Render(tabNames[m.activeTab]) - separator := lipgloss.NewStyle().Foreground(dimColor).Render(" ยท ") + badge := lipgloss.NewStyle(). + Foreground(roseColor). + Bold(true). + Render("muyue") + versionBadge := lipgloss.NewStyle(). + Foreground(dimColor). + Render("v" + version.Version) - rightPart := separator + activeTabName + anim := lipgloss.NewStyle().Foreground(warmColor).Render(getAnimFrame(m.animationFrame)) - line := lipgloss.NewStyle().Background(bgDark).Padding(0, 1).Render( - lipgloss.JoinHorizontal(lipgloss.Center, logo, " ", badge, rightPart), + logoLine := lipgloss.NewStyle().Background(bgDark).Padding(0, 1).Render( + lipgloss.JoinHorizontal(lipgloss.Center, badge, " ", versionBadge, " ", anim), ) - return line + return lipgloss.JoinVertical(lipgloss.Left, logoLine, tabLine) } func (m Model) renderContent() string { switch m.activeTab { case tabDashboard: return m.renderDashboard() - case tabChat: - return m.renderChat() - case tabWorkflow: - return m.renderWorkflow() - case tabTerminal: - return m.renderTerminal() - case tabAgents: - return m.renderAgents() + case tabStudio: + return m.renderStudio() + case tabShell: + return m.renderShell() case tabConfig: return m.renderConfig() default: @@ -294,10 +338,10 @@ func (m Model) renderContent() string { } func (m *Model) resizeViewport() { - headerH := 1 + headerH := 2 footerH := 2 inputH := 0 - if m.activeTab == tabChat || m.activeTab == tabWorkflow || m.activeTab == tabTerminal { + if m.activeTab == tabStudio || m.activeTab == tabShell { inputH = 2 } contentH := m.height - headerH - footerH - inputH @@ -316,21 +360,23 @@ func (m Model) renderFooter() string { profile = m.config.Profile.Pseudo } - left := fmt.Sprintf(" %s@%s", profile, version.Name) + left := fmt.Sprintf(" %s@%s", + lipgloss.NewStyle().Foreground(roseColor).Bold(true).Render(profile), + lipgloss.NewStyle().Foreground(dimColor).Render(version.Name)) leftR := statusBarStyle.Render(left) var helpText string switch m.activeTab { case tabDashboard: - helpText = "[i] install [u] update [s] scan" - case tabChat, tabWorkflow: - helpText = "[ctrl+t] switch tab [ctrl+c] quit" - case tabTerminal: - helpText = "[enter] run [ctrl+c] kill [clear] clear" - case tabAgents: - helpText = "[c] crush [l] claude" + helpText = "[i] install [u] update [s] scan [ctrl+t] tabs" + case tabStudio: + helpText = "[enter] send [ctrl+s] sidebar [ctrl+t] tabs" + case tabShell: + helpText = "[enter] run [ctrl+a] AI panel [ctrl+c] kill" + case tabConfig: + helpText = "[โ†‘โ†“] sections [ctrl+t] tabs" default: - helpText = "[ctrl+t] switch tab [ctrl+c] quit" + helpText = "[ctrl+t] tabs [ctrl+c] quit" } rightR := statusBarStyle.Render(helpText) @@ -346,7 +392,7 @@ func (m Model) renderFooter() string { ) return lipgloss.JoinVertical(lipgloss.Left, statusLine, - lipgloss.NewStyle().Foreground(dimColor).Render( + lipgloss.NewStyle().Background(bgPanel).Foreground(dimColor).Render( lipgloss.NewStyle().Padding(0, 1).Render(m.helpModel.View(keys)))) } @@ -358,7 +404,10 @@ func (m Model) renderQuitOverlay() string { noStyle = confirmNoStyle } - box := fmt.Sprintf("\n\n Quit muyue?\n\n %s %s", + frame := lipgloss.NewStyle().Foreground(primaryColor).Render(getAnimFrame(m.animationFrame)) + + box := fmt.Sprintf("\n\n %s Quit muyue?\n\n %s %s", + frame, yesStyle.Render("[ Yes ]"), noStyle.Render("[ No ]"), ) @@ -374,52 +423,54 @@ func (m Model) renderQuitOverlay() string { } func (m Model) renderTabMenuOverlay() string { - tabMenuStyle := lipgloss.NewStyle(). + menuStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). - BorderForeground(baseColor). + BorderForeground(primaryColor). Background(bgCard). Padding(1, 3) tabItemStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#A0A0B0")). + Foreground(textDimColor). Padding(0, 2) tabItemActiveStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). - Background(baseColor). + Background(primaryColor). Bold(true). Padding(0, 2) - tabNumStyle := lipgloss.NewStyle(). - Foreground(dimColor). - Width(4) + descs := []string{ + "tools, updates & system status", + "chat, agents & workflows", + "terminal + AI assistant", + "profile, API keys & settings", + } var items []string - descs := []string{"system overview & tools", "AI chat & conversation", "plan & execute workflows", "integrated shell", "background AI agents", "profile & settings"} - for i, name := range tabNames { - num := tabNumStyle.Render(fmt.Sprintf(" %d.", i+1)) + num := lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %d.", i+1)) + icon := tabIcons[i] + " " if i == m.tabMenuCursor { - item := fmt.Sprintf("%s %-12s %s", num, name, lipgloss.NewStyle().Foreground(mutedColor).Render(descs[i])) - items = append(items, tabItemActiveStyle.Render(">"+item)) + item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(roseLightColor).Render(descs[i])) + items = append(items, tabItemActiveStyle.Render("โ–ธ"+item)) } else { - item := fmt.Sprintf("%s %-12s %s", num, name, lipgloss.NewStyle().Foreground(dimColor).Render(descs[i])) + item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(dimColor).Render(descs[i])) items = append(items, tabItemStyle.Render(" "+item)) } } - content := lipgloss.NewStyle().Foreground(accentColor).Bold(true).Render("Switch Tab") + - "\n\n" + + header := lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Switch Tab") + content := header + "\n\n" + strings.Join(items, "\n") + "\n\n" + - lipgloss.NewStyle().Foreground(dimColor).Render("up/down navigate enter/select esc cancel") + lipgloss.NewStyle().Foreground(dimColor).Render("โ†‘โ†“ navigate ยท enter select ยท esc cancel") - box := tabMenuStyle.Render(content) + box := menuStyle.Render(content) return lipgloss.Place(m.width, m.height, 0.5, 0.5, box, - lipgloss.WithWhitespaceBackground(bgPanel), + lipgloss.WithWhitespaceBackground(bgDark), lipgloss.WithWhitespaceForeground(dimColor), ) } @@ -438,9 +489,28 @@ func (m *Model) handlePreview(files []previewFile) { } m.previewSrv = preview.NewPreviewServer(dir) if err := m.previewSrv.Start(8765); err != nil { - m.chatLog = append(m.chatLog, errMsgStyle.Render("preview error: "+err.Error())) + m.chatLog = append(m.chatLog, errMsgStyle.Render(" preview error: "+err.Error())) } else { m.previewURL = "http://127.0.0.1:8765" - m.chatLog = append(m.chatLog, itemOKStyle.Render("Preview opened in browser: http://127.0.0.1:8765")) + m.chatLog = append(m.chatLog, itemOKStyle.Render(" Preview: http://127.0.0.1:8765")) } } + +func (m Model) renderStudioInput() string { + if m.chatLoading { + return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render( + inputStyle.Render("โŸฉ ") + m.spinner.View() + lipgloss.NewStyle().Foreground(mutedColor).Render(" thinking..."), + ) + } + cursor := lipgloss.NewStyle().Foreground(primaryColor).Render("โ–Ž") + return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render( + inputStyle.Render("โŸฉ ") + m.chatInput + cursor, + ) +} + +func (m Model) renderShellInput() string { + prompt := lipgloss.NewStyle().Foreground(successColor).Render("โฏ ") + return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render( + prompt + m.termInput + lipgloss.NewStyle().Foreground(primaryColor).Render("โ–Ž"), + ) +} diff --git a/internal/tui/studio.go b/internal/tui/studio.go new file mode 100644 index 0000000..50f0102 --- /dev/null +++ b/internal/tui/studio.go @@ -0,0 +1,241 @@ +package tui + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/muyue/muyue/internal/proxy" + "github.com/muyue/muyue/internal/workflow" +) + +func (m Model) renderStudio() string { + if m.studioSidebarOpen { + sidebarWidth := 28 + chatWidth := m.width - sidebarWidth - 2 + if chatWidth < 20 { + chatWidth = 20 + sidebarWidth = m.width - chatWidth - 2 + } + + sidebar := m.renderStudioSidebar(sidebarWidth) + chat := m.renderStudioChat(chatWidth) + + return lipgloss.JoinHorizontal(lipgloss.Top, sidebar, chat) + } + + return m.renderStudioChat(m.width) +} + +func (m Model) renderStudioSidebar(width int) string { + var b strings.Builder + + b.WriteString(renderSectionWithIcon("Studio", "โ—ˆ")) + b.WriteString("\n\n") + + panels := []struct { + name string + panel studioPanel + icon string + }{ + {"Chat", panelChat, "๐Ÿ’ฌ"}, + {"Agents", panelAgents, "โ—‰"}, + {"Workflows", panelWorkflows, "โŸ"}, + } + + for _, p := range panels { + if m.studioPanel == p.panel { + activeStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFFFF")). + Background(primaryColor). + Bold(true). + Padding(0, 1) + b.WriteString(activeStyle.Render(p.icon + " " + p.name)) + b.WriteString("\n") + } else { + inactiveStyle := lipgloss.NewStyle(). + Foreground(textDimColor). + Padding(0, 1) + b.WriteString(inactiveStyle.Render(p.icon + " " + p.name)) + b.WriteString("\n") + } + } + + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("โ”€", width-4))) + b.WriteString("\n\n") + + switch m.studioPanel { + case panelAgents: + m.renderAgentsSidebar(&b, width) + case panelWorkflows: + m.renderWorkflowSidebar(&b, width) + default: + m.renderChatSidebar(&b, width) + } + + return sidebarStyle.Width(width).Render(b.String()) +} + +func (m Model) renderChatSidebar(b *strings.Builder, width int) { + b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Active Provider")) + b.WriteString("\n") + provider := "none" + if m.config != nil { + provider = m.config.Profile.Preferences.DefaultAI + } + b.WriteString(lipgloss.NewStyle().Foreground(roseColor).Bold(true).Render(" " + provider)) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Commands")) + b.WriteString("\n") + cmds := []string{"/plan ", "/help"} + for _, c := range cmds { + b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(" " + c)) + b.WriteString("\n") + } + + if m.previewURL != "" { + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Preview")) + b.WriteString("\n") + b.WriteString(itemOKStyle.Render(" " + m.previewURL)) + b.WriteString("\n") + } +} + +func (m Model) renderAgentsSidebar(b *strings.Builder, width int) { + agents := []struct { + name string + agentType proxy.AgentType + tool string + }{ + {"Crush", proxy.AgentCrush, "GLM"}, + {"Claude Code", proxy.AgentClaude, "Anthropic"}, + } + + for _, a := range agents { + status, _ := m.proxyMgr.Status(a.agentType) + available := m.proxyMgr.IsAvailable(a.agentType) + + var statusIcon string + switch status { + case proxy.StatusRunning: + statusIcon = lipgloss.NewStyle().Foreground(warmColor).Render("โ— running") + case proxy.StatusStopped: + statusIcon = lipgloss.NewStyle().Foreground(mutedColor).Render("โ—‹ stopped") + case proxy.StatusError: + statusIcon = lipgloss.NewStyle().Foreground(errorColor).Render("โœ— error") + default: + if available { + statusIcon = lipgloss.NewStyle().Foreground(successColor).Render("โœ“ available") + } else { + statusIcon = lipgloss.NewStyle().Foreground(dimColor).Render("โœ— not installed") + } + } + + b.WriteString(lipgloss.NewStyle().Foreground(textColor).Bold(true).Render(a.name)) + b.WriteString("\n") + b.WriteString(fmt.Sprintf(" %s\n", statusIcon)) + b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s\n", a.tool))) + b.WriteString("\n") + } + + b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Actions")) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [c]")) + b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Crush")) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [l]")) + b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Claude")) + b.WriteString("\n") +} + +func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { + if m.orch == nil || m.orch.Workflow == nil { + b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("No active workflow.")) + b.WriteString("\n\n") + b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("Use /plan in chat")) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("to start a workflow.")) + b.WriteString("\n") + return + } + + wf := m.orch.Workflow + + 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(roseColor).Bold(true), + workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(accentColor).Bold(true), + workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(primaryColor).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(lipgloss.NewStyle().Foreground(mutedColor).Render("Goal")) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(wf.Plan.Goal)) + b.WriteString("\n\n") + } + + if wf.Phase == workflow.PhaseExecuting { + done, total := wf.Progress() + m.progressBar.SetPercent(float64(done) / float64(max(total, 1))) + b.WriteString(m.progressBar.View()) + b.WriteString(fmt.Sprintf(" %d/%d", done, total)) + b.WriteString("\n\n") + } + + b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Controls")) + b.WriteString("\n") + controls := []struct { + key string + desc string + }{ + {"[a]", "Approve plan"}, + {"[r]", "Reject plan"}, + {"[g]", "Generate plan"}, + {"[n]", "Next step"}, + {"[x]", "Cancel"}, + } + for _, c := range controls { + b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" " + c.key)) + b.WriteString(lipgloss.NewStyle().Foreground(textDimColor).Render(" " + c.desc)) + b.WriteString("\n") + } +} + +func (m Model) renderStudioChat(width int) string { + var b strings.Builder + + chatHeader := renderSectionWithIcon("Chat", "๐Ÿ’ฌ") + if m.chatLoading { + chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warningColor).Render("thinking...") + } + b.WriteString(chatHeader) + b.WriteString("\n\n") + + sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("โ”€", max(width-4, 10))) + b.WriteString(" " + sep) + b.WriteString("\n\n") + + for _, msg := range m.chatLog { + b.WriteString(msg) + b.WriteString("\n\n") + } + + return b.String() +} + +func (m Model) handleStudioPanelSwitch(panel studioPanel) { + m.studioPanel = panel + m.viewport.SetContent(m.renderContent()) +} diff --git a/internal/tui/styles.go b/internal/tui/styles.go index d6804bb..53bd3c7 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -5,20 +5,37 @@ import ( ) var ( - baseColor = lipgloss.Color("#FF6B9D") - accentColor = lipgloss.Color("#A0D2FF") - aiColor = lipgloss.Color("#C4B5FD") - successColor = lipgloss.Color("#4ADE80") - warningColor = lipgloss.Color("#FBBF24") - errorColor = lipgloss.Color("#FF6B6B") - mutedColor = lipgloss.Color("#666680") - dimColor = lipgloss.Color("#444460") - bgDark = lipgloss.Color("#1A1A2E") - bgPanel = lipgloss.Color("#16213E") - bgCard = lipgloss.Color("#1F2937") + primaryColor = lipgloss.Color("#E8364F") + roseColor = lipgloss.Color("#FF6B8A") + roseLightColor = lipgloss.Color("#FFB3C6") + accentColor = lipgloss.Color("#FF8FA3") + warmColor = lipgloss.Color("#FF4D6D") + successColor = lipgloss.Color("#4ADE80") + warningColor = lipgloss.Color("#FBBF24") + errorColor = lipgloss.Color("#FF4D4D") + mutedColor = lipgloss.Color("#8B7E8E") + dimColor = lipgloss.Color("#5A4F5E") + textColor = lipgloss.Color("#F0E6E8") + textDimColor = lipgloss.Color("#B8A9AD") + + bgDark = lipgloss.Color("#0D0A0B") + bgPanel = lipgloss.Color("#1A1215") + bgCard = lipgloss.Color("#231A1D") + bgInput = lipgloss.Color("#2A2023") + bgHover = lipgloss.Color("#332528") + + borderColor = lipgloss.Color("#3D2E32") + borderAccent = lipgloss.Color("#E8364F") + + tabActiveBg = lipgloss.Color("#E8364F") + tabInactiveBg = lipgloss.Color("#1A1215") sectionStyle = lipgloss.NewStyle(). - Foreground(accentColor). + Foreground(roseColor). + Bold(true) + + sectionIconStyle = lipgloss.NewStyle(). + Foreground(primaryColor). Bold(true) itemOKStyle = lipgloss.NewStyle(). @@ -34,16 +51,16 @@ var ( Foreground(mutedColor) userMsgStyle = lipgloss.NewStyle(). - Foreground(accentColor) + Foreground(roseLightColor) aiMsgStyle = lipgloss.NewStyle(). - Foreground(aiColor) + Foreground(textColor) errMsgStyle = lipgloss.NewStyle(). Foreground(errorColor) inputStyle = lipgloss.NewStyle(). - Foreground(baseColor) + Foreground(roseColor) stepDoneStyle = lipgloss.NewStyle(). Foreground(successColor) @@ -52,22 +69,22 @@ var ( Foreground(mutedColor) stepCurrentStyle = lipgloss.NewStyle(). - Foreground(baseColor). + Foreground(primaryColor). Bold(true) stepErrorStyle = lipgloss.NewStyle(). Foreground(errorColor) statusBarStyle = lipgloss.NewStyle(). - Background(bgDark). - Foreground(lipgloss.Color("#A0A0B0")). + Background(bgPanel). + Foreground(textDimColor). Padding(0, 1) confirmBoxStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). - BorderForeground(baseColor). + BorderForeground(primaryColor). Background(bgCard). - Foreground(lipgloss.Color("#FFFFFF")). + Foreground(textColor). Padding(1, 3). Bold(true) @@ -77,4 +94,38 @@ var ( confirmNoStyle = lipgloss.NewStyle(). Foreground(mutedColor) + + cardStyle = lipgloss.NewStyle(). + Background(bgCard). + Border(lipgloss.RoundedBorder()). + BorderForeground(borderColor). + Padding(0, 1) + + sidebarStyle = lipgloss.NewStyle(). + Background(bgPanel). + Border(lipgloss.Border{Right: "โ”‚"}). + BorderForeground(borderColor). + Padding(0, 1) + + badgeStyle = lipgloss.NewStyle(). + Background(primaryColor). + Foreground(lipgloss.Color("#FFFFFF")). + Padding(0, 1). + Bold(true) + + labelStyle = lipgloss.NewStyle(). + Foreground(mutedColor). + Width(14) + + valueStyle = lipgloss.NewStyle(). + Foreground(textColor) + + tabBarStyle = lipgloss.NewStyle(). + Background(bgPanel) + + pulseFrames = []string{"โฃพ", "โฃฝ", "โฃป", "โขฟ", "โกฟ", "โฃŸ", "โฃฏ", "โฃท"} ) + +func getAnimFrame(frame int) string { + return pulseFrames[frame%len(pulseFrames)] +} diff --git a/internal/tui/terminal.go b/internal/tui/terminal.go index d41ca6f..1afd6af 100644 --- a/internal/tui/terminal.go +++ b/internal/tui/terminal.go @@ -33,7 +33,78 @@ func isDangerousCommand(input string) bool { return false } -func (m Model) handleTerminalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m Model) renderShell() string { + if m.termAIShow { + aiWidth := 36 + termWidth := m.width - aiWidth - 2 + if termWidth < 20 { + termWidth = 20 + aiWidth = m.width - termWidth - 2 + } + + termPanel := m.renderTermPanel(termWidth) + aiPanel := m.renderAIPanel(aiWidth) + + return lipgloss.JoinHorizontal(lipgloss.Top, termPanel, aiPanel) + } + + return m.renderTermPanel(m.width) +} + +func (m Model) renderTermPanel(width int) string { + var b strings.Builder + + cwdStyle := lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd) + b.WriteString(renderSectionWithIcon("Terminal", "โ–ถ")) + b.WriteString(" ") + b.WriteString(cwdStyle) + b.WriteString("\n\n") + + sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("โ”€", max(width-4, 10))) + b.WriteString(" " + sep) + b.WriteString("\n") + + for _, line := range m.termLog { + b.WriteString(line + "\n") + } + + return b.String() +} + +func (m Model) renderAIPanel(width int) string { + var b strings.Builder + + b.WriteString(renderSectionWithIcon("AI Assistant", "โ—ˆ")) + b.WriteString("\n\n") + + sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("โ”€", max(width-4, 10))) + b.WriteString(" " + sep) + b.WriteString("\n\n") + + for _, msg := range m.termAIChat { + b.WriteString(msg) + b.WriteString("\n\n") + } + + if m.termAILoading { + b.WriteString(lipgloss.NewStyle().Foreground(warmColor).Render(" " + getAnimFrame(m.animationFrame) + " thinking...")) + b.WriteString("\n") + } + + inputLabel := lipgloss.NewStyle().Foreground(roseColor).Render("โŸฉ ") + b.WriteString(inputLabel) + b.WriteString(m.termAIInput) + + return lipgloss.NewStyle(). + Background(bgPanel). + Border(lipgloss.Border{Left: "โ”‚"}). + BorderForeground(borderColor). + Width(width). + Padding(0, 1). + Render(b.String()) +} + +func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": if m.termCmd != nil && m.termCmd.Process != nil { @@ -58,6 +129,10 @@ func (m Model) handleTerminalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.showingTabMenu = true m.tabMenuCursor = int(m.activeTab) return m, nil + case "ctrl+a": + m.termAIShow = !m.termAIShow + m.viewport.SetContent(m.renderContent()) + return m, nil case "enter": if m.termRunning { return m, nil @@ -76,7 +151,7 @@ func (m Model) handleTerminalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } if isDangerousCommand(input) { - m.termLog = append(m.termLog, errMsgStyle.Render("blocked: potentially dangerous command")) + m.termLog = append(m.termLog, errMsgStyle.Render(" blocked: potentially dangerous command")) m.viewport.SetContent(m.renderContent()) m.viewport.GotoBottom() return m, nil @@ -92,7 +167,7 @@ func (m Model) handleTerminalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.termCwd, _ = os.Getwd() m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ "+input)) } else { - m.termLog = append(m.termLog, errMsgStyle.Render("cd: "+err.Error())) + m.termLog = append(m.termLog, errMsgStyle.Render(" cd: "+err.Error())) } m.viewport.SetContent(m.renderContent()) m.viewport.GotoBottom() @@ -132,23 +207,3 @@ func (m Model) runTermCommand(input string) tea.Cmd { return termOutputMsg{line: string(out)} }) } - -func (m Model) renderTerminal() string { - var b strings.Builder - - b.WriteString(sectionStyle.Render("Terminal")) - b.WriteString(" ") - b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd)) - b.WriteString("\n\n") - - for _, line := range m.termLog { - b.WriteString(line + "\n") - } - - return b.String() -} - -func (m Model) renderTermInput() string { - prompt := lipgloss.NewStyle().Foreground(successColor).Render("$ ") - return prompt + m.termInput + lipgloss.NewStyle().Foreground(baseColor).Render("") -} diff --git a/internal/tui/types.go b/internal/tui/types.go index cae54e3..b69d811 100644 --- a/internal/tui/types.go +++ b/internal/tui/types.go @@ -26,15 +26,14 @@ type tab int const ( tabDashboard tab = iota - tabChat - tabWorkflow - tabTerminal - tabAgents + tabStudio + tabShell tabConfig tabCount ) -var tabNames = []string{"Dashboard", "Chat", "Workflow", "Terminal", "Agents", "Config"} +var tabNames = []string{"Dashboard", "Studio", "Shell", "Config"} +var tabIcons = []string{"โ—‰", "โ—ˆ", "โ–ถ", "โš™"} type aiResponseMsg struct{ content string } type aiErrMsg struct{ err error } @@ -61,20 +60,40 @@ type skillsListMsg struct{ skills []skills.Skill } type spinnerTickMsg struct{ time time.Time } type termOutputMsg struct{ line string } type termExitMsg struct{} +type animTickMsg struct{ time time.Time } + +type studioPanel int + +const ( + panelChat studioPanel = iota + panelAgents + panelWorkflows +) + +type configSection int + +const ( + configProfile configSection = iota + configProviders + configTerminal + configSkills +) type Model struct { - config *config.MuyueConfig - scanResult *scanner.ScanResult - activeTab tab - width int - height int - viewport viewport.Model - ready bool - chatInput string - chatLog []string - chatLoading bool - orch *orchestrator.Orchestrator - proxyMgr *proxy.Manager + config *config.MuyueConfig + scanResult *scanner.ScanResult + activeTab tab + width int + height int + viewport viewport.Model + ready bool + + chatInput string + chatLog []string + chatLoading bool + orch *orchestrator.Orchestrator + proxyMgr *proxy.Manager + updateStatus []updater.UpdateStatus installLog []string previewURL string @@ -106,18 +125,26 @@ type Model struct { termLog []string termRunning bool termCwd string + + studioPanel studioPanel + studioSidebarOpen bool + + termAIChat []string + termAIInput string + termAILoading bool + termAIShow bool + + configSection configSection + configField int + + animationFrame int } type keyMap struct { Tab key.Binding Prev key.Binding Quit key.Binding - Confirm key.Binding - Cancel key.Binding TabMenu key.Binding - Install key.Binding - Update key.Binding - Scan key.Binding Enter key.Binding Backspace key.Binding } @@ -125,39 +152,19 @@ type keyMap struct { var keys = keyMap{ Tab: key.NewBinding( key.WithKeys("tab"), - key.WithHelp("tab", "indent"), + key.WithHelp("tab", "next"), ), Prev: key.NewBinding( key.WithKeys("shift+tab"), - key.WithHelp("shift+tab", "unindent"), + key.WithHelp("shift+tab", "prev"), ), Quit: key.NewBinding( key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit"), ), - Confirm: key.NewBinding( - key.WithKeys("y"), - key.WithHelp("y", "yes"), - ), - Cancel: key.NewBinding( - key.WithKeys("n", "esc"), - key.WithHelp("n/esc", "no"), - ), TabMenu: key.NewBinding( key.WithKeys("ctrl+t"), - key.WithHelp("ctrl+t", "switch tab"), - ), - Install: key.NewBinding( - key.WithKeys("i"), - key.WithHelp("i", "install"), - ), - Update: key.NewBinding( - key.WithKeys("u"), - key.WithHelp("u", "update"), - ), - Scan: key.NewBinding( - key.WithKeys("s"), - key.WithHelp("s", "scan"), + key.WithHelp("ctrl+t", "tabs"), ), Enter: key.NewBinding( key.WithKeys("enter"), diff --git a/internal/tui/workflow_tab.go b/internal/tui/workflow_tab.go deleted file mode 100644 index b9aa545..0000000 --- a/internal/tui/workflow_tab.go +++ /dev/null @@ -1,213 +0,0 @@ -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 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() -}