diff --git a/commands/run.go b/commands/run.go index cbe4f060..2cb91900 100644 --- a/commands/run.go +++ b/commands/run.go @@ -12,6 +12,71 @@ import ( "github.com/spf13/cobra" ) +// readMultilineInput reads input from stdin, supporting both single-line and multiline input. +// For multiline input, it detects triple-quoted strings and shows continuation prompts. +func readMultilineInput(cmd *cobra.Command, scanner *bufio.Scanner) (string, error) { + cmd.Print("> ") + + if !scanner.Scan() { + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error reading input: %v", err) + } + return "", fmt.Errorf("EOF") + } + + line := scanner.Text() + + // Check if this is the start of a multiline input (triple quotes) + tripleQuoteStart := "" + if strings.HasPrefix(line, `"""`) { + tripleQuoteStart = `"""` + } else if strings.HasPrefix(line, "'''") { + tripleQuoteStart = "'''" + } + + // If no triple quotes, return a single line + if tripleQuoteStart == "" { + return line, nil + } + + // Check if the triple quotes are closed on the same line + restOfLine := line[3:] + if strings.HasSuffix(restOfLine, tripleQuoteStart) && len(restOfLine) >= 3 { + // Complete multiline string on single line + return line, nil + } + + // Start collecting multiline input + var multilineInput strings.Builder + multilineInput.WriteString(line) + multilineInput.WriteString("\n") + + // Continue reading lines until we find the closing triple quotes + for { + cmd.Print("... ") + + if !scanner.Scan() { + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error reading input: %v", err) + } + return "", fmt.Errorf("unclosed multiline input (EOF)") + } + + line = scanner.Text() + multilineInput.WriteString(line) + + // Check if this line contains the closing triple quotes + if strings.Contains(line, tripleQuoteStart) { + // Found closing quotes, we're done + break + } + + multilineInput.WriteString("\n") + } + + return multilineInput.String(), nil +} + func newRunCmd() *cobra.Command { var debug bool @@ -58,32 +123,32 @@ func newRunCmd() *cobra.Command { scanner := bufio.NewScanner(os.Stdin) cmd.Println("Interactive chat mode started. Type '/bye' to exit.") - cmd.Print("> ") - for scanner.Scan() { - userInput := scanner.Text() + for { + userInput, err := readMultilineInput(cmd, scanner) + if err != nil { + if err.Error() == "EOF" { + cmd.Println("\nChat session ended.") + break + } + return fmt.Errorf("Error reading input: %v", err) + } - if strings.ToLower(userInput) == "/bye" { + if strings.ToLower(strings.TrimSpace(userInput)) == "/bye" { cmd.Println("Chat session ended.") break } if strings.TrimSpace(userInput) == "" { - cmd.Print("> ") continue } if err := desktopClient.Chat(model, userInput); err != nil { cmd.PrintErr(handleClientError(err, "Failed to generate a response")) - cmd.Print("> ") continue } - cmd.Print("\n> ") - } - - if err := scanner.Err(); err != nil { - return fmt.Errorf("Error reading input: %v\n", err) + cmd.Println() } return nil }, diff --git a/commands/run_test.go b/commands/run_test.go new file mode 100644 index 00000000..f8674822 --- /dev/null +++ b/commands/run_test.go @@ -0,0 +1,115 @@ +package commands + +import ( + "bufio" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestReadMultilineInput(t *testing.T) { + tests := []struct { + name string + input string + expected string + wantErr bool + }{ + { + name: "single line input", + input: "hello world", + expected: "hello world", + wantErr: false, + }, + { + name: "single line with triple quotes", + input: `"""hello world"""`, + expected: `"""hello world"""`, + wantErr: false, + }, + { + name: "multiline input with double quotes", + input: `"""tell +me +a +joke"""`, + expected: `"""tell +me +a +joke"""`, + wantErr: false, + }, + { + name: "multiline input with single quotes", + input: `'''tell +me +a +joke'''`, + expected: `'''tell +me +a +joke'''`, + wantErr: false, + }, + { + name: "empty input", + input: "", + expected: "", + wantErr: true, // EOF should be treated as an error + }, + { + name: "multiline with empty lines", + input: `"""first line + +third line"""`, + expected: `"""first line + +third line"""`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock command for testing + cmd := &cobra.Command{} + + // Create a scanner from the test input + scanner := bufio.NewScanner(strings.NewReader(tt.input)) + + // Capture output to avoid printing during tests + var output strings.Builder + cmd.SetOut(&output) + + result, err := readMultilineInput(cmd, scanner) + + if (err != nil) != tt.wantErr { + t.Errorf("readMultilineInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if result != tt.expected { + t.Errorf("readMultilineInput() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestReadMultilineInputUnclosed(t *testing.T) { + // Test unclosed multiline input (should return error) + input := `"""unclosed multiline` + cmd := &cobra.Command{} + var output strings.Builder + cmd.SetOut(&output) + + scanner := bufio.NewScanner(strings.NewReader(input)) + + _, err := readMultilineInput(cmd, scanner) + if err == nil { + t.Error("readMultilineInput() should return error for unclosed multiline input") + } + + if !strings.Contains(err.Error(), "unclosed multiline input") { + t.Errorf("readMultilineInput() error should mention unclosed multiline input, got: %v", err) + } +}