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 ", "/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 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()) }