From 8b162ef34f0755e2224c43499218def16d4b6845 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 17 Jan 2026 11:42:35 +0300 Subject: Enha: change textview chat history based on current user persona --- tui.go | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index a7570cf..aa9972a 100644 --- a/tui.go +++ b/tui.go @@ -310,7 +310,7 @@ func performSearch(term string) { searchResultLengths = nil originalTextForSearch = "" // Re-render text without highlights - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() return } @@ -517,8 +517,8 @@ func init() { searchResults = nil // Clear search results searchResultLengths = nil // Clear search result lengths originalTextForSearch = "" - textView.SetText(chatToText(cfg.ShowSys)) // Reset text without search regions - colorText() // Apply normal chat coloring + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) // Reset text without search regions + colorText() // Apply normal chat coloring } else { // Original logic if no search is active currentSelection := textView.GetHighlights() @@ -594,7 +594,7 @@ func init() { } chatBody.Messages[selectedIndex].Content = editedMsg // change textarea - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) pages.RemovePage(editMsgPage) editMode = false return nil @@ -627,7 +627,7 @@ func init() { } if selectedIndex >= 0 && selectedIndex < len(chatBody.Messages) { chatBody.Messages[selectedIndex].Role = newRole - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() pages.RemovePage(roleEditPage) } @@ -739,7 +739,7 @@ func init() { searchResults = nil searchResultLengths = nil originalTextForSearch = "" - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() return } else { @@ -787,7 +787,7 @@ func init() { // textArea.SetMovedFunc(updateStatusLine) updateStatusLine() - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() if scrollToEndEnabled { textView.ScrollToEnd() @@ -801,7 +801,7 @@ func init() { if event.Key() == tcell.KeyRune && event.Rune() == '5' && event.Modifiers()&tcell.ModAlt != 0 { // switch cfg.ShowSys cfg.ShowSys = !cfg.ShowSys - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() } if event.Key() == tcell.KeyRune && event.Rune() == '3' && event.Modifiers()&tcell.ModAlt != 0 { @@ -866,7 +866,7 @@ func init() { chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1] // there is no case where user msg is regenerated // lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) go chatRound("", cfg.UserRole, textView, true, false) return nil } @@ -888,7 +888,7 @@ func init() { return nil } chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1] - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() return nil } @@ -1052,7 +1052,7 @@ func init() { // clear context // remove tools and thinking removeThinking(chatBody) - textView.SetText(chatToText(cfg.ShowSys)) + textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() return nil } @@ -1184,20 +1184,26 @@ func init() { 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 logger.Info("picked role", "roles", roles, "index", i+1) 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() return nil } if event.Key() == tcell.KeyCtrlX { - persona := cfg.AssistantRole + botPersona := cfg.AssistantRole if cfg.WriteNextMsgAsCompletionAgent != "" { - persona = cfg.WriteNextMsgAsCompletionAgent + botPersona = cfg.WriteNextMsgAsCompletionAgent } roles := chatBody.ListRoles() if len(roles) == 0 { @@ -1207,12 +1213,14 @@ func init() { roles = append(roles, cfg.AssistantRole) } for i, role := range roles { - if strings.EqualFold(role, persona) { + if strings.EqualFold(role, botPersona) { if i == len(roles)-1 { cfg.WriteNextMsgAsCompletionAgent = roles[0] // reached last, get first + botPersona = cfg.WriteNextMsgAsCompletionAgent break } cfg.WriteNextMsgAsCompletionAgent = roles[i+1] // get next role + botPersona = cfg.WriteNextMsgAsCompletionAgent logger.Info("picked role", "roles", roles, "index", i+1) break } @@ -1295,7 +1303,6 @@ func init() { // cannot send msg in editMode or botRespMode if event.Key() == tcell.KeyEscape && !editMode && !botRespMode { msgText := textArea.GetText() - // TODO: add shellmode command -> output to the chat history, or at least have an option if shellMode && msgText != "" { // In shell mode, execute command instead of sending to LLM executeCommandAndDisplay(msgText) -- cgit v1.2.3 From 3e2a1b6f9975aaa2b9cb45bcb77aac146a37fd3c Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 17 Jan 2026 13:03:30 +0300 Subject: Fix: KnowTo is added only if tag present --- tui.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'tui.go') diff --git a/tui.go b/tui.go index aa9972a..8454d45 100644 --- a/tui.go +++ b/tui.go @@ -93,6 +93,7 @@ var ( [yellow]Alt+4[white]: edit msg role [yellow]Alt+5[white]: toggle system and tool messages display [yellow]Alt+6[white]: toggle status line visibility +[yellow]Alt+7[white]: toggle role injection (inject role in messages) [yellow]Alt+8[white]: show char img or last picked img [yellow]Alt+9[white]: warm up (load) selected llama.cpp model @@ -828,6 +829,18 @@ func init() { } updateStatusLine() } + // Handle Alt+7 to toggle injectRole + if event.Key() == tcell.KeyRune && event.Rune() == '7' && event.Modifiers()&tcell.ModAlt != 0 { + injectRole = !injectRole + status := "disabled" + if injectRole { + status = "enabled" + } + if err := notifyUser("injectRole", fmt.Sprintf("Role injection %s", status)); err != nil { + logger.Error("failed to send notification", "error", err) + } + updateStatusLine() + } if event.Key() == tcell.KeyF1 { // chatList, err := loadHistoryChats() chatList, err := store.GetChatByChar(cfg.AssistantRole) -- cgit v1.2.3 From 4e597e944eacbeb5269dfdf586dd4a2163762a17 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 17 Jan 2026 13:07:14 +0300 Subject: Chore: log cleanup --- tui.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 8454d45..dc90db9 100644 --- a/tui.go +++ b/tui.go @@ -1202,7 +1202,7 @@ func init() { } cfg.WriteNextMsgAs = roles[i+1] // get next role persona = cfg.WriteNextMsgAs - logger.Info("picked role", "roles", roles, "index", i+1) + // logger.Info("picked role", "roles", roles, "index", i+1) break } } @@ -1234,7 +1234,7 @@ func init() { } cfg.WriteNextMsgAsCompletionAgent = roles[i+1] // get next role botPersona = cfg.WriteNextMsgAsCompletionAgent - logger.Info("picked role", "roles", roles, "index", i+1) + // logger.Info("picked role", "roles", roles, "index", i+1) break } } -- cgit v1.2.3 From a28e8ef9e250ace5c9624393da308c189c0839f6 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Wed, 21 Jan 2026 21:01:01 +0300 Subject: Enha: charlist in cards --- tui.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index dc90db9..54a4e32 100644 --- a/tui.go +++ b/tui.go @@ -836,7 +836,7 @@ func init() { if injectRole { status = "enabled" } - if err := notifyUser("injectRole", fmt.Sprintf("Role injection %s", status)); err != nil { + if err := notifyUser("injectRole", "Role injection "+status); err != nil { logger.Error("failed to send notification", "error", err) } updateStatusLine() @@ -1218,7 +1218,8 @@ func init() { if cfg.WriteNextMsgAsCompletionAgent != "" { botPersona = cfg.WriteNextMsgAsCompletionAgent } - roles := chatBody.ListRoles() + // roles := chatBody.ListRoles() + roles := listChatRoles() if len(roles) == 0 { logger.Warn("empty roles in chat") } @@ -1229,11 +1230,9 @@ func init() { if strings.EqualFold(role, botPersona) { if i == len(roles)-1 { cfg.WriteNextMsgAsCompletionAgent = roles[0] // reached last, get first - botPersona = cfg.WriteNextMsgAsCompletionAgent break } cfg.WriteNextMsgAsCompletionAgent = roles[i+1] // get next role - botPersona = cfg.WriteNextMsgAsCompletionAgent // logger.Info("picked role", "roles", roles, "index", i+1) break } -- cgit v1.2.3 From 98138728542d0ed529d9d3a389c3531945d971f3 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 22 Jan 2026 09:29:56 +0300 Subject: Chore: bool colors for statusline --- tui.go | 7 ------- 1 file changed, 7 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 54a4e32..d222d15 100644 --- a/tui.go +++ b/tui.go @@ -832,13 +832,6 @@ func init() { // Handle Alt+7 to toggle injectRole if event.Key() == tcell.KeyRune && event.Rune() == '7' && event.Modifiers()&tcell.ModAlt != 0 { injectRole = !injectRole - status := "disabled" - if injectRole { - status = "enabled" - } - if err := notifyUser("injectRole", "Role injection "+status); err != nil { - logger.Error("failed to send notification", "error", err) - } updateStatusLine() } if event.Key() == tcell.KeyF1 { -- cgit v1.2.3 From 3a11210f52a850f84771e1642cafcc3027b85075 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 31 Jan 2026 12:57:53 +0300 Subject: Enha: avoid recursion in llm calls --- tui.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index d222d15..e164423 100644 --- a/tui.go +++ b/tui.go @@ -873,7 +873,8 @@ func init() { // there is no case where user msg is regenerated // lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) - go chatRound("", cfg.UserRole, textView, true, false) + // go chatRound("", cfg.UserRole, textView, true, false) + chatRoundChan <- &models.ChatRoundReq{Role: cfg.UserRole} return nil } if event.Key() == tcell.KeyF3 && !botRespMode { @@ -1176,7 +1177,8 @@ func init() { // INFO: continue bot/text message // without new role lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role - go chatRound("", lastRole, textView, false, true) + // go chatRound("", lastRole, textView, false, true) + chatRoundChan <- &models.ChatRoundReq{Role: lastRole, Resume: true} return nil } if event.Key() == tcell.KeyCtrlQ { @@ -1347,7 +1349,8 @@ func init() { } colorText() } - go chatRound(msgText, persona, textView, false, false) + // go chatRound(msgText, persona, textView, false, false) + chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText} // Also clear any image attachment after sending the message go func() { // Wait a short moment for the message to be processed, then clear the image attachment -- cgit v1.2.3 From e3965db3c7e7f5e3cdbf5d03ac06103c2709c0d8 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Wed, 4 Feb 2026 08:26:30 +0300 Subject: Enha: use slices methods --- tui.go | 3 --- 1 file changed, 3 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index e164423..8b1c520 100644 --- a/tui.go +++ b/tui.go @@ -1187,7 +1187,6 @@ func init() { persona = cfg.WriteNextMsgAs } roles := listRolesWithUser() - logger.Info("list roles", "roles", roles) for i, role := range roles { if strings.EqualFold(role, persona) { if i == len(roles)-1 { @@ -1197,7 +1196,6 @@ func init() { } cfg.WriteNextMsgAs = roles[i+1] // get next role persona = cfg.WriteNextMsgAs - // logger.Info("picked role", "roles", roles, "index", i+1) break } } @@ -1228,7 +1226,6 @@ func init() { break } cfg.WriteNextMsgAsCompletionAgent = roles[i+1] // get next role - // logger.Info("picked role", "roles", roles, "index", i+1) break } } -- cgit v1.2.3 From d0722c6f98aa4755f271aefdd8af1cca28fb6f35 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 5 Feb 2026 08:24:51 +0300 Subject: Fix: add regen param for f2 --- tui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 8b1c520..c1f2917 100644 --- a/tui.go +++ b/tui.go @@ -874,7 +874,7 @@ func init() { // lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) // go chatRound("", cfg.UserRole, textView, true, false) - chatRoundChan <- &models.ChatRoundReq{Role: cfg.UserRole} + chatRoundChan <- &models.ChatRoundReq{Role: cfg.UserRole, Regen: true} return nil } if event.Key() == tcell.KeyF3 && !botRespMode { -- cgit v1.2.3 From 4af866079c3f21eab12b02c3158567539ca40c50 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 6 Feb 2026 12:42:06 +0300 Subject: Chore: linter complaints --- tui.go | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index c1f2917..70f67f1 100644 --- a/tui.go +++ b/tui.go @@ -533,8 +533,7 @@ func init() { }) textView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { // Handle vim-like navigation in TextView - switch event.Key() { - case tcell.KeyRune: + if event.Key() == tcell.KeyRune { switch event.Rune() { case 'j': // For line down @@ -672,17 +671,18 @@ func init() { return nil } m := chatBody.Messages[selectedIndex] - if roleEditMode { + switch { + case roleEditMode: hideIndexBar() // Hide overlay first // Set the current role as the default text in the input field roleEditWindow.SetText(m.Role) pages.AddPage(roleEditPage, roleEditWindow, true, true) roleEditMode = false // Reset the flag - } else if editMode { + case editMode: hideIndexBar() // Hide overlay first pages.AddPage(editMsgPage, editArea, true, true) editArea.SetText(m.Content, true) - } else { + default: if err := copyToClipboard(m.Content); err != nil { logger.Error("failed to copy to clipboard", "error", err) } @@ -760,22 +760,19 @@ func init() { pages.RemovePage(helpPage) }) helpView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyEnter: + if event.Key() == tcell.KeyEnter { return event - default: - if event.Key() == tcell.KeyRune && event.Rune() == 'x' { - pages.RemovePage(helpPage) - return nil - } + } + if event.Key() == tcell.KeyRune && event.Rune() == 'x' { + pages.RemovePage(helpPage) + return nil } return nil }) // imgView = tview.NewImage() imgView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyEnter: + if event.Key() == tcell.KeyEnter { pages.RemovePage(imgPage) return event } -- cgit v1.2.3 From 77ad2a7e7e2c3bade4d949d8eb5c36e0126f4668 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Mon, 9 Feb 2026 08:52:11 +0300 Subject: Enha: popups from the main window no longer user has to go to the props table to get a pleasant popup to choose an option --- tui.go | 108 ++++++++--------------------------------------------------------- 1 file changed, 12 insertions(+), 96 deletions(-) (limited to 'tui.go') 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 () 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 { -- cgit v1.2.3 From 3f4d8a946775cfba6fc6d0ac7ade30b310bb883b Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Mon, 9 Feb 2026 11:29:47 +0300 Subject: Fix (f1): load from the card --- tui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 87e878a..cac8faa 100644 --- a/tui.go +++ b/tui.go @@ -1016,7 +1016,7 @@ func init() { return nil } if event.Key() == tcell.KeyCtrlN { - startNewChat() + startNewChat(true) return nil } if event.Key() == tcell.KeyCtrlO { -- cgit v1.2.3