feat: Ctrl+T tab switcher, minimal header, integrated terminal
All checks were successful
CI / build (push) Successful in 1m43s
All checks were successful
CI / build (push) Successful in 1m43s
- Fix Ctrl+M → Ctrl+T (Ctrl+M is Enter in terminals) - Simplify header to single line: muyue v0.2.0 · Dashboard - Add Terminal tab with shell command execution - Run commands, cd, clear, ctrl+c to kill process - Shows CWD and command history - 6 tabs now: Dashboard, Chat, Workflow, Terminal, Agents, Config 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -83,7 +83,7 @@ Commands:
|
|||||||
help Show this help
|
help Show this help
|
||||||
|
|
||||||
TUI Controls:
|
TUI Controls:
|
||||||
Ctrl+M Open tab switcher (navigate with arrows, select with enter)
|
Ctrl+T Open tab switcher (navigate with arrows, select with enter)
|
||||||
Tab / Shift+Tab Cycle tabs
|
Tab / Shift+Tab Cycle tabs
|
||||||
Ctrl+C Show quit confirmation (press twice quickly to force quit)
|
Ctrl+C Show quit confirmation (press twice quickly to force quit)
|
||||||
|
|
||||||
|
|||||||
@@ -37,42 +37,26 @@ const (
|
|||||||
tabDashboard tab = iota
|
tabDashboard tab = iota
|
||||||
tabChat
|
tabChat
|
||||||
tabWorkflow
|
tabWorkflow
|
||||||
|
tabTerminal
|
||||||
tabAgents
|
tabAgents
|
||||||
tabConfig
|
tabConfig
|
||||||
tabCount
|
tabCount
|
||||||
)
|
)
|
||||||
|
|
||||||
var tabNames = []string{"Dashboard", "Chat", "Workflow", "Agents", "Config"}
|
var tabNames = []string{"Dashboard", "Chat", "Workflow", "Terminal", "Agents", "Config"}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
baseColor = lipgloss.Color("#FF6B9D")
|
baseColor = lipgloss.Color("#FF6B9D")
|
||||||
accentColor = lipgloss.Color("#A0D2FF")
|
accentColor = lipgloss.Color("#A0D2FF")
|
||||||
aiColor = lipgloss.Color("#C4B5FD")
|
aiColor = lipgloss.Color("#C4B5FD")
|
||||||
successColor = lipgloss.Color("#4ADE80")
|
successColor = lipgloss.Color("#4ADE80")
|
||||||
warningColor = lipgloss.Color("#FBBF24")
|
warningColor = lipgloss.Color("#FBBF24")
|
||||||
errorColor = lipgloss.Color("#FF6B6B")
|
errorColor = lipgloss.Color("#FF6B6B")
|
||||||
mutedColor = lipgloss.Color("#666680")
|
mutedColor = lipgloss.Color("#666680")
|
||||||
dimColor = lipgloss.Color("#444460")
|
dimColor = lipgloss.Color("#444460")
|
||||||
bgDark = lipgloss.Color("#1A1A2E")
|
bgDark = lipgloss.Color("#1A1A2E")
|
||||||
bgPanel = lipgloss.Color("#16213E")
|
bgPanel = lipgloss.Color("#16213E")
|
||||||
bgCard = lipgloss.Color("#1F2937")
|
bgCard = lipgloss.Color("#1F2937")
|
||||||
|
|
||||||
titleStyle = lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#FF6B9D")).
|
|
||||||
Background(bgDark).
|
|
||||||
Padding(0, 2)
|
|
||||||
|
|
||||||
tabStyle = lipgloss.NewStyle().
|
|
||||||
Padding(0, 2).
|
|
||||||
Foreground(mutedColor)
|
|
||||||
|
|
||||||
activeTabStyle = lipgloss.NewStyle().
|
|
||||||
Padding(0, 2).
|
|
||||||
Foreground(baseColor).
|
|
||||||
Border(lipgloss.NormalBorder(), false, false, true, false).
|
|
||||||
BorderForeground(baseColor).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
sectionStyle = lipgloss.NewStyle().
|
sectionStyle = lipgloss.NewStyle().
|
||||||
Foreground(accentColor).
|
Foreground(accentColor).
|
||||||
@@ -102,10 +86,6 @@ var (
|
|||||||
inputStyle = lipgloss.NewStyle().
|
inputStyle = lipgloss.NewStyle().
|
||||||
Foreground(baseColor)
|
Foreground(baseColor)
|
||||||
|
|
||||||
phaseStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(warningColor).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
stepDoneStyle = lipgloss.NewStyle().
|
stepDoneStyle = lipgloss.NewStyle().
|
||||||
Foreground(successColor)
|
Foreground(successColor)
|
||||||
|
|
||||||
@@ -119,24 +99,18 @@ var (
|
|||||||
stepErrorStyle = lipgloss.NewStyle().
|
stepErrorStyle = lipgloss.NewStyle().
|
||||||
Foreground(errorColor)
|
Foreground(errorColor)
|
||||||
|
|
||||||
cardStyle = lipgloss.NewStyle().
|
|
||||||
Border(lipgloss.RoundedBorder()).
|
|
||||||
BorderForeground(dimColor).
|
|
||||||
Padding(0, 1).
|
|
||||||
BorderBackground(bgPanel)
|
|
||||||
|
|
||||||
statusBarStyle = lipgloss.NewStyle().
|
statusBarStyle = lipgloss.NewStyle().
|
||||||
Background(bgDark).
|
Background(bgDark).
|
||||||
Foreground(lipgloss.Color("#A0A0B0")).
|
Foreground(lipgloss.Color("#A0A0B0")).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
confirmBoxStyle = lipgloss.NewStyle().
|
confirmBoxStyle = lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(baseColor).
|
BorderForeground(baseColor).
|
||||||
Background(bgCard).
|
Background(bgCard).
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
Foreground(lipgloss.Color("#FFFFFF")).
|
||||||
Padding(1, 3).
|
Padding(1, 3).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
confirmYesStyle = lipgloss.NewStyle().
|
confirmYesStyle = lipgloss.NewStyle().
|
||||||
Foreground(successColor).
|
Foreground(successColor).
|
||||||
@@ -144,19 +118,6 @@ var (
|
|||||||
|
|
||||||
confirmNoStyle = lipgloss.NewStyle().
|
confirmNoStyle = lipgloss.NewStyle().
|
||||||
Foreground(mutedColor)
|
Foreground(mutedColor)
|
||||||
|
|
||||||
gradientStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF6B9D"))
|
|
||||||
|
|
||||||
logoStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(baseColor).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
versionBadgeStyle = lipgloss.NewStyle().
|
|
||||||
Background(baseColor).
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Padding(0, 1).
|
|
||||||
Bold(true)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type aiResponseMsg struct{ content string }
|
type aiResponseMsg struct{ content string }
|
||||||
@@ -180,6 +141,9 @@ type skillsListMsg struct{ skills []skills.Skill }
|
|||||||
|
|
||||||
type spinnerTickMsg struct{ time time.Time }
|
type spinnerTickMsg struct{ time time.Time }
|
||||||
|
|
||||||
|
type termOutputMsg struct{ line string }
|
||||||
|
type termExitMsg struct{}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
config *config.MuyueConfig
|
config *config.MuyueConfig
|
||||||
scanResult *scanner.ScanResult
|
scanResult *scanner.ScanResult
|
||||||
@@ -202,22 +166,28 @@ type Model struct {
|
|||||||
mcpConfigured bool
|
mcpConfigured bool
|
||||||
skillList []skills.Skill
|
skillList []skills.Skill
|
||||||
|
|
||||||
helpModel help.Model
|
helpModel help.Model
|
||||||
progressBar progress.Model
|
progressBar progress.Model
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
|
|
||||||
showingQuit bool
|
showingQuit bool
|
||||||
confirmCursor int
|
confirmCursor int
|
||||||
showingTabMenu bool
|
showingTabMenu bool
|
||||||
tabMenuCursor int
|
tabMenuCursor int
|
||||||
|
|
||||||
ctrlCCount int
|
ctrlCCount int
|
||||||
lastCtrlC time.Time
|
lastCtrlC time.Time
|
||||||
|
|
||||||
installing bool
|
installing bool
|
||||||
installCurrent int
|
installCurrent int
|
||||||
installTotal int
|
installTotal int
|
||||||
installTool string
|
installTool string
|
||||||
|
|
||||||
|
termCmd *exec.Cmd
|
||||||
|
termInput string
|
||||||
|
termLog []string
|
||||||
|
termRunning bool
|
||||||
|
termCwd string
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyMap struct {
|
type keyMap struct {
|
||||||
@@ -241,7 +211,7 @@ var keys = keyMap{
|
|||||||
),
|
),
|
||||||
Prev: key.NewBinding(
|
Prev: key.NewBinding(
|
||||||
key.WithKeys("shift+tab"),
|
key.WithKeys("shift+tab"),
|
||||||
key.WithHelp("⇧+tab", "prev tab"),
|
key.WithHelp("shift+tab", "prev tab"),
|
||||||
),
|
),
|
||||||
Quit: key.NewBinding(
|
Quit: key.NewBinding(
|
||||||
key.WithKeys("ctrl+c"),
|
key.WithKeys("ctrl+c"),
|
||||||
@@ -256,8 +226,8 @@ var keys = keyMap{
|
|||||||
key.WithHelp("n/esc", "no"),
|
key.WithHelp("n/esc", "no"),
|
||||||
),
|
),
|
||||||
TabMenu: key.NewBinding(
|
TabMenu: key.NewBinding(
|
||||||
key.WithKeys("ctrl+m"),
|
key.WithKeys("ctrl+t"),
|
||||||
key.WithHelp("ctrl+m", "switch tab"),
|
key.WithHelp("ctrl+t", "switch tab"),
|
||||||
),
|
),
|
||||||
Install: key.NewBinding(
|
Install: key.NewBinding(
|
||||||
key.WithKeys("i"),
|
key.WithKeys("i"),
|
||||||
@@ -277,7 +247,7 @@ var keys = keyMap{
|
|||||||
),
|
),
|
||||||
Backspace: key.NewBinding(
|
Backspace: key.NewBinding(
|
||||||
key.WithKeys("backspace"),
|
key.WithKeys("backspace"),
|
||||||
key.WithHelp("⌫", "delete"),
|
key.WithHelp("backspace", "delete"),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +286,8 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
|||||||
|
|
||||||
prog := progress.New(progress.WithGradient("#FF6B9D", "#A0D2FF"))
|
prog := progress.New(progress.WithGradient("#FF6B9D", "#A0D2FF"))
|
||||||
|
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
scanResult: scan,
|
scanResult: scan,
|
||||||
@@ -339,6 +311,7 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
|||||||
confirmCursor: 1,
|
confirmCursor: 1,
|
||||||
showingTabMenu: false,
|
showingTabMenu: false,
|
||||||
tabMenuCursor: 0,
|
tabMenuCursor: 0,
|
||||||
|
termCwd: cwd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +331,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
pm, cmd := m.progressBar.Update(msg)
|
pm, cmd := m.progressBar.Update(msg)
|
||||||
m.progressBar = pm.(progress.Model)
|
m.progressBar = pm.(progress.Model)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
case termOutputMsg:
|
||||||
|
m.termLog = append(m.termLog, msg.line)
|
||||||
|
if m.activeTab == tabTerminal {
|
||||||
|
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.termCmd = nil
|
||||||
|
if m.activeTab == tabTerminal {
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case aiResponseMsg:
|
case aiResponseMsg:
|
||||||
m.chatLoading = false
|
m.chatLoading = false
|
||||||
content := msg.content
|
content := msg.content
|
||||||
@@ -449,12 +437,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
m.helpModel.Width = msg.Width
|
m.helpModel.Width = msg.Width
|
||||||
headerH := 4
|
headerH := 1
|
||||||
footerH := 2
|
footerH := 2
|
||||||
inputH := 0
|
inputH := 0
|
||||||
if m.activeTab == tabChat || m.activeTab == tabWorkflow {
|
if m.activeTab == tabChat || m.activeTab == tabWorkflow {
|
||||||
inputH = 2
|
inputH = 2
|
||||||
}
|
}
|
||||||
|
if m.activeTab == tabTerminal {
|
||||||
|
inputH = 2
|
||||||
|
}
|
||||||
contentH := msg.Height - headerH - footerH - inputH
|
contentH := msg.Height - headerH - footerH - inputH
|
||||||
if contentH < 1 {
|
if contentH < 1 {
|
||||||
contentH = 1
|
contentH = 1
|
||||||
@@ -478,6 +469,10 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m.handleTabMenu(msg)
|
return m.handleTabMenu(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.activeTab == tabTerminal {
|
||||||
|
return m.handleTerminalKey(msg)
|
||||||
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -490,7 +485,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.confirmCursor = 1
|
m.confirmCursor = 1
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, nil
|
return m, nil
|
||||||
case "ctrl+m":
|
case "ctrl+t":
|
||||||
m.showingTabMenu = true
|
m.showingTabMenu = true
|
||||||
m.tabMenuCursor = int(m.activeTab)
|
m.tabMenuCursor = int(m.activeTab)
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
@@ -532,6 +527,108 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) handleTerminalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c":
|
||||||
|
if m.termCmd != nil && m.termCmd.Process != nil {
|
||||||
|
m.termCmd.Process.Kill()
|
||||||
|
m.termRunning = false
|
||||||
|
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorColor).Render("^C"))
|
||||||
|
m.termCmd = nil
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
if m.ctrlCCount > 0 && now.Sub(m.lastCtrlC) < 2*time.Second {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
m.ctrlCCount++
|
||||||
|
m.lastCtrlC = now
|
||||||
|
m.showingQuit = true
|
||||||
|
m.confirmCursor = 1
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
return m, nil
|
||||||
|
case "ctrl+t":
|
||||||
|
m.showingTabMenu = true
|
||||||
|
m.tabMenuCursor = int(m.activeTab)
|
||||||
|
return m, nil
|
||||||
|
case "enter":
|
||||||
|
if m.termRunning {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
input := strings.TrimSpace(m.termInput)
|
||||||
|
m.termInput = ""
|
||||||
|
if input == "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if input == "exit" || input == "quit" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if input == "clear" {
|
||||||
|
m.termLog = nil
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(input, "cd ") {
|
||||||
|
dir := strings.TrimPrefix(input, "cd ")
|
||||||
|
dir = strings.TrimSpace(dir)
|
||||||
|
if dir == "~" {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
dir = home
|
||||||
|
}
|
||||||
|
if err := os.Chdir(dir); err == nil {
|
||||||
|
m.termCwd, _ = os.Getwd()
|
||||||
|
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ "+input))
|
||||||
|
} else {
|
||||||
|
m.termLog = append(m.termLog, errMsgStyle.Render("cd: "+err.Error()))
|
||||||
|
}
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ ")+input)
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
return m, m.runTermCommand(input)
|
||||||
|
case "backspace":
|
||||||
|
if len(m.termInput) > 0 {
|
||||||
|
m.termInput = m.termInput[:len(m.termInput)-1]
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case "tab":
|
||||||
|
m.activeTab = (m.activeTab + 1) % tabCount
|
||||||
|
m.resizeViewport()
|
||||||
|
return m, nil
|
||||||
|
case "shift+tab":
|
||||||
|
m.activeTab = (m.activeTab - 1 + tabCount) % tabCount
|
||||||
|
m.resizeViewport()
|
||||||
|
return m, nil
|
||||||
|
default:
|
||||||
|
if len(msg.String()) == 1 {
|
||||||
|
m.termInput += msg.String()
|
||||||
|
m.viewport.SetContent(m.renderContent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) runTermCommand(input string) tea.Cmd {
|
||||||
|
return tea.Cmd(func() tea.Msg {
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
if shell == "" {
|
||||||
|
shell = "/bin/sh"
|
||||||
|
}
|
||||||
|
cmd := exec.Command(shell, "-c", input)
|
||||||
|
cmd.Dir = m.termCwd
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return termOutputMsg{line: string(out) + errMsgStyle.Render(err.Error())}
|
||||||
|
}
|
||||||
|
return termOutputMsg{line: string(out)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) handleQuitConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m Model) handleQuitConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "y", "Y", "o", "O":
|
case "y", "Y", "o", "O":
|
||||||
@@ -587,31 +684,15 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.showingTabMenu = false
|
m.showingTabMenu = false
|
||||||
m.resizeViewport()
|
m.resizeViewport()
|
||||||
return m, nil
|
return m, nil
|
||||||
case "1":
|
default:
|
||||||
m.activeTab = tabDashboard
|
for i := 0; i < int(tabCount); i++ {
|
||||||
m.showingTabMenu = false
|
if msg.String() == fmt.Sprintf("%d", i+1) {
|
||||||
m.resizeViewport()
|
m.activeTab = tab(i)
|
||||||
return m, nil
|
m.showingTabMenu = false
|
||||||
case "2":
|
m.resizeViewport()
|
||||||
m.activeTab = tabChat
|
return m, nil
|
||||||
m.showingTabMenu = false
|
}
|
||||||
m.resizeViewport()
|
}
|
||||||
return m, nil
|
|
||||||
case "3":
|
|
||||||
m.activeTab = tabWorkflow
|
|
||||||
m.showingTabMenu = false
|
|
||||||
m.resizeViewport()
|
|
||||||
return m, nil
|
|
||||||
case "4":
|
|
||||||
m.activeTab = tabAgents
|
|
||||||
m.showingTabMenu = false
|
|
||||||
m.resizeViewport()
|
|
||||||
return m, nil
|
|
||||||
case "5":
|
|
||||||
m.activeTab = tabConfig
|
|
||||||
m.showingTabMenu = false
|
|
||||||
m.resizeViewport()
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -638,10 +719,9 @@ func (m Model) renderTabMenuOverlay() string {
|
|||||||
Width(4)
|
Width(4)
|
||||||
|
|
||||||
var items []string
|
var items []string
|
||||||
icons := []string{"Dashboard", "Chat", "Workflow", "Agents", "Config"}
|
descs := []string{"system overview & tools", "AI chat & conversation", "plan & execute workflows", "integrated shell", "background AI agents", "profile & settings"}
|
||||||
descs := []string{"system overview & tools", "AI chat & conversation", "plan & execute workflows", "background AI agents", "profile & settings"}
|
|
||||||
|
|
||||||
for i, name := range icons {
|
for i, name := range tabNames {
|
||||||
num := tabNumStyle.Render(fmt.Sprintf(" %d.", i+1))
|
num := tabNumStyle.Render(fmt.Sprintf(" %d.", i+1))
|
||||||
if i == m.tabMenuCursor {
|
if i == m.tabMenuCursor {
|
||||||
item := fmt.Sprintf("%s %-12s %s", num, name, lipgloss.NewStyle().Foreground(mutedColor).Render(descs[i]))
|
item := fmt.Sprintf("%s %-12s %s", num, name, lipgloss.NewStyle().Foreground(mutedColor).Render(descs[i]))
|
||||||
@@ -656,7 +736,7 @@ func (m Model) renderTabMenuOverlay() string {
|
|||||||
"\n\n" +
|
"\n\n" +
|
||||||
strings.Join(items, "\n") +
|
strings.Join(items, "\n") +
|
||||||
"\n\n" +
|
"\n\n" +
|
||||||
lipgloss.NewStyle().Foreground(dimColor).Render("↑↓ navigate enter/select esc cancel")
|
lipgloss.NewStyle().Foreground(dimColor).Render("up/down navigate enter/select esc cancel")
|
||||||
|
|
||||||
box := tabMenuStyle.Render(content)
|
box := tabMenuStyle.Render(content)
|
||||||
|
|
||||||
@@ -826,12 +906,15 @@ func (m *Model) handlePreview(files []workflow.PreviewFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) resizeViewport() {
|
func (m *Model) resizeViewport() {
|
||||||
headerH := 4
|
headerH := 1
|
||||||
footerH := 2
|
footerH := 2
|
||||||
inputH := 0
|
inputH := 0
|
||||||
if m.activeTab == tabChat || m.activeTab == tabWorkflow {
|
if m.activeTab == tabChat || m.activeTab == tabWorkflow {
|
||||||
inputH = 2
|
inputH = 2
|
||||||
}
|
}
|
||||||
|
if m.activeTab == tabTerminal {
|
||||||
|
inputH = 2
|
||||||
|
}
|
||||||
contentH := m.height - headerH - footerH - inputH
|
contentH := m.height - headerH - footerH - inputH
|
||||||
if contentH < 1 {
|
if contentH < 1 {
|
||||||
contentH = 1
|
contentH = 1
|
||||||
@@ -983,6 +1066,10 @@ func (m Model) View() string {
|
|||||||
return m.renderQuitOverlay()
|
return m.renderQuitOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.showingTabMenu {
|
||||||
|
return m.renderTabMenuOverlay()
|
||||||
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(m.renderHeader())
|
b.WriteString(m.renderHeader())
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -991,14 +1078,13 @@ func (m Model) View() string {
|
|||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(m.renderChatInput())
|
b.WriteString(m.renderChatInput())
|
||||||
}
|
}
|
||||||
|
if m.activeTab == tabTerminal {
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(m.renderTermInput())
|
||||||
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(m.renderFooter())
|
b.WriteString(m.renderFooter())
|
||||||
|
|
||||||
if m.showingTabMenu {
|
|
||||||
b = strings.Builder{}
|
|
||||||
b.WriteString(m.renderTabMenuOverlay())
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,23 +1112,25 @@ func (m Model) renderQuitOverlay() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderHeader() 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)
|
||||||
|
|
||||||
logo := logoStyle.Render("muyue")
|
logo := logoStyle.Render("muyue")
|
||||||
badge := versionBadgeStyle.Render(" v" + version.Version + " ")
|
badge := badgeStyle.Render("v" + version.Version)
|
||||||
separator := lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("─", max(m.width-lipgloss.Width(logo)-lipgloss.Width(badge)-4, 0)))
|
|
||||||
|
|
||||||
topLine := lipgloss.JoinHorizontal(lipgloss.Center, " ", logo, " ", badge, " ", separator)
|
activeTabName := lipgloss.NewStyle().Foreground(accentColor).Bold(true).Render(tabNames[m.activeTab])
|
||||||
|
separator := lipgloss.NewStyle().Foreground(dimColor).Render(" · ")
|
||||||
|
|
||||||
tabs := make([]string, len(tabNames))
|
rightPart := separator + activeTabName
|
||||||
for i, name := range tabNames {
|
|
||||||
if tab(i) == m.activeTab {
|
|
||||||
tabs[i] = activeTabStyle.Render(name)
|
|
||||||
} else {
|
|
||||||
tabs[i] = tabStyle.Render(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabsRow := lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...)
|
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, topLine, tabsRow)
|
line := lipgloss.NewStyle().Background(bgDark).Padding(0, 1).Render(
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Center, logo, " ", badge, rightPart),
|
||||||
|
)
|
||||||
|
|
||||||
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderContent() string {
|
func (m Model) renderContent() string {
|
||||||
@@ -1053,6 +1141,8 @@ func (m Model) renderContent() string {
|
|||||||
return m.renderChat()
|
return m.renderChat()
|
||||||
case tabWorkflow:
|
case tabWorkflow:
|
||||||
return m.renderWorkflow()
|
return m.renderWorkflow()
|
||||||
|
case tabTerminal:
|
||||||
|
return m.renderTerminal()
|
||||||
case tabAgents:
|
case tabAgents:
|
||||||
return m.renderAgents()
|
return m.renderAgents()
|
||||||
case tabConfig:
|
case tabConfig:
|
||||||
@@ -1062,6 +1152,26 @@ func (m Model) renderContent() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("")
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) renderDashboard() string {
|
func (m Model) renderDashboard() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
@@ -1574,13 +1684,15 @@ func (m Model) renderFooter() string {
|
|||||||
var helpText string
|
var helpText string
|
||||||
switch m.activeTab {
|
switch m.activeTab {
|
||||||
case tabDashboard:
|
case tabDashboard:
|
||||||
helpText = "[i] install [u] update [s] scan [l] lsp [m] mcp"
|
helpText = "[i] install [u] update [s] scan"
|
||||||
case tabChat, tabWorkflow:
|
case tabChat, tabWorkflow:
|
||||||
helpText = "[ctrl+m] switch tab [tab] next [ctrl+c] quit"
|
helpText = "[ctrl+t] tabs [tab] next [ctrl+c] quit"
|
||||||
|
case tabTerminal:
|
||||||
|
helpText = "[enter] run [ctrl+c] kill [clear] clear"
|
||||||
case tabAgents:
|
case tabAgents:
|
||||||
helpText = "[c] crush [l] claude"
|
helpText = "[c] crush [l] claude"
|
||||||
default:
|
default:
|
||||||
helpText = "[ctrl+m] switch tab [tab] next [ctrl+c] quit"
|
helpText = "[ctrl+t] tabs [tab] next [ctrl+c] quit"
|
||||||
}
|
}
|
||||||
rightR := statusBarStyle.Render(helpText)
|
rightR := statusBarStyle.Render(helpText)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user