516 lines
17 KiB
Go
516 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
// showModelSelectionPopup creates a modal popup to select a model
|
|
func showModelSelectionPopup() {
|
|
// Helper function to get model list for a given API
|
|
getModelListForAPI := func(api string) []string {
|
|
if strings.Contains(api, "api.deepseek.com/") {
|
|
return []string{"deepseek-chat", "deepseek-reasoner"}
|
|
} else if strings.Contains(api, "openrouter.ai") {
|
|
return ORFreeModels
|
|
}
|
|
// Assume local llama.cpp - fetch with load status
|
|
models, err := fetchLCPModelsWithLoadStatus()
|
|
if err != nil {
|
|
logger.Error("failed to fetch models with load status", "error", err)
|
|
return LocalModels
|
|
}
|
|
return models
|
|
}
|
|
// Get the current model list based on the API
|
|
modelList := getModelListForAPI(cfg.CurrentAPI)
|
|
// Check for empty options list
|
|
if len(modelList) == 0 {
|
|
logger.Warn("empty model list for", "api", cfg.CurrentAPI, "localModelsLen", len(LocalModels), "orModelsLen", len(ORFreeModels))
|
|
var message string
|
|
switch {
|
|
case strings.Contains(cfg.CurrentAPI, "openrouter.ai"):
|
|
message = "No OpenRouter models available. Check token and connection."
|
|
case strings.Contains(cfg.CurrentAPI, "api.deepseek.com"):
|
|
message = "DeepSeek models should be available. Please report bug."
|
|
default:
|
|
message = "No llama.cpp models loaded. Ensure llama.cpp server is running with models."
|
|
}
|
|
if err := notifyUser("Empty list", message); err != nil {
|
|
logger.Error("failed to send notification", "error", err)
|
|
}
|
|
return
|
|
}
|
|
// Create a list primitive
|
|
modelListWidget := tview.NewList().ShowSecondaryText(false).
|
|
SetSelectedBackgroundColor(tcell.ColorGray)
|
|
modelListWidget.SetTitle("Select Model").SetBorder(true)
|
|
// Find the current model index to set as selected
|
|
currentModelIndex := -1
|
|
for i, model := range modelList {
|
|
if model == chatBody.Model {
|
|
currentModelIndex = i
|
|
}
|
|
modelListWidget.AddItem(model, "", 0, nil)
|
|
}
|
|
// Set the current selection if found
|
|
if currentModelIndex != -1 {
|
|
modelListWidget.SetCurrentItem(currentModelIndex)
|
|
}
|
|
modelListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
|
// Strip "(loaded)" suffix if present for local llama.cpp models
|
|
modelName := strings.TrimPrefix(mainText, "(loaded) ")
|
|
// Update the model in both chatBody and config
|
|
chatBody.Model = modelName
|
|
cfg.CurrentModel = chatBody.Model
|
|
// Remove the popup page
|
|
pages.RemovePage("modelSelectionPopup")
|
|
// Update the status line to reflect the change
|
|
updateStatusLine()
|
|
})
|
|
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
pages.RemovePage("modelSelectionPopup")
|
|
return nil
|
|
}
|
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
|
pages.RemovePage("modelSelectionPopup")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
modal := func(p tview.Primitive, width, height int) tview.Primitive {
|
|
return tview.NewFlex().
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(p, height, 1, true).
|
|
AddItem(nil, 0, 1, false), width, 1, true).
|
|
AddItem(nil, 0, 1, false)
|
|
}
|
|
// Add modal page and make it visible
|
|
pages.AddPage("modelSelectionPopup", modal(modelListWidget, 80, 20), true, true)
|
|
app.SetFocus(modelListWidget)
|
|
}
|
|
|
|
// showAPILinkSelectionPopup creates a modal popup to select an API link
|
|
func showAPILinkSelectionPopup() {
|
|
// Prepare API links dropdown - ensure current API is in the list, avoid duplicates
|
|
apiLinks := make([]string, 0, len(cfg.ApiLinks)+1)
|
|
// Add current API first if it's not already in ApiLinks
|
|
foundCurrentAPI := false
|
|
for _, api := range cfg.ApiLinks {
|
|
if api == cfg.CurrentAPI {
|
|
foundCurrentAPI = true
|
|
}
|
|
apiLinks = append(apiLinks, api)
|
|
}
|
|
// If current API is not in the list, add it at the beginning
|
|
if !foundCurrentAPI {
|
|
apiLinks = make([]string, 0, len(cfg.ApiLinks)+1)
|
|
apiLinks = append(apiLinks, cfg.CurrentAPI)
|
|
apiLinks = append(apiLinks, cfg.ApiLinks...)
|
|
}
|
|
// Check for empty options list
|
|
if len(apiLinks) == 0 {
|
|
logger.Warn("no API links available for selection")
|
|
message := "No API links available. Please configure API links in your config file."
|
|
if err := notifyUser("Empty list", message); err != nil {
|
|
logger.Error("failed to send notification", "error", err)
|
|
}
|
|
return
|
|
}
|
|
// Create a list primitive
|
|
apiListWidget := tview.NewList().ShowSecondaryText(false).
|
|
SetSelectedBackgroundColor(tcell.ColorGray)
|
|
apiListWidget.SetTitle("Select API Link").SetBorder(true)
|
|
// Find the current API index to set as selected
|
|
currentAPIIndex := -1
|
|
for i, api := range apiLinks {
|
|
if api == cfg.CurrentAPI {
|
|
currentAPIIndex = i
|
|
}
|
|
apiListWidget.AddItem(api, "", 0, nil)
|
|
}
|
|
// Set the current selection if found
|
|
if currentAPIIndex != -1 {
|
|
apiListWidget.SetCurrentItem(currentAPIIndex)
|
|
}
|
|
apiListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
|
// Update the API in config
|
|
cfg.CurrentAPI = mainText
|
|
// Update model list based on new API
|
|
// Helper function to get model list for a given API (same as in props_table.go)
|
|
getModelListForAPI := func(api string) []string {
|
|
if strings.Contains(api, "api.deepseek.com/") {
|
|
return []string{"deepseek-chat", "deepseek-reasoner"}
|
|
} else if strings.Contains(api, "openrouter.ai") {
|
|
return ORFreeModels
|
|
}
|
|
// Assume local llama.cpp
|
|
refreshLocalModelsIfEmpty()
|
|
localModelsMu.RLock()
|
|
defer localModelsMu.RUnlock()
|
|
return LocalModels
|
|
}
|
|
newModelList := getModelListForAPI(cfg.CurrentAPI)
|
|
// Ensure chatBody.Model is in the new list; if not, set to first available model
|
|
if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) {
|
|
chatBody.Model = newModelList[0]
|
|
cfg.CurrentModel = chatBody.Model
|
|
}
|
|
// Remove the popup page
|
|
pages.RemovePage("apiLinkSelectionPopup")
|
|
// Update the parser and status line to reflect the change
|
|
choseChunkParser()
|
|
updateStatusLine()
|
|
})
|
|
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
pages.RemovePage("apiLinkSelectionPopup")
|
|
return nil
|
|
}
|
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
|
pages.RemovePage("apiLinkSelectionPopup")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
modal := func(p tview.Primitive, width, height int) tview.Primitive {
|
|
return tview.NewFlex().
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(p, height, 1, true).
|
|
AddItem(nil, 0, 1, false), width, 1, true).
|
|
AddItem(nil, 0, 1, false)
|
|
}
|
|
// Add modal page and make it visible
|
|
pages.AddPage("apiLinkSelectionPopup", modal(apiListWidget, 80, 20), true, true)
|
|
app.SetFocus(apiListWidget)
|
|
}
|
|
|
|
// showUserRoleSelectionPopup creates a modal popup to select a user role
|
|
func showUserRoleSelectionPopup() {
|
|
// Get the list of available roles
|
|
roles := listRolesWithUser()
|
|
// Check for empty options list
|
|
if len(roles) == 0 {
|
|
logger.Warn("no roles available for selection")
|
|
message := "No roles available for selection."
|
|
if err := notifyUser("Empty list", message); err != nil {
|
|
logger.Error("failed to send notification", "error", err)
|
|
}
|
|
return
|
|
}
|
|
// Create a list primitive
|
|
roleListWidget := tview.NewList().ShowSecondaryText(false).
|
|
SetSelectedBackgroundColor(tcell.ColorGray)
|
|
roleListWidget.SetTitle("Select User Role").SetBorder(true)
|
|
// Find the current role index to set as selected
|
|
currentRole := cfg.UserRole
|
|
if cfg.WriteNextMsgAs != "" {
|
|
currentRole = cfg.WriteNextMsgAs
|
|
}
|
|
currentRoleIndex := -1
|
|
for i, role := range roles {
|
|
if strings.EqualFold(role, currentRole) {
|
|
currentRoleIndex = i
|
|
}
|
|
roleListWidget.AddItem(role, "", 0, nil)
|
|
}
|
|
// Set the current selection if found
|
|
if currentRoleIndex != -1 {
|
|
roleListWidget.SetCurrentItem(currentRoleIndex)
|
|
}
|
|
roleListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
|
// Update the user role in config
|
|
cfg.WriteNextMsgAs = mainText
|
|
// role got switch, update textview with character specific context for user
|
|
filtered := filterMessagesForCharacter(chatBody.Messages, mainText)
|
|
textView.SetText(chatToText(filtered, cfg.ShowSys))
|
|
// Remove the popup page
|
|
pages.RemovePage("userRoleSelectionPopup")
|
|
// Update the status line to reflect the change
|
|
updateStatusLine()
|
|
colorText()
|
|
})
|
|
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
pages.RemovePage("userRoleSelectionPopup")
|
|
return nil
|
|
}
|
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
|
pages.RemovePage("userRoleSelectionPopup")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
modal := func(p tview.Primitive, width, height int) tview.Primitive {
|
|
return tview.NewFlex().
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(p, height, 1, true).
|
|
AddItem(nil, 0, 1, false), width, 1, true).
|
|
AddItem(nil, 0, 1, false)
|
|
}
|
|
// Add modal page and make it visible
|
|
pages.AddPage("userRoleSelectionPopup", modal(roleListWidget, 80, 20), true, true)
|
|
app.SetFocus(roleListWidget)
|
|
}
|
|
|
|
// showBotRoleSelectionPopup creates a modal popup to select a bot role
|
|
func showBotRoleSelectionPopup() {
|
|
// Get the list of available roles
|
|
roles := listChatRoles()
|
|
if len(roles) == 0 {
|
|
logger.Warn("empty roles in chat")
|
|
}
|
|
if !strInSlice(cfg.AssistantRole, roles) {
|
|
roles = append(roles, cfg.AssistantRole)
|
|
}
|
|
// Check for empty options list
|
|
if len(roles) == 0 {
|
|
logger.Warn("no roles available for selection")
|
|
message := "No roles available for selection."
|
|
if err := notifyUser("Empty list", message); err != nil {
|
|
logger.Error("failed to send notification", "error", err)
|
|
}
|
|
return
|
|
}
|
|
// Create a list primitive
|
|
roleListWidget := tview.NewList().ShowSecondaryText(false).
|
|
SetSelectedBackgroundColor(tcell.ColorGray)
|
|
roleListWidget.SetTitle("Select Bot Role").SetBorder(true)
|
|
// Find the current role index to set as selected
|
|
currentRole := cfg.AssistantRole
|
|
if cfg.WriteNextMsgAsCompletionAgent != "" {
|
|
currentRole = cfg.WriteNextMsgAsCompletionAgent
|
|
}
|
|
currentRoleIndex := -1
|
|
for i, role := range roles {
|
|
if strings.EqualFold(role, currentRole) {
|
|
currentRoleIndex = i
|
|
}
|
|
roleListWidget.AddItem(role, "", 0, nil)
|
|
}
|
|
// Set the current selection if found
|
|
if currentRoleIndex != -1 {
|
|
roleListWidget.SetCurrentItem(currentRoleIndex)
|
|
}
|
|
roleListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
|
// Update the bot role in config
|
|
cfg.WriteNextMsgAsCompletionAgent = mainText
|
|
// Remove the popup page
|
|
pages.RemovePage("botRoleSelectionPopup")
|
|
// Update the status line to reflect the change
|
|
updateStatusLine()
|
|
})
|
|
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
pages.RemovePage("botRoleSelectionPopup")
|
|
return nil
|
|
}
|
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
|
pages.RemovePage("botRoleSelectionPopup")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
modal := func(p tview.Primitive, width, height int) tview.Primitive {
|
|
return tview.NewFlex().
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(p, height, 1, true).
|
|
AddItem(nil, 0, 1, false), width, 1, true).
|
|
AddItem(nil, 0, 1, false)
|
|
}
|
|
// Add modal page and make it visible
|
|
pages.AddPage("botRoleSelectionPopup", modal(roleListWidget, 80, 20), true, true)
|
|
app.SetFocus(roleListWidget)
|
|
}
|
|
|
|
func showFileCompletionPopup(filter string) {
|
|
baseDir := cfg.CodingDir
|
|
if baseDir == "" {
|
|
baseDir = "."
|
|
}
|
|
complMatches := scanFiles(baseDir, filter)
|
|
if len(complMatches) == 0 {
|
|
return
|
|
}
|
|
// If only one match, auto-complete without showing popup
|
|
if len(complMatches) == 1 {
|
|
currentText := textArea.GetText()
|
|
atIdx := strings.LastIndex(currentText, "@")
|
|
if atIdx >= 0 {
|
|
before := currentText[:atIdx]
|
|
textArea.SetText(before+complMatches[0], true)
|
|
}
|
|
return
|
|
}
|
|
widget := tview.NewList().ShowSecondaryText(false).
|
|
SetSelectedBackgroundColor(tcell.ColorGray)
|
|
widget.SetTitle("file completion").SetBorder(true)
|
|
for _, m := range complMatches {
|
|
widget.AddItem(m, "", 0, nil)
|
|
}
|
|
widget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
|
currentText := textArea.GetText()
|
|
atIdx := strings.LastIndex(currentText, "@")
|
|
if atIdx >= 0 {
|
|
before := currentText[:atIdx]
|
|
textArea.SetText(before+mainText, true)
|
|
}
|
|
pages.RemovePage("fileCompletionPopup")
|
|
})
|
|
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
pages.RemovePage("fileCompletionPopup")
|
|
return nil
|
|
}
|
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
|
pages.RemovePage("fileCompletionPopup")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
modal := func(p tview.Primitive, width, height int) tview.Primitive {
|
|
return tview.NewFlex().
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(p, height, 1, true).
|
|
AddItem(nil, 0, 1, false), width, 1, true).
|
|
AddItem(nil, 0, 1, false)
|
|
}
|
|
// Add modal page and make it visible
|
|
pages.AddPage("fileCompletionPopup", modal(widget, 80, 20), true, true)
|
|
app.SetFocus(widget)
|
|
}
|
|
|
|
func updateWidgetColors(theme *tview.Theme) {
|
|
bgColor := theme.PrimitiveBackgroundColor
|
|
fgColor := theme.PrimaryTextColor
|
|
borderColor := theme.BorderColor
|
|
titleColor := theme.TitleColor
|
|
|
|
textView.SetBackgroundColor(bgColor)
|
|
textView.SetTextColor(fgColor)
|
|
textView.SetBorderColor(borderColor)
|
|
textView.SetTitleColor(titleColor)
|
|
|
|
textArea.SetBackgroundColor(bgColor)
|
|
textArea.SetBorderColor(borderColor)
|
|
textArea.SetTitleColor(titleColor)
|
|
textArea.SetTextStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
|
|
textArea.SetPlaceholderStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
|
|
// Force textarea refresh by restoring text (SetTextStyle doesn't trigger redraw)
|
|
textArea.SetText(textArea.GetText(), true)
|
|
|
|
editArea.SetBackgroundColor(bgColor)
|
|
editArea.SetBorderColor(borderColor)
|
|
editArea.SetTitleColor(titleColor)
|
|
editArea.SetTextStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
|
|
editArea.SetPlaceholderStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
|
|
// Force textarea refresh by restoring text (SetTextStyle doesn't trigger redraw)
|
|
editArea.SetText(editArea.GetText(), true)
|
|
|
|
statusLineWidget.SetBackgroundColor(bgColor)
|
|
statusLineWidget.SetTextColor(fgColor)
|
|
statusLineWidget.SetBorderColor(borderColor)
|
|
statusLineWidget.SetTitleColor(titleColor)
|
|
|
|
helpView.SetBackgroundColor(bgColor)
|
|
helpView.SetTextColor(fgColor)
|
|
helpView.SetBorderColor(borderColor)
|
|
helpView.SetTitleColor(titleColor)
|
|
|
|
searchField.SetBackgroundColor(bgColor)
|
|
searchField.SetBorderColor(borderColor)
|
|
searchField.SetTitleColor(titleColor)
|
|
}
|
|
|
|
// showColorschemeSelectionPopup creates a modal popup to select a colorscheme
|
|
func showColorschemeSelectionPopup() {
|
|
// Get the list of available colorschemes
|
|
schemeNames := make([]string, 0, len(colorschemes))
|
|
for name := range colorschemes {
|
|
schemeNames = append(schemeNames, name)
|
|
}
|
|
slices.Sort(schemeNames)
|
|
// Check for empty options list
|
|
if len(schemeNames) == 0 {
|
|
logger.Warn("no colorschemes available for selection")
|
|
message := "No colorschemes available."
|
|
if err := notifyUser("Empty list", message); err != nil {
|
|
logger.Error("failed to send notification", "error", err)
|
|
}
|
|
return
|
|
}
|
|
// Create a list primitive
|
|
schemeListWidget := tview.NewList().ShowSecondaryText(false).
|
|
SetSelectedBackgroundColor(tcell.ColorGray)
|
|
schemeListWidget.SetTitle("Select Colorscheme").SetBorder(true)
|
|
|
|
currentScheme := "default"
|
|
for name := range colorschemes {
|
|
if tview.Styles == colorschemes[name] {
|
|
currentScheme = name
|
|
break
|
|
}
|
|
}
|
|
currentSchemeIndex := -1
|
|
for i, scheme := range schemeNames {
|
|
if scheme == currentScheme {
|
|
currentSchemeIndex = i
|
|
}
|
|
schemeListWidget.AddItem(scheme, "", 0, nil)
|
|
}
|
|
// Set the current selection if found
|
|
if currentSchemeIndex != -1 {
|
|
schemeListWidget.SetCurrentItem(currentSchemeIndex)
|
|
}
|
|
schemeListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
|
// Update the colorscheme
|
|
if theme, ok := colorschemes[mainText]; ok {
|
|
tview.Styles = theme
|
|
go func() {
|
|
app.QueueUpdateDraw(func() {
|
|
updateWidgetColors(&theme)
|
|
})
|
|
}()
|
|
}
|
|
// Remove the popup page
|
|
pages.RemovePage("colorschemeSelectionPopup")
|
|
})
|
|
schemeListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
pages.RemovePage("colorschemeSelectionPopup")
|
|
return nil
|
|
}
|
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
|
pages.RemovePage("colorschemeSelectionPopup")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
modal := func(p tview.Primitive, width, height int) tview.Primitive {
|
|
return tview.NewFlex().
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(nil, 0, 1, false).
|
|
AddItem(p, height, 1, true).
|
|
AddItem(nil, 0, 1, false), width, 1, true).
|
|
AddItem(nil, 0, 1, false)
|
|
}
|
|
// Add modal page and make it visible
|
|
pages.AddPage("colorschemeSelectionPopup", modal(schemeListWidget, 40, len(schemeNames)+2), true, true)
|
|
app.SetFocus(schemeListWidget)
|
|
}
|