diff --git a/cmd/interactive.go b/cmd/interactive.go index 9c4e32a2e..aad3eccfc 100644 --- a/cmd/interactive.go +++ b/cmd/interactive.go @@ -116,7 +116,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error { Prompt: ">>> ", AltPrompt: "... ", Placeholder: "Send a message (/? for help)", - AltPlaceholder: `Use """ to end multi-line input`, + AltPlaceholder: "Press Enter to send", }) if err != nil { return err diff --git a/readline/readline.go b/readline/readline.go index c12327472..e18a25648 100644 --- a/readline/readline.go +++ b/readline/readline.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" ) type Prompt struct { @@ -36,10 +37,11 @@ type Terminal struct { } type Instance struct { - Prompt *Prompt - Terminal *Terminal - History *History - Pasting bool + Prompt *Prompt + Terminal *Terminal + History *History + Pasting bool + pastedLines []string } func New(prompt Prompt) (*Instance, error) { @@ -174,6 +176,8 @@ func (i *Instance) Readline() (string, error) { case CharEsc: esc = true case CharInterrupt: + i.pastedLines = nil + i.Prompt.UseAlt = false return "", ErrInterrupt case CharPrev: i.historyPrev(buf, ¤tLineBuf) @@ -188,7 +192,23 @@ func (i *Instance) Readline() (string, error) { case CharForward: buf.MoveRight() case CharBackspace, CharCtrlH: - buf.Remove() + if buf.IsEmpty() && len(i.pastedLines) > 0 { + lastIdx := len(i.pastedLines) - 1 + prevLine := i.pastedLines[lastIdx] + i.pastedLines = i.pastedLines[:lastIdx] + fmt.Print(CursorBOL + ClearToEOL + CursorUp + CursorBOL + ClearToEOL) + if len(i.pastedLines) == 0 { + fmt.Print(i.Prompt.Prompt) + i.Prompt.UseAlt = false + } else { + fmt.Print(i.Prompt.AltPrompt) + } + for _, r := range prevLine { + buf.Add(r) + } + } else { + buf.Remove() + } case CharTab: // todo: convert back to real tabs for range 8 { @@ -211,13 +231,28 @@ func (i *Instance) Readline() (string, error) { case CharCtrlZ: fd := os.Stdin.Fd() return handleCharCtrlZ(fd, i.Terminal.termios) - case CharEnter, CharCtrlJ: + case CharCtrlJ: + i.pastedLines = append(i.pastedLines, buf.String()) + buf.Buf.Clear() + buf.Pos = 0 + buf.DisplayPos = 0 + buf.LineHasSpace.Clear() + fmt.Println() + fmt.Print(i.Prompt.AltPrompt) + i.Prompt.UseAlt = true + continue + case CharEnter: output := buf.String() + if len(i.pastedLines) > 0 { + output = strings.Join(i.pastedLines, "\n") + "\n" + output + i.pastedLines = nil + } if output != "" { i.History.Add(output) } buf.MoveToEnd() fmt.Println() + i.Prompt.UseAlt = false return output, nil default: diff --git a/x/cmd/run.go b/x/cmd/run.go index f32254ebe..1bd452cd8 100644 --- a/x/cmd/run.go +++ b/x/cmd/run.go @@ -25,14 +25,6 @@ import ( "github.com/ollama/ollama/x/tools" ) -// MultilineState tracks the state of multiline input -type MultilineState int - -const ( - MultilineNone MultilineState = iota - MultilineSystem -) - // Tool output capping constants const ( // localModelTokenLimit is the token limit for local models (smaller context). @@ -656,7 +648,7 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op Prompt: ">>> ", AltPrompt: "... ", Placeholder: "Send a message (/? for help)", - AltPlaceholder: `Use """ to end multi-line input`, + AltPlaceholder: "Press Enter to send", }) if err != nil { return err @@ -707,7 +699,6 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op var sb strings.Builder var format string var system string - var multiline MultilineState = MultilineNone for { line, err := scanner.Readline() @@ -721,37 +712,12 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op } scanner.Prompt.UseAlt = false sb.Reset() - multiline = MultilineNone continue case err != nil: return err } switch { - case multiline != MultilineNone: - // check if there's a multiline terminating string - before, ok := strings.CutSuffix(line, `"""`) - sb.WriteString(before) - if !ok { - fmt.Fprintln(&sb) - continue - } - - switch multiline { - case MultilineSystem: - system = sb.String() - newMessage := api.Message{Role: "system", Content: system} - if len(messages) > 0 && messages[len(messages)-1].Role == "system" { - messages[len(messages)-1] = newMessage - } else { - messages = append(messages, newMessage) - } - fmt.Println("Set system message.") - sb.Reset() - } - - multiline = MultilineNone - scanner.Prompt.UseAlt = false case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"): return nil case strings.HasPrefix(line, "/clear"): @@ -860,41 +826,18 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op options[args[2]] = fp[args[2]] case "system": if len(args) < 3 { - fmt.Println("Usage: /set system or /set system \"\"\"\"\"\"") + fmt.Println("Usage: /set system ") continue } - multiline = MultilineSystem - - line := strings.Join(args[2:], " ") - line, ok := strings.CutPrefix(line, `"""`) - if !ok { - multiline = MultilineNone - } else { - // only cut suffix if the line is multiline - line, ok = strings.CutSuffix(line, `"""`) - if ok { - multiline = MultilineNone - } - } - - sb.WriteString(line) - if multiline != MultilineNone { - scanner.Prompt.UseAlt = true - continue - } - - system = sb.String() - newMessage := api.Message{Role: "system", Content: sb.String()} - // Check if the slice is not empty and the last message is from 'system' + system = strings.Join(args[2:], " ") + newMessage := api.Message{Role: "system", Content: system} if len(messages) > 0 && messages[len(messages)-1].Role == "system" { - // Replace the last message messages[len(messages)-1] = newMessage } else { messages = append(messages, newMessage) } fmt.Println("Set system message.") - sb.Reset() continue default: fmt.Printf("Unknown command '/set %s'. Type /? for help\n", args[1]) @@ -1081,7 +1024,7 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op sb.WriteString(line) } - if sb.Len() > 0 && multiline == MultilineNone { + if sb.Len() > 0 { newMessage := api.Message{Role: "user", Content: sb.String()} messages = append(messages, newMessage)