summaryrefslogtreecommitdiff
path: root/tui.go
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2026-03-04 11:25:13 +0300
committerGrail Finder <wohilas@gmail.com>2026-03-04 11:25:13 +0300
commitabcaad66093bdf9a98f10c909a05ab1ebcb431f3 (patch)
treec1c4d9d30ef13a2aca63c4b6b1a74a7ccea556e9 /tui.go
parent50ce0200af5648818e6e4f51d6541bd3bdb7e036 (diff)
Enha: native notification implementation
Diffstat (limited to 'tui.go')
-rw-r--r--tui.go102
1 files changed, 35 insertions, 67 deletions
diff --git a/tui.go b/tui.go
index 67f2775..b23c3ff 100644
--- a/tui.go
+++ b/tui.go
@@ -140,7 +140,20 @@ func setShellMode(enabled bool) {
// showToast displays a temporary message in the top‑right corner.
// It auto‑hides after 3 seconds and disappears when clicked.
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().
SetTextAlign(tview.AlignCenter).
SetDynamicColors(true).
@@ -363,9 +376,7 @@ func init() {
defer colorText()
editedMsg := editArea.GetText()
if editedMsg == "" {
- if err := notifyUser("edit", "no edit provided"); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("edit", "no edit provided")
pages.RemovePage(editMsgPage)
return nil
}
@@ -395,9 +406,7 @@ func init() {
case tcell.KeyEnter:
newRole := roleEditWindow.GetText()
if newRole == "" {
- if err := notifyUser("edit", "no role provided"); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("edit", "no role provided")
pages.RemovePage(roleEditPage)
return
}
@@ -424,9 +433,7 @@ func init() {
siInt, err := strconv.Atoi(si)
if err != nil {
logger.Error("failed to convert provided index", "error", err, "si", si)
- if err := notifyUser("cancel", "no index provided, copying user input"); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("cancel", "no index provided, copying user input")
if err := copyToClipboard(textArea.GetText()); err != nil {
logger.Error("failed to copy to clipboard", "error", err)
}
@@ -437,9 +444,7 @@ func init() {
if len(chatBody.Messages)-1 < selectedIndex || selectedIndex < 0 {
msg := "chosen index is out of bounds, will copy user input"
logger.Warn(msg, "index", selectedIndex)
- if err := notifyUser("error", msg); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("error", msg)
if err := copyToClipboard(textArea.GetText()); err != nil {
logger.Error("failed to copy to clipboard", "error", err)
}
@@ -465,9 +470,7 @@ func init() {
}
previewLen := min(30, len(msgText))
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen])
- if err := notifyUser("copied", notification); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("copied", notification)
hideIndexBar() // Hide overlay after copying
}
return nil
@@ -499,9 +502,7 @@ func init() {
logger.Error("failed to upsert chat", "error", err, "chat", currentChat)
}
notification := fmt.Sprintf("renamed chat to '%s'", activeChatName)
- if err := notifyUser("renamed", notification); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("renamed", notification)
}
return event
})
@@ -612,9 +613,6 @@ func init() {
status = "enabled"
}
showToast("autoscroll", "Auto-scrolling "+status)
- if err := notifyUser("autoscroll", "Auto-scrolling "+status); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
updateStatusLine()
}
// Handle Alt+7 to toggle injectRole
@@ -631,9 +629,7 @@ func init() {
if thinkingCollapsed {
status = "collapsed"
}
- if err := notifyUser("thinking", "Thinking blocks "+status); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("thinking", "Thinking blocks "+status)
return nil
}
// Handle Ctrl+T to toggle tool call/response visibility
@@ -645,9 +641,7 @@ func init() {
if toolCollapsed {
status = "collapsed"
}
- if err := notifyUser("tools", "Tool calls/responses "+status); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("tools", "Tool calls/responses "+status)
return nil
}
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
if len(chatList) == 0 {
notification := "no chats found for agent: " + cfg.AssistantRole
- if err := notifyUser("info", notification); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("info", notification)
return nil
}
chatMap := make(map[string]models.Chat)
@@ -687,9 +679,7 @@ func init() {
if event.Key() == tcell.KeyF2 && !botRespMode {
// regen last msg
if len(chatBody.Messages) == 0 {
- if err := notifyUser("info", "no messages to regenerate"); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("info", "no messages to regenerate")
return nil
}
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
@@ -715,9 +705,7 @@ func init() {
return nil
}
if len(chatBody.Messages) == 0 {
- if err := notifyUser("info", "no messages to delete"); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("info", "no messages to delete")
return nil
}
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
@@ -776,9 +764,7 @@ func init() {
}
previewLen := min(30, len(msgText))
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen])
- if err := notifyUser("copied", notification); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("copied", notification)
return nil
}
if event.Key() == tcell.KeyF8 {
@@ -792,9 +778,7 @@ func init() {
text := textView.GetText(false)
cb := codeBlockRE.FindAllString(text, -1)
if len(cb) == 0 {
- if err := notifyUser("notify", "no code blocks in chat"); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("notify", "no code blocks in chat")
return nil
}
table := makeCodeBlockTable(cb)
@@ -809,9 +793,7 @@ func init() {
// read files in chat_exports
filelist, err := os.ReadDir(exportDir)
if err != nil {
- if err := notifyUser("failed to load exports", err.Error()); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("failed to load exports", err.Error())
return nil
}
fli := []string{}
@@ -841,9 +823,7 @@ func init() {
logger.Error("failed to export chat;", "error", err, "chat_name", activeChatName)
return nil
}
- if err := notifyUser("exported chat", "chat: "+activeChatName+" was exported"); err != nil {
- logger.Error("failed to send notification", "error", err)
- }
+ showToast("exported chat", "chat: "+activeChatName+" was exported")
return nil
}
if event.Key() == tcell.KeyCtrlP {
@@ -882,9 +862,7 @@ func init() {
labels, err := initSysCards()
if err != nil {
logger.Error("failed to read sys dir", "error", err)
- if err := notifyUser("error", "failed to read: "+cfg.SysDir); err != nil {
- logger.Debug("failed to notify user", "error", err)
- }
+ showToast("error", "failed to read: "+cfg.SysDir)
return nil
}
at := makeAgentTable(labels)
@@ -941,9 +919,7 @@ func init() {
if err != nil {
msg := "failed to inference user speech; error:" + err.Error()
logger.Error(msg)
- if err := notifyUser("stt error", msg); err != nil {
- logger.Error("failed to notify user", "error", err)
- }
+ showToast("stt error", msg)
return nil
}
if userSpeech != "" {
@@ -1023,26 +999,20 @@ func init() {
// Create the RAG directory if it doesn't exist
if mkdirErr := os.MkdirAll(cfg.RAGDir, 0755); mkdirErr != nil {
logger.Error("failed to create RAG directory", "dir", cfg.RAGDir, "error", mkdirErr)
- if notifyerr := notifyUser("failed to create RAG directory", mkdirErr.Error()); notifyerr != nil {
- logger.Error("failed to send notification", "error", notifyerr)
- }
+ showToast("failed to create RAG directory", mkdirErr.Error())
return nil
}
// Now try to read the directory again after creating it
files, err = os.ReadDir(cfg.RAGDir)
if err != nil {
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 {
- logger.Error("failed to send notification", "error", notifyerr)
- }
+ showToast("failed to read RAG directory", err.Error())
return nil
}
} else {
// Other error (permissions, etc.)
logger.Error("failed to read dir", "dir", cfg.RAGDir, "error", err)
- if notifyerr := notifyUser("failed to open RAG files dir", err.Error()); notifyerr != nil {
- logger.Error("failed to send notification", "error", notifyerr)
- }
+ showToast("failed to open RAG files dir", err.Error())
return nil
}
}
@@ -1072,9 +1042,7 @@ func init() {
if event.Key() == tcell.KeyRune && event.Modifiers() == tcell.ModAlt && event.Rune() == '9' {
// Warm up (load) the currently selected model
go warmUpModel()
- if err := notifyUser("model warmup", "loading model: "+chatBody.Model); err != nil {
- logger.Debug("failed to notify user", "error", err)
- }
+ showToast("model warmup", "loading model: "+chatBody.Model)
return nil
}
// cannot send msg in editMode or botRespMode