Files
MuyueWorkspace/internal/tui/studio.go
Augustin ROUX cb8e3d0d26
Some checks failed
Stable Release / stable (push) Failing after 22s
feat: complete TUI redesign with cyberpunk theme (#1)
- Dark theme with red accents (cyberpunk aesthetic)
- Epuré cyberpunk style: clean dark backgrounds, sharp red highlights
- Full cyberpunk animations: glitch effect, scan line, typewriter
- Mixed Unicode + ASCII icons
- Rounded borders (╭ ╮ ╯ ╰) on cards and panels
- ASCII art block titles (■) with red styling
- Header: MUYUE branding, status indicators, live clock
- Footer: shortcuts, version, update indicator
- Tab transitions: glitch → scan → typewriter sequence
- Extracted header.go, footer.go, animations.go as new files

Controls unchanged: ctrl+t tabs, ctrl+s sidebar, ctrl+a AI panel

file changes:
- styles.go: new color palette (cyberRed, bgVoid, dimRed), block titles
- types.go: added transition state, clock tick, glitch/scan/done messages
- animations.go: new file with glitch, scan, typewriter, hex stream effects
- header.go: new file with logo, tabs, status dots, live clock
- footer.go: new file with shortcuts, version, update indicator
- model.go: integrated transition state machine, clock updates
- dashboard.go, studio.go, terminal.go, config_tab.go: updated icons/styles

Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>

Co-authored-by: Augustin <muyue@legion-muyue.fr>
Reviewed-on: #1
2026-04-20 20:17:06 +00:00

254 lines
7.5 KiB
Go

package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/muyue/muyue/internal/proxy"
"github.com/muyue/muyue/internal/workflow"
)
func (m Model) renderStudio() string {
if m.studioSidebarOpen {
sidebarWidth := 28
chatWidth := m.width - sidebarWidth - 2
if chatWidth < 20 {
chatWidth = 20
sidebarWidth = m.width - chatWidth - 2
}
sidebar := m.renderStudioSidebar(sidebarWidth)
chat := m.renderStudioChat(chatWidth)
return lipgloss.JoinHorizontal(lipgloss.Top, sidebar, chat)
}
return m.renderStudioChat(m.width)
}
func (m Model) renderStudioSidebar(width int) string {
var b strings.Builder
b.WriteString(renderSectionHeader("STUDIO", "[<>]"))
b.WriteString("\n\n")
panels := []struct {
name string
panel studioPanel
icon string
}{
{"Chat", panelChat, "[#]"},
{"Agents", panelAgents, "[*]"},
{"Workflows", panelWorkflows, "[~]"},
}
for _, p := range panels {
if m.studioPanel == p.panel {
activeStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(cyberRed).
Bold(true).
Padding(0, 1)
b.WriteString(activeStyle.Render(p.icon + " " + p.name))
b.WriteString("\n")
} else {
inactiveStyle := lipgloss.NewStyle().
Foreground(textDim).
Padding(0, 1)
b.WriteString(inactiveStyle.Render(p.icon + " " + p.name))
b.WriteString("\n")
}
}
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", width-4)))
b.WriteString("\n\n")
switch m.studioPanel {
case panelAgents:
m.renderAgentsSidebar(&b, width)
case panelWorkflows:
m.renderWorkflowSidebar(&b, width)
default:
m.renderChatSidebar(&b, width)
}
return sidebarStyle.Width(width).Render(b.String())
}
func (m Model) renderChatSidebar(b *strings.Builder, width int) {
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Active Provider"))
b.WriteString("\n")
provider := "none"
if m.config != nil {
provider = m.config.Profile.Preferences.DefaultAI
}
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" " + provider))
b.WriteString("\n\n")
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Commands"))
b.WriteString("\n")
cmds := []string{"/plan <goal>", "/help"}
for _, c := range cmds {
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(" " + c))
b.WriteString("\n")
}
if m.previewURL != "" {
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Preview"))
b.WriteString("\n")
b.WriteString(itemOKStyle.Render(" " + m.previewURL))
b.WriteString("\n")
}
}
func (m Model) renderAgentsSidebar(b *strings.Builder, width int) {
agents := []struct {
name string
agentType proxy.AgentType
tool string
}{
{"Crush", proxy.AgentCrush, "GLM"},
{"Claude Code", proxy.AgentClaude, "Anthropic"},
}
for _, a := range agents {
status, _ := m.proxyMgr.Status(a.agentType)
available := m.proxyMgr.IsAvailable(a.agentType)
var statusIcon string
switch status {
case proxy.StatusRunning:
statusIcon = lipgloss.NewStyle().Foreground(neonRed).Render("[>> running]")
case proxy.StatusStopped:
statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[|| stopped]")
case proxy.StatusError:
statusIcon = lipgloss.NewStyle().Foreground(errorRed).Render("[!! error]")
default:
if available {
statusIcon = lipgloss.NewStyle().Foreground(successGreen).Render("[OK available]")
} else {
statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[-- not installed]")
}
}
b.WriteString(lipgloss.NewStyle().Foreground(textBright).Bold(true).Render(a.name))
b.WriteString("\n")
b.WriteString(fmt.Sprintf(" %s\n", statusIcon))
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s\n", a.tool)))
b.WriteString("\n")
}
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Actions"))
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [c]"))
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Crush"))
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [l]"))
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Claude"))
b.WriteString("\n")
}
func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
if m.orch == nil || m.orch.Workflow == nil {
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("No active workflow."))
b.WriteString("\n\n")
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("Use /plan <goal> in chat"))
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("to start a workflow."))
b.WriteString("\n")
return
}
wf := m.orch.Workflow
phaseColors := map[workflow.Phase]lipgloss.Style{
workflow.PhaseIdle: lipgloss.NewStyle().Foreground(textMuted),
workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warnAmber).Bold(true),
workflow.PhasePlanning: lipgloss.NewStyle().Foreground(cyberPink).Bold(true),
workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(cyberRose).Bold(true),
workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(cyberRed).Bold(true),
workflow.PhaseDone: lipgloss.NewStyle().Foreground(successGreen).Bold(true),
workflow.PhaseError: lipgloss.NewStyle().Foreground(errorRed).Bold(true),
}
if style, ok := phaseColors[wf.Phase]; ok {
b.WriteString(style.Render(string(wf.Phase)))
}
b.WriteString("\n\n")
if wf.Plan.Goal != "" {
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Goal"))
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(wf.Plan.Goal))
b.WriteString("\n\n")
}
if wf.Phase == workflow.PhaseExecuting {
done, total := wf.Progress()
m.progressBar.SetPercent(float64(done) / float64(max(total, 1)))
b.WriteString(m.progressBar.View())
b.WriteString(fmt.Sprintf(" %d/%d", done, total))
b.WriteString("\n\n")
}
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Controls"))
b.WriteString("\n")
controls := []struct {
key string
desc string
}{
{"[a]", "Approve plan"},
{"[r]", "Reject plan"},
{"[g]", "Generate plan"},
{"[n]", "Next step"},
{"[x]", "Cancel"},
}
for _, c := range controls {
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" "+c.key))
b.WriteString(lipgloss.NewStyle().Foreground(textDim).Render(" "+c.desc))
b.WriteString("\n")
}
}
func (m Model) renderStudioChat(width int) string {
var b strings.Builder
chatHeader := renderSectionHeader("CHAT", "[#]")
if m.chatLoading {
chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warnAmber).Render("thinking...")
}
b.WriteString(chatHeader)
b.WriteString("\n\n")
sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
b.WriteString(" " + sep)
b.WriteString("\n\n")
for _, msg := range m.chatLog {
b.WriteString(msg)
b.WriteString("\n\n")
}
return b.String()
}
func (m Model) renderStudioInput() string {
if m.chatLoading {
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
inputStyle.Render(">> ") + m.spinner.View() + lipgloss.NewStyle().Foreground(textMuted).Render(" thinking..."),
)
}
cursor := lipgloss.NewStyle().Foreground(cyberRed).Render("▎")
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
inputStyle.Render(">> ") + m.chatInput + cursor,
)
}
func (m Model) handleStudioPanelSwitch(panel studioPanel) {
m.studioPanel = panel
m.viewport.SetContent(m.renderContent())
}