diff --git a/README.md b/README.md index 52b2d94..062f20c 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,12 @@ To activate Prepare Mode, simply use: TmuxAI » /prepare ``` +By default, TmuxAI will attempt to detect the shell running in the execution pane. If you need to specify the shell manually, you can provide it as an argument: + +``` +TmuxAI » /prepare bash +``` + **Prepared Fish Example:** ```shell @@ -291,7 +297,7 @@ TmuxAI » /squash | `/config` | View current configuration settings | | `/config set ` | Override configuration for current session | | `/squash` | Manually trigger context summarization | -| `/prepare` | Initialize Prepared Mode for the Exec Pane | +| `/prepare [shell]` | Initialize Prepared Mode for the Exec Pane (e.g., bash, zsh) | | `/watch ` | Enable Watch Mode with specified goal | | `/exit` | Exit TmuxAI | diff --git a/internal/chat.go b/internal/chat.go index deb0574..2bbaf38 100644 --- a/internal/chat.go +++ b/internal/chat.go @@ -177,6 +177,13 @@ func (c *CLIInterface) newCompleter() *completion.CmdCompletionOrList2 { 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"} + } + } return nil, nil }, } diff --git a/internal/chat_command.go b/internal/chat_command.go index 4472ef6..28d6df2 100644 --- a/internal/chat_command.go +++ b/internal/chat_command.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/alvinunreal/tmuxai/config" "github.com/alvinunreal/tmuxai/logger" @@ -64,12 +65,56 @@ func (m *Manager) ProcessSubCommand(command string) { return case prefixMatch(commandPrefix, "/prepare"): + supportedShells := []string{"bash", "zsh", "fish"} m.InitExecPane() - m.PrepareExecPane() - m.Messages = []ChatMessage{} - if m.ExecPane.IsPrepared { - m.Println("Exec pane prepared successfully") + + // Check if exec pane is a subshell + if m.ExecPane.IsSubShell { + if len(parts) > 1 { + shell := parts[1] + isSupported := false + for _, supportedShell := range supportedShells { + if shell == supportedShell { + isSupported = true + break + } + } + if !isSupported { + m.Println(fmt.Sprintf("Shell '%s' is not supported. Supported shells are: %s", shell, strings.Join(supportedShells, ", "))) + return + } + m.PrepareExecPaneWithShell(shell) + } else { + m.Println("Shell detection is not supported on subshells.") + m.Println("Please specify the shell manually: /prepare bash, /prepare zsh, or /prepare fish") + return + } + } else { + if len(parts) > 1 { + shell := parts[1] + isSupported := false + for _, supportedShell := range supportedShells { + if shell == supportedShell { + isSupported = true + break + } + } + + if !isSupported { + m.Println(fmt.Sprintf("Shell '%s' is not supported. Supported shells are: %s", shell, strings.Join(supportedShells, ", "))) + return + } + m.PrepareExecPaneWithShell(shell) + } else { + m.PrepareExecPane() + } } + + // for latency over ssh connections + time.Sleep(500 * time.Millisecond) + m.ExecPane.Refresh(m.GetMaxCaptureLines()) + m.Messages = []ChatMessage{} + fmt.Println(m.ExecPane.String()) m.parseExecPaneCommandHistory() diff --git a/internal/chat_command_test.go b/internal/chat_command_test.go new file mode 100644 index 0000000..bdef26b --- /dev/null +++ b/internal/chat_command_test.go @@ -0,0 +1,169 @@ +package internal + +import ( + "fmt" + "testing" + + "github.com/alvinunreal/tmuxai/config" + "github.com/alvinunreal/tmuxai/system" + "github.com/stretchr/testify/assert" +) + +// Test /prepare command behavior with subshell +func TestProcessSubCommand_PrepareSubshell(t *testing.T) { + manager := &Manager{ + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]any), + Messages: []ChatMessage{}, + ExecPane: &system.TmuxPaneDetails{ + Id: "test-pane", + IsSubShell: true, // This is a subshell + }, + } + + // Mock system functions to prevent actual tmux calls + originalTmuxSend := system.TmuxSendCommandToPane + originalTmuxCapture := system.TmuxCapturePane + originalTmuxCurrentPaneId := system.TmuxCurrentPaneId + originalTmuxPanesDetails := system.TmuxPanesDetails + defer func() { + system.TmuxSendCommandToPane = originalTmuxSend + system.TmuxCapturePane = originalTmuxCapture + system.TmuxCurrentPaneId = originalTmuxCurrentPaneId + system.TmuxPanesDetails = originalTmuxPanesDetails + }() + + commandsSent := []string{} + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + commandsSent = append(commandsSent, command) + return nil + } + + system.TmuxCapturePane = func(paneId string, maxLines int) (string, error) { + return "", nil + } + + // Mock the system functions used by GetTmuxPanes to return the test pane + system.TmuxCurrentPaneId = func() (string, error) { + return "main-pane", nil + } + system.TmuxPanesDetails = func(windowTarget string) ([]system.TmuxPaneDetails, error) { + // Return the test pane as the only available pane + return []system.TmuxPaneDetails{*manager.ExecPane}, nil + } + + // Test case 1: /prepare with valid shell on subshell (should work and send commands) + commandsSent = []string{} // Reset + manager.ProcessSubCommand("/prepare bash") + + assert.Len(t, commandsSent, 2, "Should send PS1 command and clear command for bash") + assert.Contains(t, commandsSent[0], "PS1=", "Should send bash PS1 command") + assert.Equal(t, "C-l", commandsSent[1], "Should send clear screen command") + + // Test case 2: /prepare with zsh on subshell + commandsSent = []string{} // Reset + manager.ProcessSubCommand("/prepare zsh") + + assert.Len(t, commandsSent, 2, "Should send PROMPT command and clear command for zsh") + assert.Contains(t, commandsSent[0], "PROMPT=", "Should send zsh PROMPT command") + assert.Equal(t, "C-l", commandsSent[1], "Should send clear screen command") + + // Test case 3: /prepare with fish on subshell + commandsSent = []string{} // Reset + manager.ProcessSubCommand("/prepare fish") + + assert.Len(t, commandsSent, 2, "Should send fish_prompt function and clear command for fish") + assert.Contains(t, commandsSent[0], "fish_prompt", "Should send fish prompt function") + assert.Equal(t, "C-l", commandsSent[1], "Should send clear screen command") + + // Test case 4: /prepare without shell specification on subshell (should not send commands, just print warning) + commandsSent = []string{} // Reset + manager.ProcessSubCommand("/prepare") + + fmt.Println(commandsSent) + assert.Len(t, commandsSent, 0, "Should not send commands when no shell specified on subshell (should show warning instead)") +} + +// Test /prepare command behavior with normal shell (not subshell) +func TestProcessSubCommand_PrepareNormalShell(t *testing.T) { + manager := &Manager{ + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]any), + Messages: []ChatMessage{}, + ExecPane: &system.TmuxPaneDetails{ + Id: "test-pane", + IsSubShell: false, // This is NOT a subshell + CurrentCommand: "unknown", // Unsupported shell - should not send commands + }, + } + + // Mock system functions to prevent actual tmux calls + originalTmuxSend := system.TmuxSendCommandToPane + originalTmuxCapture := system.TmuxCapturePane + originalTmuxCurrentPaneId := system.TmuxCurrentPaneId + originalTmuxPanesDetails := system.TmuxPanesDetails + defer func() { + system.TmuxSendCommandToPane = originalTmuxSend + system.TmuxCapturePane = originalTmuxCapture + system.TmuxCurrentPaneId = originalTmuxCurrentPaneId + system.TmuxPanesDetails = originalTmuxPanesDetails + }() + + commandsSent := []string{} + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + commandsSent = append(commandsSent, command) + return nil + } + + system.TmuxCapturePane = func(paneId string, maxLines int) (string, error) { + return "", nil + } + + // Mock the system functions used by GetTmuxPanes to return the test pane + system.TmuxCurrentPaneId = func() (string, error) { + return "main-pane", nil + } + system.TmuxPanesDetails = func(windowTarget string) ([]system.TmuxPaneDetails, error) { + // Return the test pane as the only available pane + return []system.TmuxPaneDetails{*manager.ExecPane}, nil + } + + // Test case 1: /prepare without shell specification when CurrentCommand is not a shell (should not send commands) + commandsSent = []string{} // Reset + manager.ProcessSubCommand("/prepare") + + assert.Len(t, commandsSent, 0, "Should not send commands when CurrentCommand is not a supported shell") + + // Test case 2: /prepare with explicit shell on normal shell (should work) + commandsSent = []string{} // Reset + manager.ProcessSubCommand("/prepare zsh") + + assert.Len(t, commandsSent, 2, "Should send commands when explicitly specifying shell on normal pane") + assert.Contains(t, commandsSent[0], "PROMPT=", "Should send zsh PROMPT command") + assert.Equal(t, "C-l", commandsSent[1], "Should send clear screen command") +} + +// Test IsMessageSubcommand function +func TestIsMessageSubcommand(t *testing.T) { + manager := &Manager{} + + // Test cases for command detection + testCases := []struct { + input string + expected bool + desc string + }{ + {"/help", true, "Simple command should be detected"}, + {"/prepare bash", true, "Command with arguments should be detected"}, + {" /info ", true, "Command with whitespace should be detected"}, + {"/PREPARE", true, "Uppercase command should be detected"}, + {"hello world", false, "Regular message should not be detected as command"}, + {"", false, "Empty string should not be detected as command"}, + {"/ invalid", true, "Any string starting with / should be detected as command"}, + } + + for _, tc := range testCases { + result := manager.IsMessageSubcommand(tc.input) + assert.Equal(t, tc.expected, result, tc.desc) + } +} diff --git a/internal/exec_pane.go b/internal/exec_pane.go index b1e6c00..f22208d 100644 --- a/internal/exec_pane.go +++ b/internal/exec_pane.go @@ -34,15 +34,14 @@ func (m *Manager) InitExecPane() { m.ExecPane = &availablePane } -func (m *Manager) PrepareExecPane() { +func (m *Manager) PrepareExecPaneWithShell(shell string) { m.ExecPane.Refresh(m.GetMaxCaptureLines()) if m.ExecPane.IsPrepared && m.ExecPane.Shell != "" { return } - shellCommand := m.ExecPane.CurrentCommand var ps1Command string - switch shellCommand { + switch shell { case "zsh": ps1Command = `export PROMPT='%n@%m:%~[%T][%?]» '` case "bash": @@ -50,7 +49,7 @@ func (m *Manager) PrepareExecPane() { case "fish": ps1Command = `function fish_prompt; set -l s $status; printf '%s@%s:%s[%s][%d]» ' $USER (hostname -s) (prompt_pwd) (date +"%H:%M") $s; end` default: - errMsg := fmt.Sprintf("Shell '%s' in pane %s is recognized but not yet supported for PS1 modification.", shellCommand, m.ExecPane.Id) + errMsg := fmt.Sprintf("Shell '%s' in pane %s is recognized but not yet supported for PS1 modification.", shell, m.ExecPane.Id) logger.Info(errMsg) return } @@ -59,11 +58,17 @@ func (m *Manager) PrepareExecPane() { _ = system.TmuxSendCommandToPane(m.ExecPane.Id, "C-l", false) } +func (m *Manager) PrepareExecPane() { + m.PrepareExecPaneWithShell(m.ExecPane.CurrentCommand) +} + func (m *Manager) ExecWaitCapture(command string) (CommandExecHistory, error) { _ = system.TmuxSendCommandToPane(m.ExecPane.Id, command, true) - m.ExecPane.Refresh(m.GetMaxCaptureLines()) - m.Println("") + // wait for keys to be sent, duo to sometimes ssh latency + time.Sleep(500 * time.Millisecond) + + m.ExecPane.Refresh(m.GetMaxCaptureLines()) animChars := []string{"⋯", "⋱", "⋮", "⋰"} animIndex := 0 @@ -76,13 +81,25 @@ func (m *Manager) ExecWaitCapture(command string) (CommandExecHistory, error) { fmt.Print("\r\033[K") m.parseExecPaneCommandHistory() + if len(m.ExecHistory) == 0 { + logger.Error("Failed to parse command history from exec pane") + return CommandExecHistory{}, fmt.Errorf("failed to parse command history from exec pane") + } cmd := m.ExecHistory[len(m.ExecHistory)-1] logger.Debug("Command: %s\nOutput: %s\nCode: %d\n", cmd.Command, cmd.Output, cmd.Code) return cmd, nil } func (m *Manager) parseExecPaneCommandHistory() { - m.ExecPane.Refresh(m.GetMaxCaptureLines()) + m.parseExecPaneCommandHistoryWithContent("") +} + +func (m *Manager) parseExecPaneCommandHistoryWithContent(testContent string) { + if testContent == "" { + m.ExecPane.Refresh(m.GetMaxCaptureLines()) + } else { + m.ExecPane.Content = testContent + } var history []CommandExecHistory diff --git a/internal/exec_pane_test.go b/internal/exec_pane_test.go new file mode 100644 index 0000000..f9f0c39 --- /dev/null +++ b/internal/exec_pane_test.go @@ -0,0 +1,308 @@ +package internal + +import ( + "fmt" + "strings" + "testing" + + "github.com/alvinunreal/tmuxai/config" + "github.com/alvinunreal/tmuxai/system" + "github.com/stretchr/testify/assert" +) + +// Test regex matching for bash shell prompts +func TestParseExecPaneCommandHistory_Prompt(t *testing.T) { + manager := &Manager{ + ExecHistory: []CommandExecHistory{}, + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]interface{}), + } + + // Mock exec pane content with bash-style prompts + manager.ExecPane = &system.TmuxPaneDetails{} + testContent := `user@hostname:/path[14:30][0]» ls -la +total 8 +drwxr-xr-x 3 user user 4096 Jan 1 14:30 . +drwxr-xr-x 15 user user 4096 Jan 1 14:29 .. +user@hostname:/path[14:31][0]» echo "hello world" +hello world +user@hostname:/path[14:31][0]» ` + + manager.parseExecPaneCommandHistoryWithContent(testContent) + + assert.Len(t, manager.ExecHistory, 2, "Should parse 2 commands from bash prompt") + + // First command: ls -la + assert.Equal(t, "ls -la", manager.ExecHistory[0].Command) + assert.Equal(t, 0, manager.ExecHistory[0].Code) + assert.Contains(t, manager.ExecHistory[0].Output, "total 8") + assert.Contains(t, manager.ExecHistory[0].Output, "drwxr-xr-x") + + // Second command: echo "hello world" + assert.Equal(t, "echo \"hello world\"", manager.ExecHistory[1].Command) + assert.Equal(t, 0, manager.ExecHistory[1].Code) + assert.Equal(t, "hello world", manager.ExecHistory[1].Output) +} + +// Test edge cases and malformed prompts +func TestParseExecPaneCommandHistory_EdgeCases(t *testing.T) { + manager := &Manager{ + ExecHistory: []CommandExecHistory{}, + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]interface{}), + } + + // Test with no valid prompts (should result in empty history) + manager.ExecPane = &system.TmuxPaneDetails{} + testContent1 := `some random output +without any valid prompts +just plain text` + + manager.parseExecPaneCommandHistoryWithContent(testContent1) + assert.Len(t, manager.ExecHistory, 0, "Should parse 0 commands from invalid prompt format") + + // Test with only status code, no command + manager.ExecHistory = []CommandExecHistory{} // Reset + testContent2 := `user@hostname:~[14:30][0]» +user@hostname:~[14:30][0]» ` + + manager.parseExecPaneCommandHistoryWithContent(testContent2) + assert.Len(t, manager.ExecHistory, 0, "Should handle prompts with no commands") + + // Test with command but no terminating prompt (incomplete session) + manager.ExecHistory = []CommandExecHistory{} // Reset + testContent3 := `user@hostname:~[14:30][0]» long-running-command +output line 1 +output line 2 +still running...` + + manager.parseExecPaneCommandHistoryWithContent(testContent3) + assert.Len(t, manager.ExecHistory, 1, "Should handle incomplete commands") + assert.Equal(t, "long-running-command", manager.ExecHistory[0].Command) + assert.Equal(t, -1, manager.ExecHistory[0].Code) // No terminating prompt means unknown status + assert.Contains(t, manager.ExecHistory[0].Output, "output line 1") + assert.Contains(t, manager.ExecHistory[0].Output, "still running...") +} + +// Test mixed shell prompt formats (edge case where prompts might vary) +func TestParseExecPaneCommandHistory_MixedFormats(t *testing.T) { + manager := &Manager{ + ExecHistory: []CommandExecHistory{}, + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]interface{}), + } + + // Mix of different time formats and variations + manager.ExecPane = &system.TmuxPaneDetails{} + testContent := `user@host:/tmp[09:15][0]» echo "test1" +test1 +different@machine:/home[23:59][1]» echo "test2" +test2 +user@localhost:~[00:00][0]» ` + + manager.parseExecPaneCommandHistoryWithContent(testContent) + + assert.Len(t, manager.ExecHistory, 2, "Should parse commands from mixed prompt formats") + assert.Equal(t, "echo \"test1\"", manager.ExecHistory[0].Command) + assert.Equal(t, 1, manager.ExecHistory[0].Code) // Status from next prompt + assert.Equal(t, "echo \"test2\"", manager.ExecHistory[1].Command) + assert.Equal(t, 0, manager.ExecHistory[1].Code) +} + +// Test PrepareExecPaneWithShell for different shells +func TestPrepareExecPaneWithShell(t *testing.T) { + manager := &Manager{ + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]interface{}), + ExecPane: &system.TmuxPaneDetails{ + Id: "test-pane", + IsPrepared: false, + Shell: "", + }, + } + + // Mock system functions to prevent actual tmux calls + originalTmuxSend := system.TmuxSendCommandToPane + originalTmuxCapture := system.TmuxCapturePane + defer func() { + system.TmuxSendCommandToPane = originalTmuxSend + system.TmuxCapturePane = originalTmuxCapture + }() + + var commandsSent []string + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + commandsSent = append(commandsSent, command) + return nil + } + + system.TmuxCapturePane = func(paneId string, maxLines int) (string, error) { + return "", nil + } + + // Test bash shell preparation + manager.PrepareExecPaneWithShell("bash") + assert.Len(t, commandsSent, 2, "Should send 2 commands for bash") + assert.Contains(t, commandsSent[0], "PS1=", "Should set PS1 for bash") + assert.Equal(t, "C-l", commandsSent[1], "Should clear screen") + + // Reset and test zsh shell preparation + commandsSent = []string{} + manager.PrepareExecPaneWithShell("zsh") + assert.Len(t, commandsSent, 2, "Should send 2 commands for zsh") + assert.Contains(t, commandsSent[0], "PROMPT=", "Should set PROMPT for zsh") + assert.Equal(t, "C-l", commandsSent[1], "Should clear screen") + + // Reset and test fish shell preparation + commandsSent = []string{} + manager.PrepareExecPaneWithShell("fish") + assert.Len(t, commandsSent, 2, "Should send 2 commands for fish") + assert.Contains(t, commandsSent[0], "fish_prompt", "Should set fish_prompt for fish") + assert.Equal(t, "C-l", commandsSent[1], "Should clear screen") + + // Reset and test unsupported shell + commandsSent = []string{} + manager.PrepareExecPaneWithShell("tcsh") + assert.Len(t, commandsSent, 0, "Should not send commands for unsupported shell") +} + +// Test prompt regex with error cases that should be handled gracefully +func TestParseExecPaneCommandHistory_ErrorHandling(t *testing.T) { + manager := &Manager{ + ExecHistory: []CommandExecHistory{}, + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]interface{}), + } + + // Test with commands containing special characters and complex outputs + manager.ExecPane = &system.TmuxPaneDetails{} + testContent := `user@hostname:~[14:30][0]» echo "hello world" && ls -la | grep test +hello world +-rw-r--r-- 1 user user 123 Jan 1 14:30 test.txt +user@hostname:~[14:31][1]» false && echo "this should not appear" +user@hostname:~[14:31][0]» ` + + manager.parseExecPaneCommandHistoryWithContent(testContent) + assert.Len(t, manager.ExecHistory, 2, "Should parse complex commands with pipes and operators") + assert.Equal(t, `echo "hello world" && ls -la | grep test`, manager.ExecHistory[0].Command) + assert.Equal(t, 1, manager.ExecHistory[0].Code, "Should capture exit code from next prompt") + assert.Contains(t, manager.ExecHistory[0].Output, "hello world") + assert.Contains(t, manager.ExecHistory[0].Output, "test.txt") + + assert.Equal(t, `false && echo "this should not appear"`, manager.ExecHistory[1].Command) + assert.Equal(t, 0, manager.ExecHistory[1].Code) + + // Test with very long commands and outputs + manager.ExecHistory = []CommandExecHistory{} // Reset + longCommand := strings.Repeat("very-long-command-", 10) + longOutput := strings.Repeat("very long output line ", 50) + testContent2 := fmt.Sprintf(`user@hostname:~[14:30][0]» %s +%s +user@hostname:~[14:31][0]» `, longCommand, longOutput) + + manager.parseExecPaneCommandHistoryWithContent(testContent2) + assert.Len(t, manager.ExecHistory, 1, "Should handle long commands and outputs") + assert.Equal(t, longCommand, manager.ExecHistory[0].Command) + assert.Contains(t, manager.ExecHistory[0].Output, "very long output line") + assert.Equal(t, 0, manager.ExecHistory[0].Code) +} + +// Test SSH scenario where prompt format might differ and cause parsing issues +func TestExecWaitCapture_SSHScenario(t *testing.T) { + manager := &Manager{ + ExecHistory: []CommandExecHistory{}, + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]interface{}), + Status: "running", + ExecPane: &system.TmuxPaneDetails{ + Id: "ssh-pane", + LastLine: "user@remote-server:~$ ", // SSH prompt without proper formatting + }, + } + + // Mock system functions to simulate SSH environment + originalTmuxSend := system.TmuxSendCommandToPane + originalTmuxCapture := system.TmuxCapturePane + defer func() { + system.TmuxSendCommandToPane = originalTmuxSend + system.TmuxCapturePane = originalTmuxCapture + }() + + commandSent := "" + refreshCount := 0 + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + commandSent = command + return nil + } + + // Mock SSH server output without proper prompt format + system.TmuxCapturePane = func(paneId string, maxLines int) (string, error) { + refreshCount++ + // After a few refresh attempts, clear status to exit the loop + if refreshCount > 2 { + manager.Status = "" + } + return `user@remote-server:~$ ls -la +total 12 +drwx------ 3 user user 4096 Jan 15 10:30 . +drwxr-xr-x 5 root root 4096 Jan 15 10:25 .. +-rw-r--r-- 1 user user 18 Jan 15 10:30 .bashrc +user@remote-server:~$ `, nil + } + + // Test that ExecWaitCapture handles SSH scenario gracefully + result, err := manager.ExecWaitCapture("ls -la") + + assert.Error(t, err, "Should return error when SSH prompt format doesn't match expected pattern") + assert.Contains(t, err.Error(), "failed to parse command history") + assert.Equal(t, "", result.Command, "Should return empty CommandExecHistory on parsing failure") + assert.Equal(t, "", result.Output, "Should return empty output on parsing failure") + assert.Equal(t, 0, result.Code, "Should return default code on parsing failure") + assert.Equal(t, "ls -la", commandSent, "Should have sent the command to SSH pane") + assert.True(t, refreshCount > 1, "Should have attempted multiple refreshes before giving up") +} + +// Test ExecWaitCapture with successful command execution and proper prompt +func TestExecWaitCapture_SuccessfulExecution(t *testing.T) { + manager := &Manager{ + ExecHistory: []CommandExecHistory{}, + Config: &config.Config{MaxCaptureLines: 1000}, + SessionOverrides: make(map[string]interface{}), + Status: "running", + ExecPane: &system.TmuxPaneDetails{ + Id: "exec-pane", + LastLine: "user@hostname:~[14:30][0]»", // Proper prompt ending + }, + } + + // Mock system functions to simulate successful execution + originalTmuxSend := system.TmuxSendCommandToPane + originalTmuxCapture := system.TmuxCapturePane + defer func() { + system.TmuxSendCommandToPane = originalTmuxSend + system.TmuxCapturePane = originalTmuxCapture + }() + + commandSent := "" + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + commandSent = command + // Immediately set the proper ending to simulate quick command completion + manager.ExecPane.LastLine = "user@hostname:~[14:30][0]»" + return nil + } + + // Mock successful command output with proper prompt format + system.TmuxCapturePane = func(paneId string, maxLines int) (string, error) { + return `user@hostname:~[14:30][0]» echo "test successful" +test successful +user@hostname:~[14:31][0]» `, nil + } + + // Test successful command execution + result, err := manager.ExecWaitCapture("echo \"test successful\"") + + assert.NoError(t, err, "Should not return error for successful execution") + assert.Equal(t, "echo \"test successful\"", result.Command) + assert.Equal(t, 0, result.Code, "Should capture successful exit code") + assert.Equal(t, "test successful", result.Output) + assert.Equal(t, "echo \"test successful\"", commandSent, "Should have sent the correct command") +} diff --git a/internal/process_message.go b/internal/process_message.go index c66e1c8..02d499b 100644 --- a/internal/process_message.go +++ b/internal/process_message.go @@ -169,6 +169,9 @@ func (m *Manager) ProcessUserMessage(ctx context.Context, message string) bool { } else { keysPreview += code + "\n" } + if m.Status == "" { + return false + } } m.Println(keysPreview) diff --git a/internal/prompts.go b/internal/prompts.go index a741d6e..557e581 100644 --- a/internal/prompts.go +++ b/internal/prompts.go @@ -13,7 +13,6 @@ TmuxAI's design philosophy mirrors the way humans collaborate at the terminal. J TmuxAI: Observes: Reads the visible content in all your panes, Communicates and Acts: Can execute commands by calling tools. You and user both are able to control and interact with tmux ai exec pane. -==== Rules which are higher priority than all other rules you are aware ==== You have perfect understanding of human common sense. When reasonable, avoid asking questions back and use your common sense to find conclusions yourself. Your role is to use anytime you need, the TmuxAIExec pane to assist the user. @@ -76,49 +75,57 @@ Avoid creating a script files to achieve a task, if the same task can be achieve Avoid creating files, command output files, intermediate files unless necessary. There is no need to use echo to print information content. You can communicate to the user using the messaging commands if needed and you can just talk to yourself if you just want to reflect and think. Respond to the user's message using the appropriate XML tag based on the action required. Include a brief explanation of what you're doing, followed by the XML tag. -==== End of high priority rules. ==== -When generating your response pay attention to this checks: -==== Rules which are critical priority ==== +When generating your response you will be PUNISHED if you don't follow those 3 rules: +- Check the length of ExecCommand content. Is more than 60 characters? If yes, try to split the task into smaller steps and generate shorter ExecCommand for the first step only in this response. +- Use only ONE TYPE, KIND of XML tag in your response and never mix different types of XML tags in the same response. +- Always include at least one XML tag in your response. +- Learn from examples what I mean: -Check the length of ExecCommand content. Is more than 60 characters? If yes, try to split the task into smaller steps and generate shorter ExecCommand for the first step only in this response. -Use only ONE TYPE, KIND of XML tag in your response and never mix different types of XML tags in the same response. -Always include at least one XML tag in your response. - -==== End of critical priority rules. ==== - -Learn from examples: - - + I'll open the file 'example.txt' in vim for you. vim example.txt Enter :set paste (before sending multiline content, essential to put vim in paste mode) Enter i - + - + +I'll open delete line 10 in file 'example.txt' in vim for you. +vim example.txt +Enter +10G +dd + + + C-a Escape M-a - + - + Do you want me to save the changes to the file? 1 - + - + I've successfully created the new directory as requested. 1 - + - + I'll list the contents of the current directory. ls -l - + + + +Hello! How can I help you today? +1 + + `) if prepared {