Enha: native notification implementation

This commit is contained in:
Grail Finder
2026-03-04 11:25:13 +03:00
parent 50ce0200af
commit abcaad6609
8 changed files with 74 additions and 183 deletions

44
bot.go
View File

@@ -268,9 +268,7 @@ func warmUpModel() {
// Continue with warmup attempt anyway // Continue with warmup attempt anyway
} }
if loaded { if loaded {
if err := notifyUser("model already loaded", "Model "+chatBody.Model+" is already loaded."); err != nil { showToast("model already loaded", "Model "+chatBody.Model+" is already loaded.")
logger.Debug("failed to notify user", "error", err)
}
return return
} }
go func() { go func() {
@@ -483,9 +481,7 @@ func monitorModelLoad(modelID string) {
continue continue
} }
if loaded { if loaded {
if err := notifyUser("model loaded", "Model "+modelID+" is now loaded and ready."); err != nil { showToast("model loaded", "Model "+modelID+" is now loaded and ready.")
logger.Debug("failed to notify user", "error", err)
}
refreshChatDisplay() refreshChatDisplay()
return return
} }
@@ -572,9 +568,7 @@ func sendMsgToLLM(body io.Reader) {
req, err := http.NewRequest("POST", cfg.CurrentAPI, body) req, err := http.NewRequest("POST", cfg.CurrentAPI, body)
if err != nil { if err != nil {
logger.Error("newreq error", "error", err) logger.Error("newreq error", "error", err)
if err := notifyUser("error", "apicall failed:"+err.Error()); err != nil { showToast("error", "apicall failed:"+err.Error())
logger.Error("failed to notify", "error", err)
}
streamDone <- true streamDone <- true
return return
} }
@@ -586,9 +580,7 @@ func sendMsgToLLM(body io.Reader) {
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
logger.Error("llamacpp api", "error", err) logger.Error("llamacpp api", "error", err)
if err := notifyUser("error", "apicall failed:"+err.Error()); err != nil { showToast("error", "apicall failed:"+err.Error())
logger.Error("failed to notify", "error", err)
}
streamDone <- true streamDone <- true
return return
} }
@@ -599,9 +591,7 @@ func sendMsgToLLM(body io.Reader) {
if err != nil { if err != nil {
logger.Error("failed to read error response body", "error", err, "status_code", resp.StatusCode) logger.Error("failed to read error response body", "error", err, "status_code", resp.StatusCode)
detailedError := fmt.Sprintf("HTTP Status: %d, Failed to read response body: %v", resp.StatusCode, err) detailedError := fmt.Sprintf("HTTP Status: %d, Failed to read response body: %v", resp.StatusCode, err)
if err := notifyUser("API Error", detailedError); err != nil { showToast("API Error", detailedError)
logger.Error("failed to notify", "error", err)
}
resp.Body.Close() resp.Body.Close()
streamDone <- true streamDone <- true
return return
@@ -609,9 +599,7 @@ func sendMsgToLLM(body io.Reader) {
// Parse the error response for detailed information // Parse the error response for detailed information
detailedError := extractDetailedErrorFromBytes(bodyBytes, resp.StatusCode) detailedError := extractDetailedErrorFromBytes(bodyBytes, resp.StatusCode)
logger.Error("API returned error status", "status_code", resp.StatusCode, "detailed_error", detailedError) logger.Error("API returned error status", "status_code", resp.StatusCode, "detailed_error", detailedError)
if err := notifyUser("API Error", detailedError); err != nil { showToast("API Error", detailedError)
logger.Error("failed to notify", "error", err)
}
resp.Body.Close() resp.Body.Close()
streamDone <- true streamDone <- true
return return
@@ -648,16 +636,12 @@ func sendMsgToLLM(body io.Reader) {
detailedError := fmt.Sprintf("Streaming connection closed unexpectedly (Status: %d). This may indicate an API error. Check your API provider and model settings.", resp.StatusCode) detailedError := fmt.Sprintf("Streaming connection closed unexpectedly (Status: %d). This may indicate an API error. Check your API provider and model settings.", resp.StatusCode)
logger.Error("error reading response body", "error", err, "detailed_error", detailedError, logger.Error("error reading response body", "error", err, "detailed_error", detailedError,
"status_code", resp.StatusCode, "user_role", cfg.UserRole, "parser", chunkParser, "link", cfg.CurrentAPI) "status_code", resp.StatusCode, "user_role", cfg.UserRole, "parser", chunkParser, "link", cfg.CurrentAPI)
if err := notifyUser("API Error", detailedError); err != nil { showToast("API Error", detailedError)
logger.Error("failed to notify", "error", err)
}
} else { } else {
logger.Error("error reading response body", "error", err, "line", string(line), logger.Error("error reading response body", "error", err, "line", string(line),
"user_role", cfg.UserRole, "parser", chunkParser, "link", cfg.CurrentAPI) "user_role", cfg.UserRole, "parser", chunkParser, "link", cfg.CurrentAPI)
// if err.Error() != "EOF" { // if err.Error() != "EOF" {
if err := notifyUser("API error", err.Error()); err != nil { showToast("API error", err.Error())
logger.Error("failed to notify", "error", err)
}
} }
streamDone <- true streamDone <- true
break break
@@ -684,9 +668,7 @@ func sendMsgToLLM(body io.Reader) {
if err != nil { if err != nil {
logger.Error("error parsing response body", "error", err, logger.Error("error parsing response body", "error", err,
"line", string(line), "url", cfg.CurrentAPI) "line", string(line), "url", cfg.CurrentAPI)
if err := notifyUser("LLM Response Error", "Failed to parse LLM response: "+err.Error()); err != nil { showToast("LLM Response Error", "Failed to parse LLM response: "+err.Error())
logger.Error("failed to notify user", "error", err)
}
streamDone <- true streamDone <- true
break break
} }
@@ -1456,15 +1438,15 @@ func refreshLocalModelsIfEmpty() {
func summarizeAndStartNewChat() { func summarizeAndStartNewChat() {
if len(chatBody.Messages) == 0 { if len(chatBody.Messages) == 0 {
_ = notifyUser("info", "No chat history to summarize") showToast("info", "No chat history to summarize")
return return
} }
_ = notifyUser("info", "Summarizing chat history...") showToast("info", "Summarizing chat history...")
// Call the summarize_chat tool via agent // Call the summarize_chat tool via agent
summaryBytes := callToolWithAgent("summarize_chat", map[string]string{}) summaryBytes := callToolWithAgent("summarize_chat", map[string]string{})
summary := string(summaryBytes) summary := string(summaryBytes)
if summary == "" { if summary == "" {
_ = notifyUser("error", "Failed to generate summary") showToast("error", "Failed to generate summary")
return return
} }
// Start a new chat // Start a new chat
@@ -1483,7 +1465,7 @@ func summarizeAndStartNewChat() {
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil { if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
logger.Warn("failed to update storage after injecting summary", "error", err) logger.Warn("failed to update storage after injecting summary", "error", err)
} }
_ = notifyUser("info", "Chat summarized and new chat started with summary as tool response") showToast("info", "Chat summarized and new chat started with summary as tool response")
} }
func init() { func init() {

View File

@@ -491,10 +491,7 @@ func listChatRoles() []string {
func deepseekModelValidator() error { func deepseekModelValidator() error {
if cfg.CurrentAPI == cfg.DeepSeekChatAPI || cfg.CurrentAPI == cfg.DeepSeekCompletionAPI { if cfg.CurrentAPI == cfg.DeepSeekChatAPI || cfg.CurrentAPI == cfg.DeepSeekCompletionAPI {
if chatBody.Model != "deepseek-chat" && chatBody.Model != "deepseek-reasoner" { if chatBody.Model != "deepseek-chat" && chatBody.Model != "deepseek-reasoner" {
if err := notifyUser("bad request", "wrong deepseek model name"); err != nil { showToast("bad request", "wrong deepseek model name")
logger.Warn("failed ot notify user", "error", err)
return err
}
return nil return nil
} }
} }
@@ -694,9 +691,7 @@ func performSearch(term string) {
searchResults = nil searchResults = nil
searchResultLengths = nil searchResultLengths = nil
notification := "Pattern not found: " + term notification := "Pattern not found: " + term
if err := notifyUser("search", notification); err != nil { showToast("search", notification)
logger.Error("failed to send notification", "error", err)
}
return return
} }
// Store the formatted text positions and lengths for accurate highlighting // Store the formatted text positions and lengths for accurate highlighting
@@ -729,9 +724,7 @@ func highlightCurrentMatch() {
textView.Highlight(currentRegion).ScrollToHighlight() textView.Highlight(currentRegion).ScrollToHighlight()
// Send notification about which match we're at // Send notification about which match we're at
notification := fmt.Sprintf("Match %d of %d", searchIndex+1, len(searchResults)) notification := fmt.Sprintf("Match %d of %d", searchIndex+1, len(searchResults))
if err := notifyUser("search", notification); err != nil { showToast("search", notification)
logger.Error("failed to send notification", "error", err)
}
} }
// showSearchBar shows the search input field as an overlay // showSearchBar shows the search input field as an overlay
@@ -821,9 +814,7 @@ func addRegionTags(text string, positions []int, lengths []int, currentIdx int,
// searchNext finds the next occurrence of the search term // searchNext finds the next occurrence of the search term
func searchNext() { func searchNext() {
if len(searchResults) == 0 { if len(searchResults) == 0 {
if err := notifyUser("search", "No search results to navigate"); err != nil { showToast("search", "No search results to navigate")
logger.Error("failed to send notification", "error", err)
}
return return
} }
searchIndex = (searchIndex + 1) % len(searchResults) searchIndex = (searchIndex + 1) % len(searchResults)
@@ -833,9 +824,7 @@ func searchNext() {
// searchPrev finds the previous occurrence of the search term // searchPrev finds the previous occurrence of the search term
func searchPrev() { func searchPrev() {
if len(searchResults) == 0 { if len(searchResults) == 0 {
if err := notifyUser("search", "No search results to navigate"); err != nil { showToast("search", "No search results to navigate")
logger.Error("failed to send notification", "error", err)
}
return return
} }
if searchIndex == 0 { if searchIndex == 0 {

View File

@@ -40,9 +40,7 @@ func showModelSelectionPopup() {
default: default:
message = "No llama.cpp models loaded. Ensure llama.cpp server is running with models." message = "No llama.cpp models loaded. Ensure llama.cpp server is running with models."
} }
if err := notifyUser("Empty list", message); err != nil { showToast("Empty list", message)
logger.Error("failed to send notification", "error", err)
}
return return
} }
// Create a list primitive // Create a list primitive
@@ -119,9 +117,7 @@ func showAPILinkSelectionPopup() {
if len(apiLinks) == 0 { if len(apiLinks) == 0 {
logger.Warn("no API links available for selection") logger.Warn("no API links available for selection")
message := "No API links available. Please configure API links in your config file." message := "No API links available. Please configure API links in your config file."
if err := notifyUser("Empty list", message); err != nil { showToast("Empty list", message)
logger.Error("failed to send notification", "error", err)
}
return return
} }
// Create a list primitive // Create a list primitive
@@ -206,9 +202,7 @@ func showUserRoleSelectionPopup() {
if len(roles) == 0 { if len(roles) == 0 {
logger.Warn("no roles available for selection") logger.Warn("no roles available for selection")
message := "No roles available for selection." message := "No roles available for selection."
if err := notifyUser("Empty list", message); err != nil { showToast("Empty list", message)
logger.Error("failed to send notification", "error", err)
}
return return
} }
// Create a list primitive // Create a list primitive
@@ -285,9 +279,7 @@ func showBotRoleSelectionPopup() {
if len(roles) == 0 { if len(roles) == 0 {
logger.Warn("no roles available for selection") logger.Warn("no roles available for selection")
message := "No roles available for selection." message := "No roles available for selection."
if err := notifyUser("Empty list", message); err != nil { showToast("Empty list", message)
logger.Error("failed to send notification", "error", err)
}
return return
} }
// Create a list primitive // Create a list primitive
@@ -512,9 +504,7 @@ func showColorschemeSelectionPopup() {
if len(schemeNames) == 0 { if len(schemeNames) == 0 {
logger.Warn("no colorschemes available for selection") logger.Warn("no colorschemes available for selection")
message := "No colorschemes available." message := "No colorschemes available."
if err := notifyUser("Empty list", message); err != nil { showToast("Empty list", message)
logger.Error("failed to send notification", "error", err)
}
return return
} }
// Create a list primitive // Create a list primitive

View File

@@ -259,9 +259,7 @@ func makePropsTable(props map[string]float32) *tview.Table {
// Handle nil options // Handle nil options
if data.Options == nil { if data.Options == nil {
logger.Error("options list is nil for", "label", label) logger.Error("options list is nil for", "label", label)
if err := notifyUser("Configuration error", "Options list is nil for "+label); err != nil { showToast("Configuration error", "Options list is nil for "+label)
logger.Error("failed to send notification", "error", err)
}
return return
} }
@@ -279,9 +277,7 @@ func makePropsTable(props map[string]float32) *tview.Table {
message = "No llama.cpp models loaded. Ensure llama.cpp server is running with models." message = "No llama.cpp models loaded. Ensure llama.cpp server is running with models."
} }
} }
if err := notifyUser("Empty list", message); err != nil { showToast("Empty list", message)
logger.Error("failed to send notification", "error", err)
}
return return
} }
// Create a list primitive // Create a list primitive

View File

@@ -168,19 +168,3 @@ func copyToClipboard(text string) error {
cmd.Stdin = strings.NewReader(text) cmd.Stdin = strings.NewReader(text)
return cmd.Run() return cmd.Run()
} }
func notifyUser(topic, message string) error {
// Sanitize message to remove control characters that notify-send doesn't handle
sanitized := strings.Map(func(r rune) rune {
if r < 32 && r != '\t' {
return -1
}
return r
}, message)
// Truncate if too long
if len(sanitized) > 200 {
sanitized = sanitized[:197] + "..."
}
cmd := exec.Command("notify-send", topic, sanitized)
return cmd.Run()
}

View File

@@ -147,9 +147,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
if err := store.RemoveChat(sc.ID); err != nil { if err := store.RemoveChat(sc.ID); err != nil {
logger.Error("failed to remove chat from db", "chat_id", sc.ID, "chat_name", sc.Name) logger.Error("failed to remove chat from db", "chat_id", sc.ID, "chat_name", sc.Name)
} }
if err := notifyUser("chat deleted", selectedChat+" was deleted"); err != nil { showToast("chat deleted", selectedChat+" was deleted")
logger.Error("failed to send notification", "error", err)
}
// load last chat // load last chat
chatBody.Messages = loadOldChatOrGetNew() chatBody.Messages = loadOldChatOrGetNew()
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
@@ -162,9 +160,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
cc := GetCardByRole(agentName) cc := GetCardByRole(agentName)
if cc == nil { if cc == nil {
logger.Warn("no such card", "agent", agentName) logger.Warn("no such card", "agent", agentName)
if err := notifyUser("error", "no such card: "+agentName); err != nil { showToast("error", "no such card: "+agentName)
logger.Warn("failed ot notify", "error", err)
}
return return
} }
cc.SysPrompt = chatBody.Messages[0].Content cc.SysPrompt = chatBody.Messages[0].Content
@@ -186,9 +182,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
cc := GetCardByRole(agentName) cc := GetCardByRole(agentName)
if cc == nil { if cc == nil {
logger.Warn("no such card", "agent", agentName) logger.Warn("no such card", "agent", agentName)
if err := notifyUser("error", "no such card: "+agentName); err != nil { showToast("error", "no such card: "+agentName)
logger.Warn("failed to notify", "error", err)
}
return return
} }
newCard, err := pngmeta.ReadCard(cc.FilePath, cfg.UserRole) newCard, err := pngmeta.ReadCard(cc.FilePath, cfg.UserRole)
@@ -197,9 +191,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
newCard, err = pngmeta.ReadCardJson(cc.FilePath) newCard, err = pngmeta.ReadCardJson(cc.FilePath)
if err != nil { if err != nil {
logger.Error("failed to reload charcard", "path", cc.FilePath, "error", err) logger.Error("failed to reload charcard", "path", cc.FilePath, "error", err)
if err := notifyUser("error", "failed to reload card: "+cc.FilePath); err != nil { showToast("error", "failed to reload card: "+cc.FilePath)
logger.Warn("failed to notify", "error", err)
}
return return
} }
} }
@@ -448,13 +440,13 @@ func makeRAGTable(fileList []string, loadedFiles []string) *tview.Flex {
go func() { go func() {
if err := ragger.LoadRAG(fpath); err != nil { if err := ragger.LoadRAG(fpath); err != nil {
logger.Error("failed to embed file", "chat", fpath, "error", err) logger.Error("failed to embed file", "chat", fpath, "error", err)
_ = notifyUser("RAG", "failed to embed file; error: "+err.Error()) showToast("RAG", "failed to embed file; error: "+err.Error())
app.QueueUpdate(func() { app.QueueUpdate(func() {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
}) })
return return
} }
_ = notifyUser("RAG", "file loaded successfully") showToast("RAG", "file loaded successfully")
app.QueueUpdate(func() { app.QueueUpdate(func() {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
}) })
@@ -465,13 +457,13 @@ func makeRAGTable(fileList []string, loadedFiles []string) *tview.Flex {
go func() { go func() {
if err := ragger.RemoveFile(f.name); err != nil { if err := ragger.RemoveFile(f.name); err != nil {
logger.Error("failed to unload file from RAG", "filename", f.name, "error", err) logger.Error("failed to unload file from RAG", "filename", f.name, "error", err)
_ = notifyUser("RAG", "failed to unload file; error: "+err.Error()) showToast("RAG", "failed to unload file; error: "+err.Error())
app.QueueUpdate(func() { app.QueueUpdate(func() {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
}) })
return return
} }
_ = notifyUser("RAG", "file unloaded successfully") showToast("RAG", "file unloaded successfully")
app.QueueUpdate(func() { app.QueueUpdate(func() {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
}) })
@@ -483,9 +475,7 @@ func makeRAGTable(fileList []string, loadedFiles []string) *tview.Flex {
logger.Error("failed to delete file", "filename", fpath, "error", err) logger.Error("failed to delete file", "filename", fpath, "error", err)
return return
} }
if err := notifyUser("chat deleted", fpath+" was deleted"); err != nil { showToast("chat deleted", fpath+" was deleted")
logger.Error("failed to send notification", "error", err)
}
return return
default: default:
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
@@ -594,9 +584,7 @@ func makeAgentTable(agentList []string) *tview.Table {
if err := store.RemoveChat(sc.ID); err != nil { if err := store.RemoveChat(sc.ID); err != nil {
logger.Error("failed to remove chat from db", "chat_id", sc.ID, "chat_name", sc.Name) logger.Error("failed to remove chat from db", "chat_id", sc.ID, "chat_name", sc.Name)
} }
if err := notifyUser("chat deleted", selected+" was deleted"); err != nil { showToast("chat deleted", selected+" was deleted")
logger.Error("failed to send notification", "error", err)
}
pages.RemovePage(agentPage) pages.RemovePage(agentPage)
return return
default: default:
@@ -667,13 +655,9 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
switch tc.Text { switch tc.Text {
case "copy": case "copy":
if err := copyToClipboard(selected); err != nil { if err := copyToClipboard(selected); err != nil {
if err := notifyUser("error", err.Error()); err != nil { showToast("error", err.Error())
logger.Error("failed to send notification", "error", err)
}
}
if err := notifyUser("copied", selected); err != nil {
logger.Error("failed to send notification", "error", err)
} }
showToast("copied", selected)
pages.RemovePage(codeBlockPage) pages.RemovePage(codeBlockPage)
app.SetFocus(textArea) app.SetFocus(textArea)
return return
@@ -766,9 +750,7 @@ func makeImportChatTable(filenames []string) *tview.Table {
if err := store.RemoveChat(sc.ID); err != nil { if err := store.RemoveChat(sc.ID); err != nil {
logger.Error("failed to remove chat from db", "chat_id", sc.ID, "chat_name", sc.Name) logger.Error("failed to remove chat from db", "chat_id", sc.ID, "chat_name", sc.Name)
} }
if err := notifyUser("chat deleted", selected+" was deleted"); err != nil { showToast("chat deleted", selected+" was deleted")
logger.Error("failed to send notification", "error", err)
}
pages.RemovePage(historyPage) pages.RemovePage(historyPage)
return return
default: default:

View File

@@ -268,7 +268,7 @@ func updateToolCapabilities() {
} else { } else {
logger.Info("model does not have vision support", "model", cfg.CurrentModel, "api", cfg.CurrentAPI) logger.Info("model does not have vision support", "model", cfg.CurrentModel, "api", cfg.CurrentAPI)
if windowToolsAvailable && !prevHasVision && !modelHasVision { if windowToolsAvailable && !prevHasVision && !modelHasVision {
_ = notifyUser("window tools", "Window capture-and-view unavailable: model lacks vision support") showToast("window tools", "Window capture-and-view unavailable: model lacks vision support")
} }
} }
registerWindowTools() registerWindowTools()

102
tui.go
View File

@@ -140,7 +140,20 @@ func setShellMode(enabled bool) {
// showToast displays a temporary message in the topright corner. // showToast displays a temporary message in the topright corner.
// It autohides after 3 seconds and disappears when clicked. // It autohides after 3 seconds and disappears when clicked.
func showToast(title, message string) { func showToast(title, message string) {
// Create a small, bordered text view for the notification. sanitize := func(s string, maxLen int) string {
sanitized := strings.Map(func(r rune) rune {
if r < 32 && r != '\t' {
return -1
}
return r
}, s)
if len(sanitized) > maxLen {
sanitized = sanitized[:maxLen-3] + "..."
}
return sanitized
}
title = sanitize(title, 50)
message = sanitize(message, 197)
notification := tview.NewTextView(). notification := tview.NewTextView().
SetTextAlign(tview.AlignCenter). SetTextAlign(tview.AlignCenter).
SetDynamicColors(true). SetDynamicColors(true).
@@ -363,9 +376,7 @@ func init() {
defer colorText() defer colorText()
editedMsg := editArea.GetText() editedMsg := editArea.GetText()
if editedMsg == "" { if editedMsg == "" {
if err := notifyUser("edit", "no edit provided"); err != nil { showToast("edit", "no edit provided")
logger.Error("failed to send notification", "error", err)
}
pages.RemovePage(editMsgPage) pages.RemovePage(editMsgPage)
return nil return nil
} }
@@ -395,9 +406,7 @@ func init() {
case tcell.KeyEnter: case tcell.KeyEnter:
newRole := roleEditWindow.GetText() newRole := roleEditWindow.GetText()
if newRole == "" { if newRole == "" {
if err := notifyUser("edit", "no role provided"); err != nil { showToast("edit", "no role provided")
logger.Error("failed to send notification", "error", err)
}
pages.RemovePage(roleEditPage) pages.RemovePage(roleEditPage)
return return
} }
@@ -424,9 +433,7 @@ func init() {
siInt, err := strconv.Atoi(si) siInt, err := strconv.Atoi(si)
if err != nil { if err != nil {
logger.Error("failed to convert provided index", "error", err, "si", si) logger.Error("failed to convert provided index", "error", err, "si", si)
if err := notifyUser("cancel", "no index provided, copying user input"); err != nil { showToast("cancel", "no index provided, copying user input")
logger.Error("failed to send notification", "error", err)
}
if err := copyToClipboard(textArea.GetText()); err != nil { if err := copyToClipboard(textArea.GetText()); err != nil {
logger.Error("failed to copy to clipboard", "error", err) logger.Error("failed to copy to clipboard", "error", err)
} }
@@ -437,9 +444,7 @@ func init() {
if len(chatBody.Messages)-1 < selectedIndex || selectedIndex < 0 { if len(chatBody.Messages)-1 < selectedIndex || selectedIndex < 0 {
msg := "chosen index is out of bounds, will copy user input" msg := "chosen index is out of bounds, will copy user input"
logger.Warn(msg, "index", selectedIndex) logger.Warn(msg, "index", selectedIndex)
if err := notifyUser("error", msg); err != nil { showToast("error", msg)
logger.Error("failed to send notification", "error", err)
}
if err := copyToClipboard(textArea.GetText()); err != nil { if err := copyToClipboard(textArea.GetText()); err != nil {
logger.Error("failed to copy to clipboard", "error", err) logger.Error("failed to copy to clipboard", "error", err)
} }
@@ -465,9 +470,7 @@ func init() {
} }
previewLen := min(30, len(msgText)) previewLen := min(30, len(msgText))
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen]) notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen])
if err := notifyUser("copied", notification); err != nil { showToast("copied", notification)
logger.Error("failed to send notification", "error", err)
}
hideIndexBar() // Hide overlay after copying hideIndexBar() // Hide overlay after copying
} }
return nil return nil
@@ -499,9 +502,7 @@ func init() {
logger.Error("failed to upsert chat", "error", err, "chat", currentChat) logger.Error("failed to upsert chat", "error", err, "chat", currentChat)
} }
notification := fmt.Sprintf("renamed chat to '%s'", activeChatName) notification := fmt.Sprintf("renamed chat to '%s'", activeChatName)
if err := notifyUser("renamed", notification); err != nil { showToast("renamed", notification)
logger.Error("failed to send notification", "error", err)
}
} }
return event return event
}) })
@@ -612,9 +613,6 @@ func init() {
status = "enabled" status = "enabled"
} }
showToast("autoscroll", "Auto-scrolling "+status) showToast("autoscroll", "Auto-scrolling "+status)
if err := notifyUser("autoscroll", "Auto-scrolling "+status); err != nil {
logger.Error("failed to send notification", "error", err)
}
updateStatusLine() updateStatusLine()
} }
// Handle Alt+7 to toggle injectRole // Handle Alt+7 to toggle injectRole
@@ -631,9 +629,7 @@ func init() {
if thinkingCollapsed { if thinkingCollapsed {
status = "collapsed" status = "collapsed"
} }
if err := notifyUser("thinking", "Thinking blocks "+status); err != nil { showToast("thinking", "Thinking blocks "+status)
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
// Handle Ctrl+T to toggle tool call/response visibility // Handle Ctrl+T to toggle tool call/response visibility
@@ -645,9 +641,7 @@ func init() {
if toolCollapsed { if toolCollapsed {
status = "collapsed" status = "collapsed"
} }
if err := notifyUser("tools", "Tool calls/responses "+status); err != nil { showToast("tools", "Tool calls/responses "+status)
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'i' && event.Modifiers()&tcell.ModAlt != 0 { if event.Key() == tcell.KeyRune && event.Rune() == 'i' && event.Modifiers()&tcell.ModAlt != 0 {
@@ -667,9 +661,7 @@ func init() {
// Check if there are no chats for this agent // Check if there are no chats for this agent
if len(chatList) == 0 { if len(chatList) == 0 {
notification := "no chats found for agent: " + cfg.AssistantRole notification := "no chats found for agent: " + cfg.AssistantRole
if err := notifyUser("info", notification); err != nil { showToast("info", notification)
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
chatMap := make(map[string]models.Chat) chatMap := make(map[string]models.Chat)
@@ -687,9 +679,7 @@ func init() {
if event.Key() == tcell.KeyF2 && !botRespMode { if event.Key() == tcell.KeyF2 && !botRespMode {
// regen last msg // regen last msg
if len(chatBody.Messages) == 0 { if len(chatBody.Messages) == 0 {
if err := notifyUser("info", "no messages to regenerate"); err != nil { showToast("info", "no messages to regenerate")
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1] chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
@@ -715,9 +705,7 @@ func init() {
return nil return nil
} }
if len(chatBody.Messages) == 0 { if len(chatBody.Messages) == 0 {
if err := notifyUser("info", "no messages to delete"); err != nil { showToast("info", "no messages to delete")
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1] chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
@@ -776,9 +764,7 @@ func init() {
} }
previewLen := min(30, len(msgText)) previewLen := min(30, len(msgText))
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen]) notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen])
if err := notifyUser("copied", notification); err != nil { showToast("copied", notification)
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
if event.Key() == tcell.KeyF8 { if event.Key() == tcell.KeyF8 {
@@ -792,9 +778,7 @@ func init() {
text := textView.GetText(false) text := textView.GetText(false)
cb := codeBlockRE.FindAllString(text, -1) cb := codeBlockRE.FindAllString(text, -1)
if len(cb) == 0 { if len(cb) == 0 {
if err := notifyUser("notify", "no code blocks in chat"); err != nil { showToast("notify", "no code blocks in chat")
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
table := makeCodeBlockTable(cb) table := makeCodeBlockTable(cb)
@@ -809,9 +793,7 @@ func init() {
// read files in chat_exports // read files in chat_exports
filelist, err := os.ReadDir(exportDir) filelist, err := os.ReadDir(exportDir)
if err != nil { if err != nil {
if err := notifyUser("failed to load exports", err.Error()); err != nil { showToast("failed to load exports", err.Error())
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
fli := []string{} fli := []string{}
@@ -841,9 +823,7 @@ func init() {
logger.Error("failed to export chat;", "error", err, "chat_name", activeChatName) logger.Error("failed to export chat;", "error", err, "chat_name", activeChatName)
return nil return nil
} }
if err := notifyUser("exported chat", "chat: "+activeChatName+" was exported"); err != nil { showToast("exported chat", "chat: "+activeChatName+" was exported")
logger.Error("failed to send notification", "error", err)
}
return nil return nil
} }
if event.Key() == tcell.KeyCtrlP { if event.Key() == tcell.KeyCtrlP {
@@ -882,9 +862,7 @@ func init() {
labels, err := initSysCards() labels, err := initSysCards()
if err != nil { if err != nil {
logger.Error("failed to read sys dir", "error", err) logger.Error("failed to read sys dir", "error", err)
if err := notifyUser("error", "failed to read: "+cfg.SysDir); err != nil { showToast("error", "failed to read: "+cfg.SysDir)
logger.Debug("failed to notify user", "error", err)
}
return nil return nil
} }
at := makeAgentTable(labels) at := makeAgentTable(labels)
@@ -941,9 +919,7 @@ func init() {
if err != nil { if err != nil {
msg := "failed to inference user speech; error:" + err.Error() msg := "failed to inference user speech; error:" + err.Error()
logger.Error(msg) logger.Error(msg)
if err := notifyUser("stt error", msg); err != nil { showToast("stt error", msg)
logger.Error("failed to notify user", "error", err)
}
return nil return nil
} }
if userSpeech != "" { if userSpeech != "" {
@@ -1023,26 +999,20 @@ func init() {
// Create the RAG directory if it doesn't exist // Create the RAG directory if it doesn't exist
if mkdirErr := os.MkdirAll(cfg.RAGDir, 0755); mkdirErr != nil { if mkdirErr := os.MkdirAll(cfg.RAGDir, 0755); mkdirErr != nil {
logger.Error("failed to create RAG directory", "dir", cfg.RAGDir, "error", mkdirErr) logger.Error("failed to create RAG directory", "dir", cfg.RAGDir, "error", mkdirErr)
if notifyerr := notifyUser("failed to create RAG directory", mkdirErr.Error()); notifyerr != nil { showToast("failed to create RAG directory", mkdirErr.Error())
logger.Error("failed to send notification", "error", notifyerr)
}
return nil return nil
} }
// Now try to read the directory again after creating it // Now try to read the directory again after creating it
files, err = os.ReadDir(cfg.RAGDir) files, err = os.ReadDir(cfg.RAGDir)
if err != nil { if err != nil {
logger.Error("failed to read dir after creating it", "dir", cfg.RAGDir, "error", err) logger.Error("failed to read dir after creating it", "dir", cfg.RAGDir, "error", err)
if notifyerr := notifyUser("failed to read RAG directory", err.Error()); notifyerr != nil { showToast("failed to read RAG directory", err.Error())
logger.Error("failed to send notification", "error", notifyerr)
}
return nil return nil
} }
} else { } else {
// Other error (permissions, etc.) // Other error (permissions, etc.)
logger.Error("failed to read dir", "dir", cfg.RAGDir, "error", err) logger.Error("failed to read dir", "dir", cfg.RAGDir, "error", err)
if notifyerr := notifyUser("failed to open RAG files dir", err.Error()); notifyerr != nil { showToast("failed to open RAG files dir", err.Error())
logger.Error("failed to send notification", "error", notifyerr)
}
return nil return nil
} }
} }
@@ -1072,9 +1042,7 @@ func init() {
if event.Key() == tcell.KeyRune && event.Modifiers() == tcell.ModAlt && event.Rune() == '9' { if event.Key() == tcell.KeyRune && event.Modifiers() == tcell.ModAlt && event.Rune() == '9' {
// Warm up (load) the currently selected model // Warm up (load) the currently selected model
go warmUpModel() go warmUpModel()
if err := notifyUser("model warmup", "loading model: "+chatBody.Model); err != nil { showToast("model warmup", "loading model: "+chatBody.Model)
logger.Debug("failed to notify user", "error", err)
}
return nil return nil
} }
// cannot send msg in editMode or botRespMode // cannot send msg in editMode or botRespMode