Enha: native notification implementation
This commit is contained in:
44
bot.go
44
bot.go
@@ -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() {
|
||||||
|
|||||||
21
helpfuncs.go
21
helpfuncs.go
@@ -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 {
|
||||||
|
|||||||
20
popups.go
20
popups.go
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
16
session.go
16
session.go
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
44
tables.go
44
tables.go
@@ -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:
|
||||||
|
|||||||
2
tools.go
2
tools.go
@@ -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
102
tui.go
@@ -140,7 +140,20 @@ func setShellMode(enabled bool) {
|
|||||||
// showToast displays a temporary message in the top‑right corner.
|
// showToast displays a temporary message in the top‑right corner.
|
||||||
// It auto‑hides after 3 seconds and disappears when clicked.
|
// It auto‑hides 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
|
||||||
|
|||||||
Reference in New Issue
Block a user