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