4 Commits

Author SHA1 Message Date
Grail Finder
4bddce3700 Enha: compute estimate of non llm text 2026-03-02 15:21:45 +03:00
Grail Finder
fcc71987bf Feat: token use estimation 2026-03-02 14:54:20 +03:00
Grail Finder
8458edf5a8 Enha: interrupt llm and tool both 2026-03-02 12:19:50 +03:00
Grail Finder
07b06bb0d3 Enha: tabcompletion is back in textarea 2026-03-02 12:09:27 +03:00
4 changed files with 165 additions and 2 deletions

10
bot.go
View File

@@ -64,6 +64,8 @@ var (
"meta-llama/llama-3.3-70b-instruct:free", "meta-llama/llama-3.3-70b-instruct:free",
} }
LocalModels = []string{} LocalModels = []string{}
localModelsData *models.LCPModels
orModelsData *models.ORModels
) )
var thinkBlockRE = regexp.MustCompile(`(?s)<think>.*?</think>`) var thinkBlockRE = regexp.MustCompile(`(?s)<think>.*?</think>`)
@@ -355,6 +357,7 @@ func fetchORModels(free bool) ([]string, error) {
if err := json.NewDecoder(resp.Body).Decode(data); err != nil { if err := json.NewDecoder(resp.Body).Decode(data); err != nil {
return nil, err return nil, err
} }
orModelsData = data
freeModels := data.ListModels(free) freeModels := data.ListModels(free)
return freeModels, nil return freeModels, nil
} }
@@ -416,6 +419,7 @@ func fetchLCPModelsWithStatus() (*models.LCPModels, error) {
if err := json.NewDecoder(resp.Body).Decode(data); err != nil { if err := json.NewDecoder(resp.Body).Decode(data); err != nil {
return nil, err return nil, err
} }
localModelsData = data
return data, nil return data, nil
} }
@@ -745,7 +749,7 @@ func sendMsgToLLM(body io.Reader) {
} }
interrupt: interrupt:
if interruptResp { // read bytes, so it would not get into beginning of the next req if interruptResp { // read bytes, so it would not get into beginning of the next req
interruptResp = false // interruptResp = false
logger.Info("interrupted bot response", "chunk_counter", counter) logger.Info("interrupted bot response", "chunk_counter", counter)
streamDone <- true streamDone <- true
break break
@@ -799,6 +803,7 @@ func showSpinner() {
} }
func chatRound(r *models.ChatRoundReq) error { func chatRound(r *models.ChatRoundReq) error {
interruptResp = false
botRespMode = true botRespMode = true
go showSpinner() go showSpinner()
updateStatusLine() updateStatusLine()
@@ -964,6 +969,9 @@ out:
} }
// Strip think blocks before parsing for tool calls // Strip think blocks before parsing for tool calls
respTextNoThink := thinkBlockRE.ReplaceAllString(respText.String(), "") respTextNoThink := thinkBlockRE.ReplaceAllString(respText.String(), "")
if interruptResp {
return nil
}
if findCall(respTextNoThink, toolResp.String()) { if findCall(respTextNoThink, toolResp.String()) {
return nil return nil
} }

View File

