refactor: remove TUI, desktop web UI is now the default and only mode
All checks were successful
Beta Release / beta (push) Successful in 2m17s
All checks were successful
Beta Release / beta (push) Successful in 2m17s
- 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 <crush@charm.land>
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/muyue/muyue/internal/config"
|
"github.com/muyue/muyue/internal/config"
|
||||||
"github.com/muyue/muyue/internal/desktop"
|
"github.com/muyue/muyue/internal/desktop"
|
||||||
"github.com/muyue/muyue/internal/installer"
|
"github.com/muyue/muyue/internal/installer"
|
||||||
@@ -15,26 +14,33 @@ import (
|
|||||||
"github.com/muyue/muyue/internal/profiler"
|
"github.com/muyue/muyue/internal/profiler"
|
||||||
"github.com/muyue/muyue/internal/scanner"
|
"github.com/muyue/muyue/internal/scanner"
|
||||||
"github.com/muyue/muyue/internal/skills"
|
"github.com/muyue/muyue/internal/skills"
|
||||||
"github.com/muyue/muyue/internal/tui"
|
|
||||||
"github.com/muyue/muyue/internal/updater"
|
"github.com/muyue/muyue/internal/updater"
|
||||||
"github.com/muyue/muyue/internal/version"
|
"github.com/muyue/muyue/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
handleCommand(os.Args[1:])
|
if isCommand(os.Args[1]) {
|
||||||
return
|
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) {
|
func handleCommand(args []string) {
|
||||||
if len(args) == 0 {
|
|
||||||
runTUI()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "version", "-v", "--version":
|
case "version", "-v", "--version":
|
||||||
fmt.Println(version.FullVersion())
|
fmt.Println(version.FullVersion())
|
||||||
@@ -54,16 +60,10 @@ func handleCommand(args []string) {
|
|||||||
runLSP(args[1:])
|
runLSP(args[1:])
|
||||||
case "mcp":
|
case "mcp":
|
||||||
runMCP(args[1:])
|
runMCP(args[1:])
|
||||||
case "desktop":
|
|
||||||
runDesktop(args[1:])
|
|
||||||
case "skills":
|
case "skills":
|
||||||
runSkills(args[1:])
|
runSkills(args[1:])
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
printHelp()
|
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
|
fmt.Printf(`%s - AI-powered development environment assistant
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
muyue Start the interactive TUI
|
muyue Launch desktop app (opens browser)
|
||||||
muyue <command> Run a specific command
|
muyue <command> Run a specific command
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--port=PORT Specify port (default: auto)
|
||||||
|
--no-open Don't open browser automatically
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
version Show version
|
version Show version
|
||||||
scan Scan your system for tools and runtimes
|
scan Scan your system for tools and runtimes
|
||||||
@@ -82,46 +86,17 @@ Commands:
|
|||||||
setup Run first-time setup wizard
|
setup Run first-time setup wizard
|
||||||
config Show current configuration
|
config Show current configuration
|
||||||
doctor Check that everything is properly configured
|
doctor Check that everything is properly configured
|
||||||
desktop Launch desktop web UI (opens browser)
|
|
||||||
lsp [scan|install] Scan or install LSP servers
|
lsp [scan|install] Scan or install LSP servers
|
||||||
mcp [config|scan] Configure MCP servers for Crush and Claude Code
|
mcp [config|scan] Configure MCP servers for Crush and Claude Code
|
||||||
skills [list|generate|deploy|init|delete] Manage AI coding skills
|
skills [list|generate|deploy|init|delete] Manage AI coding skills
|
||||||
help Show this help
|
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 <goal> 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:
|
Note:
|
||||||
Some tools (docker, gh, etc.) require elevated privileges.
|
Some tools (docker, gh, etc.) require elevated privileges.
|
||||||
Run 'sudo muyue install' or use 'pkexec muyue install' if needed.
|
Run 'sudo muyue install' or use 'pkexec muyue install' if needed.
|
||||||
`, version.FullVersion())
|
`, 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) {
|
func runDesktop(args []string) {
|
||||||
cfg := loadOrSetupConfig()
|
cfg := loadOrSetupConfig()
|
||||||
if err := desktop.Run(cfg, args); err != nil {
|
if err := desktop.Run(cfg, args); err != nil {
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -3,10 +3,7 @@ module github.com/muyue/muyue
|
|||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
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/huh v1.0.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,8 +11,10 @@ require (
|
|||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/catppuccin/go v0.3.0 // 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/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/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
|
|||||||
2
go.sum
2
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/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 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
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 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
||||||
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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 <goal> 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"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 <goal>", "/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 <goal> 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())
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user