summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--popups.go315
-rw-r--r--props_table.go48
-rw-r--r--tui.go108
3 files changed, 327 insertions, 144 deletions
diff --git a/popups.go b/popups.go
new file mode 100644
index 0000000..559a2aa
--- /dev/null
+++ b/popups.go
@@ -0,0 +1,315 @@
+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
+ refreshLocalModelsIfEmpty()
+ localModelsMu.RLock()
+ defer localModelsMu.RUnlock()
+ return LocalModels
+ }
+ // 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))
+ message := "No models available for selection"
+ if strings.Contains(cfg.CurrentAPI, "openrouter.ai") {
+ message = "No OpenRouter models available. Check token and connection."
+ } else if strings.Contains(cfg.CurrentAPI, "api.deepseek.com") {
+ message = "DeepSeek models should be available. Please report bug."
+ } else {
+ 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) {
+ // Update the model in both chatBody and config
+ chatBody.Model = mainText
+ 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
+ }
+ 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
+ }
+ 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
+ }
+ 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
+ }
+ 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)
+}
diff --git a/props_table.go b/props_table.go
index 50c8886..a7ad067 100644
--- a/props_table.go
+++ b/props_table.go
@@ -2,7 +2,6 @@ package main
import (
"fmt"
- "slices"
"strconv"
"strings"
"sync"
@@ -53,7 +52,6 @@ func makePropsTable(props map[string]float32) *tview.Table {
row++
// Store cell data for later use in selection functions
cellData := make(map[string]*CellData)
- var modelCellID string // will be set for the model selection row
// Helper function to add a checkbox-like row
addCheckboxRow := func(label string, initialValue bool, onChange func(bool)) {
table.SetCell(row, 0,
@@ -161,52 +159,6 @@ func makePropsTable(props map[string]float32) *tview.Table {
defer localModelsMu.RUnlock()
return LocalModels
}
- var modelRowIndex int // will be set before model row is added
- // Prepare API links dropdown - ensure current API is first, avoid duplicates
- apiLinks := make([]string, 0, len(cfg.ApiLinks)+1)
- apiLinks = append(apiLinks, cfg.CurrentAPI)
- for _, api := range cfg.ApiLinks {
- if api != cfg.CurrentAPI {
- apiLinks = append(apiLinks, api)
- }
- }
- addListPopupRow("Select an api", apiLinks, cfg.CurrentAPI, func(option string) {
- cfg.CurrentAPI = option
- // Update model list based on new API
- newModelList := getModelListForAPI(cfg.CurrentAPI)
- if modelCellID != "" {
- if data := cellData[modelCellID]; data != nil {
- data.Options = newModelList
- }
- }
- // 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
- // Update the displayed cell text - need to find model row
- // Search for model row by label
- for r := 0; r < table.GetRowCount(); r++ {
- if cell := table.GetCell(r, 0); cell != nil && cell.Text == "Select a model" {
- if valueCell := table.GetCell(r, 1); valueCell != nil {
- valueCell.SetText(chatBody.Model)
- }
- break
- }
- }
- }
- })
- // Prepare model list dropdown
- modelRowIndex = row
- modelCellID = fmt.Sprintf("listpopup_%d", modelRowIndex)
- modelList := getModelListForAPI(cfg.CurrentAPI)
- addListPopupRow("Select a model", modelList, chatBody.Model, func(option string) {
- chatBody.Model = option
- cfg.CurrentModel = chatBody.Model
- })
- // Role selection dropdown
- addListPopupRow("Write next message as", listRolesWithUser(), cfg.WriteNextMsgAs, func(option string) {
- cfg.WriteNextMsgAs = option
- })
// Add input fields
addInputRow("New char to write msg as", "", func(text string) {
if text != "" {
diff --git a/tui.go b/tui.go
index 70f67f1..87e878a 100644
--- a/tui.go
+++ b/tui.go
@@ -77,16 +77,16 @@ var (
[yellow]Ctrl+n[white]: start a new chat
[yellow]Ctrl+o[white]: open image file picker
[yellow]Ctrl+p[white]: props edit form (min-p, dry, etc.)
-[yellow]Ctrl+v[white]: switch between /completion and /chat api (if provided in config)
+[yellow]Ctrl+v[white]: show API link selection popup to choose current API
[yellow]Ctrl+r[white]: start/stop recording from your microphone (needs stt server or whisper binary)
[yellow]Ctrl+t[white]: remove thinking (<think>) and tool messages from context (delete from chat)
-[yellow]Ctrl+l[white]: rotate through free OpenRouter models (if openrouter api) or update connected model name (llamacpp)
+[yellow]Ctrl+l[white]: show model selection popup to choose current model
[yellow]Ctrl+k[white]: switch tool use (recommend tool use to llm after user msg)
[yellow]Ctrl+a[white]: interrupt tts (needs tts server)
[yellow]Ctrl+g[white]: open RAG file manager (load files for context retrieval)
[yellow]Ctrl+y[white]: list loaded RAG files (view and manage loaded files)
-[yellow]Ctrl+q[white]: cycle through mentioned chars in chat, to pick persona to send next msg as
-[yellow]Ctrl+x[white]: cycle through mentioned chars in chat, to pick persona to send next msg as (for llm)
+[yellow]Ctrl+q[white]: show user role selection popup to choose who sends next msg as
+[yellow]Ctrl+x[white]: show bot role selection popup to choose which agent responds next
[yellow]Alt+1[white]: toggle shell mode (execute commands locally)
[yellow]Alt+2[white]: toggle auto-scrolling (for reading while LLM types)
[yellow]Alt+3[white]: summarize chat history and start new chat with summary as tool response
@@ -1026,30 +1026,8 @@ func init() {
return nil
}
if event.Key() == tcell.KeyCtrlL {
- // Check if the current API is an OpenRouter API
- if strings.Contains(cfg.CurrentAPI, "openrouter.ai/api/v1/") {
- // Rotate through OpenRouter free models
- if len(ORFreeModels) > 0 {
- currentORModelIndex = (currentORModelIndex + 1) % len(ORFreeModels)
- chatBody.Model = ORFreeModels[currentORModelIndex]
- cfg.CurrentModel = chatBody.Model
- }
- updateStatusLine()
- } else {
- localModelsMu.RLock()
- if len(LocalModels) > 0 {
- currentLocalModelIndex = (currentLocalModelIndex + 1) % len(LocalModels)
- chatBody.Model = LocalModels[currentLocalModelIndex]
- cfg.CurrentModel = chatBody.Model
- }
- localModelsMu.RUnlock()
- updateStatusLine()
- // // For non-OpenRouter APIs, use the old logic
- // go func() {
- // fetchLCPModelName() // blocks
- // updateStatusLine()
- // }()
- }
+ // Show model selection popup instead of rotating models
+ showModelSelectionPopup()
return nil
}
if event.Key() == tcell.KeyCtrlT {
@@ -1061,29 +1039,8 @@ func init() {
return nil
}
if event.Key() == tcell.KeyCtrlV {
- // switch between API links using index-based rotation
- if len(cfg.ApiLinks) == 0 {
- // No API links to rotate through
- return nil
- }
- // Find current API in the list to get the current index
- currentIndex := -1
- for i, api := range cfg.ApiLinks {
- if api == cfg.CurrentAPI {
- currentIndex = i
- break
- }
- }
- // If current API is not in the list, start from beginning
- // Otherwise, advance to next API in the list (with wrap-around)
- if currentIndex == -1 {
- currentAPIIndex = 0
- } else {
- currentAPIIndex = (currentIndex + 1) % len(cfg.ApiLinks)
- }
- cfg.CurrentAPI = cfg.ApiLinks[currentAPIIndex]
- choseChunkParser()
- updateStatusLine()
+ // Show API link selection popup instead of rotating APIs
+ showAPILinkSelectionPopup()
return nil
}
if event.Key() == tcell.KeyCtrlS {
@@ -1179,54 +1136,13 @@ func init() {
return nil
}
if event.Key() == tcell.KeyCtrlQ {
- persona := cfg.UserRole
- if cfg.WriteNextMsgAs != "" {
- persona = cfg.WriteNextMsgAs
- }
- roles := listRolesWithUser()
- for i, role := range roles {
- if strings.EqualFold(role, persona) {
- if i == len(roles)-1 {
- cfg.WriteNextMsgAs = roles[0] // reached last, get first
- persona = cfg.WriteNextMsgAs
- break
- }
- cfg.WriteNextMsgAs = roles[i+1] // get next role
- persona = cfg.WriteNextMsgAs
- break
- }
- }
- // role got switch, update textview with character specific context for user
- filtered := filterMessagesForCharacter(chatBody.Messages, persona)
- textView.SetText(chatToText(filtered, cfg.ShowSys))
- updateStatusLine()
- colorText()
+ // Show user role selection popup instead of cycling through roles
+ showUserRoleSelectionPopup()
return nil
}
if event.Key() == tcell.KeyCtrlX {
- botPersona := cfg.AssistantRole
- if cfg.WriteNextMsgAsCompletionAgent != "" {
- botPersona = cfg.WriteNextMsgAsCompletionAgent
- }
- // roles := chatBody.ListRoles()
- roles := listChatRoles()
- if len(roles) == 0 {
- logger.Warn("empty roles in chat")
- }
- if !strInSlice(cfg.AssistantRole, roles) {
- roles = append(roles, cfg.AssistantRole)
- }
- for i, role := range roles {
- if strings.EqualFold(role, botPersona) {
- if i == len(roles)-1 {
- cfg.WriteNextMsgAsCompletionAgent = roles[0] // reached last, get first
- break
- }
- cfg.WriteNextMsgAsCompletionAgent = roles[i+1] // get next role
- break
- }
- }
- updateStatusLine()
+ // Show bot role selection popup instead of cycling through roles
+ showBotRoleSelectionPopup()
return nil
}
if event.Key() == tcell.KeyCtrlG {