feat: complete TUI redesign with cyberpunk theme #1
115
internal/tui/animations.go
Normal file
115
internal/tui/animations.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
@@ -2,18 +2,11 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
|
||||||
|
|
||||||
func extractVersion(s string) string {
|
|
||||||
return versionRegex.FindString(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderConfig() string {
|
func (m Model) renderConfig() string {
|
||||||
colWidth := m.width / 2
|
colWidth := m.width / 2
|
||||||
if colWidth < 30 {
|
if colWidth < 30 {
|
||||||
@@ -22,7 +15,7 @@ func (m Model) renderConfig() string {
|
|||||||
|
|
||||||
var left, right strings.Builder
|
var left, right strings.Builder
|
||||||
|
|
||||||
left.WriteString(renderSectionWithIcon("Profile", "👤"))
|
left.WriteString(renderSectionHeader("PROFILE", "[@]"))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
if m.config != nil {
|
if m.config != nil {
|
||||||
fields := []struct {
|
fields := []struct {
|
||||||
@@ -50,28 +43,28 @@ func (m Model) renderConfig() string {
|
|||||||
}
|
}
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
|
|
||||||
left.WriteString(renderSectionWithIcon("AI Providers", "◆"))
|
left.WriteString(renderSectionHeader("AI PROVIDERS", "[AI]"))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
if m.config != nil {
|
if m.config != nil {
|
||||||
for _, p := range m.config.AI.Providers {
|
for _, p := range m.config.AI.Providers {
|
||||||
active := ""
|
active := ""
|
||||||
if p.Active {
|
if p.Active {
|
||||||
active = lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" ●")
|
active = lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" >>")
|
||||||
}
|
}
|
||||||
keyStatus := itemMissingStyle.Render("no key")
|
keyStatus := itemMissingStyle.Render("no key")
|
||||||
if p.APIKey != "" {
|
if p.APIKey != "" {
|
||||||
keyStatus = itemOKStyle.Render("configured")
|
keyStatus = itemOKStyle.Render("configured")
|
||||||
}
|
}
|
||||||
nameStyle := lipgloss.NewStyle().Foreground(textColor).Bold(true)
|
nameStyle := lipgloss.NewStyle().Foreground(textBright).Bold(true)
|
||||||
left.WriteString(fmt.Sprintf(" %s %s key=%s%s\n",
|
left.WriteString(fmt.Sprintf(" %s %s key=%s%s\n",
|
||||||
nameStyle.Render(p.Name),
|
nameStyle.Render(p.Name),
|
||||||
lipgloss.NewStyle().Foreground(dimColor).Render("model="+p.Model),
|
lipgloss.NewStyle().Foreground(dimRed).Render("model="+p.Model),
|
||||||
keyStatus, active))
|
keyStatus, active))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
|
|
||||||
right.WriteString(renderSectionWithIcon("Terminal", "▶"))
|
right.WriteString(renderSectionHeader("TERMINAL", "[$]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
if m.config != nil {
|
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 %v\n", labelStyle.Render("Custom Prompt:"), valueStyle.Render(fmt.Sprintf("%v", m.config.Terminal.CustomPrompt))))
|
||||||
@@ -81,12 +74,12 @@ func (m Model) renderConfig() string {
|
|||||||
}
|
}
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
|
|
||||||
right.WriteString(renderSectionWithIcon("BMAD Method", "◈"))
|
right.WriteString(renderSectionHeader("BMAD METHOD", "[B]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
if m.config != nil {
|
if m.config != nil {
|
||||||
installed := itemMissingStyle.Render("no")
|
installed := itemMissingStyle.Render("[--] no")
|
||||||
if m.config.BMAD.Installed {
|
if m.config.BMAD.Installed {
|
||||||
installed = itemOKStyle.Render("yes")
|
installed = itemOKStyle.Render("[OK] yes")
|
||||||
}
|
}
|
||||||
right.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Installed:"), installed))
|
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))))
|
right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Global:"), valueStyle.Render(fmt.Sprintf("%v", m.config.BMAD.Global))))
|
||||||
@@ -96,7 +89,7 @@ func (m Model) renderConfig() string {
|
|||||||
}
|
}
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
|
|
||||||
right.WriteString(renderSectionWithIcon(fmt.Sprintf("Skills (%d)", len(m.skillList)), "⚡"))
|
right.WriteString(renderSectionHeader(fmt.Sprintf("SKILLS (%d)", len(m.skillList)), "[!]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
if len(m.skillList) > 0 {
|
if len(m.skillList) > 0 {
|
||||||
for _, s := range m.skillList {
|
for _, s := range m.skillList {
|
||||||
@@ -105,12 +98,12 @@ func (m Model) renderConfig() string {
|
|||||||
target = "both"
|
target = "both"
|
||||||
}
|
}
|
||||||
right.WriteString(fmt.Sprintf(" %s %s %s\n",
|
right.WriteString(fmt.Sprintf(" %s %s %s\n",
|
||||||
lipgloss.NewStyle().Foreground(textColor).Render(s.Name),
|
lipgloss.NewStyle().Foreground(textMain).Render(s.Name),
|
||||||
lipgloss.NewStyle().Foreground(primaryColor).Render("["+target+"]"),
|
lipgloss.NewStyle().Foreground(cyberRed).Render("["+target+"]"),
|
||||||
lipgloss.NewStyle().Foreground(dimColor).Render(s.Description)))
|
lipgloss.NewStyle().Foreground(dimRed).Render(s.Description)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(" No skills. Run `muyue skills init`."))
|
right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(" No skills. Run `muyue skills init`."))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ func (m Model) renderDashboard() string {
|
|||||||
|
|
||||||
var left, right strings.Builder
|
var left, right strings.Builder
|
||||||
|
|
||||||
left.WriteString(renderSectionWithIcon("System", "◉"))
|
left.WriteString(renderSectionHeader("SYSTEM", "[*]"))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
if m.scanResult != nil {
|
if m.scanResult != nil {
|
||||||
sysInfo := m.scanResult.System.String()
|
sysInfo := m.scanResult.System.String()
|
||||||
left.WriteString(" ")
|
left.WriteString(" ")
|
||||||
left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(sysInfo))
|
left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(sysInfo))
|
||||||
}
|
}
|
||||||
left.WriteString("\n\n")
|
left.WriteString("\n\n")
|
||||||
|
|
||||||
left.WriteString(renderSectionWithIcon("Installed Tools", "◆"))
|
left.WriteString(renderSectionHeader("INSTALLED TOOLS", "[+]"))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
if m.scanResult != nil {
|
if m.scanResult != nil {
|
||||||
installed := 0
|
installed := 0
|
||||||
@@ -33,14 +33,14 @@ func (m Model) renderDashboard() string {
|
|||||||
if t.Installed {
|
if t.Installed {
|
||||||
installed++
|
installed++
|
||||||
left.WriteString(" ")
|
left.WriteString(" ")
|
||||||
left.WriteString(itemOKStyle.Render("✓ "))
|
left.WriteString(itemOKStyle.Render("[OK] "))
|
||||||
left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(t.Name))
|
left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(t.Name))
|
||||||
left.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s", extractVersion(t.Version))))
|
left.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s", extractVersion(t.Version))))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
left.WriteString(" ")
|
left.WriteString(" ")
|
||||||
left.WriteString(itemMissingStyle.Render("✗ "))
|
left.WriteString(itemMissingStyle.Render("[--] "))
|
||||||
left.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(t.Name))
|
left.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(t.Name))
|
||||||
left.WriteString(itemPendingStyle.Render(" (missing)"))
|
left.WriteString(itemPendingStyle.Render(" (missing)"))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
}
|
}
|
||||||
@@ -51,14 +51,14 @@ func (m Model) renderDashboard() string {
|
|||||||
if total > 0 {
|
if total > 0 {
|
||||||
pct = (installed * barWidth) / total
|
pct = (installed * barWidth) / total
|
||||||
}
|
}
|
||||||
bar := lipgloss.NewStyle().Foreground(primaryColor).Render(strings.Repeat("█", pct)) +
|
bar := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(strings.Repeat("█", pct)) +
|
||||||
lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("░", barWidth-pct))
|
lipgloss.NewStyle().Foreground(dimRed).Render(strings.Repeat("░", barWidth-pct))
|
||||||
left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total))
|
left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total))
|
||||||
}
|
}
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
|
|
||||||
if m.installing {
|
if m.installing {
|
||||||
left.WriteString(renderSectionWithIcon("Installing", "⏳"))
|
left.WriteString(renderSectionHeader("INSTALLING", "[~]"))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
progBar := m.progressBar.View()
|
progBar := m.progressBar.View()
|
||||||
label := ""
|
label := ""
|
||||||
@@ -72,7 +72,7 @@ func (m Model) renderDashboard() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(m.installLog) > 0 {
|
if len(m.installLog) > 0 {
|
||||||
left.WriteString(renderSectionWithIcon("Install Log", "📋"))
|
left.WriteString(renderSectionHeader("INSTALL LOG", "[#]"))
|
||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
for _, l := range m.installLog {
|
for _, l := range m.installLog {
|
||||||
left.WriteString(l + "\n")
|
left.WriteString(l + "\n")
|
||||||
@@ -80,27 +80,27 @@ func (m Model) renderDashboard() string {
|
|||||||
left.WriteString("\n")
|
left.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
right.WriteString(renderSectionWithIcon("Quick Actions", "⚡"))
|
right.WriteString(renderSectionHeader("QUICK ACTIONS", "[!]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
actions := []struct {
|
actions := []struct {
|
||||||
key string
|
key string
|
||||||
desc string
|
desc string
|
||||||
color lipgloss.Color
|
color lipgloss.Color
|
||||||
}{
|
}{
|
||||||
{"i", "Install missing tools", primaryColor},
|
{"i", "Install missing tools", cyberRed},
|
||||||
{"u", "Check for updates", warmColor},
|
{"u", "Check for updates", neonRed},
|
||||||
{"s", "Rescan system", roseColor},
|
{"s", "Rescan system", cyberPink},
|
||||||
{"l", "Scan LSP servers", accentColor},
|
{"l", "Scan LSP servers", cyberRose},
|
||||||
{"m", "Configure MCP servers", roseLightColor},
|
{"m", "Configure MCP servers", brightRed},
|
||||||
}
|
}
|
||||||
for _, a := range actions {
|
for _, a := range actions {
|
||||||
right.WriteString(fmt.Sprintf(" %s %s\n",
|
right.WriteString(fmt.Sprintf(" %s %s\n",
|
||||||
lipgloss.NewStyle().Foreground(a.color).Bold(true).Render("["+a.key+"]"),
|
lipgloss.NewStyle().Foreground(a.color).Bold(true).Render("["+a.key+"]"),
|
||||||
lipgloss.NewStyle().Foreground(textColor).Render(a.desc)))
|
lipgloss.NewStyle().Foreground(textMain).Render(a.desc)))
|
||||||
}
|
}
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
|
|
||||||
right.WriteString(renderSectionWithIcon("Active Agents", "◉"))
|
right.WriteString(renderSectionHeader("ACTIVE AGENTS", "[*]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
|
|
||||||
agents := []struct {
|
agents := []struct {
|
||||||
@@ -111,24 +111,24 @@ func (m Model) renderDashboard() string {
|
|||||||
}
|
}
|
||||||
for _, a := range agents {
|
for _, a := range agents {
|
||||||
right.WriteString(" ")
|
right.WriteString(" ")
|
||||||
right.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("● "))
|
right.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(">> "))
|
||||||
right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(a.name + " "))
|
right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(a.name + " "))
|
||||||
right.WriteString(itemPendingStyle.Render("stopped"))
|
right.WriteString(itemPendingStyle.Render("[stopped]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
}
|
}
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
|
|
||||||
if len(m.updateStatus) > 0 {
|
if len(m.updateStatus) > 0 {
|
||||||
right.WriteString(renderSectionWithIcon("Updates", "↻"))
|
right.WriteString(renderSectionHeader("UPDATES", "[^]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
for _, s := range m.updateStatus {
|
for _, s := range m.updateStatus {
|
||||||
if s.NeedsUpdate {
|
if s.NeedsUpdate {
|
||||||
right.WriteString(" ")
|
right.WriteString(" ")
|
||||||
right.WriteString(itemWarnStyle.Render("⚠ "))
|
right.WriteString(itemWarnStyle.Render("[!!] "))
|
||||||
right.WriteString(fmt.Sprintf("%s: %s → %s\n", s.Tool, s.Current, s.Latest))
|
right.WriteString(fmt.Sprintf("%s: %s -> %s\n", s.Tool, s.Current, s.Latest))
|
||||||
} else if s.Error == "" {
|
} else if s.Error == "" {
|
||||||
right.WriteString(" ")
|
right.WriteString(" ")
|
||||||
right.WriteString(itemOKStyle.Render("✓ "))
|
right.WriteString(itemOKStyle.Render("[OK] "))
|
||||||
right.WriteString(fmt.Sprintf("%s: up to date\n", s.Tool))
|
right.WriteString(fmt.Sprintf("%s: up to date\n", s.Tool))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,18 +136,18 @@ func (m Model) renderDashboard() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(m.lspServers) > 0 {
|
if len(m.lspServers) > 0 {
|
||||||
right.WriteString(renderSectionWithIcon("LSP Servers", "§"))
|
right.WriteString(renderSectionHeader("LSP SERVERS", "[L]"))
|
||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
lspInstalled := 0
|
lspInstalled := 0
|
||||||
for _, s := range m.lspServers {
|
for _, s := range m.lspServers {
|
||||||
if s.Installed {
|
if s.Installed {
|
||||||
lspInstalled++
|
lspInstalled++
|
||||||
right.WriteString(" ")
|
right.WriteString(" ")
|
||||||
right.WriteString(itemOKStyle.Render("✓ "))
|
right.WriteString(itemOKStyle.Render("[OK] "))
|
||||||
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
|
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
|
||||||
} else {
|
} else {
|
||||||
right.WriteString(" ")
|
right.WriteString(" ")
|
||||||
right.WriteString(itemPendingStyle.Render("○ "))
|
right.WriteString(itemPendingStyle.Render("[ ] "))
|
||||||
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
|
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,16 +155,16 @@ func (m Model) renderDashboard() string {
|
|||||||
right.WriteString("\n")
|
right.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpStatus := itemPendingStyle.Render("○ not configured")
|
mcpStatus := itemPendingStyle.Render("[ ] not configured")
|
||||||
if m.mcpConfigured {
|
if m.mcpConfigured {
|
||||||
mcpStatus = itemOKStyle.Render("✓ configured")
|
mcpStatus = itemOKStyle.Render("[OK] configured")
|
||||||
}
|
}
|
||||||
right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus))
|
right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus))
|
||||||
|
|
||||||
if m.daemon != nil {
|
if m.daemon != nil {
|
||||||
daemonStatus := itemPendingStyle.Render("○ stopped")
|
daemonStatus := itemPendingStyle.Render("[ ] stopped")
|
||||||
if m.daemon.IsRunning() {
|
if m.daemon.IsRunning() {
|
||||||
daemonStatus = itemOKStyle.Render("✓ running")
|
daemonStatus = itemOKStyle.Render("[OK] running")
|
||||||
}
|
}
|
||||||
right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus))
|
right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus))
|
||||||
}
|
}
|
||||||
@@ -174,8 +174,3 @@ func (m Model) renderDashboard() string {
|
|||||||
|
|
||||||
return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol)
|
return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderSectionWithIcon(title string, icon string) string {
|
|
||||||
return lipgloss.NewStyle().Foreground(primaryColor).Render(icon+" ") +
|
|
||||||
sectionStyle.Render(title)
|
|
||||||
}
|
|
||||||
|
|||||||
74
internal/tui/footer.go
Normal file
74
internal/tui/footer.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -142,16 +142,14 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "enter":
|
case "enter":
|
||||||
m.activeTab = tab(m.tabMenuCursor)
|
m.switchTab(tab(m.tabMenuCursor))
|
||||||
m.showingTabMenu = false
|
m.showingTabMenu = false
|
||||||
m.resizeViewport()
|
|
||||||
return m, nil
|
return m, nil
|
||||||
default:
|
default:
|
||||||
for i := 0; i < int(tabCount); i++ {
|
for i := 0; i < int(tabCount); i++ {
|
||||||
if msg.String() == fmt.Sprintf("%d", i+1) {
|
if msg.String() == fmt.Sprintf("%d", i+1) {
|
||||||
m.activeTab = tab(i)
|
m.switchTab(tab(i))
|
||||||
m.showingTabMenu = false
|
m.showingTabMenu = false
|
||||||
m.resizeViewport()
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +157,17 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
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) {
|
func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "i":
|
case "i":
|
||||||
@@ -174,13 +183,13 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(missing) == 0 {
|
if len(missing) == 0 {
|
||||||
m.installLog = append(m.installLog, itemOKStyle.Render("✓ All tools already installed!"))
|
m.installLog = append(m.installLog, itemOKStyle.Render("[OK] All tools already installed!"))
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
needsSudo := checkNeedsSudo(m.scanResult)
|
needsSudo := checkNeedsSudo(m.scanResult)
|
||||||
if needsSudo && !hasSudo() {
|
if needsSudo && !hasSudo() {
|
||||||
m.installLog = append(m.installLog, errMsgStyle.Render("✗ Some tools require sudo. Run: sudo muyue install"))
|
m.installLog = append(m.installLog, errMsgStyle.Render("[!!] Some tools require sudo. Run: sudo muyue install"))
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -267,7 +276,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "a":
|
case "a":
|
||||||
if wf.Phase == workflow.PhaseReviewing {
|
if wf.Phase == workflow.PhaseReviewing {
|
||||||
m.chatLog = append(m.chatLog, userMsgStyle.Render("⟩ [Plan approved]"))
|
m.chatLog = append(m.chatLog, userMsgStyle.Render(">> [Plan approved]"))
|
||||||
m.chatLoading = true
|
m.chatLoading = true
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, reviewPlanCmd(m.orch, true, "")
|
return m, reviewPlanCmd(m.orch, true, "")
|
||||||
@@ -280,7 +289,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case "g":
|
case "g":
|
||||||
if wf.Phase == workflow.PhaseGathering && len(wf.Plan.Questions) > 0 && len(wf.Plan.Answers) >= len(wf.Plan.Questions) {
|
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.chatLog = append(m.chatLog, userMsgStyle.Render(">> [Generate plan]"))
|
||||||
m.chatLoading = true
|
m.chatLoading = true
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, generatePlanCmd(m.orch)
|
return m, generatePlanCmd(m.orch)
|
||||||
@@ -332,7 +341,7 @@ func hasSudo() bool {
|
|||||||
|
|
||||||
func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) {
|
func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) {
|
||||||
input := m.chatInput
|
input := m.chatInput
|
||||||
m.chatLog = append(m.chatLog, userMsgStyle.Render("⟩ "+input))
|
m.chatLog = append(m.chatLog, userMsgStyle.Render(">> "+input))
|
||||||
m.chatInput = ""
|
m.chatInput = ""
|
||||||
m.chatLoading = true
|
m.chatLoading = true
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
|||||||
178
internal/tui/header.go
Normal file
178
internal/tui/header.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
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,9 +1,17 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/muyue/muyue/internal/workflow"
|
"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
|
type previewFile = workflow.PreviewFile
|
||||||
|
|
||||||
func parsePreviewFiles(response string) []previewFile {
|
func parsePreviewFiles(response string) []previewFile {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"github.com/muyue/muyue/internal/proxy"
|
"github.com/muyue/muyue/internal/proxy"
|
||||||
"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/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
||||||
@@ -44,9 +43,9 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
|||||||
|
|
||||||
sp := spinner.New()
|
sp := spinner.New()
|
||||||
sp.Spinner = spinner.Dot
|
sp.Spinner = spinner.Dot
|
||||||
sp.Style = lipgloss.NewStyle().Foreground(primaryColor)
|
sp.Style = lipgloss.NewStyle().Foreground(cyberRed)
|
||||||
|
|
||||||
prog := progress.New(progress.WithGradient("#E8364F", "#FF6B8A"))
|
prog := progress.New(progress.WithGradient("#FF0033", "#FF1A5E"))
|
||||||
|
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
@@ -55,8 +54,8 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
|||||||
scanResult: scan,
|
scanResult: scan,
|
||||||
activeTab: tabDashboard,
|
activeTab: tabDashboard,
|
||||||
chatLog: []string{
|
chatLog: []string{
|
||||||
aiMsgStyle.Render(" Welcome to Studio! Chat with your AI assistant here."),
|
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."),
|
aiMsgStyle.Render(" >> Configure agents and workflows from the sidebar. Type /plan <goal> to start."),
|
||||||
},
|
},
|
||||||
orch: orch,
|
orch: orch,
|
||||||
proxyMgr: proxyMgr,
|
proxyMgr: proxyMgr,
|
||||||
@@ -77,12 +76,14 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
|||||||
studioPanel: panelChat,
|
studioPanel: panelChat,
|
||||||
studioSidebarOpen: true,
|
studioSidebarOpen: true,
|
||||||
termAIChat: []string{
|
termAIChat: []string{
|
||||||
aiMsgStyle.Render(" I know your system inside out. Ask me anything."),
|
aiMsgStyle.Render(" >> I know your system inside out. Ask me anything."),
|
||||||
},
|
},
|
||||||
termAIShow: true,
|
termAIShow: true,
|
||||||
configSection: configProfile,
|
configSection: configProfile,
|
||||||
configField: 0,
|
configField: 0,
|
||||||
animationFrame: 0,
|
animationFrame: 0,
|
||||||
|
currentTime: time.Now(),
|
||||||
|
transition: transitionNone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +93,14 @@ func animTick() tea.Cmd {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (m Model) Init() tea.Cmd {
|
||||||
return tea.Batch(spinner.Tick, animTick(), tea.EnterAltScreen)
|
return tea.Batch(spinner.Tick, animTick(), clockTick(), tea.EnterAltScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
@@ -106,7 +113,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, cmd
|
return m, cmd
|
||||||
case animTickMsg:
|
case animTickMsg:
|
||||||
m.animationFrame++
|
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()
|
return m, animTick()
|
||||||
|
case clockTickMsg:
|
||||||
|
m.currentTime = msg.time
|
||||||
|
return m, clockTick()
|
||||||
case progress.FrameMsg:
|
case progress.FrameMsg:
|
||||||
pm, cmd := m.progressBar.Update(msg)
|
pm, cmd := m.progressBar.Update(msg)
|
||||||
m.progressBar = pm.(progress.Model)
|
m.progressBar = pm.(progress.Model)
|
||||||
@@ -120,7 +152,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
case termExitMsg:
|
case termExitMsg:
|
||||||
m.termRunning = false
|
m.termRunning = false
|
||||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render("(process exited)"))
|
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render("(process exited)"))
|
||||||
m.termCmd = nil
|
m.termCmd = nil
|
||||||
if m.activeTab == tabShell {
|
if m.activeTab == tabShell {
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
@@ -152,7 +184,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case aiErrMsg:
|
case aiErrMsg:
|
||||||
m.chatLoading = false
|
m.chatLoading = false
|
||||||
m.termAILoading = false
|
m.termAILoading = false
|
||||||
errText := errMsgStyle.Render(" error: " + msg.err.Error())
|
errText := errMsgStyle.Render(" [ERROR] " + msg.err.Error())
|
||||||
if m.activeTab == tabShell && m.termAIShow {
|
if m.activeTab == tabShell && m.termAIShow {
|
||||||
m.termAIChat = append(m.termAIChat, errText)
|
m.termAIChat = append(m.termAIChat, errText)
|
||||||
} else {
|
} else {
|
||||||
@@ -168,9 +200,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case installCompleteMsg:
|
case installCompleteMsg:
|
||||||
m.installing = false
|
m.installing = false
|
||||||
for _, r := range msg.results {
|
for _, r := range msg.results {
|
||||||
status := itemOKStyle.Render("✓")
|
status := itemOKStyle.Render("[OK]")
|
||||||
if !r.Success {
|
if !r.Success {
|
||||||
status = itemMissingStyle.Render("✗")
|
status = itemMissingStyle.Render("[--]")
|
||||||
}
|
}
|
||||||
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message))
|
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message))
|
||||||
}
|
}
|
||||||
@@ -179,7 +211,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, nil
|
return m, nil
|
||||||
case installProgressMsg:
|
case installProgressMsg:
|
||||||
status := itemOKStyle.Render("✓")
|
status := itemOKStyle.Render("[OK]")
|
||||||
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool))
|
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool))
|
||||||
m.installCurrent = msg.current
|
m.installCurrent = msg.current
|
||||||
m.installTool = ""
|
m.installTool = ""
|
||||||
@@ -188,9 +220,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, nil
|
return m, nil
|
||||||
case installBatchMsg:
|
case installBatchMsg:
|
||||||
status := itemOKStyle.Render("✓")
|
status := itemOKStyle.Render("[OK]")
|
||||||
if !msg.result.Success {
|
if !msg.result.Success {
|
||||||
status = itemMissingStyle.Render("✗")
|
status = itemMissingStyle.Render("[--]")
|
||||||
}
|
}
|
||||||
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message))
|
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message))
|
||||||
m.installCurrent = msg.index + 1
|
m.installCurrent = msg.index + 1
|
||||||
@@ -254,7 +286,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
if !m.ready {
|
if !m.ready {
|
||||||
return lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Loading muyue...")
|
return lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("Initializing muyue...")
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.showingQuit {
|
if m.showingQuit {
|
||||||
@@ -265,6 +297,32 @@ func (m Model) View() string {
|
|||||||
return m.renderTabMenuOverlay()
|
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
|
var b strings.Builder
|
||||||
b.WriteString(m.renderHeader())
|
b.WriteString(m.renderHeader())
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -283,45 +341,6 @@ func (m Model) View() string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
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(primaryColor).
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
Padding(0, 2)
|
|
||||||
tabs = append(tabs, tabStyle.Render(icon+" "+name))
|
|
||||||
} else {
|
|
||||||
tabStyle := lipgloss.NewStyle().
|
|
||||||
Background(bgPanel).
|
|
||||||
Foreground(textDimColor).
|
|
||||||
Padding(0, 2)
|
|
||||||
tabs = append(tabs, tabStyle.Render(icon+" "+name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tabLine := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...))
|
|
||||||
|
|
||||||
badge := lipgloss.NewStyle().
|
|
||||||
Foreground(roseColor).
|
|
||||||
Bold(true).
|
|
||||||
Render("muyue")
|
|
||||||
versionBadge := lipgloss.NewStyle().
|
|
||||||
Foreground(dimColor).
|
|
||||||
Render("v" + version.Version)
|
|
||||||
|
|
||||||
anim := lipgloss.NewStyle().Foreground(warmColor).Render(getAnimFrame(m.animationFrame))
|
|
||||||
|
|
||||||
logoLine := lipgloss.NewStyle().Background(bgDark).Padding(0, 1).Render(
|
|
||||||
lipgloss.JoinHorizontal(lipgloss.Center, badge, " ", versionBadge, " ", anim),
|
|
||||||
)
|
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, logoLine, tabLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderContent() string {
|
func (m Model) renderContent() string {
|
||||||
switch m.activeTab {
|
switch m.activeTab {
|
||||||
case tabDashboard:
|
case tabDashboard:
|
||||||
@@ -354,127 +373,6 @@ func (m *Model) resizeViewport() {
|
|||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
}
|
}
|
||||||
|
|
||||||
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(roseColor).Bold(true).Render(profile),
|
|
||||||
lipgloss.NewStyle().Foreground(dimColor).Render(version.Name))
|
|
||||||
leftR := statusBarStyle.Render(left)
|
|
||||||
|
|
||||||
var helpText string
|
|
||||||
switch m.activeTab {
|
|
||||||
case tabDashboard:
|
|
||||||
helpText = "[i] install [u] update [s] scan [ctrl+t] tabs"
|
|
||||||
case tabStudio:
|
|
||||||
helpText = "[enter] send [ctrl+s] sidebar [ctrl+t] tabs"
|
|
||||||
case tabShell:
|
|
||||||
helpText = "[enter] run [ctrl+a] AI panel [ctrl+c] kill"
|
|
||||||
case tabConfig:
|
|
||||||
helpText = "[↑↓] sections [ctrl+t] tabs"
|
|
||||||
default:
|
|
||||||
helpText = "[ctrl+t] tabs [ctrl+c] quit"
|
|
||||||
}
|
|
||||||
rightR := statusBarStyle.Render(helpText)
|
|
||||||
|
|
||||||
gap := m.width - lipgloss.Width(leftR) - lipgloss.Width(rightR)
|
|
||||||
if gap < 0 {
|
|
||||||
gap = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
statusLine := lipgloss.JoinHorizontal(lipgloss.Bottom,
|
|
||||||
leftR,
|
|
||||||
strings.Repeat(" ", gap),
|
|
||||||
rightR,
|
|
||||||
)
|
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, statusLine,
|
|
||||||
lipgloss.NewStyle().Background(bgPanel).Foreground(dimColor).Render(
|
|
||||||
lipgloss.NewStyle().Padding(0, 1).Render(m.helpModel.View(keys))))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderQuitOverlay() string {
|
|
||||||
yesStyle := confirmNoStyle
|
|
||||||
noStyle := confirmYesStyle
|
|
||||||
if m.confirmCursor == 0 {
|
|
||||||
yesStyle = confirmYesStyle
|
|
||||||
noStyle = confirmNoStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
frame := lipgloss.NewStyle().Foreground(primaryColor).Render(getAnimFrame(m.animationFrame))
|
|
||||||
|
|
||||||
box := fmt.Sprintf("\n\n %s Quit muyue?\n\n %s %s",
|
|
||||||
frame,
|
|
||||||
yesStyle.Render("[ Yes ]"),
|
|
||||||
noStyle.Render("[ No ]"),
|
|
||||||
)
|
|
||||||
|
|
||||||
content := confirmBoxStyle.Render(box)
|
|
||||||
|
|
||||||
return lipgloss.Place(m.width, m.height,
|
|
||||||
0.5, 0.5,
|
|
||||||
content,
|
|
||||||
lipgloss.WithWhitespaceBackground(bgDark),
|
|
||||||
lipgloss.WithWhitespaceForeground(dimColor),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderTabMenuOverlay() string {
|
|
||||||
menuStyle := lipgloss.NewStyle().
|
|
||||||
Border(lipgloss.RoundedBorder()).
|
|
||||||
BorderForeground(primaryColor).
|
|
||||||
Background(bgCard).
|
|
||||||
Padding(1, 3)
|
|
||||||
|
|
||||||
tabItemStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(textDimColor).
|
|
||||||
Padding(0, 2)
|
|
||||||
|
|
||||||
tabItemActiveStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Background(primaryColor).
|
|
||||||
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(dimColor).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(roseLightColor).Render(descs[i]))
|
|
||||||
items = append(items, tabItemActiveStyle.Render("▸"+item))
|
|
||||||
} else {
|
|
||||||
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(dimColor).Render(descs[i]))
|
|
||||||
items = append(items, tabItemStyle.Render(" "+item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header := lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Switch Tab")
|
|
||||||
content := header + "\n\n" +
|
|
||||||
strings.Join(items, "\n") +
|
|
||||||
"\n\n" +
|
|
||||||
lipgloss.NewStyle().Foreground(dimColor).Render("↑↓ navigate · enter select · esc cancel")
|
|
||||||
|
|
||||||
box := menuStyle.Render(content)
|
|
||||||
|
|
||||||
return lipgloss.Place(m.width, m.height,
|
|
||||||
0.5, 0.5,
|
|
||||||
box,
|
|
||||||
lipgloss.WithWhitespaceBackground(bgDark),
|
|
||||||
lipgloss.WithWhitespaceForeground(dimColor),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) handlePreview(files []previewFile) {
|
func (m *Model) handlePreview(files []previewFile) {
|
||||||
dir := filepath.Join(os.TempDir(), "muyue-preview")
|
dir := filepath.Join(os.TempDir(), "muyue-preview")
|
||||||
os.RemoveAll(dir)
|
os.RemoveAll(dir)
|
||||||
@@ -495,22 +393,3 @@ func (m *Model) handlePreview(files []previewFile) {
|
|||||||
m.chatLog = append(m.chatLog, itemOKStyle.Render(" Preview: http://127.0.0.1:8765"))
|
m.chatLog = append(m.chatLog, itemOKStyle.Render(" Preview: http://127.0.0.1:8765"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderStudioInput() string {
|
|
||||||
if m.chatLoading {
|
|
||||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
|
||||||
inputStyle.Render("⟩ ") + m.spinner.View() + lipgloss.NewStyle().Foreground(mutedColor).Render(" thinking..."),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
cursor := lipgloss.NewStyle().Foreground(primaryColor).Render("▎")
|
|
||||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
|
||||||
inputStyle.Render("⟩ ") + m.chatInput + cursor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderShellInput() string {
|
|
||||||
prompt := lipgloss.NewStyle().Foreground(successColor).Render("❯ ")
|
|
||||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
|
||||||
prompt + m.termInput + lipgloss.NewStyle().Foreground(primaryColor).Render("▎"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (m Model) renderStudio() string {
|
|||||||
func (m Model) renderStudioSidebar(width int) string {
|
func (m Model) renderStudioSidebar(width int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(renderSectionWithIcon("Studio", "◈"))
|
b.WriteString(renderSectionHeader("STUDIO", "[<>]"))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
panels := []struct {
|
panels := []struct {
|
||||||
@@ -38,23 +38,23 @@ func (m Model) renderStudioSidebar(width int) string {
|
|||||||
panel studioPanel
|
panel studioPanel
|
||||||
icon string
|
icon string
|
||||||
}{
|
}{
|
||||||
{"Chat", panelChat, "💬"},
|
{"Chat", panelChat, "[#]"},
|
||||||
{"Agents", panelAgents, "◉"},
|
{"Agents", panelAgents, "[*]"},
|
||||||
{"Workflows", panelWorkflows, "⟐"},
|
{"Workflows", panelWorkflows, "[~]"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range panels {
|
for _, p := range panels {
|
||||||
if m.studioPanel == p.panel {
|
if m.studioPanel == p.panel {
|
||||||
activeStyle := lipgloss.NewStyle().
|
activeStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
Foreground(lipgloss.Color("#FFFFFF")).
|
||||||
Background(primaryColor).
|
Background(cyberRed).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
b.WriteString(activeStyle.Render(p.icon + " " + p.name))
|
b.WriteString(activeStyle.Render(p.icon + " " + p.name))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
inactiveStyle := lipgloss.NewStyle().
|
inactiveStyle := lipgloss.NewStyle().
|
||||||
Foreground(textDimColor).
|
Foreground(textDim).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
b.WriteString(inactiveStyle.Render(p.icon + " " + p.name))
|
b.WriteString(inactiveStyle.Render(p.icon + " " + p.name))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -62,7 +62,7 @@ func (m Model) renderStudioSidebar(width int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", width-4)))
|
b.WriteString(lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", width-4)))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
switch m.studioPanel {
|
switch m.studioPanel {
|
||||||
@@ -78,26 +78,26 @@ func (m Model) renderStudioSidebar(width int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderChatSidebar(b *strings.Builder, width int) {
|
func (m Model) renderChatSidebar(b *strings.Builder, width int) {
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Active Provider"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Active Provider"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
provider := "none"
|
provider := "none"
|
||||||
if m.config != nil {
|
if m.config != nil {
|
||||||
provider = m.config.Profile.Preferences.DefaultAI
|
provider = m.config.Profile.Preferences.DefaultAI
|
||||||
}
|
}
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(roseColor).Bold(true).Render(" " + provider))
|
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" " + provider))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Commands"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Commands"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
cmds := []string{"/plan <goal>", "/help"}
|
cmds := []string{"/plan <goal>", "/help"}
|
||||||
for _, c := range cmds {
|
for _, c := range cmds {
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(" " + c))
|
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(" " + c))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.previewURL != "" {
|
if m.previewURL != "" {
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Preview"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Preview"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(itemOKStyle.Render(" " + m.previewURL))
|
b.WriteString(itemOKStyle.Render(" " + m.previewURL))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -121,43 +121,43 @@ func (m Model) renderAgentsSidebar(b *strings.Builder, width int) {
|
|||||||
var statusIcon string
|
var statusIcon string
|
||||||
switch status {
|
switch status {
|
||||||
case proxy.StatusRunning:
|
case proxy.StatusRunning:
|
||||||
statusIcon = lipgloss.NewStyle().Foreground(warmColor).Render("● running")
|
statusIcon = lipgloss.NewStyle().Foreground(neonRed).Render("[>> running]")
|
||||||
case proxy.StatusStopped:
|
case proxy.StatusStopped:
|
||||||
statusIcon = lipgloss.NewStyle().Foreground(mutedColor).Render("○ stopped")
|
statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[|| stopped]")
|
||||||
case proxy.StatusError:
|
case proxy.StatusError:
|
||||||
statusIcon = lipgloss.NewStyle().Foreground(errorColor).Render("✗ error")
|
statusIcon = lipgloss.NewStyle().Foreground(errorRed).Render("[!! error]")
|
||||||
default:
|
default:
|
||||||
if available {
|
if available {
|
||||||
statusIcon = lipgloss.NewStyle().Foreground(successColor).Render("✓ available")
|
statusIcon = lipgloss.NewStyle().Foreground(successGreen).Render("[OK available]")
|
||||||
} else {
|
} else {
|
||||||
statusIcon = lipgloss.NewStyle().Foreground(dimColor).Render("✗ not installed")
|
statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[-- not installed]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Bold(true).Render(a.name))
|
b.WriteString(lipgloss.NewStyle().Foreground(textBright).Bold(true).Render(a.name))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" %s\n", statusIcon))
|
b.WriteString(fmt.Sprintf(" %s\n", statusIcon))
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s\n", a.tool)))
|
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s\n", a.tool)))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Actions"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Actions"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [c]"))
|
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [c]"))
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Crush"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Crush"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [l]"))
|
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [l]"))
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Claude"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Claude"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
||||||
if m.orch == nil || m.orch.Workflow == nil {
|
if m.orch == nil || m.orch.Workflow == nil {
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("No active workflow."))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("No active workflow."))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("Use /plan <goal> in chat"))
|
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("Use /plan <goal> in chat"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("to start a workflow."))
|
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("to start a workflow."))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -165,13 +165,13 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
|||||||
wf := m.orch.Workflow
|
wf := m.orch.Workflow
|
||||||
|
|
||||||
phaseColors := map[workflow.Phase]lipgloss.Style{
|
phaseColors := map[workflow.Phase]lipgloss.Style{
|
||||||
workflow.PhaseIdle: lipgloss.NewStyle().Foreground(mutedColor),
|
workflow.PhaseIdle: lipgloss.NewStyle().Foreground(textMuted),
|
||||||
workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warningColor).Bold(true),
|
workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warnAmber).Bold(true),
|
||||||
workflow.PhasePlanning: lipgloss.NewStyle().Foreground(roseColor).Bold(true),
|
workflow.PhasePlanning: lipgloss.NewStyle().Foreground(cyberPink).Bold(true),
|
||||||
workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(accentColor).Bold(true),
|
workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(cyberRose).Bold(true),
|
||||||
workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(primaryColor).Bold(true),
|
workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(cyberRed).Bold(true),
|
||||||
workflow.PhaseDone: lipgloss.NewStyle().Foreground(successColor).Bold(true),
|
workflow.PhaseDone: lipgloss.NewStyle().Foreground(successGreen).Bold(true),
|
||||||
workflow.PhaseError: lipgloss.NewStyle().Foreground(errorColor).Bold(true),
|
workflow.PhaseError: lipgloss.NewStyle().Foreground(errorRed).Bold(true),
|
||||||
}
|
}
|
||||||
|
|
||||||
if style, ok := phaseColors[wf.Phase]; ok {
|
if style, ok := phaseColors[wf.Phase]; ok {
|
||||||
@@ -180,9 +180,9 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
|||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
if wf.Plan.Goal != "" {
|
if wf.Plan.Goal != "" {
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Goal"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Goal"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(wf.Plan.Goal))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(wf.Plan.Goal))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
|||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Controls"))
|
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Controls"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
controls := []struct {
|
controls := []struct {
|
||||||
key string
|
key string
|
||||||
@@ -207,8 +207,8 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
|||||||
{"[x]", "Cancel"},
|
{"[x]", "Cancel"},
|
||||||
}
|
}
|
||||||
for _, c := range controls {
|
for _, c := range controls {
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" " + c.key))
|
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" "+c.key))
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(textDimColor).Render(" " + c.desc))
|
b.WriteString(lipgloss.NewStyle().Foreground(textDim).Render(" "+c.desc))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,14 +216,14 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
|||||||
func (m Model) renderStudioChat(width int) string {
|
func (m Model) renderStudioChat(width int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
chatHeader := renderSectionWithIcon("Chat", "💬")
|
chatHeader := renderSectionHeader("CHAT", "[#]")
|
||||||
if m.chatLoading {
|
if m.chatLoading {
|
||||||
chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warningColor).Render("thinking...")
|
chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warnAmber).Render("thinking...")
|
||||||
}
|
}
|
||||||
b.WriteString(chatHeader)
|
b.WriteString(chatHeader)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10)))
|
sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
|
||||||
b.WriteString(" " + sep)
|
b.WriteString(" " + sep)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
@@ -235,6 +235,18 @@ func (m Model) renderStudioChat(width int) string {
|
|||||||
return b.String()
|
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) {
|
func (m Model) handleStudioPanelSwitch(panel studioPanel) {
|
||||||
m.studioPanel = panel
|
m.studioPanel = panel
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
|||||||
@@ -5,127 +5,192 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
primaryColor = lipgloss.Color("#E8364F")
|
cyberRed = lipgloss.Color("#FF0033")
|
||||||
roseColor = lipgloss.Color("#FF6B8A")
|
cyberRedDark = lipgloss.Color("#8B0020")
|
||||||
roseLightColor = lipgloss.Color("#FFB3C6")
|
cyberRedDeep = lipgloss.Color("#5C0015")
|
||||||
accentColor = lipgloss.Color("#FF8FA3")
|
cyberPink = lipgloss.Color("#FF1A5E")
|
||||||
warmColor = lipgloss.Color("#FF4D6D")
|
cyberRose = lipgloss.Color("#FF4D6D")
|
||||||
successColor = lipgloss.Color("#4ADE80")
|
neonRed = lipgloss.Color("#FF1744")
|
||||||
warningColor = lipgloss.Color("#FBBF24")
|
brightRed = lipgloss.Color("#FF5252")
|
||||||
errorColor = lipgloss.Color("#FF4D4D")
|
dimRed = lipgloss.Color("#6B2033")
|
||||||
mutedColor = lipgloss.Color("#8B7E8E")
|
mutedRed = lipgloss.Color("#4A1525")
|
||||||
dimColor = lipgloss.Color("#5A4F5E")
|
|
||||||
textColor = lipgloss.Color("#F0E6E8")
|
|
||||||
textDimColor = lipgloss.Color("#B8A9AD")
|
|
||||||
|
|
||||||
bgDark = lipgloss.Color("#0D0A0B")
|
textBright = lipgloss.Color("#EAE0E2")
|
||||||
bgPanel = lipgloss.Color("#1A1215")
|
textMain = lipgloss.Color("#D4C4C8")
|
||||||
bgCard = lipgloss.Color("#231A1D")
|
textDim = lipgloss.Color("#8A7A7E")
|
||||||
bgInput = lipgloss.Color("#2A2023")
|
textMuted = lipgloss.Color("#5A4F52")
|
||||||
bgHover = lipgloss.Color("#332528")
|
|
||||||
|
|
||||||
borderColor = lipgloss.Color("#3D2E32")
|
successGreen = lipgloss.Color("#00E676")
|
||||||
borderAccent = lipgloss.Color("#E8364F")
|
warnAmber = lipgloss.Color("#FFD740")
|
||||||
|
errorRed = lipgloss.Color("#FF1744")
|
||||||
|
|
||||||
tabActiveBg = lipgloss.Color("#E8364F")
|
bgVoid = lipgloss.Color("#0A0A0C")
|
||||||
tabInactiveBg = lipgloss.Color("#1A1215")
|
bgBase = lipgloss.Color("#0F0D10")
|
||||||
|
bgSurface = lipgloss.Color("#161218")
|
||||||
|
bgPanel = lipgloss.Color("#1C1719")
|
||||||
|
bgCard = lipgloss.Color("#221B1E")
|
||||||
|
bgInput = lipgloss.Color("#2A2225")
|
||||||
|
|
||||||
sectionStyle = lipgloss.NewStyle().
|
borderDim = lipgloss.Color("#2A1F22")
|
||||||
Foreground(roseColor).
|
borderRed = lipgloss.Color("#FF003344")
|
||||||
|
borderRedFull = lipgloss.Color("#FF0033")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseStyle = lipgloss.NewStyle()
|
||||||
|
|
||||||
|
titleBlockStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(cyberRed).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
sectionIconStyle = lipgloss.NewStyle().
|
sectionTitleStyle = lipgloss.NewStyle().
|
||||||
Foreground(primaryColor).
|
Foreground(cyberRed).
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
itemOKStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(successColor)
|
|
||||||
|
|
||||||
itemMissingStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(errorColor)
|
|
||||||
|
|
||||||
itemWarnStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(warningColor)
|
|
||||||
|
|
||||||
itemPendingStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(mutedColor)
|
|
||||||
|
|
||||||
userMsgStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(roseLightColor)
|
|
||||||
|
|
||||||
aiMsgStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(textColor)
|
|
||||||
|
|
||||||
errMsgStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(errorColor)
|
|
||||||
|
|
||||||
inputStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(roseColor)
|
|
||||||
|
|
||||||
stepDoneStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(successColor)
|
|
||||||
|
|
||||||
stepPendingStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(mutedColor)
|
|
||||||
|
|
||||||
stepCurrentStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(primaryColor).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
stepErrorStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(errorColor)
|
|
||||||
|
|
||||||
statusBarStyle = lipgloss.NewStyle().
|
|
||||||
Background(bgPanel).
|
|
||||||
Foreground(textDimColor).
|
|
||||||
Padding(0, 1)
|
|
||||||
|
|
||||||
confirmBoxStyle = lipgloss.NewStyle().
|
|
||||||
Border(lipgloss.RoundedBorder()).
|
|
||||||
BorderForeground(primaryColor).
|
|
||||||
Background(bgCard).
|
|
||||||
Foreground(textColor).
|
|
||||||
Padding(1, 3).
|
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
confirmYesStyle = lipgloss.NewStyle().
|
labelStyle = lipgloss.NewStyle().
|
||||||
Foreground(successColor).
|
Foreground(textDim).
|
||||||
Bold(true)
|
Width(14)
|
||||||
|
|
||||||
confirmNoStyle = lipgloss.NewStyle().
|
valueStyle = lipgloss.NewStyle().
|
||||||
Foreground(mutedColor)
|
Foreground(textMain)
|
||||||
|
|
||||||
cardStyle = lipgloss.NewStyle().
|
cardStyle = lipgloss.NewStyle().
|
||||||
Background(bgCard).
|
Background(bgCard).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(borderColor).
|
BorderForeground(borderDim).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
cardActiveStyle = lipgloss.NewStyle().
|
||||||
|
Background(bgCard).
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(cyberRed).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
sidebarStyle = lipgloss.NewStyle().
|
sidebarStyle = lipgloss.NewStyle().
|
||||||
Background(bgPanel).
|
Background(bgSurface).
|
||||||
Border(lipgloss.Border{Right: "│"}).
|
Border(lipgloss.Border{Right: "│"}).
|
||||||
BorderForeground(borderColor).
|
BorderForeground(borderDim).
|
||||||
Padding(0, 1)
|
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().
|
badgeStyle = lipgloss.NewStyle().
|
||||||
Background(primaryColor).
|
Background(cyberRed).
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
Foreground(lipgloss.Color("#FFFFFF")).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
labelStyle = lipgloss.NewStyle().
|
tabBarStyle = lipgloss.NewStyle().Background(bgSurface)
|
||||||
Foreground(mutedColor).
|
|
||||||
Width(14)
|
|
||||||
|
|
||||||
valueStyle = lipgloss.NewStyle().
|
stepDoneStyle = lipgloss.NewStyle().Foreground(successGreen)
|
||||||
Foreground(textColor)
|
stepPendingStyle = lipgloss.NewStyle().Foreground(textMuted)
|
||||||
|
stepCurrentStyle = lipgloss.NewStyle().Foreground(cyberRed).Bold(true)
|
||||||
tabBarStyle = lipgloss.NewStyle().
|
stepErrorStyle = lipgloss.NewStyle().Foreground(errorRed)
|
||||||
Background(bgPanel)
|
|
||||||
|
|
||||||
pulseFrames = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getAnimFrame(frame int) string {
|
var logoLines = []string{
|
||||||
return pulseFrames[frame%len(pulseFrames)]
|
"███╗ ███╗██╗ ██╗ █████╗ ███╗ ██╗███████╗",
|
||||||
|
"████╗ ████║╚██╗ ██╔╝██╔══██╗████╗ ██║██╔════╝",
|
||||||
|
"██╔████╔██║ ╚████╔╝ ███████║██╔██╗ ██║███████╗",
|
||||||
|
"██║╚██╔╝██║ ╚██╔╝ ██╔══██║██║╚██╗██║╚════██║",
|
||||||
|
"██║ ╚═╝ ██║ ██║ ██║ ██║██║ ╚████║███████║",
|
||||||
|
"╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ func (m Model) renderShell() string {
|
|||||||
func (m Model) renderTermPanel(width int) string {
|
func (m Model) renderTermPanel(width int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
cwdStyle := lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd)
|
cwdStyle := lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd)
|
||||||
b.WriteString(renderSectionWithIcon("Terminal", "▶"))
|
b.WriteString(renderSectionHeader("TERMINAL", "[$]"))
|
||||||
b.WriteString(" ")
|
b.WriteString(" ")
|
||||||
b.WriteString(cwdStyle)
|
b.WriteString(cwdStyle)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10)))
|
sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
|
||||||
b.WriteString(" " + sep)
|
b.WriteString(" " + sep)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
@@ -74,10 +74,10 @@ func (m Model) renderTermPanel(width int) string {
|
|||||||
func (m Model) renderAIPanel(width int) string {
|
func (m Model) renderAIPanel(width int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(renderSectionWithIcon("AI Assistant", "◈"))
|
b.WriteString(renderSectionHeader("AI ASSISTANT", "[?]"))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10)))
|
sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
|
||||||
b.WriteString(" " + sep)
|
b.WriteString(" " + sep)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
@@ -87,30 +87,37 @@ func (m Model) renderAIPanel(width int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.termAILoading {
|
if m.termAILoading {
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(warmColor).Render(" " + getAnimFrame(m.animationFrame) + " thinking..."))
|
b.WriteString(lipgloss.NewStyle().Foreground(neonRed).Render(" " + getAnimFrame(m.animationFrame) + " thinking..."))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
inputLabel := lipgloss.NewStyle().Foreground(roseColor).Render("⟩ ")
|
inputLabel := lipgloss.NewStyle().Foreground(cyberRed).Render(">> ")
|
||||||
b.WriteString(inputLabel)
|
b.WriteString(inputLabel)
|
||||||
b.WriteString(m.termAIInput)
|
b.WriteString(m.termAIInput)
|
||||||
|
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Background(bgPanel).
|
Background(bgSurface).
|
||||||
Border(lipgloss.Border{Left: "│"}).
|
Border(lipgloss.Border{Left: "│"}).
|
||||||
BorderForeground(borderColor).
|
BorderForeground(borderDim).
|
||||||
Width(width).
|
Width(width).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Render(b.String())
|
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) {
|
func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
if m.termCmd != nil && m.termCmd.Process != nil {
|
if m.termCmd != nil && m.termCmd.Process != nil {
|
||||||
m.termCmd.Process.Kill()
|
m.termCmd.Process.Kill()
|
||||||
m.termRunning = false
|
m.termRunning = false
|
||||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorColor).Render("^C"))
|
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorRed).Render("^C"))
|
||||||
m.termCmd = nil
|
m.termCmd = nil
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -151,7 +158,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if isDangerousCommand(input) {
|
if isDangerousCommand(input) {
|
||||||
m.termLog = append(m.termLog, errMsgStyle.Render(" blocked: potentially dangerous command"))
|
m.termLog = append(m.termLog, errMsgStyle.Render(" [BLOCKED] potentially dangerous command"))
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -165,7 +172,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
if err := os.Chdir(dir); err == nil {
|
if err := os.Chdir(dir); err == nil {
|
||||||
m.termCwd, _ = os.Getwd()
|
m.termCwd, _ = os.Getwd()
|
||||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ "+input))
|
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input)
|
||||||
} else {
|
} else {
|
||||||
m.termLog = append(m.termLog, errMsgStyle.Render(" cd: "+err.Error()))
|
m.termLog = append(m.termLog, errMsgStyle.Render(" cd: "+err.Error()))
|
||||||
}
|
}
|
||||||
@@ -173,7 +180,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ ")+input)
|
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input)
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
return m, m.runTermCommand(input)
|
return m, m.runTermCommand(input)
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const (
|
|||||||
tabCount
|
tabCount
|
||||||
)
|
)
|
||||||
|
|
||||||
var tabNames = []string{"Dashboard", "Studio", "Shell", "Config"}
|
var tabNames = []string{"DASH", "STUDIO", "SHELL", "CONFIG"}
|
||||||
var tabIcons = []string{"◉", "◈", "▶", "⚙"}
|
var tabIcons = []string{"[■]", "[<>]", "[>$]", "[//]"}
|
||||||
|
|
||||||
type aiResponseMsg struct{ content string }
|
type aiResponseMsg struct{ content string }
|
||||||
type aiErrMsg struct{ err error }
|
type aiErrMsg struct{ err error }
|
||||||
@@ -61,6 +61,9 @@ type spinnerTickMsg struct{ time time.Time }
|
|||||||
type termOutputMsg struct{ line string }
|
type termOutputMsg struct{ line string }
|
||||||
type termExitMsg struct{}
|
type termExitMsg struct{}
|
||||||
type animTickMsg struct{ time time.Time }
|
type animTickMsg struct{ time time.Time }
|
||||||
|
type clockTickMsg struct{ time time.Time }
|
||||||
|
type glitchDoneMsg struct{}
|
||||||
|
type scanDoneMsg struct{}
|
||||||
|
|
||||||
type studioPanel int
|
type studioPanel int
|
||||||
|
|
||||||
@@ -79,10 +82,20 @@ const (
|
|||||||
configSkills
|
configSkills
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type transitionState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
transitionNone transitionState = iota
|
||||||
|
transitionGlitch
|
||||||
|
transitionScan
|
||||||
|
transitionTypewriter
|
||||||
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
config *config.MuyueConfig
|
config *config.MuyueConfig
|
||||||
scanResult *scanner.ScanResult
|
scanResult *scanner.ScanResult
|
||||||
activeTab tab
|
activeTab tab
|
||||||
|
prevTab tab
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
@@ -126,7 +139,7 @@ type Model struct {
|
|||||||
termRunning bool
|
termRunning bool
|
||||||
termCwd string
|
termCwd string
|
||||||
|
|
||||||
studioPanel studioPanel
|
studioPanel studioPanel
|
||||||
studioSidebarOpen bool
|
studioSidebarOpen bool
|
||||||
|
|
||||||
termAIChat []string
|
termAIChat []string
|
||||||
@@ -138,6 +151,12 @@ type Model struct {
|
|||||||
configField int
|
configField int
|
||||||
|
|
||||||
animationFrame int
|
animationFrame int
|
||||||
|
|
||||||
|
transition transitionState
|
||||||
|
transitionTick int
|
||||||
|
typewriterBuf string
|
||||||
|
typewriterPos int
|
||||||
|
currentTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyMap struct {
|
type keyMap struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user