Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ require (
github.com/alecthomas/chroma v0.10.0
github.com/briandowns/spinner v1.23.2
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/ergochat/readline v0.1.3
github.com/fatih/color v1.18.0
github.com/nyaosorg/go-readline-ny v1.13.0
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
Expand All @@ -18,18 +18,13 @@ require (
)

require (
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-tty v0.0.7 // indirect
github.com/nyaosorg/go-box/v3 v3.0.0 // indirect
github.com/nyaosorg/go-ttyadapter v0.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
Expand Down
14 changes: 2 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=
github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -14,6 +12,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/ergochat/readline v0.1.3 h1:/DytGTmwdUJcLAe3k3VJgowh5vNnsdifYT6uVaf4pSo=
github.com/ergochat/readline v0.1.3/go.mod h1:o3ux9QLHLm77bq7hDB21UTm6HlV2++IPDMfIfKDuOgY=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
Expand All @@ -34,16 +34,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
github.com/nyaosorg/go-box/v3 v3.0.0 h1:W5qfScEkKBoD68gbP/lwfWlvcTRB0rwXkhL+9iC62xI=
github.com/nyaosorg/go-box/v3 v3.0.0/go.mod h1:70GsE9mIh7JKVCxt71q3jEijO6C9YJmOZqWpPa9w+GY=
github.com/nyaosorg/go-readline-ny v1.13.0 h1:3ifu0FQsswdB9N5vcph7lVzQvu20kVt6pMt1JuSz45o=
github.com/nyaosorg/go-readline-ny v1.13.0/go.mod h1:pFXLTklUi8JVi6gI3HdnA3Wak/7cl+kx2q8kIIiAf/c=
github.com/nyaosorg/go-ttyadapter v0.1.0 h1:3U3ytc35SOdkrn15rHLG36ozkmqBeGqhQgNufoep1AI=
github.com/nyaosorg/go-ttyadapter v0.1.0/go.mod h1:w6ySb/Y8rpr0uIju4vN/TMRHC/6ayabORHmEVs6d/qE=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
192 changes: 65 additions & 127 deletions internal/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import (
"time"

"github.com/alvinunreal/tmuxai/config"
"github.com/nyaosorg/go-readline-ny"
"github.com/nyaosorg/go-readline-ny/completion"
"github.com/nyaosorg/go-readline-ny/keys"
"github.com/nyaosorg/go-readline-ny/simplehistory"
"github.com/ergochat/readline"
)

// Message represents a chat message
Expand All @@ -26,6 +23,8 @@ type ChatMessage struct {
type CLIInterface struct {
manager *Manager
initMessage string
pasteMode bool
pasteBuffer strings.Builder
}

func NewCLIInterface(manager *Manager) *CLIInterface {
Expand All @@ -39,74 +38,61 @@ func NewCLIInterface(manager *Manager) *CLIInterface {
func (c *CLIInterface) Start(initMessage string) error {
c.printWelcomeMessage()

// Initialize history
history := simplehistory.New()
historyFilePath := config.GetConfigFilePath("history")

// Load history from file if it exists
if historyData, err := os.ReadFile(historyFilePath); err == nil {
for _, line := range strings.Split(string(historyData), "\n") {
if line = strings.TrimSpace(line); line != "" {
history.Add(line)
}
}
// Initialize readline
rl, err := readline.NewEx(&readline.Config{
Prompt: c.manager.GetPrompt(),
HistoryFile: historyFilePath,
AutoComplete: NewTmuxAICompleter(c.manager),
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
})
if err != nil {
return err
}

// Initialize editor
editor := &readline.Editor{
PromptWriter: func(w io.Writer) (int, error) {
return io.WriteString(w, c.manager.GetPrompt())
},
History: history,
HistoryCycling: true,
}

// Bind TAB key to completion
editor.BindKey(keys.CtrlI, c.newCompleter())
defer rl.Close()
rl.CaptureExitSignal()

if initMessage != "" {
fmt.Printf("%s%s\n", c.manager.GetPrompt(), initMessage)
c.processInput(initMessage)
}

ctx := context.Background()

for {
line, err := editor.ReadLine(ctx)
// Update prompt (in case state changed)
if c.pasteMode {
rl.SetPrompt("... ")
} else {
rl.SetPrompt(c.manager.GetPrompt())
}

if err == readline.CtrlC {
// Ctrl+C pressed, clear the line and continue
continue
line, err := rl.Readline()
if err == readline.ErrInterrupt {
if len(line) == 0 {
continue
} else {
continue
}
} else if err == io.EOF {
// Ctrl+D pressed, exit
return nil
} else if err != nil {
return err
}

// Save history
if line != "" {
history.Add(line)

// Build history data by iterating through all entries
historyLines := make([]string, 0, history.Len())
for i := 0; i < history.Len(); i++ {
historyLines = append(historyLines, history.At(i))
}
historyData := strings.Join(historyLines, "\n")
_ = os.WriteFile(historyFilePath, []byte(historyData), 0644)
}

// Process the input (preserving multiline content)
input := line // Keep the original line including newlines
// Process the input
input := line

// Check for exit/quit commands (only if it's the entire line content)
// Check for exit/quit commands
trimmed := strings.TrimSpace(input)
if trimmed == "exit" || trimmed == "quit" {
return nil
}
if trimmed == "" {
continue
if !c.pasteMode {
if trimmed == "exit" || trimmed == "quit" {
return nil
}
if trimmed == "" {
continue
}
}

c.processInput(input)
Expand All @@ -116,11 +102,36 @@ func (c *CLIInterface) Start(initMessage string) error {
// printWelcomeMessage prints a welcome message
func (c *CLIInterface) printWelcomeMessage() {
fmt.Println()
fmt.Println("Type '/help' for a list of commands, '/exit' to quit")
fmt.Println("Type '/help' for a list of commands, '/paste' to enter paste mode, '/exit' to quit")
fmt.Println()
}

func (c *CLIInterface) processInput(input string) {
if input == "/paste" {
c.pasteMode = true
c.pasteBuffer.Reset()
fmt.Println("Entering paste mode. Type '/end' to submit, or '/cancel' to abort.")
return
}

if c.pasteMode {
trimmed := strings.TrimSpace(input)
if trimmed == "/end" {
c.pasteMode = false
input = c.pasteBuffer.String()
c.pasteBuffer.Reset()
fmt.Println("Processing pasted content...")
} else if trimmed == "/cancel" {
c.pasteMode = false
c.pasteBuffer.Reset()
fmt.Println("Paste mode cancelled.")
return
} else {
c.pasteBuffer.WriteString(input + "\n")
return
}
}

if c.manager.IsMessageSubcommand(input) {
c.manager.ProcessSubCommand(input)
return
Expand Down Expand Up @@ -157,76 +168,3 @@ func (c *CLIInterface) processInput(input string) {

signal.Stop(sigChan)
}

// newCompleter creates a completion handler for command completion
func (c *CLIInterface) newCompleter() *completion.CmdCompletionOrList2 {
return &completion.CmdCompletionOrList2{
Delimiter: " ",
Postfix: " ",
Candidates: func(field []string) (forComp []string, forList []string) {
// Handle top-level commands
if len(field) == 0 || (len(field) == 1 && !strings.HasSuffix(field[0], " ")) {
return commands, commands
}

// Handle /config subcommands
if len(field) > 0 && field[0] == "/config" {
if len(field) == 1 || (len(field) == 2 && !strings.HasSuffix(field[1], " ")) {
return []string{"set", "get"}, []string{"set", "get"}
} else if len(field) == 2 || (len(field) == 3 && !strings.HasSuffix(field[2], " ")) {
return AllowedConfigKeys, AllowedConfigKeys
}
}

// Handle /prepare subcommands
if len(field) > 0 && field[0] == "/prepare" {
if len(field) == 1 || (len(field) == 2 && !strings.HasSuffix(field[1], " ")) {
return []string{"bash", "zsh", "fish"}, []string{"bash", "zsh", "fish"}
}
}

// Handle /kb subcommands
if len(field) > 0 && field[0] == "/kb" {
if len(field) == 1 || (len(field) == 2 && !strings.HasSuffix(field[1], " ")) {
return []string{"list", "load", "unload"}, []string{"list", "load", "unload"}
} else if (len(field) == 2 && field[1] == "load") || (len(field) >= 3 && field[1] == "load") {
// Get available knowledge bases for completion
kbs, err := c.manager.listKBs()
if err != nil {
return nil, nil
}
// Disable autocompletion when there's only one KB, bug with readline
if len(kbs) == 1 {
return nil, nil
}
return kbs, kbs
} else if (len(field) == 2 && field[1] == "unload") || (len(field) >= 3 && field[1] == "unload") {
// For unload, show loaded knowledge bases and --all option
var kbNames []string
for name := range c.manager.LoadedKBs {
kbNames = append(kbNames, name)
}
kbNames = append(kbNames, "--all")
return kbNames, kbNames
}
}

// Handle /model subcommands
if len(field) > 0 && field[0] == "/model" {
if len(field) == 1 || (len(field) == 2 && !strings.HasSuffix(field[1], " ")) {
// Return available models for completion
availableModels := c.manager.GetAvailableModels()
if len(availableModels) == 0 {
return nil, nil
}
// Disable autocompletion when there's only one model, bug with readline
if len(availableModels) == 1 {
return nil, nil
}
return availableModels, availableModels
}
}
return nil, nil
},
}
}
2 changes: 2 additions & 0 deletions internal/chat_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const helpMessage = `Available commands:
- /info: Display system information
- /clear: Clear the chat history
- /reset: Reset the chat history
- /paste: Enter paste mode for multiline input
- /prepare: Prepare the pane for TmuxAI automation
- /watch <prompt>: Start watch mode
- /squash: Summarize the chat history
Expand All @@ -30,6 +31,7 @@ var commands = []string{
"/help",
"/clear",
"/reset",
"/paste",
"/exit",
"/info",
"/watch",
Expand Down
Loading