Merge branch 'master' into doc/tutorial

This commit is contained in:
Grail Finder
2025-12-14 10:04:48 +03:00
4 changed files with 166 additions and 96 deletions

41
bot.go
View File

@@ -826,6 +826,30 @@ func charToStart(agentName string) bool {
return true return true
} }
func updateModelLists() {
var err error
if cfg.OpenRouterToken != "" {
ORFreeModels, err = fetchORModels(true)
if err != nil {
logger.Warn("failed to fetch or models", "error", err)
}
}
// if llama.cpp started after gf-lt?
LocalModels, err = fetchLCPModels()
if err != nil {
logger.Warn("failed to fetch llama.cpp models", "error", err)
}
}
func updateModelListsTicker() {
updateModelLists() // run on the start
ticker := time.NewTicker(time.Minute * 1)
for {
<-ticker.C
updateModelLists()
}
}
func init() { func init() {
var err error var err error
cfg, err = config.LoadConfig("config.toml") cfg, err = config.LoadConfig("config.toml")
@@ -878,22 +902,6 @@ func init() {
playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2} playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2}
cluedoState = extra.CluedoPrepCards(playerOrder) cluedoState = extra.CluedoPrepCards(playerOrder)
} }
if cfg.OpenRouterToken != "" {
go func() {
ORModels, err := fetchORModels(true)
if err != nil {
logger.Error("failed to fetch or models", "error", err)
} else {
ORFreeModels = ORModels
}
}()
}
go func() {
LocalModels, err = fetchLCPModels()
if err != nil {
logger.Error("failed to fetch llama.cpp models", "error", err)
}
}()
choseChunkParser() choseChunkParser()
httpClient = createClient(time.Second * 15) httpClient = createClient(time.Second * 15)
if cfg.TTS_ENABLED { if cfg.TTS_ENABLED {
@@ -902,4 +910,5 @@ func init() {
if cfg.STT_ENABLED { if cfg.STT_ENABLED {
asr = extra.NewSTT(logger, cfg) asr = extra.NewSTT(logger, cfg)
} }
go updateModelListsTicker()
} }

View File

@@ -1,6 +1,8 @@
package config package config
import ( import (
"os"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
@@ -84,6 +86,13 @@ func LoadConfig(fn string) (*Config, error) {
config.OpenRouterCompletionAPI: config.OpenRouterChatAPI, config.OpenRouterCompletionAPI: config.OpenRouterChatAPI,
config.OpenRouterChatAPI: config.ChatAPI, config.OpenRouterChatAPI: config.ChatAPI,
} }
// check env if keys not in config
if config.OpenRouterToken == "" {
config.OpenRouterToken = os.Getenv("OPENROUTER_API_KEY")
}
if config.DeepSeekToken == "" {
config.DeepSeekToken = os.Getenv("DEEPSEEK_API_KEY")
}
// Build ApiLinks slice with only non-empty API links // Build ApiLinks slice with only non-empty API links
// Only include DeepSeek APIs if DeepSeekToken is provided // Only include DeepSeek APIs if DeepSeekToken is provided
if config.DeepSeekToken != "" { if config.DeepSeekToken != "" {
@@ -94,7 +103,6 @@ func LoadConfig(fn string) (*Config, error) {
config.ApiLinks = append(config.ApiLinks, config.DeepSeekCompletionAPI) config.ApiLinks = append(config.ApiLinks, config.DeepSeekCompletionAPI)
} }
} }
// Only include OpenRouter APIs if OpenRouterToken is provided // Only include OpenRouter APIs if OpenRouterToken is provided
if config.OpenRouterToken != "" { if config.OpenRouterToken != "" {
if config.OpenRouterChatAPI != "" { if config.OpenRouterChatAPI != "" {
@@ -104,7 +112,6 @@ func LoadConfig(fn string) (*Config, error) {
config.ApiLinks = append(config.ApiLinks, config.OpenRouterCompletionAPI) config.ApiLinks = append(config.ApiLinks, config.OpenRouterCompletionAPI)
} }
} }
// Always include basic APIs // Always include basic APIs
if config.ChatAPI != "" { if config.ChatAPI != "" {
config.ApiLinks = append(config.ApiLinks, config.ChatAPI) config.ApiLinks = append(config.ApiLinks, config.ChatAPI)

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"slices" "slices"
"strconv" "strconv"
"strings"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/rivo/tview" "github.com/rivo/tview"
@@ -134,9 +135,16 @@ func makePropsTable(props map[string]float32) *tview.Table {
addListPopupRow("Select an api", apiLinks, cfg.CurrentAPI, func(option string) { addListPopupRow("Select an api", apiLinks, cfg.CurrentAPI, func(option string) {
cfg.CurrentAPI = option cfg.CurrentAPI = option
}) })
var modelList []string
// INFO: modelList is chosen based on current api link
if strings.Contains(cfg.CurrentAPI, "api.deepseek.com/") {
modelList = []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"}
} else if strings.Contains(cfg.CurrentAPI, "opentouter.ai") {
modelList = ORFreeModels
} else { // would match on localhost but what if llama.cpp served non localy?
modelList = LocalModels
}
// Prepare model list dropdown // Prepare model list dropdown
modelList := []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"}
modelList = append(modelList, ORFreeModels...)
addListPopupRow("Select a model", modelList, chatBody.Model, func(option string) { addListPopupRow("Select a model", modelList, chatBody.Model, func(option string) {
chatBody.Model = option chatBody.Model = option
}) })

168
tables.go
View File

@@ -33,11 +33,13 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
case 0: case 0:
chatActTable.SetCell(r, c, chatActTable.SetCell(r, c,
tview.NewTableCell(chatList[r]). tview.NewTableCell(chatList[r]).
SetSelectable(false).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
case 1: case 1:
chatActTable.SetCell(r, c, chatActTable.SetCell(r, c,
tview.NewTableCell(chatMap[chatList[r]].Msgs[len(chatMap[chatList[r]].Msgs)-30:]). tview.NewTableCell(chatMap[chatList[r]].Msgs[len(chatMap[chatList[r]].Msgs)-30:]).
SetSelectable(false).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
default: default:
@@ -48,14 +50,11 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
} }
} }
} }
chatActTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { chatActTable.Select(0, 0).SetSelectable(true, true).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(historyPage) pages.RemovePage(historyPage)
return return
} }
if key == tcell.KeyEnter {
chatActTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) { }).SetSelectedFunc(func(row int, column int) {
tc := chatActTable.GetCell(row, column) tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed) tc.SetTextColor(tcell.ColorRed)
@@ -192,21 +191,21 @@ func makeRAGTable(fileList []string) *tview.Flex {
ragflex := tview.NewFlex().SetDirection(tview.FlexRow). ragflex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(longStatusView, 0, 10, false). AddItem(longStatusView, 0, 10, false).
AddItem(fileTable, 0, 60, true) AddItem(fileTable, 0, 60, true)
// Add the exit option as the first row (row 0) // Add the exit option as the first row (row 0)
fileTable.SetCell(0, 0, fileTable.SetCell(0, 0,
tview.NewTableCell("Exit RAG manager"). tview.NewTableCell("Exit RAG manager").
SetTextColor(tcell.ColorWhite). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 1, fileTable.SetCell(0, 1,
tview.NewTableCell("(Close without action)"). tview.NewTableCell("(Close without action)").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 2, fileTable.SetCell(0, 2,
tview.NewTableCell("exit"). tview.NewTableCell("exit").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
// Add the file rows starting from row 1 // Add the file rows starting from row 1
for r := 0; r < rows; r++ { for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ { for c := 0; c < cols; c++ {
@@ -215,8 +214,15 @@ func makeRAGTable(fileList []string) *tview.Flex {
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(fileList[r]). tview.NewTableCell(fileList[r]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
} else { SetSelectable(false))
} else if c == 1 { // Action description column - not selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell("(Action)").
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else { // Action button column - selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(actions[c-1]). tview.NewTableCell(actions[c-1]).
SetTextColor(color). SetTextColor(color).
@@ -250,29 +256,32 @@ func makeRAGTable(fileList []string) *tview.Flex {
} }
} }
}() }()
fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { fileTable.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
return return
} }
if key == tcell.KeyEnter {
fileTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) { }).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0 or 1), move to first action column (2)
if column <= 1 {
if fileTable.GetColumnCount() > 2 {
fileTable.Select(row, 2) // Select first action column
}
return
}
// defer pages.RemovePage(RAGPage) // defer pages.RemovePage(RAGPage)
tc := fileTable.GetCell(row, column) tc := fileTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
fileTable.SetSelectable(false, false)
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues // Check if the selected row is the exit row (row 0) - do this first to avoid index issues
if row == 0 { if row == 0 {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
return return
} }
// For file rows, get the filename (row index - 1 because of the exit row at index 0) // For file rows, get the filename (row index - 1 because of the exit row at index 0)
fpath := fileList[row-1] // -1 to account for the exit row at index 0 fpath := fileList[row-1] // -1 to account for the exit row at index 0
// notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text) // notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text)
switch tc.Text { switch tc.Text {
case "load": case "load":
@@ -303,7 +312,6 @@ func makeRAGTable(fileList []string) *tview.Flex {
return return
} }
}) })
// Add input capture to the flex container to handle 'x' key for closing // Add input capture to the flex container to handle 'x' key for closing
ragflex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { ragflex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
@@ -312,7 +320,6 @@ func makeRAGTable(fileList []string) *tview.Flex {
} }
return event return event
}) })
return ragflex return ragflex
} }
@@ -331,21 +338,21 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
ragflex := tview.NewFlex().SetDirection(tview.FlexRow). ragflex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(longStatusView, 0, 10, false). AddItem(longStatusView, 0, 10, false).
AddItem(fileTable, 0, 60, true) AddItem(fileTable, 0, 60, true)
// Add the exit option as the first row (row 0) // Add the exit option as the first row (row 0)
fileTable.SetCell(0, 0, fileTable.SetCell(0, 0,
tview.NewTableCell("Exit Loaded Files manager"). tview.NewTableCell("Exit Loaded Files manager").
SetTextColor(tcell.ColorWhite). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 1, fileTable.SetCell(0, 1,
tview.NewTableCell("(Close without action)"). tview.NewTableCell("(Close without action)").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 2, fileTable.SetCell(0, 2,
tview.NewTableCell("exit"). tview.NewTableCell("exit").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
// Add the file rows starting from row 1 // Add the file rows starting from row 1
for r := 0; r < rows; r++ { for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ { for c := 0; c < cols; c++ {
@@ -354,8 +361,15 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(fileList[r]). tview.NewTableCell(fileList[r]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
} else { SetSelectable(false))
} else if c == 1 { // Action description column - not selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell("(Action)").
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else { // Action button column - selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(actions[c-1]). tview.NewTableCell(actions[c-1]).
SetTextColor(color). SetTextColor(color).
@@ -363,29 +377,33 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
} }
} }
} }
fileTable.Select(0, 0).
fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
pages.RemovePage(RAGLoadedPage) pages.RemovePage(RAGLoadedPage)
return return
} }
if key == tcell.KeyEnter {
fileTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) { }).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0 or 1), move to first action column (2)
if column <= 1 {
if fileTable.GetColumnCount() > 2 {
fileTable.Select(row, 2) // Select first action column
}
return
}
tc := fileTable.GetCell(row, column) tc := fileTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
fileTable.SetSelectable(false, false)
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues // Check if the selected row is the exit row (row 0) - do this first to avoid index issues
if row == 0 { if row == 0 {
pages.RemovePage(RAGLoadedPage) pages.RemovePage(RAGLoadedPage)
return return
} }
// For file rows, get the filename (row index - 1 because of the exit row at index 0) // For file rows, get the filename (row index - 1 because of the exit row at index 0)
fpath := fileList[row-1] // -1 to account for the exit row at index 0 fpath := fileList[row-1] // -1 to account for the exit row at index 0
switch tc.Text { switch tc.Text {
case "delete": case "delete":
if err := ragger.RemoveFile(fpath); err != nil { if err := ragger.RemoveFile(fpath); err != nil {
@@ -403,7 +421,6 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
return return
} }
}) })
// Add input capture to the flex container to handle 'x' key for closing // Add input capture to the flex container to handle 'x' key for closing
ragflex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { ragflex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
@@ -412,7 +429,6 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
} }
return event return event
}) })
return ragflex return ragflex
} }
@@ -428,8 +444,9 @@ func makeAgentTable(agentList []string) *tview.Table {
chatActTable.SetCell(r, c, chatActTable.SetCell(r, c,
tview.NewTableCell(agentList[r]). tview.NewTableCell(agentList[r]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
} else { SetSelectable(false))
} else if c == 1 {
if actions[c-1] == "filepath" { if actions[c-1] == "filepath" {
cc, ok := sysMap[agentList[r]] cc, ok := sysMap[agentList[r]]
if !ok { if !ok {
@@ -438,28 +455,41 @@ func makeAgentTable(agentList []string) *tview.Table {
chatActTable.SetCell(r, c, chatActTable.SetCell(r, c,
tview.NewTableCell(cc.FilePath). tview.NewTableCell(cc.FilePath).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
continue continue
} }
chatActTable.SetCell(r, c, chatActTable.SetCell(r, c,
tview.NewTableCell(actions[c-1]). tview.NewTableCell(actions[c-1]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
} else {
chatActTable.SetCell(r, c,
tview.NewTableCell(actions[c-1]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
} }
} }
} }
chatActTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { chatActTable.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(agentPage) pages.RemovePage(agentPage)
return return
} }
if key == tcell.KeyEnter {
chatActTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) { }).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0 or 1), move to first action column (2)
if column <= 1 {
if chatActTable.GetColumnCount() > 2 {
chatActTable.Select(row, 2) // Select first action column
}
return
}
tc := chatActTable.GetCell(row, column) tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
chatActTable.SetSelectable(false, false)
selected := agentList[row] selected := agentList[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text { switch tc.Text {
@@ -528,7 +558,8 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
table.SetCell(r, c, table.SetCell(r, c,
tview.NewTableCell(codeBlocks[r][:previewLen]). tview.NewTableCell(codeBlocks[r][:previewLen]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
} else { } else {
table.SetCell(r, c, table.SetCell(r, c,
tview.NewTableCell(actions[c-1]). tview.NewTableCell(actions[c-1]).
@@ -537,18 +568,25 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
} }
} }
} }
table.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { table.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(codeBlockPage) pages.RemovePage(codeBlockPage)
return return
} }
if key == tcell.KeyEnter {
table.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) { }).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0), move to first action column (1)
if column == 0 {
if table.GetColumnCount() > 1 {
table.Select(row, 1) // Select first action column
}
return
}
tc := table.GetCell(row, column) tc := table.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
table.SetSelectable(false, false)
selected := codeBlocks[row] selected := codeBlocks[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text { switch tc.Text {
@@ -592,7 +630,8 @@ func makeImportChatTable(filenames []string) *tview.Table {
chatActTable.SetCell(r, c, chatActTable.SetCell(r, c,
tview.NewTableCell(filenames[r]). tview.NewTableCell(filenames[r]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
} else { } else {
chatActTable.SetCell(r, c, chatActTable.SetCell(r, c,
tview.NewTableCell(actions[c-1]). tview.NewTableCell(actions[c-1]).
@@ -601,18 +640,25 @@ func makeImportChatTable(filenames []string) *tview.Table {
} }
} }
} }
chatActTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { chatActTable.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(historyPage) pages.RemovePage(historyPage)
return return
} }
if key == tcell.KeyEnter {
chatActTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) { }).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0), move to first action column (1)
if column == 0 {
if chatActTable.GetColumnCount() > 1 {
chatActTable.Select(row, 1) // Select first action column
}
return
}
tc := chatActTable.GetCell(row, column) tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
chatActTable.SetSelectable(false, false)
selected := filenames[row] selected := filenames[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text { switch tc.Text {