diff options
| author | Grail Finder <wohilas@gmail.com> | 2026-03-09 07:07:36 +0300 |
|---|---|---|
| committer | Grail Finder <wohilas@gmail.com> | 2026-03-09 07:07:36 +0300 |
| commit | 0e42a6f069ceea40485162c014c04cf718568cfe (patch) | |
| tree | 583a6a6cb91b315e506990a03fdda1b32d0fe985 /tui.go | |
| parent | 2687f38d00ceaa4f61034e3e02b9b59d08efc017 (diff) | |
| parent | a1b5f9cdc59938901123650fc0900067ac3447ca (diff) | |
Merge branch 'master' into feat/agent-flow
Diffstat (limited to 'tui.go')
| -rw-r--r-- | tui.go | 145 |
1 files changed, 100 insertions, 45 deletions
@@ -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 @@ -36,10 +38,10 @@ var ( roleEditWindow *tview.InputField shellInput *tview.InputField confirmModal *tview.Modal + toastTimer *time.Timer confirmPageName = "confirm" fullscreenMode bool positionVisible bool = true - scrollToEndEnabled bool = true // pages historyPage = "historyPage" agentPage = "agentPage" @@ -48,7 +50,6 @@ var ( helpPage = "helpPage" renamePage = "renamePage" RAGPage = "RAGPage" - RAGLoadedPage = "RAGLoadedPage" propsPage = "propsPage" codeBlockPage = "codeBlockPage" imgPage = "imgPage" @@ -137,8 +138,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,39 +155,74 @@ 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() + 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) + } + }) }) - 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() { + return + } + notificationWidget.SetTitle(title) + notificationWidget.SetText(fmt.Sprintf("[yellow]%s[-]", message)) + go 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, 40, 1, false) + flex.AddItem(bottomFlex, 0, 10, true) + if positionVisible { + flex.AddItem(statusLineWidget, 0, 2, false) + } + }) + }() + toastTimer = time.AfterFunc(3*time.Second, func() { + app.QueueUpdateDraw(func() { + 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) } }) }) } -func init() { +func initTUI() { // Start background goroutine to update model color cache startModelColorUpdater() tview.Styles = colorschemes["default"] @@ -235,7 +271,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 { @@ -286,12 +322,26 @@ func init() { SetDynamicColors(true). SetRegions(true). SetChangedFunc(func() { + // INFO: + // https://github.com/rivo/tview/wiki/Concurrency#event-handlers + // 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). + 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 +410,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) } @@ -578,7 +632,7 @@ func init() { updateStatusLine() textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) colorText() - if scrollToEndEnabled { + if cfg.AutoScrollEnabled { textView.ScrollToEnd() } // init sysmap @@ -607,9 +661,9 @@ func init() { } 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) @@ -676,7 +730,7 @@ func init() { 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") @@ -693,7 +747,7 @@ func init() { 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) @@ -749,9 +803,9 @@ func init() { 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 { @@ -1046,7 +1100,7 @@ func init() { 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 != "" { @@ -1083,7 +1137,7 @@ func init() { 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() @@ -1095,7 +1149,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() @@ -1112,9 +1166,10 @@ func init() { app.SetFocus(focusSwitcher[currentF]) return nil } - if isASCII(string(event.Rune())) && !botRespMode { + if isASCII(string(event.Rune())) && !botRespMode.Load() { return event } return event }) + go updateModelLists() } |
