refactor: redesign TUI with 4 tabs, red/rose theme, split layouts
- Dashboard: tools, agents status, updates, quick actions - Studio: central chat + agents/workflows sidebar (Ctrl+S toggle) - Shell: terminal + AI assistant panel side-by-side (Ctrl+A toggle) - Config: profile, API keys, terminal/starship settings in 2 columns - New red/rose color scheme (#E8364F → #FF6B8A → #FFB3C6) - Animated header with visual tab bar and pulse loading - Remove old chat.go, agents.go, workflow_tab.go (merged into studio.go) - All tests pass, build clean 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user