From aa0ff199c6d21a814af054f83acca979929f559e Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 21 Apr 2026 21:10:31 +0200 Subject: [PATCH] refactor: remove TUI, desktop web UI is now the default and only mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove internal/tui/ entirely (2600+ lines of Bubble Tea code) - Remove bubbletea, bubbles, lipgloss direct dependencies - `muyue` now launches desktop web UI (opens browser) - CLI subcommands preserved (scan, install, setup, etc.) - Unknown args passed as desktop flags (--port, --no-open) - Update help text to reflect new default behavior ๐Ÿ’˜ Generated with Crush Assisted-by: GLM-5.1 via Crush --- cmd/muyue/main.go | 67 ++----- go.mod | 7 +- go.sum | 2 - internal/tui/animations.go | 115 ----------- internal/tui/commands.go | 108 ---------- internal/tui/config_tab.go | 114 ----------- internal/tui/dashboard.go | 176 ----------------- internal/tui/footer.go | 74 ------- internal/tui/handlers.go | 360 --------------------------------- internal/tui/header.go | 178 ----------------- internal/tui/helpers.go | 19 -- internal/tui/model.go | 395 ------------------------------------- internal/tui/studio.go | 253 ------------------------ internal/tui/styles.go | 196 ------------------ internal/tui/terminal.go | 216 -------------------- internal/tui/types.go | 207 ------------------- 16 files changed, 24 insertions(+), 2463 deletions(-) delete mode 100644 internal/tui/animations.go delete mode 100644 internal/tui/commands.go delete mode 100644 internal/tui/config_tab.go delete mode 100644 internal/tui/dashboard.go delete mode 100644 internal/tui/footer.go delete mode 100644 internal/tui/handlers.go delete mode 100644 internal/tui/header.go delete mode 100644 internal/tui/helpers.go delete mode 100644 internal/tui/model.go delete mode 100644 internal/tui/studio.go delete mode 100644 internal/tui/styles.go delete mode 100644 internal/tui/terminal.go delete mode 100644 internal/tui/types.go diff --git a/cmd/muyue/main.go b/cmd/muyue/main.go index 3da658d..cdc9de9 100644 --- a/cmd/muyue/main.go +++ b/cmd/muyue/main.go @@ -5,7 +5,6 @@ import ( "os" "os/exec" - tea "github.com/charmbracelet/bubbletea" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/desktop" "github.com/muyue/muyue/internal/installer" @@ -15,26 +14,33 @@ import ( "github.com/muyue/muyue/internal/profiler" "github.com/muyue/muyue/internal/scanner" "github.com/muyue/muyue/internal/skills" - "github.com/muyue/muyue/internal/tui" "github.com/muyue/muyue/internal/updater" "github.com/muyue/muyue/internal/version" ) func main() { if len(os.Args) > 1 { - handleCommand(os.Args[1:]) - return + if isCommand(os.Args[1]) { + handleCommand(os.Args[1:]) + return + } } - runTUI() + runDesktop(os.Args[1:]) +} + +func isCommand(arg string) bool { + switch arg { + case "version", "-v", "--version", + "scan", "install", "update", "setup", + "config", "doctor", "lsp", "mcp", "skills", + "help", "-h", "--help": + return true + } + return false } func handleCommand(args []string) { - if len(args) == 0 { - runTUI() - return - } - switch args[0] { case "version", "-v", "--version": fmt.Println(version.FullVersion()) @@ -54,16 +60,10 @@ func handleCommand(args []string) { runLSP(args[1:]) case "mcp": runMCP(args[1:]) - case "desktop": - runDesktop(args[1:]) case "skills": runSkills(args[1:]) case "help", "-h", "--help": printHelp() - default: - fmt.Printf("Unknown command: %s\n", args[0]) - printHelp() - os.Exit(1) } } @@ -71,9 +71,13 @@ func printHelp() { fmt.Printf(`%s - AI-powered development environment assistant Usage: - muyue Start the interactive TUI + muyue Launch desktop app (opens browser) muyue Run a specific command +Options: + --port=PORT Specify port (default: auto) + --no-open Don't open browser automatically + Commands: version Show version scan Scan your system for tools and runtimes @@ -82,46 +86,17 @@ Commands: setup Run first-time setup wizard config Show current configuration doctor Check that everything is properly configured - desktop Launch desktop web UI (opens browser) lsp [scan|install] Scan or install LSP servers mcp [config|scan] Configure MCP servers for Crush and Claude Code skills [list|generate|deploy|init|delete] Manage AI coding skills help Show this help -TUI Controls: - Ctrl+T Open tab switcher (navigate with arrows, select with enter) - Tab / Shift+Tab Cycle tabs - Ctrl+C Show quit confirmation (press twice quickly to force quit) - -Chat Commands: - /plan Start a structured Planโ†’Execute workflow - -Workflow Controls: - [a] Approve plan - [r] Reject plan (type feedback) - [g] Generate plan (after answering questions) - [n] Execute next step - [x] Cancel/reset workflow - Note: Some tools (docker, gh, etc.) require elevated privileges. Run 'sudo muyue install' or use 'pkexec muyue install' if needed. `, version.FullVersion()) } -func runTUI() { - cfg := loadOrSetupConfig() - result := scanner.ScanSystem() - - model := tui.NewModel(cfg, result) - p := tea.NewProgram(model, tea.WithAltScreen()) - - if _, err := p.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} - func runDesktop(args []string) { cfg := loadOrSetupConfig() if err := desktop.Run(cfg, args); err != nil { diff --git a/go.mod b/go.mod index 61a2949..86e96fc 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,7 @@ module github.com/muyue/muyue go 1.24.3 require ( - github.com/charmbracelet/bubbles v1.0.0 - github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/huh v1.0.0 - github.com/charmbracelet/lipgloss v1.1.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -14,8 +11,10 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect diff --git a/go.sum b/go.sum index a0bbf61..05e1d54 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= diff --git a/internal/tui/animations.go b/internal/tui/animations.go deleted file mode 100644 index c38bfbf..0000000 --- a/internal/tui/animations.go +++ /dev/null @@ -1,115 +0,0 @@ -package tui - -import ( - "fmt" - "math/rand" - "strings" - "time" - - "github.com/charmbracelet/lipgloss" -) - -var glitchChars = "!@#$%^&*()_+-=[]{}|;':,./<>?~`" - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func randomGlitchChar() string { - return string(glitchChars[rand.Intn(len(glitchChars))]) -} - -func glitchText(text string, intensity int) string { - runes := []rune(text) - for i := 0; i < intensity; i++ { - pos := rand.Intn(len(runes)) - if runes[pos] != ' ' && runes[pos] != '\n' { - runes[pos] = []rune(randomGlitchChar())[0] - } - } - return string(runes) -} - -func generateScanLine(width int, frame int) string { - pos := frame % width - line := strings.Repeat(" ", pos) + - lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(strings.Repeat("โ”", min(20, width-pos))) - if pos+20 < width { - line += strings.Repeat(" ", width-pos-20) - } - return line[:min(width, len(line))] -} - -func typewriterRender(text string, pos int) string { - if pos >= len(text) { - return text - } - if pos <= 0 { - return "" - } - shown := text[:pos] - cursor := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("โ–ˆ") - return shown + cursor -} - -func renderGlitchEffect(width, height, frame int) string { - var b strings.Builder - for y := 0; y < height; y++ { - line := "" - for x := 0; x < width; x++ { - if rand.Float64() < 0.15 { - c := randomGlitchChar() - style := lipgloss.NewStyle() - r := rand.Float64() - if r < 0.4 { - style = style.Foreground(cyberRed) - } else if r < 0.7 { - style = style.Foreground(cyberPink) - } else { - style = style.Foreground(textMuted) - } - line += style.Render(c) - } else { - line += " " - } - } - b.WriteString(line) - if y < height-1 { - b.WriteString("\n") - } - } - return b.String() -} - -func renderScanEffect(width, height, frame int) string { - var b strings.Builder - scanY := frame % (height + 10) - for y := 0; y < height; y++ { - if y == scanY || y == scanY+1 { - b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Render(strings.Repeat("โ”", width))) - } else if y == scanY-1 || y == scanY+2 { - b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(strings.Repeat("โ”€", width))) - } else if y < scanY { - b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf("%*s", width, ""))) - } else { - b.WriteString(strings.Repeat(" ", width)) - } - if y < height-1 { - b.WriteString("\n") - } - } - return b.String() -} - -func generateHexStream(width, lines int) string { - var b strings.Builder - for y := 0; y < lines; y++ { - for x := 0; x < width/3; x++ { - b.WriteString(fmt.Sprintf("%02X", rand.Intn(256))) - } - if y < lines-1 { - b.WriteString("\n") - } - } - return lipgloss.NewStyle().Foreground(dimRed).Render(b.String()) -} diff --git a/internal/tui/commands.go b/internal/tui/commands.go deleted file mode 100644 index 2a793bd..0000000 --- a/internal/tui/commands.go +++ /dev/null @@ -1,108 +0,0 @@ -package tui - -import ( - "fmt" - - tea "github.com/charmbracelet/bubbletea" - "github.com/muyue/muyue/internal/config" - "github.com/muyue/muyue/internal/installer" - "github.com/muyue/muyue/internal/orchestrator" - "github.com/muyue/muyue/internal/workflow" -) - -func startInstallCmd(cfg *config.MuyueConfig, tools []string, index int) tea.Cmd { - return tea.Cmd(func() tea.Msg { - inst := installer.New(cfg) - result := inst.InstallTool(tools[index]) - - if index+1 < len(tools) { - return installBatchMsg{ - result: result, - tools: tools, - index: index, - config: cfg, - } - } - return installCompleteMsg{results: []installer.InstallResult{result}} - }) -} - -func sendAIMessage(orch *orchestrator.Orchestrator, input string) tea.Cmd { - return tea.Cmd(func() tea.Msg { - if orch == nil { - return aiErrMsg{err: fmt.Errorf("orchestrator not configured")} - } - resp, err := orch.Send(input) - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - }) -} - -func startWorkflowCmd(orch *orchestrator.Orchestrator, goal string) tea.Cmd { - return tea.Cmd(func() tea.Msg { - resp, err := orch.StartWorkflow(goal) - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - }) -} - -func workflowChatCmd(orch *orchestrator.Orchestrator, input string) tea.Cmd { - return tea.Cmd(func() tea.Msg { - wf := orch.Workflow - switch wf.Phase { - case workflow.PhaseGathering: - resp, err := orch.AnswerQuestion(input) - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - case workflow.PhaseReviewing: - approved, feedback := workflow.ParseApproval(input) - resp, err := orch.ReviewPlan(approved, feedback) - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - default: - resp, err := orch.Send(input) - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - } - }) -} - -func generatePlanCmd(orch *orchestrator.Orchestrator) tea.Cmd { - return tea.Cmd(func() tea.Msg { - resp, err := orch.GeneratePlan() - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - }) -} - -func reviewPlanCmd(orch *orchestrator.Orchestrator, approved bool, feedback string) tea.Cmd { - return tea.Cmd(func() tea.Msg { - resp, err := orch.ReviewPlan(approved, feedback) - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - }) -} - -func continueWorkflowCmd(orch *orchestrator.Orchestrator, output string) tea.Cmd { - return tea.Cmd(func() tea.Msg { - resp, err := orch.ContinueExecution(output) - if err != nil { - return aiErrMsg{err: err} - } - return aiResponseMsg{content: resp} - }) -} diff --git a/internal/tui/config_tab.go b/internal/tui/config_tab.go deleted file mode 100644 index 44e4222..0000000 --- a/internal/tui/config_tab.go +++ /dev/null @@ -1,114 +0,0 @@ -package tui - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" -) - -func (m Model) renderConfig() string { - colWidth := m.width / 2 - if colWidth < 30 { - colWidth = 30 - } - - var left, right strings.Builder - - left.WriteString(renderSectionHeader("PROFILE", "[@]")) - left.WriteString("\n") - if m.config != nil { - fields := []struct { - label string - value string - }{ - {"Name", m.config.Profile.Name}, - {"Pseudo", m.config.Profile.Pseudo}, - {"Email", m.config.Profile.Email}, - {"Editor", m.config.Profile.Preferences.Editor}, - {"Shell", m.config.Profile.Preferences.Shell}, - {"Theme", m.config.Profile.Preferences.Theme}, - {"Default AI", m.config.Profile.Preferences.DefaultAI}, - } - for _, f := range fields { - left.WriteString(fmt.Sprintf(" %s %s\n", - labelStyle.Render(f.label+":"), - valueStyle.Render(f.value))) - } - if len(m.config.Profile.Languages) > 0 { - left.WriteString(fmt.Sprintf(" %s %s\n", - labelStyle.Render("Languages:"), - valueStyle.Render(strings.Join(m.config.Profile.Languages, ", ")))) - } - } - left.WriteString("\n") - - left.WriteString(renderSectionHeader("AI PROVIDERS", "[AI]")) - left.WriteString("\n") - if m.config != nil { - for _, p := range m.config.AI.Providers { - active := "" - if p.Active { - active = lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" >>") - } - keyStatus := itemMissingStyle.Render("no key") - if p.APIKey != "" { - keyStatus = itemOKStyle.Render("configured") - } - nameStyle := lipgloss.NewStyle().Foreground(textBright).Bold(true) - left.WriteString(fmt.Sprintf(" %s %s key=%s%s\n", - nameStyle.Render(p.Name), - lipgloss.NewStyle().Foreground(dimRed).Render("model="+p.Model), - keyStatus, active)) - } - } - left.WriteString("\n") - - right.WriteString(renderSectionHeader("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(renderSectionHeader("BMAD METHOD", "[B]")) - right.WriteString("\n") - if m.config != nil { - installed := itemMissingStyle.Render("[--] no") - if m.config.BMAD.Installed { - installed = itemOKStyle.Render("[OK] yes") - } - 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))) - } - } - right.WriteString("\n") - - right.WriteString(renderSectionHeader(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" - } - right.WriteString(fmt.Sprintf(" %s %s %s\n", - lipgloss.NewStyle().Foreground(textMain).Render(s.Name), - lipgloss.NewStyle().Foreground(cyberRed).Render("["+target+"]"), - lipgloss.NewStyle().Foreground(dimRed).Render(s.Description))) - } - } else { - right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(" No skills. Run `muyue skills init`.")) - right.WriteString("\n") - } - - 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 deleted file mode 100644 index 6bb2589..0000000 --- a/internal/tui/dashboard.go +++ /dev/null @@ -1,176 +0,0 @@ -package tui - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" -) - -func (m Model) renderDashboard() string { - colWidth := m.width / 2 - if colWidth < 30 { - colWidth = 30 - } - - var left, right strings.Builder - - left.WriteString(renderSectionHeader("SYSTEM", "[*]")) - left.WriteString("\n") - if m.scanResult != nil { - sysInfo := m.scanResult.System.String() - left.WriteString(" ") - left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(sysInfo)) - } - left.WriteString("\n\n") - - left.WriteString(renderSectionHeader("INSTALLED TOOLS", "[+]")) - left.WriteString("\n") - if m.scanResult != nil { - installed := 0 - total := len(m.scanResult.Tools) - for _, t := range m.scanResult.Tools { - if t.Installed { - installed++ - left.WriteString(" ") - left.WriteString(itemOKStyle.Render("[OK] ")) - left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(t.Name)) - left.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s", extractVersion(t.Version)))) - left.WriteString("\n") - } else { - left.WriteString(" ") - left.WriteString(itemMissingStyle.Render("[--] ")) - left.WriteString(lipgloss.NewStyle().Foreground(textMuted).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(cyberRed).Bold(true).Render(strings.Repeat("โ–ˆ", pct)) + - lipgloss.NewStyle().Foreground(dimRed).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(renderSectionHeader("INSTALLING", "[~]")) - left.WriteString("\n") - progBar := m.progressBar.View() - label := "" - if m.installTool != "" { - label = fmt.Sprintf(" %d/%d - %s", m.installCurrent+1, m.installTotal, m.installTool) - } else { - label = fmt.Sprintf(" %d/%d", m.installCurrent, m.installTotal) - } - left.WriteString(fmt.Sprintf(" %s%s\n", progBar, label)) - left.WriteString("\n") - } - - if len(m.installLog) > 0 { - left.WriteString(renderSectionHeader("INSTALL LOG", "[#]")) - left.WriteString("\n") - for _, l := range m.installLog { - left.WriteString(l + "\n") - } - left.WriteString("\n") - } - - right.WriteString(renderSectionHeader("QUICK ACTIONS", "[!]")) - right.WriteString("\n") - actions := []struct { - key string - desc string - color lipgloss.Color - }{ - {"i", "Install missing tools", cyberRed}, - {"u", "Check for updates", neonRed}, - {"s", "Rescan system", cyberPink}, - {"l", "Scan LSP servers", cyberRose}, - {"m", "Configure MCP servers", brightRed}, - } - for _, a := range actions { - right.WriteString(fmt.Sprintf(" %s %s\n", - lipgloss.NewStyle().Foreground(a.color).Bold(true).Render("["+a.key+"]"), - lipgloss.NewStyle().Foreground(textMain).Render(a.desc))) - } - right.WriteString("\n") - - right.WriteString(renderSectionHeader("ACTIVE AGENTS", "[*]")) - right.WriteString("\n") - - agents := []struct { - name string - }{ - {"Crush"}, - {"Claude Code"}, - } - for _, a := range agents { - right.WriteString(" ") - right.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(">> ")) - right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(a.name + " ")) - right.WriteString(itemPendingStyle.Render("[stopped]")) - right.WriteString("\n") - } - right.WriteString("\n") - - if len(m.updateStatus) > 0 { - right.WriteString(renderSectionHeader("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)) - } else if s.Error == "" { - right.WriteString(" ") - right.WriteString(itemOKStyle.Render("[OK] ")) - right.WriteString(fmt.Sprintf("%s: up to date\n", s.Tool)) - } - } - right.WriteString("\n") - } - - if len(m.lspServers) > 0 { - right.WriteString(renderSectionHeader("LSP SERVERS", "[L]")) - right.WriteString("\n") - lspInstalled := 0 - for _, s := range m.lspServers { - if s.Installed { - lspInstalled++ - right.WriteString(" ") - right.WriteString(itemOKStyle.Render("[OK] ")) - 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(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("[OK] configured") - } - right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus)) - - if m.daemon != nil { - daemonStatus := itemPendingStyle.Render("[ ] stopped") - if m.daemon.IsRunning() { - daemonStatus = itemOKStyle.Render("[OK] running") - } - right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus)) - } - - 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/footer.go b/internal/tui/footer.go deleted file mode 100644 index 391d040..0000000 --- a/internal/tui/footer.go +++ /dev/null @@ -1,74 +0,0 @@ -package tui - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" - "github.com/muyue/muyue/internal/version" -) - -func (m Model) renderFooter() string { - profile := "unknown" - if m.config != nil && m.config.Profile.Pseudo != "" { - profile = m.config.Profile.Pseudo - } - - left := fmt.Sprintf(" %s@%s", - lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(profile), - lipgloss.NewStyle().Foreground(dimRed).Render(version.Name)) - leftR := statusBarStyle.Render(left) - - var helpText string - switch m.activeTab { - case tabDashboard: - 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 = "[up/down] sections [ctrl+t] tabs" - default: - helpText = "[ctrl+t] tabs [ctrl+c] quit" - } - rightR := statusBarStyle.Render(helpText) - - updateIndicator := "" - if len(m.updateStatus) > 0 { - needsUpdate := false - for _, s := range m.updateStatus { - if s.NeedsUpdate { - needsUpdate = true - break - } - } - if needsUpdate { - updateIndicator = lipgloss.NewStyle().Foreground(warnAmber).Render(" [UPD] ") - } else { - updateIndicator = lipgloss.NewStyle().Foreground(successGreen).Render(" [OK] ") - } - } - - verStr := lipgloss.NewStyle().Foreground(dimRed).Render("v" + version.Version) - midContent := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render( - updateIndicator + verStr, - ) - - gap := m.width - lipgloss.Width(leftR) - lipgloss.Width(rightR) - lipgloss.Width(midContent) - if gap < 0 { - gap = 0 - } - - statusLine := lipgloss.JoinHorizontal(lipgloss.Bottom, - leftR, - strings.Repeat(" ", gap), - midContent, - rightR, - ) - - helpLine := lipgloss.NewStyle().Background(bgSurface).Foreground(textMuted).Render( - lipgloss.NewStyle().Padding(0, 1).Render(m.helpModel.View(keys))) - - return lipgloss.JoinVertical(lipgloss.Left, statusLine, helpLine) -} diff --git a/internal/tui/handlers.go b/internal/tui/handlers.go deleted file mode 100644 index 07b0cc4..0000000 --- a/internal/tui/handlers.go +++ /dev/null @@ -1,360 +0,0 @@ -package tui - -import ( - "fmt" - "os" - "os/exec" - "strings" - "time" - - tea "github.com/charmbracelet/bubbletea" - "github.com/muyue/muyue/internal/lsp" - "github.com/muyue/muyue/internal/mcp" - "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) { - if m.showingQuit { - return m.handleQuitConfirm(msg) - } - if m.showingTabMenu { - return m.handleTabMenu(msg) - } - - if m.activeTab == tabShell { - return m.handleShellKey(msg) - } - - switch msg.String() { - case "ctrl+c": - now := time.Now() - if m.ctrlCCount > 0 && now.Sub(m.lastCtrlC) < 2*time.Second { - return m, tea.Quit - } - m.ctrlCCount++ - m.lastCtrlC = now - m.showingQuit = true - m.confirmCursor = 1 - m.viewport.SetContent(m.renderContent()) - return m, nil - case "ctrl+t": - m.showingTabMenu = true - 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 == tabStudio && m.chatInput != "" && !m.chatLoading { - return m.handleChatSubmit() - } - case "backspace": - 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 == tabStudio && len(msg.String()) == 1 && !m.chatLoading { - m.chatInput += msg.String() - m.viewport.SetContent(m.renderContent()) - } - } - - if m.activeTab == tabDashboard { - return m.handleDashboardKey(msg) - } - if m.activeTab == tabStudio { - return m.handleStudioKey(msg) - } - - return m, nil -} - -func cleanup(m Model) { - if m.daemon != nil { - m.daemon.Stop() - } - if m.previewSrv != nil { - m.previewSrv.Stop() - } - for _, agentType := range []proxy.AgentType{proxy.AgentCrush, proxy.AgentClaude} { - m.proxyMgr.Stop(agentType) - } -} - -func (m Model) handleQuitConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.String() { - case "y", "Y", "o", "O": - m.showingQuit = false - cleanup(m) - return m, tea.Quit - case "n", "N", "esc": - m.showingQuit = false - m.ctrlCCount = 0 - m.viewport.SetContent(m.renderContent()) - return m, nil - case "left", "h": - m.confirmCursor = 0 - m.viewport.SetContent(m.renderContent()) - return m, nil - case "right", "l": - m.confirmCursor = 1 - m.viewport.SetContent(m.renderContent()) - return m, nil - case "enter": - if m.confirmCursor == 0 { - m.showingQuit = false - cleanup(m) - return m, tea.Quit - } - m.showingQuit = false - m.ctrlCCount = 0 - m.viewport.SetContent(m.renderContent()) - return m, nil - case "ctrl+c": - m.showingQuit = false - cleanup(m) - return m, tea.Quit - } - return m, nil -} - -func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.String() { - case "ctrl+c", "esc": - m.showingTabMenu = false - m.viewport.SetContent(m.renderContent()) - return m, nil - case "up", "k": - if m.tabMenuCursor > 0 { - m.tabMenuCursor-- - } - return m, nil - case "down", "j": - if m.tabMenuCursor < int(tabCount)-1 { - m.tabMenuCursor++ - } - return m, nil - case "enter": - m.switchTab(tab(m.tabMenuCursor)) - m.showingTabMenu = false - return m, nil - default: - for i := 0; i < int(tabCount); i++ { - if msg.String() == fmt.Sprintf("%d", i+1) { - m.switchTab(tab(i)) - m.showingTabMenu = false - return m, nil - } - } - } - return m, nil -} - -func (m *Model) switchTab(t tab) { - if t == m.activeTab { - return - } - m.prevTab = m.activeTab - m.activeTab = t - m.transition = transitionGlitch - m.transitionTick = 0 - m.resizeViewport() -} - -func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.String() { - case "i": - if m.installing { - return m, nil - } - var missing []string - if m.scanResult != nil { - for _, t := range m.scanResult.Tools { - if !t.Installed { - missing = append(missing, t.Name) - } - } - } - if len(missing) == 0 { - m.installLog = append(m.installLog, itemOKStyle.Render("[OK] 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.viewport.SetContent(m.renderContent()) - return m, nil - } - m.installing = true - m.installCurrent = 0 - m.installTotal = len(missing) - m.installTool = missing[0] - m.progressBar.SetPercent(0) - m.viewport.SetContent(m.renderContent()) - return m, startInstallCmd(m.config, missing, 0) - case "u": - return m, tea.Cmd(func() tea.Msg { - result := scanner.ScanSystem() - return updateCheckMsg{statuses: updater.CheckUpdates(result)} - }) - case "s": - return m, tea.Cmd(func() tea.Msg { - return scanCompleteMsg{result: scanner.ScanSystem()} - }) - case "l": - return m, tea.Cmd(func() tea.Msg { - servers := lsp.ScanServers() - return lspScanMsg{servers: servers} - }) - case "m": - return m, tea.Cmd(func() tea.Msg { - err := mcp.ConfigureAll(m.config) - return mcpConfigMsg{err: err} - }) - } - 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 - } - sudoTools := map[string]bool{ - "docker": true, "git": true, "gh": true, "node": true, "python3": true, - } - for _, t := range scan.Tools { - if !t.Installed && sudoTools[t.Name] { - return true - } - } - return false -} - -func hasSudo() bool { - if os.Geteuid() == 0 { - return true - } - if _, err := exec.LookPath("sudo"); err == nil { - return true - } - if _, err := exec.LookPath("pkexec"); err == nil { - return true - } - 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/header.go b/internal/tui/header.go deleted file mode 100644 index c7b7d7e..0000000 --- a/internal/tui/header.go +++ /dev/null @@ -1,178 +0,0 @@ -package tui - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" - "github.com/muyue/muyue/internal/version" -) - -func (m Model) renderHeader() string { - var tabs []string - for i, name := range tabNames { - icon := tabIcons[i] - if tab(i) == m.activeTab { - tabStyle := lipgloss.NewStyle(). - Background(cyberRed). - Foreground(lipgloss.Color("#FFFFFF")). - Bold(true). - Padding(0, 2) - tabs = append(tabs, tabStyle.Render(icon+" "+name)) - } else { - tabStyle := lipgloss.NewStyle(). - Background(bgSurface). - Foreground(textDim). - Padding(0, 2) - tabs = append(tabs, tabStyle.Render(icon+" "+name)) - } - } - - tabLine := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...)) - - timeStr := "" - if !m.currentTime.IsZero() { - timeStr = m.currentTime.Format("15:04:05") - } - - dateStr := "" - if !m.currentTime.IsZero() { - dateStr = m.currentTime.Format("02/01/2006") - } - - rightInfo := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render( - lipgloss.JoinHorizontal(lipgloss.Center, - lipgloss.NewStyle().Foreground(textDim).Render(dateStr+" "), - lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(timeStr), - lipgloss.NewStyle().Foreground(textMuted).Render(" "+getAnimFrame(m.animationFrame)), - ), - ) - - statusDots := "" - if m.config != nil { - hasAI := false - for _, p := range m.config.AI.Providers { - if p.Active && p.APIKey != "" { - hasAI = true - break - } - } - if hasAI { - statusDots += lipgloss.NewStyle().Foreground(successGreen).Render("โ—") - } else { - statusDots += lipgloss.NewStyle().Foreground(errorRed).Render("โ—") - } - } else { - statusDots += lipgloss.NewStyle().Foreground(warnAmber).Render("โ—") - } - - statusDots += lipgloss.NewStyle().Foreground(textMuted).Render(" ") - - if m.mcpConfigured { - statusDots += lipgloss.NewStyle().Foreground(successGreen).Render("โ—") - } else { - statusDots += lipgloss.NewStyle().Foreground(warnAmber).Render("โ—") - } - - statusInfo := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render( - lipgloss.JoinHorizontal(lipgloss.Center, - lipgloss.NewStyle().Foreground(textDim).Render("SYS "), - statusDots, - ), - ) - - badge := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("MUYUE") - versionBadge := lipgloss.NewStyle().Foreground(dimRed).Render("v" + version.Version) - - logoLine := lipgloss.NewStyle().Background(bgVoid).Padding(0, 1).Render( - lipgloss.JoinHorizontal(lipgloss.Center, badge, " ", versionBadge), - ) - - topLine := lipgloss.JoinHorizontal(lipgloss.Bottom, - logoLine, - strings.Repeat(" ", max(0, m.width-lipgloss.Width(logoLine)-lipgloss.Width(rightInfo)-lipgloss.Width(statusInfo))), - statusInfo, - rightInfo, - ) - - return lipgloss.JoinVertical(lipgloss.Left, topLine, tabLine) -} - -func (m Model) renderTabMenuOverlay() string { - menuStyle := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(cyberRed). - Background(bgCard). - Padding(1, 3) - - tabItemStyle := lipgloss.NewStyle(). - Foreground(textDim). - Padding(0, 2) - - tabItemActiveStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")). - Background(cyberRed). - Bold(true). - Padding(0, 2) - - descs := []string{ - "tools, updates & system status", - "chat, agents & workflows", - "terminal + AI assistant", - "profile, API keys & settings", - } - - var items []string - for i, name := range tabNames { - num := lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %d.", i+1)) - icon := tabIcons[i] + " " - if i == m.tabMenuCursor { - item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(cyberRose).Render(descs[i])) - items = append(items, tabItemActiveStyle.Render(">"+item)) - } else { - item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(textMuted).Render(descs[i])) - items = append(items, tabItemStyle.Render(" "+item)) - } - } - - header := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("SWITCH TAB") - content := header + "\n\n" + - strings.Join(items, "\n") + - "\n\n" + - lipgloss.NewStyle().Foreground(textMuted).Render("up/down navigate | enter select | esc cancel") - - box := menuStyle.Render(content) - - return lipgloss.Place(m.width, m.height, - 0.5, 0.5, - box, - lipgloss.WithWhitespaceBackground(bgVoid), - lipgloss.WithWhitespaceForeground(textMuted), - ) -} - -func (m Model) renderQuitOverlay() string { - yesStyle := confirmNoStyle - noStyle := confirmYesStyle - if m.confirmCursor == 0 { - yesStyle = confirmYesStyle - noStyle = confirmNoStyle - } - - frame := lipgloss.NewStyle().Foreground(cyberRed).Render(getAnimFrame(m.animationFrame)) - - box := fmt.Sprintf("\n\n %s Quit muyue?\n\n %s %s", - frame, - yesStyle.Render("[ Yes ]"), - noStyle.Render("[ No ]"), - ) - - content := confirmBoxStyle.Render(box) - - return lipgloss.Place(m.width, m.height, - 0.5, 0.5, - content, - lipgloss.WithWhitespaceBackground(bgVoid), - lipgloss.WithWhitespaceForeground(textMuted), - ) -} diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go deleted file mode 100644 index 5dcce09..0000000 --- a/internal/tui/helpers.go +++ /dev/null @@ -1,19 +0,0 @@ -package tui - -import ( - "regexp" - - "github.com/muyue/muyue/internal/workflow" -) - -var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`) - -func extractVersion(s string) string { - return versionRegex.FindString(s) -} - -type previewFile = workflow.PreviewFile - -func parsePreviewFiles(response string) []previewFile { - return workflow.ParsePreviewFiles(response) -} diff --git a/internal/tui/model.go b/internal/tui/model.go deleted file mode 100644 index 4f5d60b..0000000 --- a/internal/tui/model.go +++ /dev/null @@ -1,395 +0,0 @@ -package tui - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/progress" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/muyue/muyue/internal/config" - "github.com/muyue/muyue/internal/daemon" - "github.com/muyue/muyue/internal/lsp" - "github.com/muyue/muyue/internal/mcp" - "github.com/muyue/muyue/internal/orchestrator" - "github.com/muyue/muyue/internal/preview" - "github.com/muyue/muyue/internal/proxy" - "github.com/muyue/muyue/internal/scanner" - "github.com/muyue/muyue/internal/skills" -) - -func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model { - orch, _ := orchestrator.New(cfg) - proxyMgr := proxy.NewManager() - d := daemon.NewDaemon(cfg, 1*time.Hour) - - lspServers := lsp.ScanServers() - skillList, _ := skills.List() - - mcpConfigured := false - if err := mcp.ConfigureAll(cfg); err == nil { - mcpConfigured = true - } - - if cfg.Profile.Preferences.AutoUpdate { - d.Start() - } - - sp := spinner.New() - sp.Spinner = spinner.Dot - sp.Style = lipgloss.NewStyle().Foreground(cyberRed) - - prog := progress.New(progress.WithGradient("#FF0033", "#FF1A5E")) - - cwd, _ := os.Getwd() - - return Model{ - config: cfg, - scanResult: scan, - activeTab: tabDashboard, - chatLog: []string{ - 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, - chatInput: "", - chatLoading: false, - daemon: d, - lspServers: lspServers, - mcpConfigured: mcpConfigured, - skillList: skillList, - helpModel: help.New(), - progressBar: prog, - spinner: sp, - showingQuit: false, - confirmCursor: 1, - 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, - currentTime: time.Now(), - transition: transitionNone, - } -} - -func animTick() tea.Cmd { - return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { - return animTickMsg{time: t} - }) -} - -func clockTick() tea.Cmd { - return tea.Tick(1*time.Second, func(t time.Time) tea.Msg { - return clockTickMsg{time: t} - }) -} - -func (m Model) Init() tea.Cmd { - return tea.Batch(spinner.Tick, animTick(), clockTick(), tea.EnterAltScreen) -} - -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - return m.handleKey(msg) - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd - case animTickMsg: - m.animationFrame++ - - if m.transition == transitionGlitch { - m.transitionTick++ - if m.transitionTick > 5 { - m.transition = transitionScan - m.transitionTick = 0 - } - } else if m.transition == transitionScan { - m.transitionTick++ - if m.transitionTick > 8 { - m.transition = transitionTypewriter - m.transitionTick = 0 - m.typewriterBuf = m.renderContent() - m.typewriterPos = 0 - } - } else if m.transition == transitionTypewriter { - m.typewriterPos += 3 - if m.typewriterPos >= len(m.typewriterBuf) { - m.transition = transitionNone - } - } - - return m, animTick() - case clockTickMsg: - m.currentTime = msg.time - return m, clockTick() - 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 == 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(dimRed).Render("(process exited)")) - m.termCmd = nil - if m.activeTab == tabShell { - m.viewport.SetContent(m.renderContent()) - } - return m, nil - case aiResponseMsg: - m.chatLoading = false - m.termAILoading = false - content := msg.content - - 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() - } - return m, nil - case aiErrMsg: - m.chatLoading = false - 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 - case scanCompleteMsg: - m.scanResult = msg.result - m.viewport.SetContent(m.renderContent()) - return m, nil - case installCompleteMsg: - m.installing = false - for _, r := range msg.results { - status := itemOKStyle.Render("[OK]") - if !r.Success { - status = itemMissingStyle.Render("[--]") - } - m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message)) - } - m.scanResult = scanner.ScanSystem() - m.progressBar.SetPercent(1) - m.viewport.SetContent(m.renderContent()) - return m, nil - case installProgressMsg: - status := itemOKStyle.Render("[OK]") - m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool)) - m.installCurrent = msg.current - m.installTool = "" - pct := float64(msg.current) / float64(max(msg.total, 1)) - m.progressBar.SetPercent(pct) - m.viewport.SetContent(m.renderContent()) - return m, nil - case installBatchMsg: - status := itemOKStyle.Render("[OK]") - if !msg.result.Success { - 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 - m.installTotal = len(msg.tools) - pct := float64(m.installCurrent) / float64(max(m.installTotal, 1)) - m.progressBar.SetPercent(pct) - if msg.index+1 < len(msg.tools) { - m.installTool = msg.tools[msg.index+1] - m.viewport.SetContent(m.renderContent()) - return m, startInstallCmd(msg.config, msg.tools, msg.index+1) - } - m.installing = false - m.scanResult = scanner.ScanSystem() - m.viewport.SetContent(m.renderContent()) - return m, nil - case updateCheckMsg: - m.updateStatus = msg.statuses - m.viewport.SetContent(m.renderContent()) - return m, nil - case previewReadyMsg: - m.previewURL = msg.url - m.viewport.SetContent(m.renderContent()) - return m, nil - case lspScanMsg: - m.lspServers = msg.servers - m.viewport.SetContent(m.renderContent()) - return m, nil - case mcpConfigMsg: - if msg.err == nil { - m.mcpConfigured = true - } - m.viewport.SetContent(m.renderContent()) - return m, nil - case daemonLogMsg: - m.viewport.SetContent(m.renderContent()) - return m, nil - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - m.helpModel.Width = msg.Width - headerH := 2 - footerH := 2 - inputH := 0 - if m.activeTab == tabStudio || m.activeTab == tabShell { - inputH = 2 - } - contentH := msg.Height - headerH - footerH - inputH - if contentH < 1 { - contentH = 1 - } - m.viewport = viewport.New(msg.Width, contentH) - m.viewport.Width = msg.Width - m.viewport.Height = contentH - m.progressBar.Width = msg.Width - 20 - m.ready = true - m.viewport.SetContent(m.renderContent()) - return m, nil - } - return m, nil -} - -func (m Model) View() string { - if !m.ready { - return lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("Initializing muyue...") - } - - if m.showingQuit { - return m.renderQuitOverlay() - } - - if m.showingTabMenu { - return m.renderTabMenuOverlay() - } - - if m.transition == transitionGlitch { - return renderGlitchEffect(m.width, m.height, m.transitionTick) - } - - if m.transition == transitionScan { - return renderScanEffect(m.width, m.height, m.transitionTick) - } - - if m.transition == transitionTypewriter { - var b strings.Builder - b.WriteString(m.renderHeader()) - b.WriteString("\n") - b.WriteString(typewriterRender(m.typewriterBuf, m.typewriterPos)) - if m.activeTab == tabStudio { - b.WriteString("\n") - b.WriteString(m.renderStudioInput()) - } - if m.activeTab == tabShell { - b.WriteString("\n") - b.WriteString(m.renderShellInput()) - } - b.WriteString("\n") - b.WriteString(m.renderFooter()) - return b.String() - } - - var b strings.Builder - b.WriteString(m.renderHeader()) - b.WriteString("\n") - b.WriteString(m.viewport.View()) - if m.activeTab == tabStudio { - b.WriteString("\n") - b.WriteString(m.renderStudioInput()) - } - if m.activeTab == tabShell { - b.WriteString("\n") - b.WriteString(m.renderShellInput()) - } - b.WriteString("\n") - b.WriteString(m.renderFooter()) - - return b.String() -} - -func (m Model) renderContent() string { - switch m.activeTab { - case tabDashboard: - return m.renderDashboard() - case tabStudio: - return m.renderStudio() - case tabShell: - return m.renderShell() - case tabConfig: - return m.renderConfig() - default: - return "" - } -} - -func (m *Model) resizeViewport() { - headerH := 2 - footerH := 2 - inputH := 0 - if m.activeTab == tabStudio || m.activeTab == tabShell { - inputH = 2 - } - contentH := m.height - headerH - footerH - inputH - if contentH < 1 { - contentH = 1 - } - m.viewport = viewport.New(m.width, contentH) - m.viewport.Width = m.width - m.viewport.Height = contentH - m.viewport.SetContent(m.renderContent()) -} - -func (m *Model) handlePreview(files []previewFile) { - dir := filepath.Join(os.TempDir(), "muyue-preview") - os.RemoveAll(dir) - os.MkdirAll(dir, 0755) - - for _, f := range files { - preview.CreatePreviewFile(dir, f.Filename, f.Content) - } - - if m.previewSrv != nil { - m.previewSrv.Stop() - } - m.previewSrv = preview.NewPreviewServer(dir) - if err := m.previewSrv.Start(8765); err != nil { - 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: http://127.0.0.1:8765")) - } -} diff --git a/internal/tui/studio.go b/internal/tui/studio.go deleted file mode 100644 index 6483161..0000000 --- a/internal/tui/studio.go +++ /dev/null @@ -1,253 +0,0 @@ -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(renderSectionHeader("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(cyberRed). - Bold(true). - Padding(0, 1) - b.WriteString(activeStyle.Render(p.icon + " " + p.name)) - b.WriteString("\n") - } else { - inactiveStyle := lipgloss.NewStyle(). - Foreground(textDim). - Padding(0, 1) - b.WriteString(inactiveStyle.Render(p.icon + " " + p.name)) - b.WriteString("\n") - } - } - - b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(borderDim).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(textMuted).Render("Active Provider")) - b.WriteString("\n") - provider := "none" - if m.config != nil { - provider = m.config.Profile.Preferences.DefaultAI - } - b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" " + provider)) - b.WriteString("\n\n") - - b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Commands")) - b.WriteString("\n") - cmds := []string{"/plan ", "/help"} - for _, c := range cmds { - b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(" " + c)) - b.WriteString("\n") - } - - if m.previewURL != "" { - b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(textMuted).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(neonRed).Render("[>> running]") - case proxy.StatusStopped: - statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[|| stopped]") - case proxy.StatusError: - statusIcon = lipgloss.NewStyle().Foreground(errorRed).Render("[!! error]") - default: - if available { - statusIcon = lipgloss.NewStyle().Foreground(successGreen).Render("[OK available]") - } else { - statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[-- not installed]") - } - } - - b.WriteString(lipgloss.NewStyle().Foreground(textBright).Bold(true).Render(a.name)) - b.WriteString("\n") - b.WriteString(fmt.Sprintf(" %s\n", statusIcon)) - b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s\n", a.tool))) - b.WriteString("\n") - } - - b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Actions")) - b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [c]")) - b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Crush")) - b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [l]")) - b.WriteString(lipgloss.NewStyle().Foreground(textMain).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(textMuted).Render("No active workflow.")) - b.WriteString("\n\n") - b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("Use /plan in chat")) - b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("to start a workflow.")) - b.WriteString("\n") - return - } - - wf := m.orch.Workflow - - phaseColors := map[workflow.Phase]lipgloss.Style{ - workflow.PhaseIdle: lipgloss.NewStyle().Foreground(textMuted), - workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warnAmber).Bold(true), - workflow.PhasePlanning: lipgloss.NewStyle().Foreground(cyberPink).Bold(true), - workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(cyberRose).Bold(true), - workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(cyberRed).Bold(true), - workflow.PhaseDone: lipgloss.NewStyle().Foreground(successGreen).Bold(true), - workflow.PhaseError: lipgloss.NewStyle().Foreground(errorRed).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(textMuted).Render("Goal")) - b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(textMain).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(textMuted).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(cyberRed).Bold(true).Render(" "+c.key)) - b.WriteString(lipgloss.NewStyle().Foreground(textDim).Render(" "+c.desc)) - b.WriteString("\n") - } -} - -func (m Model) renderStudioChat(width int) string { - var b strings.Builder - - chatHeader := renderSectionHeader("CHAT", "[#]") - if m.chatLoading { - chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warnAmber).Render("thinking...") - } - b.WriteString(chatHeader) - b.WriteString("\n\n") - - sep := lipgloss.NewStyle().Foreground(borderDim).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) renderStudioInput() string { - if m.chatLoading { - return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render( - inputStyle.Render(">> ") + m.spinner.View() + lipgloss.NewStyle().Foreground(textMuted).Render(" thinking..."), - ) - } - cursor := lipgloss.NewStyle().Foreground(cyberRed).Render("โ–Ž") - return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render( - inputStyle.Render(">> ") + m.chatInput + cursor, - ) -} - -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 deleted file mode 100644 index c1add3c..0000000 --- a/internal/tui/styles.go +++ /dev/null @@ -1,196 +0,0 @@ -package tui - -import ( - "github.com/charmbracelet/lipgloss" -) - -var ( - cyberRed = lipgloss.Color("#FF0033") - cyberRedDark = lipgloss.Color("#8B0020") - cyberRedDeep = lipgloss.Color("#5C0015") - cyberPink = lipgloss.Color("#FF1A5E") - cyberRose = lipgloss.Color("#FF4D6D") - neonRed = lipgloss.Color("#FF1744") - brightRed = lipgloss.Color("#FF5252") - dimRed = lipgloss.Color("#6B2033") - mutedRed = lipgloss.Color("#4A1525") - - textBright = lipgloss.Color("#EAE0E2") - textMain = lipgloss.Color("#D4C4C8") - textDim = lipgloss.Color("#8A7A7E") - textMuted = lipgloss.Color("#5A4F52") - - successGreen = lipgloss.Color("#00E676") - warnAmber = lipgloss.Color("#FFD740") - errorRed = lipgloss.Color("#FF1744") - - bgVoid = lipgloss.Color("#0A0A0C") - bgBase = lipgloss.Color("#0F0D10") - bgSurface = lipgloss.Color("#161218") - bgPanel = lipgloss.Color("#1C1719") - bgCard = lipgloss.Color("#221B1E") - bgInput = lipgloss.Color("#2A2225") - - borderDim = lipgloss.Color("#2A1F22") - borderRed = lipgloss.Color("#FF003344") - borderRedFull = lipgloss.Color("#FF0033") -) - -var ( - baseStyle = lipgloss.NewStyle() - - titleBlockStyle = lipgloss.NewStyle(). - Foreground(cyberRed). - Bold(true) - - sectionTitleStyle = lipgloss.NewStyle(). - Foreground(cyberRed). - Bold(true) - - labelStyle = lipgloss.NewStyle(). - Foreground(textDim). - Width(14) - - valueStyle = lipgloss.NewStyle(). - Foreground(textMain) - - cardStyle = lipgloss.NewStyle(). - Background(bgCard). - Border(lipgloss.RoundedBorder()). - BorderForeground(borderDim). - Padding(0, 1) - - cardActiveStyle = lipgloss.NewStyle(). - Background(bgCard). - Border(lipgloss.RoundedBorder()). - BorderForeground(cyberRed). - Padding(0, 1) - - sidebarStyle = lipgloss.NewStyle(). - Background(bgSurface). - Border(lipgloss.Border{Right: "โ”‚"}). - BorderForeground(borderDim). - Padding(0, 1) - - statusBarStyle = lipgloss.NewStyle(). - Background(bgSurface). - Foreground(textDim). - Padding(0, 1) - - inputStyle = lipgloss.NewStyle(). - Foreground(cyberRed) - - userMsgStyle = lipgloss.NewStyle(). - Foreground(cyberRose) - - aiMsgStyle = lipgloss.NewStyle(). - Foreground(textMain) - - errMsgStyle = lipgloss.NewStyle(). - Foreground(errorRed) - - itemOKStyle = lipgloss.NewStyle().Foreground(successGreen) - itemMissingStyle = lipgloss.NewStyle().Foreground(errorRed) - itemWarnStyle = lipgloss.NewStyle().Foreground(warnAmber) - itemPendingStyle = lipgloss.NewStyle().Foreground(textMuted) - - confirmBoxStyle = lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(cyberRed). - Background(bgCard). - Foreground(textBright). - Padding(1, 3). - Bold(true) - - confirmYesStyle = lipgloss.NewStyle().Foreground(successGreen).Bold(true) - confirmNoStyle = lipgloss.NewStyle().Foreground(textMuted) - - badgeStyle = lipgloss.NewStyle(). - Background(cyberRed). - Foreground(lipgloss.Color("#FFFFFF")). - Padding(0, 1). - Bold(true) - - tabBarStyle = lipgloss.NewStyle().Background(bgSurface) - - stepDoneStyle = lipgloss.NewStyle().Foreground(successGreen) - stepPendingStyle = lipgloss.NewStyle().Foreground(textMuted) - stepCurrentStyle = lipgloss.NewStyle().Foreground(cyberRed).Bold(true) - stepErrorStyle = lipgloss.NewStyle().Foreground(errorRed) -) - -var logoLines = []string{ - "โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—", - "โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•", - "โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—", - "โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘", - "โ–ˆโ–ˆโ•‘ โ•šโ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘", - "โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•", -} - -var scanFrames = []string{ - "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€", - " โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€ ", - "โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ", - "โ”€ โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", - "โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€", - " โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", -} - -func getAnimFrame(frame int) string { - frames := []string{ - "โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ ", - } - return frames[frame%len(frames)] -} - -func getScanFrame(frame int) string { - return scanFrames[frame%len(scanFrames)] -} - -func renderLogo() string { - styled := make([]string, len(logoLines)) - for i, line := range logoLines { - styled[i] = lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(line) - } - return lipgloss.JoinVertical(lipgloss.Left, styled...) -} - -func renderBlockTitle(text string) string { - width := len(text) + 6 - top := lipgloss.NewStyle().Foreground(dimRed).Render( - "โ•ญ" + repeatStr("โ”€", width) + "โ•ฎ", - ) - content := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render( - "โ”‚ โ–  " + text + " โ–  โ”‚", - ) - bottom := lipgloss.NewStyle().Foreground(dimRed).Render( - "โ•ฐ" + repeatStr("โ”€", width) + "โ•ฏ", - ) - return lipgloss.JoinVertical(lipgloss.Left, top, content, bottom) -} - -func renderSectionHeader(title string, icon string) string { - return lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render( - "โ–  "+icon+" "+title+" โ– ", - ) -} - -func renderProgressBar(pct float64, width int) string { - filled := int(float64(width) * pct) - empty := width - filled - bar := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render( - repeatStr("โ–ˆ", filled), - ) + lipgloss.NewStyle().Foreground(dimRed).Render( - repeatStr("โ–‘", empty), - ) - return bar -} - -func repeatStr(s string, n int) string { - result := "" - for i := 0; i < n; i++ { - result += s - } - return result -} diff --git a/internal/tui/terminal.go b/internal/tui/terminal.go deleted file mode 100644 index e624f0b..0000000 --- a/internal/tui/terminal.go +++ /dev/null @@ -1,216 +0,0 @@ -package tui - -import ( - "os" - "os/exec" - "regexp" - "strings" - "time" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -var dangerousPatterns = []*regexp.Regexp{ - regexp.MustCompile(`(?i)\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|/)`), - regexp.MustCompile(`(?i)\bmkfs\b`), - regexp.MustCompile(`(?i)\bdd\s+if=`), - regexp.MustCompile(`(?i)\b(format\s+[A-Za-z]:)\b`), - regexp.MustCompile(`(?i):\(\)\{.*\}`), - regexp.MustCompile(`(?i)>(/dev/|/etc/|/boot/)`), - regexp.MustCompile(`(?i)\bshutdown\b`), - regexp.MustCompile(`(?i)\breboot\b`), - regexp.MustCompile(`(?i)\bhalt\b`), - regexp.MustCompile(`(?i)\bpoweroff\b`), -} - -func isDangerousCommand(input string) bool { - for _, pat := range dangerousPatterns { - if pat.MatchString(input) { - return true - } - } - return false -} - -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(dimRed).Render(m.termCwd) - b.WriteString(renderSectionHeader("TERMINAL", "[$]")) - b.WriteString(" ") - b.WriteString(cwdStyle) - b.WriteString("\n\n") - - sep := lipgloss.NewStyle().Foreground(borderDim).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(renderSectionHeader("AI ASSISTANT", "[?]")) - b.WriteString("\n\n") - - sep := lipgloss.NewStyle().Foreground(borderDim).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(neonRed).Render(" " + getAnimFrame(m.animationFrame) + " thinking...")) - b.WriteString("\n") - } - - inputLabel := lipgloss.NewStyle().Foreground(cyberRed).Render(">> ") - b.WriteString(inputLabel) - b.WriteString(m.termAIInput) - - return lipgloss.NewStyle(). - Background(bgSurface). - Border(lipgloss.Border{Left: "โ”‚"}). - BorderForeground(borderDim). - Width(width). - Padding(0, 1). - Render(b.String()) -} - -func (m Model) renderShellInput() string { - prompt := lipgloss.NewStyle().Foreground(successGreen).Render("> ") - return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render( - prompt + m.termInput + lipgloss.NewStyle().Foreground(cyberRed).Render("โ–Ž"), - ) -} - -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 { - m.termCmd.Process.Kill() - m.termRunning = false - m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorRed).Render("^C")) - m.termCmd = nil - m.viewport.SetContent(m.renderContent()) - return m, nil - } - now := time.Now() - if m.ctrlCCount > 0 && now.Sub(m.lastCtrlC) < 2*time.Second { - return m, tea.Quit - } - m.ctrlCCount++ - m.lastCtrlC = now - m.showingQuit = true - m.confirmCursor = 1 - m.viewport.SetContent(m.renderContent()) - return m, nil - case "ctrl+t": - 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 - } - input := strings.TrimSpace(m.termInput) - m.termInput = "" - if input == "" { - return m, nil - } - if input == "exit" || input == "quit" { - return m, nil - } - if input == "clear" { - m.termLog = nil - m.viewport.SetContent(m.renderContent()) - return m, nil - } - if isDangerousCommand(input) { - m.termLog = append(m.termLog, errMsgStyle.Render(" [BLOCKED] potentially dangerous command")) - m.viewport.SetContent(m.renderContent()) - m.viewport.GotoBottom() - return m, nil - } - if strings.HasPrefix(input, "cd ") { - dir := strings.TrimPrefix(input, "cd ") - dir = strings.TrimSpace(dir) - if dir == "~" { - home, _ := os.UserHomeDir() - dir = home - } - if err := os.Chdir(dir); err == nil { - m.termCwd, _ = os.Getwd() - m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input) - } else { - m.termLog = append(m.termLog, errMsgStyle.Render(" cd: "+err.Error())) - } - m.viewport.SetContent(m.renderContent()) - m.viewport.GotoBottom() - return m, nil - } - m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input) - m.viewport.SetContent(m.renderContent()) - m.viewport.GotoBottom() - return m, m.runTermCommand(input) - case "backspace": - if len(m.termInput) > 0 { - m.termInput = m.termInput[:len(m.termInput)-1] - m.viewport.SetContent(m.renderContent()) - } - return m, nil - default: - if len(msg.String()) == 1 { - m.termInput += msg.String() - m.viewport.SetContent(m.renderContent()) - } - } - return m, nil -} - -func (m Model) runTermCommand(input string) tea.Cmd { - return tea.Cmd(func() tea.Msg { - shell := os.Getenv("SHELL") - if shell == "" { - shell = "/bin/sh" - } - cmd := exec.Command(shell, "-c", input) - cmd.Dir = m.termCwd - out, err := cmd.CombinedOutput() - if err != nil { - return termOutputMsg{line: string(out) + errMsgStyle.Render(err.Error())} - } - return termOutputMsg{line: string(out)} - }) -} diff --git a/internal/tui/types.go b/internal/tui/types.go deleted file mode 100644 index e53ca17..0000000 --- a/internal/tui/types.go +++ /dev/null @@ -1,207 +0,0 @@ -package tui - -import ( - "os/exec" - "time" - - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/progress" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" - "github.com/muyue/muyue/internal/config" - "github.com/muyue/muyue/internal/daemon" - "github.com/muyue/muyue/internal/installer" - "github.com/muyue/muyue/internal/lsp" - "github.com/muyue/muyue/internal/orchestrator" - "github.com/muyue/muyue/internal/preview" - "github.com/muyue/muyue/internal/proxy" - "github.com/muyue/muyue/internal/scanner" - "github.com/muyue/muyue/internal/skills" - "github.com/muyue/muyue/internal/updater" - "github.com/muyue/muyue/internal/workflow" -) - -type tab int - -const ( - tabDashboard tab = iota - tabStudio - tabShell - tabConfig - tabCount -) - -var tabNames = []string{"DASH", "STUDIO", "SHELL", "CONFIG"} -var tabIcons = []string{"[โ– ]", "[<>]", "[>$]", "[//]"} - -type aiResponseMsg struct{ content string } -type aiErrMsg struct{ err error } -type scanCompleteMsg struct{ result *scanner.ScanResult } -type installCompleteMsg struct{ results []installer.InstallResult } -type installProgressMsg struct { - tool string - current int - total int -} -type installBatchMsg struct { - result installer.InstallResult - tools []string - index int - config *config.MuyueConfig -} -type updateCheckMsg struct{ statuses []updater.UpdateStatus } -type previewReadyMsg struct{ url string } -type workflowPhaseMsg struct{ phase workflow.Phase } -type daemonLogMsg struct{ logs []string } -type lspScanMsg struct{ servers []lsp.LSPServer } -type mcpConfigMsg struct{ err error } -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 clockTickMsg struct{ time time.Time } -type glitchDoneMsg struct{} -type scanDoneMsg struct{} - -type studioPanel int - -const ( - panelChat studioPanel = iota - panelAgents - panelWorkflows -) - -type configSection int - -const ( - configProfile configSection = iota - configProviders - configTerminal - configSkills -) - -type transitionState int - -const ( - transitionNone transitionState = iota - transitionGlitch - transitionScan - transitionTypewriter -) - -type Model struct { - config *config.MuyueConfig - scanResult *scanner.ScanResult - activeTab tab - prevTab 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 - previewSrv *preview.PreviewServer - daemon *daemon.Daemon - lspServers []lsp.LSPServer - mcpConfigured bool - skillList []skills.Skill - - helpModel help.Model - progressBar progress.Model - spinner spinner.Model - - showingQuit bool - confirmCursor int - showingTabMenu bool - tabMenuCursor int - - ctrlCCount int - lastCtrlC time.Time - - installing bool - installCurrent int - installTotal int - installTool string - - termCmd *exec.Cmd - termInput string - termLog []string - termRunning bool - termCwd string - - studioPanel studioPanel - studioSidebarOpen bool - - termAIChat []string - termAIInput string - termAILoading bool - termAIShow bool - - configSection configSection - configField int - - animationFrame int - - transition transitionState - transitionTick int - typewriterBuf string - typewriterPos int - currentTime time.Time -} - -type keyMap struct { - Tab key.Binding - Prev key.Binding - Quit key.Binding - TabMenu key.Binding - Enter key.Binding - Backspace key.Binding -} - -var keys = keyMap{ - Tab: key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "next"), - ), - Prev: key.NewBinding( - key.WithKeys("shift+tab"), - key.WithHelp("shift+tab", "prev"), - ), - Quit: key.NewBinding( - key.WithKeys("ctrl+c"), - key.WithHelp("ctrl+c", "quit"), - ), - TabMenu: key.NewBinding( - key.WithKeys("ctrl+t"), - key.WithHelp("ctrl+t", "tabs"), - ), - Enter: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "send"), - ), - Backspace: key.NewBinding( - key.WithKeys("backspace"), - key.WithHelp("backspace", "delete"), - ), -} - -func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{k.TabMenu, k.Tab, k.Quit} -} - -func (k keyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {k.TabMenu, k.Tab, k.Prev}, - {k.Quit}, - } -}