@@ -11,6 +11,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"slices" "slices"
"strconv"
"strings" "strings"
"time" "time"
"unicode" "unicode"
@@ -376,9 +377,90 @@ func makeStatusLine() string {
roleInject := fmt.Sprintf(" | [%s:-:b]role injection[-:-:-] (alt+7)", boolColors[injectRole]) roleInject := fmt.Sprintf(" | [%s:-:b]role injection[-:-:-] (alt+7)", boolColors[injectRole])
statusLine += roleInject statusLine += roleInject
} }
// context tokens
contextTokens := getContextTokens()
maxCtx := getMaxContextTokens()
if maxCtx == 0 {
maxCtx = 16384
}
if contextTokens > 0 {
contextInfo := fmt.Sprintf(" | context-estim: [orange:-:b]%d/%d[-:-:-]", contextTokens, maxCtx)
statusLine += contextInfo
}
return statusLine + imageInfo + shellModeInfo return statusLine + imageInfo + shellModeInfo
} }
func getContextTokens() int {
if chatBody == nil || chatBody.Messages == nil {
return 0
}
total := 0
messages := chatBody.Messages
for i := range messages {
msg := &messages[i]
if msg.Stats != nil && msg.Stats.Tokens > 0 {
total += msg.Stats.Tokens
} else if msg.GetText() != "" {
total += len(msg.GetText()) / 4
}
}
return total
}
const deepseekContext = 128000
func getMaxContextTokens() int {
if chatBody == nil || chatBody.Model == "" {
return 0
}
modelName := chatBody.Model
switch {
case strings.Contains(cfg.CurrentAPI, "openrouter"):
if orModelsData != nil {
for i := range orModelsData.Data {
m := &orModelsData.Data[i]
if m.ID == modelName {
return m.ContextLength
}
}
}
case strings.Contains(cfg.CurrentAPI, "deepseek"):
return deepseekContext
default:
if localModelsData != nil {
for i := range localModelsData.Data {
m := &localModelsData.Data[i]
if m.ID == modelName {
for _, arg := range m.Status.Args {
if strings.HasPrefix(arg, "--ctx-size") {
if strings.Contains(arg, "=") {
val := strings.Split(arg, "=")[1]
if n, err := strconv.Atoi(val); err == nil {
return n
}
} else {
idx := -1
for j, a := range m.Status.Args {
if a == "--ctx-size" && j+1 < len(m.Status.Args) {
idx = j + 1
break
}
}
if idx != -1 {
if n, err := strconv.Atoi(m.Status.Args[idx]); err == nil {
return n
}
}
}
}
}
}
}
}
}
return 0
}
// set of roles within card definition and mention in chat history // set of roles within card definition and mention in chat history
func listChatRoles() []string { func listChatRoles() []string {
currentChat, ok := chatMap[activeChatName] currentChat, ok := chatMap[activeChatName]

View File

@@ -406,6 +406,66 @@ func showShellFileCompletionPopup(filter string) {
app.SetFocus(widget) app.SetFocus(widget)
} }
func showTextAreaFileCompletionPopup(filter string) {
baseDir := cfg.FilePickerDir
if baseDir == "" {
baseDir = "."
}
complMatches := scanFiles(baseDir, filter)
if len(complMatches) == 0 {
return
}
if len(complMatches) == 1 {
currentText := textArea.GetText()
atIdx := strings.LastIndex(currentText, "@")
if atIdx >= 0 {
before := currentText[:atIdx]
textArea.SetText(before+complMatches[0], true)
}
return
}
widget := tview.NewList().ShowSecondaryText(false).
SetSelectedBackgroundColor(tcell.ColorGray)
widget.SetTitle("file completion").SetBorder(true)
for _, m := range complMatches {
widget.AddItem(m, "", 0, nil)
}
widget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
currentText := textArea.GetText()
atIdx := strings.LastIndex(currentText, "@")
if atIdx >= 0 {
before := currentText[:atIdx]
textArea.SetText(before+mainText, true)
}
pages.RemovePage("textAreaFileCompletionPopup")
app.SetFocus(textArea)
})
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
pages.RemovePage("textAreaFileCompletionPopup")
app.SetFocus(textArea)
return nil
}
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("textAreaFileCompletionPopup")
app.SetFocus(textArea)
return nil
}
return event
})
modal := func(p tview.Primitive, width, height int) tview.Primitive {
return tview.NewFlex().
AddItem(nil, 0, 1, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(nil, 0, 1, false).
AddItem(p, height, 1, true).
AddItem(nil, 0, 1, false), width, 1, true).
AddItem(nil, 0, 1, false)
}
pages.AddPage("textAreaFileCompletionPopup", modal(widget, 80, 20), true, true)
app.SetFocus(widget)
}
func updateWidgetColors(theme *tview.Theme) { func updateWidgetColors(theme *tview.Theme) {
bgColor := theme.PrimitiveBackgroundColor bgColor := theme.PrimitiveBackgroundColor
fgColor := theme.PrimaryTextColor fgColor := theme.PrimaryTextColor

13
tui.go
View File

@@ -726,6 +726,7 @@ func init() {
if event.Key() == tcell.KeyF6 { if event.Key() == tcell.KeyF6 {
interruptResp = true interruptResp = true
botRespMode = false botRespMode = false
toolRunningMode = false
return nil return nil
} }
if event.Key() == tcell.KeyF7 { if event.Key() == tcell.KeyF7 {
@@ -1071,6 +1072,18 @@ func init() {
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText} chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText}
return nil return nil
} }
if event.Key() == tcell.KeyTab {
currentF := app.GetFocus()
if currentF == textArea {
currentText := textArea.GetText()
atIndex := strings.LastIndex(currentText, "@")
if atIndex >= 0 {
filter := currentText[atIndex+1:]
showTextAreaFileCompletionPopup(filter)
}
}
return nil
}
if event.Key() == tcell.KeyPgUp || event.Key() == tcell.KeyPgDn { if event.Key() == tcell.KeyPgUp || event.Key() == tcell.KeyPgDn {
currentF := app.GetFocus() currentF := app.GetFocus()
app.SetFocus(focusSwitcher[currentF]) app.SetFocus(focusSwitcher[currentF])