From 3c6f9b624ec4ef6f2e92ddb2ba794a0d8cddd6cf Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Tue, 23 Dec 2025 22:41:21 +0300 Subject: Fix: last msg duplication --- bot.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bot.go b/bot.go index 1ad3c94..f2c986f 100644 --- a/bot.go +++ b/bot.go @@ -91,11 +91,8 @@ func cleanToolCalls(messages []models.RoleMsg) []models.RoleMsg { cleaned := make([]models.RoleMsg, 0, len(messages)) for i, msg := range messages { // recognize the message as the tool call and remove it - if msg.ToolCallID == "" { - cleaned = append(cleaned, msg) - } // tool call in last msg should stay - if i == len(messages)-1 { + if msg.ToolCallID == "" || i == len(messages)-1 { cleaned = append(cleaned, msg) } } -- cgit v1.2.3 From c0ec82b579f09af020bc236eb84fa6b22d68823b Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 26 Dec 2025 10:01:49 +0300 Subject: Chore: only table switch for auto clean tool calls --- bot.go | 6 +++++- config.example.toml | 1 + config/config.go | 1 + main.go | 2 +- props_table.go | 3 +++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bot.go b/bot.go index f2c986f..8b5a889 100644 --- a/bot.go +++ b/bot.go @@ -88,6 +88,10 @@ func cleanNullMessages(messages []models.RoleMsg) []models.RoleMsg { } func cleanToolCalls(messages []models.RoleMsg) []models.RoleMsg { + // If AutoCleanToolCallsFromCtx is false, keep tool call messages in context + if cfg != nil && !cfg.AutoCleanToolCallsFromCtx { + return consolidateConsecutiveAssistantMessages(messages) + } cleaned := make([]models.RoleMsg, 0, len(messages)) for i, msg := range messages { // recognize the message as the tool call and remove it @@ -731,7 +735,7 @@ func cleanChatBody() { for i, msg := range chatBody.Messages { logger.Debug("cleanChatBody: before clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) } - // TODO: consider case where we keep tool requests + // Tool request cleaning is now configurable via AutoCleanToolCallsFromCtx (default false) // /completion msg where part meant for user and other part tool call chatBody.Messages = cleanToolCalls(chatBody.Messages) chatBody.Messages = cleanNullMessages(chatBody.Messages) diff --git a/config.example.toml b/config.example.toml index 113b7ea..594e4da 100644 --- a/config.example.toml +++ b/config.example.toml @@ -18,6 +18,7 @@ ToolRole = "tool" AssistantRole = "assistant" SysDir = "sysprompts" ChunkLimit = 100000 +# AutoCleanToolCallsFromCtx = false # rag settings RAGBatchSize = 1 RAGWordLimit = 80 diff --git a/config/config.go b/config/config.go index 5b7cc35..112986b 100644 --- a/config/config.go +++ b/config/config.go @@ -31,6 +31,7 @@ type Config struct { WriteNextMsgAs string WriteNextMsgAsCompletionAgent string SkipLLMResp bool + AutoCleanToolCallsFromCtx bool `toml:"AutoCleanToolCallsFromCtx"` // embeddings RAGEnabled bool `toml:"RAGEnabled"` EmbedURL string `toml:"EmbedURL"` diff --git a/main.go b/main.go index ec175bf..6b255a2 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ var ( currentLocalModelIndex = 0 // Index to track current llama.cpp model shellMode = false // indexLine = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | ThinkUse: [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q)" - indexLineCompletion = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | Insert : [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | Bot will write as [orange:-:b]%s[-:-:-] (ctrl+x) | role_inject [orange:-:b]%v[-:-:-]" + indexLineCompletion = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10) | Insert : [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | Bot will write as [orange:-:b]%s[-:-:-] (ctrl+x) | role_inject [orange:-:b]%v[-:-:-]" focusSwitcher = map[tview.Primitive]tview.Primitive{} ) diff --git a/props_table.go b/props_table.go index 0c49056..d86e0b4 100644 --- a/props_table.go +++ b/props_table.go @@ -129,6 +129,9 @@ func makePropsTable(props map[string]float32) *tview.Table { addCheckboxRow("TTS Enabled", cfg.TTS_ENABLED, func(checked bool) { cfg.TTS_ENABLED = checked }) + addCheckboxRow("Auto clean tool calls from context", cfg.AutoCleanToolCallsFromCtx, func(checked bool) { + cfg.AutoCleanToolCallsFromCtx = checked + }) // Add dropdowns logLevels := []string{"Debug", "Info", "Warn"} addListPopupRow("Set log level", logLevels, GetLogLevel(), func(option string) { -- cgit v1.2.3 From 61a29dee8c5d6e0a0d6ed3b4c7d06cd35c4a3094 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 26 Dec 2025 10:31:26 +0300 Subject: Fix: statusline --- helpfuncs.go | 6 ++---- main.go | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/helpfuncs.go b/helpfuncs.go index 30d9967..0efed05 100644 --- a/helpfuncs.go +++ b/helpfuncs.go @@ -227,7 +227,6 @@ func makeStatusLine() string { } else { imageInfo = "" } - // Add shell mode status to status line var shellModeInfo string if shellMode { @@ -235,9 +234,8 @@ func makeStatusLine() string { } else { shellModeInfo = "" } - - statusLine := fmt.Sprintf(indexLineCompletion, botRespMode, cfg.AssistantRole, activeChatName, - cfg.ToolUse, chatBody.Model, cfg.SkipLLMResp, cfg.CurrentAPI, cfg.ThinkUse, logLevel.Level(), + statusLine := fmt.Sprintf(indexLineCompletion, botRespMode, activeChatName, + cfg.ToolUse, chatBody.Model, cfg.SkipLLMResp, cfg.CurrentAPI, isRecording, persona, botPersona, injectRole) return statusLine + imageInfo + shellModeInfo } diff --git a/main.go b/main.go index 6b255a2..9e619a8 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ var ( currentLocalModelIndex = 0 // Index to track current llama.cpp model shellMode = false // indexLine = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | ThinkUse: [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q)" - indexLineCompletion = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10) | Insert : [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | Bot will write as [orange:-:b]%s[-:-:-] (ctrl+x) | role_inject [orange:-:b]%v[-:-:-]" + indexLineCompletion = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | Bot will write as [orange:-:b]%s[-:-:-] (ctrl+x) | role_inject [orange:-:b]%v[-:-:-]" focusSwitcher = map[tview.Primitive]tview.Primitive{} ) -- cgit v1.2.3 From c43af62dc41099d2b31e56deb20e3c81c4f97855 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 26 Dec 2025 22:05:19 +0300 Subject: Feat: alt+3 to start new chat with the summary of old one --- bot.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------- tui.go | 11 +++++++--- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/bot.go b/bot.go index 8b5a889..f5dc6d6 100644 --- a/bot.go +++ b/bot.go @@ -69,6 +69,7 @@ var ( "meta-llama/llama-3.3-70b-instruct:free", } LocalModels = []string{} + lastSummary string ) // cleanNullMessages removes messages with null or empty content to prevent API issues @@ -626,8 +627,22 @@ func checkGame(role string, tv *tview.TextView) { } } -func chatRound(userMsg, role string, tv *tview.TextView, regen, resume bool) { +func chatRound(userMsg, role string, tv *tview.TextView, regen, resume, summaryMode bool) { botRespMode = true + if summaryMode { + // Save original messages + originalMessages := chatBody.Messages + defer func() { chatBody.Messages = originalMessages }() + // Build summary prompt messages + summaryMessages := []models.RoleMsg{} + // Add system instruction + summaryMessages = append(summaryMessages, models.RoleMsg{Role: "system", Content: "Please provide a concise summary of the following conversation. Focus on key points, decisions, and actions. Provide only the summary, no additional commentary."}) + // Append all original messages (excluding system? keep them) + summaryMessages = append(summaryMessages, originalMessages...) + // Add a user message to trigger summary + summaryMessages = append(summaryMessages, models.RoleMsg{Role: cfg.UserRole, Content: "Summarize the conversation."}) + chatBody.Messages = summaryMessages + } botPersona := cfg.AssistantRole if cfg.WriteNextMsgAsCompletionAgent != "" { botPersona = cfg.WriteNextMsgAsCompletionAgent @@ -657,7 +672,7 @@ func chatRound(userMsg, role string, tv *tview.TextView, regen, resume bool) { } go sendMsgToLLM(reader) logger.Debug("looking at vars in chatRound", "msg", userMsg, "regen", regen, "resume", resume) - if !resume { + if !summaryMode && !resume { fmt.Fprintf(tv, "\n[-:-:b](%d) ", len(chatBody.Messages)) fmt.Fprint(tv, roleToIcon(botPersona)) fmt.Fprint(tv, "[-:-:-]\n") @@ -704,6 +719,9 @@ out: Role: botPersona, Content: respText.String(), }) } + if summaryMode { + lastSummary = respText.String() + } logger.Debug("chatRound: before cleanChatBody", "messages_before_clean", len(chatBody.Messages)) for i, msg := range chatBody.Messages { logger.Debug("chatRound: before cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) @@ -714,15 +732,19 @@ out: for i, msg := range chatBody.Messages { logger.Debug("chatRound: after cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) } - colorText() - updateStatusLine() + if !summaryMode { + colorText() + updateStatusLine() + } // bot msg is done; // now check it for func call // logChat(activeChatName, chatBody.Messages) - if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil { - logger.Warn("failed to update storage", "error", err, "name", activeChatName) + if !summaryMode { + if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil { + logger.Warn("failed to update storage", "error", err, "name", activeChatName) + } + findCall(respText.String(), toolResp.String(), tv) } - findCall(respText.String(), toolResp.String(), tv) } // cleanChatBody removes messages with null or empty content to prevent API issues @@ -825,7 +847,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { chatBody.Messages = append(chatBody.Messages, toolResponseMsg) // Clear the stored tool call ID after using it (no longer needed) // Trigger the assistant to continue processing with the error message - chatRound("", cfg.AssistantRole, tv, false, false) + chatRound("", cfg.AssistantRole, tv, false, false, false) return } lastToolCall.Args = openAIToolMap @@ -858,7 +880,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { chatBody.Messages = append(chatBody.Messages, toolResponseMsg) logger.Debug("findCall: added tool error response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "message_count_after_add", len(chatBody.Messages)) // Trigger the assistant to continue processing with the error message - chatRound("", cfg.AssistantRole, tv, false, false) + chatRound("", cfg.AssistantRole, tv, false, false, false) return } // Update lastToolCall with parsed function call @@ -891,7 +913,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { lastToolCall.ID = "" // Trigger the assistant to continue processing with the new tool response // by calling chatRound with empty content to continue the assistant's response - chatRound("", cfg.AssistantRole, tv, false, false) + chatRound("", cfg.AssistantRole, tv, false, false, false) return } resp := callToolWithAgent(fc.Name, fc.Args) @@ -911,7 +933,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { lastToolCall.ID = "" // Trigger the assistant to continue processing with the new tool response // by calling chatRound with empty content to continue the assistant's response - chatRound("", cfg.AssistantRole, tv, false, false) + chatRound("", cfg.AssistantRole, tv, false, false, false) } func chatToTextSlice(showSys bool) []string { @@ -1033,6 +1055,41 @@ func refreshLocalModelsIfEmpty() { localModelsMu.Unlock() } +func summarizeAndStartNewChat() { + if len(chatBody.Messages) == 0 { + notifyUser("info", "No chat history to summarize") + return + } + // Create a dummy TextView for the summary request (won't be displayed) + dummyTV := tview.NewTextView() + // Call chatRound with summaryMode true to generate summary + notifyUser("info", "Summarizing chat history...") + lastSummary = "" + chatRound("", cfg.UserRole, dummyTV, false, false, true) + summary := lastSummary + if summary == "" { + notifyUser("error", "Failed to generate summary") + return + } + // Start a new chat + startNewChat() + // Inject summary as a tool call response + toolMsg := models.RoleMsg{ + Role: cfg.ToolRole, + Content: summary, + ToolCallID: "", + } + chatBody.Messages = append(chatBody.Messages, toolMsg) + // Update UI + textView.SetText(chatToText(cfg.ShowSys)) + colorText() + // Update storage + if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil { + logger.Warn("failed to update storage after injecting summary", "error", err) + } + notifyUser("info", "Chat summarized and new chat started with summary as tool response") +} + func init() { var err error cfg, err = config.LoadConfig("config.toml") diff --git a/tui.go b/tui.go index 581ae4f..e6be85b 100644 --- a/tui.go +++ b/tui.go @@ -91,6 +91,7 @@ var ( [yellow]Alt+1[white]: toggle shell mode (execute commands locally) [yellow]Alt+4[white]: edit msg role [yellow]Alt+5[white]: toggle system and tool messages display +[yellow]Alt+3[white]: summarize chat history and start new chat with summary as tool response [yellow]Alt+6[white]: toggle status line visibility [yellow]Alt+9[white]: warm up (load) selected llama.cpp model @@ -779,6 +780,10 @@ func init() { textView.SetText(chatToText(cfg.ShowSys)) colorText() } + if event.Key() == tcell.KeyRune && event.Rune() == '3' && event.Modifiers()&tcell.ModAlt != 0 { + go summarizeAndStartNewChat() + return nil + } if event.Key() == tcell.KeyRune && event.Rune() == '6' && event.Modifiers()&tcell.ModAlt != 0 { // toggle status line visibility if name, _ := pages.GetFrontPage(); name != "main" { @@ -826,7 +831,7 @@ func init() { // there is no case where user msg is regenerated // lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role textView.SetText(chatToText(cfg.ShowSys)) - go chatRound("", cfg.UserRole, textView, true, false) + go chatRound("", cfg.UserRole, textView, true, false, false) return nil } if event.Key() == tcell.KeyF3 && !botRespMode { @@ -1129,7 +1134,7 @@ 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, false) return nil } if event.Key() == tcell.KeyCtrlQ { @@ -1289,7 +1294,7 @@ func init() { textView.ScrollToEnd() colorText() } - go chatRound(msgText, persona, textView, false, false) + go chatRound(msgText, persona, textView, false, false, false) // 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 af2684664725f3e5da79d1d53e714b48e430e6fb Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 27 Dec 2025 11:10:03 +0300 Subject: Enha: summary agent --- bot.go | 61 ++++++++++++++++++------------------------------------------- tools.go | 13 +++++++++++++ tui.go | 6 +++--- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/bot.go b/bot.go index f5dc6d6..efd72be 100644 --- a/bot.go +++ b/bot.go @@ -69,7 +69,6 @@ var ( "meta-llama/llama-3.3-70b-instruct:free", } LocalModels = []string{} - lastSummary string ) // cleanNullMessages removes messages with null or empty content to prevent API issues @@ -627,22 +626,8 @@ func checkGame(role string, tv *tview.TextView) { } } -func chatRound(userMsg, role string, tv *tview.TextView, regen, resume, summaryMode bool) { +func chatRound(userMsg, role string, tv *tview.TextView, regen, resume bool) { botRespMode = true - if summaryMode { - // Save original messages - originalMessages := chatBody.Messages - defer func() { chatBody.Messages = originalMessages }() - // Build summary prompt messages - summaryMessages := []models.RoleMsg{} - // Add system instruction - summaryMessages = append(summaryMessages, models.RoleMsg{Role: "system", Content: "Please provide a concise summary of the following conversation. Focus on key points, decisions, and actions. Provide only the summary, no additional commentary."}) - // Append all original messages (excluding system? keep them) - summaryMessages = append(summaryMessages, originalMessages...) - // Add a user message to trigger summary - summaryMessages = append(summaryMessages, models.RoleMsg{Role: cfg.UserRole, Content: "Summarize the conversation."}) - chatBody.Messages = summaryMessages - } botPersona := cfg.AssistantRole if cfg.WriteNextMsgAsCompletionAgent != "" { botPersona = cfg.WriteNextMsgAsCompletionAgent @@ -672,7 +657,7 @@ func chatRound(userMsg, role string, tv *tview.TextView, regen, resume, summaryM } go sendMsgToLLM(reader) logger.Debug("looking at vars in chatRound", "msg", userMsg, "regen", regen, "resume", resume) - if !summaryMode && !resume { + if !resume { fmt.Fprintf(tv, "\n[-:-:b](%d) ", len(chatBody.Messages)) fmt.Fprint(tv, roleToIcon(botPersona)) fmt.Fprint(tv, "[-:-:-]\n") @@ -719,9 +704,6 @@ out: Role: botPersona, Content: respText.String(), }) } - if summaryMode { - lastSummary = respText.String() - } logger.Debug("chatRound: before cleanChatBody", "messages_before_clean", len(chatBody.Messages)) for i, msg := range chatBody.Messages { logger.Debug("chatRound: before cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) @@ -732,19 +714,15 @@ out: for i, msg := range chatBody.Messages { logger.Debug("chatRound: after cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) } - if !summaryMode { - colorText() - updateStatusLine() - } + colorText() + updateStatusLine() // bot msg is done; // now check it for func call // logChat(activeChatName, chatBody.Messages) - if !summaryMode { - if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil { - logger.Warn("failed to update storage", "error", err, "name", activeChatName) - } - findCall(respText.String(), toolResp.String(), tv) + if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil { + logger.Warn("failed to update storage", "error", err, "name", activeChatName) } + findCall(respText.String(), toolResp.String(), tv) } // cleanChatBody removes messages with null or empty content to prevent API issues @@ -847,7 +825,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { chatBody.Messages = append(chatBody.Messages, toolResponseMsg) // Clear the stored tool call ID after using it (no longer needed) // Trigger the assistant to continue processing with the error message - chatRound("", cfg.AssistantRole, tv, false, false, false) + chatRound("", cfg.AssistantRole, tv, false, false) return } lastToolCall.Args = openAIToolMap @@ -880,7 +858,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { chatBody.Messages = append(chatBody.Messages, toolResponseMsg) logger.Debug("findCall: added tool error response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "message_count_after_add", len(chatBody.Messages)) // Trigger the assistant to continue processing with the error message - chatRound("", cfg.AssistantRole, tv, false, false, false) + chatRound("", cfg.AssistantRole, tv, false, false) return } // Update lastToolCall with parsed function call @@ -913,7 +891,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { lastToolCall.ID = "" // Trigger the assistant to continue processing with the new tool response // by calling chatRound with empty content to continue the assistant's response - chatRound("", cfg.AssistantRole, tv, false, false, false) + chatRound("", cfg.AssistantRole, tv, false, false) return } resp := callToolWithAgent(fc.Name, fc.Args) @@ -933,7 +911,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { lastToolCall.ID = "" // Trigger the assistant to continue processing with the new tool response // by calling chatRound with empty content to continue the assistant's response - chatRound("", cfg.AssistantRole, tv, false, false, false) + chatRound("", cfg.AssistantRole, tv, false, false) } func chatToTextSlice(showSys bool) []string { @@ -1057,18 +1035,15 @@ func refreshLocalModelsIfEmpty() { func summarizeAndStartNewChat() { if len(chatBody.Messages) == 0 { - notifyUser("info", "No chat history to summarize") + _ = notifyUser("info", "No chat history to summarize") return } - // Create a dummy TextView for the summary request (won't be displayed) - dummyTV := tview.NewTextView() - // Call chatRound with summaryMode true to generate summary - notifyUser("info", "Summarizing chat history...") - lastSummary = "" - chatRound("", cfg.UserRole, dummyTV, false, false, true) - summary := lastSummary + _ = notifyUser("info", "Summarizing chat history...") + // Call the summarize_chat tool via agent + summaryBytes := callToolWithAgent("summarize_chat", map[string]string{}) + summary := string(summaryBytes) if summary == "" { - notifyUser("error", "Failed to generate summary") + _ = notifyUser("error", "Failed to generate summary") return } // Start a new chat @@ -1087,7 +1062,7 @@ func summarizeAndStartNewChat() { if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil { logger.Warn("failed to update storage after injecting summary", "error", err) } - notifyUser("info", "Chat summarized and new chat started with summary as tool response") + _ = notifyUser("info", "Chat summarized and new chat started with summary as tool response") } func init() { diff --git a/tools.go b/tools.go index 49d8192..05ce828 100644 --- a/tools.go +++ b/tools.go @@ -129,6 +129,7 @@ After that you are free to respond to the user. ` webSearchSysPrompt = `Summarize the web search results, extracting key information and presenting a concise answer. Provide sources and URLs where relevant.` readURLSysPrompt = `Extract and summarize the content from the webpage. Provide key information, main points, and any relevant details.` + summarySysPrompt = `Please provide a concise summary of the following conversation. Focus on key points, decisions, and actions. Provide only the summary, no additional commentary.` basicCard = &models.CharCard{ SysPrompt: basicSysMsg, FirstMsg: defaultFirstMsg, @@ -178,6 +179,8 @@ func registerWebAgents() { agent.Register("websearch", agent.NewWebAgentB(client, webSearchSysPrompt)) // Register read_url agent agent.Register("read_url", agent.NewWebAgentB(client, readURLSysPrompt)) + // Register summarize_chat agent + agent.Register("summarize_chat", agent.NewWebAgentB(client, summarySysPrompt)) }) } @@ -864,6 +867,15 @@ func isCommandAllowed(command string) bool { return allowedCommands[command] } +func summarizeChat(args map[string]string) []byte { + if len(chatBody.Messages) == 0 { + return []byte("No chat history to summarize.") + } + // Format chat history for the agent + chatText := chatToText(true) // include system and tool messages + return []byte(chatText) +} + type fnSig func(map[string]string) []byte var fnMap = map[string]fnSig{ @@ -884,6 +896,7 @@ var fnMap = map[string]fnSig{ "todo_read": todoRead, "todo_update": todoUpdate, "todo_delete": todoDelete, + "summarize_chat": summarizeChat, } // callToolWithAgent calls the tool and applies any registered agent. diff --git a/tui.go b/tui.go index e6be85b..31e0f25 100644 --- a/tui.go +++ b/tui.go @@ -831,7 +831,7 @@ func init() { // there is no case where user msg is regenerated // lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role textView.SetText(chatToText(cfg.ShowSys)) - go chatRound("", cfg.UserRole, textView, true, false, false) + go chatRound("", cfg.UserRole, textView, true, false) return nil } if event.Key() == tcell.KeyF3 && !botRespMode { @@ -1134,7 +1134,7 @@ func init() { // INFO: continue bot/text message // without new role lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role - go chatRound("", lastRole, textView, false, true, false) + go chatRound("", lastRole, textView, false, true) return nil } if event.Key() == tcell.KeyCtrlQ { @@ -1294,7 +1294,7 @@ func init() { textView.ScrollToEnd() colorText() } - go chatRound(msgText, persona, textView, false, false, false) + go chatRound(msgText, persona, textView, false, false) // 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 99151672d48a419df175655cee167d8df46ee821 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 27 Dec 2025 19:57:03 +0300 Subject: Chore: alt order --- tui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tui.go b/tui.go index 31e0f25..9b94062 100644 --- a/tui.go +++ b/tui.go @@ -89,9 +89,9 @@ var ( [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]Alt+1[white]: toggle shell mode (execute commands locally) +[yellow]Alt+3[white]: summarize chat history and start new chat with summary as tool response [yellow]Alt+4[white]: edit msg role [yellow]Alt+5[white]: toggle system and tool messages display -[yellow]Alt+3[white]: summarize chat history and start new chat with summary as tool response [yellow]Alt+6[white]: toggle status line visibility [yellow]Alt+9[white]: warm up (load) selected llama.cpp model -- cgit v1.2.3 From fd6eb6b398c71d5d4c74252d98def857213ae5bf Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 27 Dec 2025 21:08:13 +0300 Subject: Enha: /completion tool advice inject only if not present --- llm.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/llm.go b/llm.go index a557e3d..5621ecf 100644 --- a/llm.go +++ b/llm.go @@ -13,6 +13,16 @@ var imageAttachmentPath string // Global variable to track image attachment for var lastImg string // for ctrl+j var RAGMsg = "Retrieved context for user's query:\n" +// containsToolSysMsg checks if the toolSysMsg already exists in the chat body +func containsToolSysMsg() bool { + for _, msg := range chatBody.Messages { + if msg.Role == cfg.ToolRole && msg.Content == toolSysMsg { + return true + } + } + return false +} + // SetImageAttachment sets an image to be attached to the next message sent to the LLM func SetImageAttachment(imagePath string) { imageAttachmentPath = imagePath @@ -122,7 +132,7 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages)) } } - if cfg.ToolUse && !resume && role == cfg.UserRole { + if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { // add to chat body chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) } @@ -358,7 +368,7 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader logger.Debug("DeepSeekerCompletion: RAG message added to chat body", "message_count", len(chatBody.Messages)) } } - if cfg.ToolUse && !resume && role == cfg.UserRole { + if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { // add to chat body chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) } @@ -420,11 +430,6 @@ func (ds DeepSeekerChat) GetToken() string { func (ds DeepSeekerChat) FormMsg(msg, role string, resume bool) (io.Reader, error) { logger.Debug("formmsg deepseekerchat", "link", cfg.CurrentAPI) - if cfg.ToolUse && !resume && role == cfg.UserRole { - // prompt += "\n" + cfg.ToolRole + ":\n" + toolSysMsg - // add to chat body - chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) - } if msg != "" { // otherwise let the bot continue newMsg := models.RoleMsg{Role: role, Content: msg} chatBody.Messages = append(chatBody.Messages, newMsg) @@ -516,7 +521,7 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages)) } } - if cfg.ToolUse && !resume && role == cfg.UserRole { + if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { // add to chat body chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) } -- cgit v1.2.3 From 9d91685e9adde94d20313fb405c4301b4dd59a75 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 27 Dec 2025 21:47:53 +0300 Subject: Enha: chat table preview update --- tables.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tables.go b/tables.go index 87c3bbb..a88b501 100644 --- a/tables.go +++ b/tables.go @@ -23,12 +23,10 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { chatList[i] = name i++ } - // Add 1 extra row for header rows, cols := len(chatMap)+1, len(actions)+4 // +2 for name, +2 for timestamps chatActTable := tview.NewTable(). SetBorders(true) - // Add header row (row 0) for c := 0; c < cols; c++ { color := tcell.ColorWhite @@ -52,7 +50,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { SetAlign(tview.AlignCenter). SetAttributes(tcell.AttrBold)) } - + previewLen := 100 // Add data rows (starting from row 1) for r := 0; r < rows-1; r++ { // rows-1 because we added a header row for c := 0; c < cols; c++ { @@ -65,8 +63,11 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { SetTextColor(color). SetAlign(tview.AlignCenter)) case 1: + if len(chatMap[chatList[r]].Msgs) < 100 { + previewLen = len(chatMap[chatList[r]].Msgs) + } chatActTable.SetCell(r+1, c, // +1 to account for header row - tview.NewTableCell(chatMap[chatList[r]].Msgs[len(chatMap[chatList[r]].Msgs)-30:]). + tview.NewTableCell(chatMap[chatList[r]].Msgs[len(chatMap[chatList[r]].Msgs)-previewLen:]). SetSelectable(false). SetTextColor(color). SetAlign(tview.AlignCenter)) @@ -87,8 +88,8 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { default: chatActTable.SetCell(r+1, c, // +1 to account for header row tview.NewTableCell(actions[c-4]). // Adjusted offset to account for 2 new timestamp columns - SetTextColor(color). - SetAlign(tview.AlignCenter)) + SetTextColor(color). + SetAlign(tview.AlignCenter)) } } } @@ -104,7 +105,6 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { chatActTable.Select(1, column) // Move selection to first data row return } - tc := chatActTable.GetCell(row, column) tc.SetTextColor(tcell.ColorRed) chatActTable.SetSelectable(false, false) @@ -443,9 +443,7 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex { } return } - tc := fileTable.GetCell(row, column) - // Check if the selected row is the exit row (row 0) - do this first to avoid index issues if row == 0 { pages.RemovePage(RAGLoadedPage) @@ -537,7 +535,6 @@ func makeAgentTable(agentList []string) *tview.Table { } return } - tc := chatActTable.GetCell(row, column) selected := agentList[row] // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) @@ -634,7 +631,6 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table { } return } - tc := table.GetCell(row, column) selected := codeBlocks[row] // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) @@ -706,7 +702,6 @@ func makeImportChatTable(filenames []string) *tview.Table { } return } - tc := chatActTable.GetCell(row, column) selected := filenames[row] // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) -- cgit v1.2.3