feat(tui): add footer view with navigation instructions and update menu structure
This commit is contained in:
parent
b23ecc0c5b
commit
05b14422a1
4 changed files with 66 additions and 23 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -67,6 +68,7 @@ func Run() error {
|
||||||
root := tview.NewFlex().SetDirection(tview.FlexRow)
|
root := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||||
root.AddItem(bannerView(), 6, 0, false)
|
root.AddItem(bannerView(), 6, 0, false)
|
||||||
root.AddItem(state.pages, 0, 1, true)
|
root.AddItem(state.pages, 0, 1, true)
|
||||||
|
root.AddItem(footerView(), 1, 0, false)
|
||||||
|
|
||||||
if err := state.app.SetRoot(root, true).EnableMouse(false).Run(); err != nil {
|
if err := state.app.SetRoot(root, true).EnableMouse(false).Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -102,7 +104,7 @@ func (s *appState) pop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) mainMenu() tview.Primitive {
|
func (s *appState) mainMenu() tview.Primitive {
|
||||||
menu := NewMenu("Config Menu", nil)
|
menu := NewMenu("Menu", nil)
|
||||||
refreshMainMenu(menu, s)
|
refreshMainMenu(menu, s)
|
||||||
menu.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
menu.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
switch event.Key() {
|
switch event.Key() {
|
||||||
|
|
@ -110,10 +112,7 @@ func (s *appState) mainMenu() tview.Primitive {
|
||||||
s.requestExit()
|
s.requestExit()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Rune() == 'q' {
|
|
||||||
s.requestExit()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -131,6 +130,32 @@ func (s *appState) refreshMenu(name string, menu *Menu) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *appState) countChannels() (enabled int, total int) {
|
||||||
|
c := s.config.Channels
|
||||||
|
entries := []bool{
|
||||||
|
c.Telegram.Enabled,
|
||||||
|
c.Discord.Enabled,
|
||||||
|
c.QQ.Enabled,
|
||||||
|
c.MaixCam.Enabled,
|
||||||
|
c.WhatsApp.Enabled,
|
||||||
|
c.Feishu.Enabled,
|
||||||
|
c.DingTalk.Enabled,
|
||||||
|
c.Slack.Enabled,
|
||||||
|
c.Matrix.Enabled,
|
||||||
|
c.LINE.Enabled,
|
||||||
|
c.OneBot.Enabled,
|
||||||
|
c.WeCom.Enabled,
|
||||||
|
c.WeComApp.Enabled,
|
||||||
|
}
|
||||||
|
total = len(entries)
|
||||||
|
for _, v := range entries {
|
||||||
|
if v {
|
||||||
|
enabled++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled, total
|
||||||
|
}
|
||||||
|
|
||||||
func refreshMainMenuIfPresent(s *appState) {
|
func refreshMainMenuIfPresent(s *appState) {
|
||||||
if menu, ok := s.menus["main"]; ok {
|
if menu, ok := s.menus["main"]; ok {
|
||||||
refreshMainMenu(menu, s)
|
refreshMainMenu(menu, s)
|
||||||
|
|
@ -141,6 +166,7 @@ func refreshMainMenu(menu *Menu, s *appState) {
|
||||||
selectedModel := s.selectedModelName()
|
selectedModel := s.selectedModelName()
|
||||||
modelReady := selectedModel != ""
|
modelReady := selectedModel != ""
|
||||||
channelReady := s.hasEnabledChannel()
|
channelReady := s.hasEnabledChannel()
|
||||||
|
enabledCount, totalChannels := s.countChannels()
|
||||||
gatewayRunning := s.gatewayCmd != nil || s.isGatewayRunning()
|
gatewayRunning := s.gatewayCmd != nil || s.isGatewayRunning()
|
||||||
|
|
||||||
gatewayLabel := "Start Gateway"
|
gatewayLabel := "Start Gateway"
|
||||||
|
|
@ -167,7 +193,7 @@ func refreshMainMenu(menu *Menu, s *appState) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Label: rootChannelLabel(channelReady),
|
Label: rootChannelLabel(channelReady),
|
||||||
Description: rootChannelDescription(channelReady),
|
Description: fmt.Sprintf("%d/%d enabled", enabledCount, totalChannels),
|
||||||
Action: func() {
|
Action: func() {
|
||||||
s.push("channel", s.channelMenu())
|
s.push("channel", s.channelMenu())
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import (
|
||||||
|
|
||||||
func (s *appState) buildChannelMenuItems() []MenuItem {
|
func (s *appState) buildChannelMenuItems() []MenuItem {
|
||||||
return []MenuItem{
|
return []MenuItem{
|
||||||
{Label: "Back", Description: "Return to main menu", Action: func() { s.pop() }},
|
|
||||||
channelItem(
|
channelItem(
|
||||||
"Telegram",
|
"Telegram",
|
||||||
"Telegram bot settings",
|
"Telegram bot settings",
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *appState) modelMenu() tview.Primitive {
|
func (s *appState) modelMenu() tview.Primitive {
|
||||||
items := make([]MenuItem, 0, 2+len(s.config.ModelList))
|
items := make([]MenuItem, 0, 1+len(s.config.ModelList))
|
||||||
items = append(items, MenuItem{Label: "Back", Description: "Return to main menu", Action: func() { s.pop() }})
|
|
||||||
currentModel := strings.TrimSpace(s.config.Agents.Defaults.Model)
|
currentModel := strings.TrimSpace(s.config.Agents.Defaults.Model)
|
||||||
for i := range s.config.ModelList {
|
for i := range s.config.ModelList {
|
||||||
index := i
|
index := i
|
||||||
|
|
@ -66,14 +65,11 @@ func (s *appState) modelMenu() tview.Primitive {
|
||||||
s.pop()
|
s.pop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Rune() == 'q' {
|
|
||||||
s.pop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if event.Rune() == ' ' {
|
if event.Rune() == ' ' {
|
||||||
row, _ := menu.GetSelection()
|
row, _ := menu.GetSelection()
|
||||||
if row > 0 && row <= len(s.config.ModelList) {
|
if row >= 0 && row < len(s.config.ModelList) {
|
||||||
model := s.config.ModelList[row-1]
|
model := s.config.ModelList[row]
|
||||||
if !isModelValid(model) {
|
if !isModelValid(model) {
|
||||||
s.showMessage(
|
s.showMessage(
|
||||||
"Invalid model",
|
"Invalid model",
|
||||||
|
|
@ -167,7 +163,21 @@ func (s *appState) modelForm(index int) tview.Primitive {
|
||||||
})
|
})
|
||||||
|
|
||||||
form.AddButton("Delete", func() {
|
form.AddButton("Delete", func() {
|
||||||
|
pageName := "confirm-delete-model"
|
||||||
|
if s.pages.HasPage(pageName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modal := tview.NewModal().
|
||||||
|
SetText("Are you sure you want to delete this model?").
|
||||||
|
AddButtons([]string{"Cancel", "Delete"}).
|
||||||
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||||
|
s.pages.RemovePage(pageName)
|
||||||
|
if buttonLabel == "Delete" {
|
||||||
s.deleteModel(index)
|
s.deleteModel(index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
modal.SetTitle("Confirm Delete").SetBorder(true)
|
||||||
|
s.pages.AddPage(pageName, modal, true, true)
|
||||||
})
|
})
|
||||||
form.AddButton("Test", func() {
|
form.AddButton("Test", func() {
|
||||||
s.testModel(model)
|
s.testModel(model)
|
||||||
|
|
@ -224,7 +234,7 @@ func modelStatusColor(valid bool, selected bool) *tcell.Color {
|
||||||
|
|
||||||
func refreshModelMenu(menu *Menu, currentModel string, models []picoclawconfig.ModelConfig) {
|
func refreshModelMenu(menu *Menu, currentModel string, models []picoclawconfig.ModelConfig) {
|
||||||
for i, model := range models {
|
for i, model := range models {
|
||||||
row := i + 1
|
row := i
|
||||||
label := fmt.Sprintf("%s (%s)", model.ModelName, model.Model)
|
label := fmt.Sprintf("%s (%s)", model.ModelName, model.Model)
|
||||||
isValid := isModelValid(model)
|
isValid := isModelValid(model)
|
||||||
if model.ModelName == currentModel && currentModel != "" {
|
if model.ModelName == currentModel && currentModel != "" {
|
||||||
|
|
@ -243,8 +253,7 @@ func refreshModelMenu(menu *Menu, currentModel string, models []picoclawconfig.M
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshModelMenuFromState(menu *Menu, s *appState) {
|
func refreshModelMenuFromState(menu *Menu, s *appState) {
|
||||||
items := make([]MenuItem, 0, 2+len(s.config.ModelList))
|
items := make([]MenuItem, 0, 1+len(s.config.ModelList))
|
||||||
items = append(items, MenuItem{Label: "Back", Description: "Return to main menu", Action: func() { s.pop() }})
|
|
||||||
currentModel := strings.TrimSpace(s.config.Agents.Defaults.Model)
|
currentModel := strings.TrimSpace(s.config.Agents.Defaults.Model)
|
||||||
for i := range s.config.ModelList {
|
for i := range s.config.ModelList {
|
||||||
index := i
|
index := i
|
||||||
|
|
@ -280,10 +289,7 @@ func refreshModelMenuFromState(menu *Menu, s *appState) {
|
||||||
s.addModel(
|
s.addModel(
|
||||||
picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.2"},
|
picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.2"},
|
||||||
)
|
)
|
||||||
s.push(
|
s.push(fmt.Sprintf("model-%d", len(s.config.ModelList)-1), s.modelForm(len(s.config.ModelList)-1))
|
||||||
fmt.Sprintf("model-%d", len(s.config.ModelList)-1),
|
|
||||||
s.modelForm(len(s.config.ModelList)-1),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,15 @@ func bannerView() *tview.TextView {
|
||||||
text.SetBorder(false)
|
text.SetBorder(false)
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const footerText = "Esc: Back/Exit | Enter: Enter | ←↓↑→ : Move | Space: Select | Tab/Shift+Tab: Switch"
|
||||||
|
|
||||||
|
func footerView() *tview.TextView {
|
||||||
|
text := tview.NewTextView()
|
||||||
|
text.SetTextAlign(tview.AlignCenter)
|
||||||
|
text.SetText(footerText)
|
||||||
|
text.SetBackgroundColor(tview.Styles.MoreContrastBackgroundColor)
|
||||||
|
text.SetTextColor(tview.Styles.PrimaryTextColor)
|
||||||
|
text.SetBorder(false)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue