From 4b6769e531ab844db2ed98445c13df9e2c781776 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 5 Mar 2026 08:43:50 +0300 Subject: Fix (notification): non-blocking way to notify --- tui.go | 78 +++++++++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 29 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index b23c3ff..9cf32de 100644 --- a/tui.go +++ b/tui.go @@ -29,6 +29,8 @@ var ( statusLineWidget *tview.TextView helpView *tview.TextView flex *tview.Flex + bottomFlex *tview.Flex + notificationWidget *tview.TextView imgView *tview.Image defaultImage = "sysprompts/llama.png" indexPickWindow *tview.InputField @@ -137,8 +139,8 @@ func setShellMode(enabled bool) { }() } -// showToast displays a temporary message in the top‑right corner. -// It auto‑hides after 3 seconds and disappears when clicked. +// showToast displays a temporary notification in the bottom-right corner. +// It auto-hides after 3 seconds. func showToast(title, message string) { sanitize := func(s string, maxLen int) string { sanitized := strings.Map(func(r rune) rune { @@ -154,33 +156,34 @@ func showToast(title, message string) { } title = sanitize(title, 50) message = sanitize(message, 197) - notification := tview.NewTextView(). - SetTextAlign(tview.AlignCenter). - SetDynamicColors(true). - SetRegions(true). - SetText(fmt.Sprintf("[yellow]%s[-]\n", message)). - SetChangedFunc(func() { - app.Draw() + + notificationWidget.SetTitle(title) + notificationWidget.SetText(fmt.Sprintf("[yellow]%s[-]", message)) + + go func() { + app.QueueUpdateDraw(func() { + flex.RemoveItem(bottomFlex) + flex.RemoveItem(statusLineWidget) + bottomFlex = tview.NewFlex().SetDirection(tview.FlexColumn). + AddItem(textArea, 0, 1, true). + AddItem(notificationWidget, 40, 1, false) + flex.AddItem(bottomFlex, 0, 10, true) + if positionVisible { + flex.AddItem(statusLineWidget, 0, 2, false) + } }) - notification.SetTitleAlign(tview.AlignLeft). - SetBorder(true). - SetTitle(title) - // Wrap it in a full‑screen Flex to position it in the top‑right corner. - // Outer Flex (row) pushes content to the top; inner Flex (column) pushes to the right. - background := tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(nil, 0, 1, false). // top spacer - AddItem(tview.NewFlex().SetDirection(tview.FlexColumn). - AddItem(nil, 0, 1, false). // left spacer - AddItem(notification, 40, 1, true), // notification width 40 - 5, 1, false) // notification height 5 - // Generate a unique page name (e.g., using timestamp) to allow multiple toasts. - pageName := fmt.Sprintf("toast-%d", time.Now().UnixNano()) - pages.AddPage(pageName, background, true, true) - // Auto‑dismiss after 3 seconds. + }() + time.AfterFunc(3*time.Second, func() { app.QueueUpdateDraw(func() { - if pages.HasPage(pageName) { - pages.RemovePage(pageName) + flex.RemoveItem(bottomFlex) + flex.RemoveItem(statusLineWidget) + bottomFlex = tview.NewFlex().SetDirection(tview.FlexColumn). + AddItem(textArea, 0, 1, true). + AddItem(notificationWidget, 0, 0, false) + flex.AddItem(bottomFlex, 0, 10, true) + if positionVisible { + flex.AddItem(statusLineWidget, 0, 2, false) } }) }) @@ -286,12 +289,25 @@ func init() { SetDynamicColors(true). SetRegions(true). SetChangedFunc(func() { - app.Draw() + // https://github.com/rivo/tview/wiki/Concurrency#event-handlers + // app.Draw() // already called by default per tview specs }) + notificationWidget = tview.NewTextView(). + SetTextAlign(tview.AlignCenter). + SetDynamicColors(true). + SetRegions(true). + SetChangedFunc(func() { + }) + notificationWidget.SetBorder(true).SetTitle("notification") + + bottomFlex = tview.NewFlex().SetDirection(tview.FlexColumn). + AddItem(textArea, 0, 1, true). + AddItem(notificationWidget, 0, 0, false) + // flex = tview.NewFlex().SetDirection(tview.FlexRow). AddItem(textView, 0, 40, false). - AddItem(textArea, 0, 10, true) // Restore original height + AddItem(bottomFlex, 0, 10, true) if positionVisible { flex.AddItem(statusLineWidget, 0, 2, false) } @@ -360,10 +376,14 @@ func init() { // y += h / 2 // return x, y, w, h // }) + notificationWidget.SetDrawFunc(func(screen tcell.Screen, x, y, w, h int) (int, int, int, int) { + y += h / 2 + return x, y, w, h + }) // Initially set up flex without search bar flex = tview.NewFlex().SetDirection(tview.FlexRow). AddItem(textView, 0, 40, false). - AddItem(textArea, 0, 10, true) // Restore original height + AddItem(bottomFlex, 0, 10, true) if positionVisible { flex.AddItem(statusLineWidget, 0, 2, false) } -- cgit v1.2.3 From 57088565bd7a3edbf55d63780573096124a1fc1b Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 5 Mar 2026 08:51:04 +0300 Subject: Fix (notification): being closed by prev notification early --- tui.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 9cf32de..b9bf35f 100644 --- a/tui.go +++ b/tui.go @@ -38,6 +38,7 @@ var ( roleEditWindow *tview.InputField shellInput *tview.InputField confirmModal *tview.Modal + toastTimer *time.Timer confirmPageName = "confirm" fullscreenMode bool positionVisible bool = true @@ -156,10 +157,11 @@ func showToast(title, message string) { } title = sanitize(title, 50) message = sanitize(message, 197) - + if toastTimer != nil { + toastTimer.Stop() + } notificationWidget.SetTitle(title) notificationWidget.SetText(fmt.Sprintf("[yellow]%s[-]", message)) - go func() { app.QueueUpdateDraw(func() { flex.RemoveItem(bottomFlex) @@ -173,8 +175,7 @@ func showToast(title, message string) { } }) }() - - time.AfterFunc(3*time.Second, func() { + toastTimer = time.AfterFunc(3*time.Second, func() { app.QueueUpdateDraw(func() { flex.RemoveItem(bottomFlex) flex.RemoveItem(statusLineWidget) @@ -299,11 +300,9 @@ func init() { SetChangedFunc(func() { }) notificationWidget.SetBorder(true).SetTitle("notification") - bottomFlex = tview.NewFlex().SetDirection(tview.FlexColumn). AddItem(textArea, 0, 1, true). AddItem(notificationWidget, 0, 0, false) - // flex = tview.NewFlex().SetDirection(tview.FlexRow). AddItem(textView, 0, 40, false). -- cgit v1.2.3 From 645b7351a80713a40e2c823479a3605baeb231b8 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 5 Mar 2026 09:09:13 +0300 Subject: Fix: add different kind of notifiction for fullscreen mode --- tui.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'tui.go') diff --git a/tui.go b/tui.go index b9bf35f..36a34bb 100644 --- a/tui.go +++ b/tui.go @@ -160,6 +160,40 @@ func showToast(title, message string) { if toastTimer != nil { toastTimer.Stop() } + // show blocking notification to not mess up flex + if fullscreenMode { + notification := tview.NewTextView(). + SetTextAlign(tview.AlignCenter). + SetDynamicColors(true). + SetRegions(true). + SetText(fmt.Sprintf("[yellow]%s[-]\n", message)). + SetChangedFunc(func() { + app.Draw() + }) + notification.SetTitleAlign(tview.AlignLeft). + SetBorder(true). + SetTitle(title) + // Wrap it in a full‑screen Flex to position it in the top‑right corner. + // Outer Flex (row) pushes content to the top; inner Flex (column) pushes to the right. + background := tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). // top spacer + AddItem(tview.NewFlex().SetDirection(tview.FlexColumn). + AddItem(nil, 0, 1, false). // left spacer + AddItem(notification, 40, 1, true), // notification width 40 + 5, 1, false) // notification height 5 + // Generate a unique page name (e.g., using timestamp) to allow multiple toasts. + pageName := fmt.Sprintf("toast-%d", time.Now().UnixNano()) + pages.AddPage(pageName, background, true, true) + // Auto‑dismiss after 2 seconds, since blocking is more annoying + time.AfterFunc(2*time.Second, func() { + app.QueueUpdateDraw(func() { + if pages.HasPage(pageName) { + pages.RemovePage(pageName) + } + }) + }) + return + } notificationWidget.SetTitle(title) notificationWidget.SetText(fmt.Sprintf("[yellow]%s[-]", message)) go func() { -- cgit v1.2.3 From 6e9c453ee0f4a0212ef3f3200156c62a5c30b1ad Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 5 Mar 2026 10:35:17 +0300 Subject: Enha: explicit app.Draw per textView update for smooth streaming --- tui.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 36a34bb..f744825 100644 --- a/tui.go +++ b/tui.go @@ -324,8 +324,11 @@ func init() { SetDynamicColors(true). SetRegions(true). SetChangedFunc(func() { + // INFO: // https://github.com/rivo/tview/wiki/Concurrency#event-handlers - // app.Draw() // already called by default per tview specs + // although already called by default per tview specs + // calling it explicitly makes text streaming to look more smooth + app.Draw() }) notificationWidget = tview.NewTextView(). SetTextAlign(tview.AlignCenter). -- cgit v1.2.3 From c65c11bcfbc563611743d02039420533bcfe9d05 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 5 Mar 2026 11:36:35 +0300 Subject: Fix: shellmode tab completion --- tui.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index f744825..c6ab392 100644 --- a/tui.go +++ b/tui.go @@ -273,7 +273,7 @@ func init() { shellHistoryPos = -1 } // Handle Tab key for @ file completion - if event.Key() == tcell.KeyTab { + if event.Key() == tcell.KeyTab && shellMode { currentText := shellInput.GetText() atIndex := strings.LastIndex(currentText, "@") if atIndex >= 0 { @@ -1151,7 +1151,7 @@ func init() { chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText} return nil } - if event.Key() == tcell.KeyTab { + if event.Key() == tcell.KeyTab && !shellMode { currentF := app.GetFocus() if currentF == textArea { currentText := textArea.GetText() -- cgit v1.2.3 From 4f0bce50c53267a9f53938ad1b264d5094a08ce4 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 7 Mar 2026 19:11:13 +0300 Subject: Chore: one init for clear call order --- tui.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index c6ab392..9c81f7d 100644 --- a/tui.go +++ b/tui.go @@ -224,7 +224,7 @@ func showToast(title, message string) { }) } -func init() { +func initTUI() { // Start background goroutine to update model color cache startModelColorUpdater() tview.Styles = colorschemes["default"] @@ -1173,4 +1173,5 @@ func init() { } return event }) + go updateModelLists() } -- cgit v1.2.3 From 23cb8f2578540e698f590bed35f973a22a8c2f90 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sun, 8 Mar 2026 06:45:51 +0300 Subject: Chore: remove AutoCleanToolCallsFromCtx, atomic model color --- tui.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 9c81f7d..d7ea57f 100644 --- a/tui.go +++ b/tui.go @@ -42,7 +42,6 @@ var ( confirmPageName = "confirm" fullscreenMode bool positionVisible bool = true - scrollToEndEnabled bool = true // pages historyPage = "historyPage" agentPage = "agentPage" @@ -634,7 +633,7 @@ func initTUI() { updateStatusLine() textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() - if scrollToEndEnabled { + if cfg.AutoScrollEnabled { textView.ScrollToEnd() } // init sysmap @@ -663,9 +662,9 @@ func initTUI() { } if event.Key() == tcell.KeyRune && event.Rune() == '2' && event.Modifiers()&tcell.ModAlt != 0 { // toggle auto-scrolling - scrollToEndEnabled = !scrollToEndEnabled + cfg.AutoScrollEnabled = !cfg.AutoScrollEnabled status := "disabled" - if scrollToEndEnabled { + if cfg.AutoScrollEnabled { status = "enabled" } showToast("autoscroll", "Auto-scrolling "+status) @@ -1139,7 +1138,7 @@ func initTUI() { fmt.Fprintf(textView, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n", nl, len(chatBody.Messages), persona, msgText) textArea.SetText("", true) - if scrollToEndEnabled { + if cfg.AutoScrollEnabled { textView.ScrollToEnd() } colorText() -- cgit v1.2.3 From c200c9328c4aa7654dc41c0eac02fe1cc267d666 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sun, 8 Mar 2026 07:13:27 +0300 Subject: Enha: botresp, toolresp to atomic --- tui.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index d7ea57f..482050a 100644 --- a/tui.go +++ b/tui.go @@ -731,7 +731,7 @@ func initTUI() { updateStatusLine() return nil } - if event.Key() == tcell.KeyF2 && !botRespMode { + if event.Key() == tcell.KeyF2 && !botRespMode.Load() { // regen last msg if len(chatBody.Messages) == 0 { showToast("info", "no messages to regenerate") @@ -748,7 +748,7 @@ func initTUI() { chatRoundChan <- &models.ChatRoundReq{Role: cfg.UserRole, Regen: true} return nil } - if event.Key() == tcell.KeyF3 && !botRespMode { + if event.Key() == tcell.KeyF3 && !botRespMode.Load() { // delete last msg // check textarea text; if it ends with bot icon delete only icon: text := textView.GetText(true) @@ -804,9 +804,9 @@ func initTUI() { return nil } if event.Key() == tcell.KeyF6 { - interruptResp = true - botRespMode = false - toolRunningMode = false + interruptResp.Store(true) + botRespMode.Store(false) + toolRunningMode.Store(false) return nil } if event.Key() == tcell.KeyF7 { @@ -1101,7 +1101,7 @@ func initTUI() { return nil } // cannot send msg in editMode or botRespMode - if event.Key() == tcell.KeyEscape && !editMode && !botRespMode { + if event.Key() == tcell.KeyEscape && !editMode && !botRespMode.Load() { if shellMode { cmdText := shellInput.GetText() if cmdText != "" { @@ -1167,7 +1167,7 @@ func initTUI() { app.SetFocus(focusSwitcher[currentF]) return nil } - if isASCII(string(event.Rune())) && !botRespMode { + if isASCII(string(event.Rune())) && !botRespMode.Load() { return event } return event -- cgit v1.2.3 From e0201886f80528790c3a05864da66bafdf07f9d8 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sun, 8 Mar 2026 08:50:50 +0300 Subject: Enha (rag): keep page open until user closes it --- tui.go | 1 - 1 file changed, 1 deletion(-) (limited to 'tui.go') diff --git a/tui.go b/tui.go index 482050a..04ce38e 100644 --- a/tui.go +++ b/tui.go @@ -50,7 +50,6 @@ var ( helpPage = "helpPage" renamePage = "renamePage" RAGPage = "RAGPage" - RAGLoadedPage = "RAGLoadedPage" propsPage = "propsPage" codeBlockPage = "codeBlockPage" imgPage = "imgPage" -- cgit v1.2.3