Skip to content
Merged
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
1 change: 0 additions & 1 deletion cmd/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ Run `./model --help` to see all commands and options.
Or enter chat mode:
```bash
./model run llama.cpp
Interactive chat mode started. Type '/bye' to exit.
> """
Tell me a joke.
"""
Expand Down
197 changes: 168 additions & 29 deletions cmd/cli/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/charmbracelet/glamour"
"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/cmd/cli/readline"
"github.com/fatih/color"
"github.com/spf13/cobra"
"golang.org/x/term"
Expand Down Expand Up @@ -81,6 +82,167 @@ func readMultilineInput(cmd *cobra.Command, scanner *bufio.Scanner) (string, err
return multilineInput.String(), nil
}

// generateInteractiveWithReadline provides an enhanced interactive mode with readline support
func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.Client, backend, model, apiKey string) error {
usage := func() {
fmt.Fprintln(os.Stderr, "Available Commands:")
fmt.Fprintln(os.Stderr, " /bye Exit")
fmt.Fprintln(os.Stderr, " /?, /help Help for a command")
fmt.Fprintln(os.Stderr, " /? shortcuts Help for keyboard shortcuts")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, `Use """ to begin a multi-line message.`)
fmt.Fprintln(os.Stderr, "")
}

usageShortcuts := func() {
fmt.Fprintln(os.Stderr, "Available keyboard shortcuts:")
fmt.Fprintln(os.Stderr, " Ctrl + a Move to the beginning of the line (Home)")
fmt.Fprintln(os.Stderr, " Ctrl + e Move to the end of the line (End)")
fmt.Fprintln(os.Stderr, " Alt + b Move back (left) one word")
fmt.Fprintln(os.Stderr, " Alt + f Move forward (right) one word")
fmt.Fprintln(os.Stderr, " Ctrl + k Delete the sentence after the cursor")
fmt.Fprintln(os.Stderr, " Ctrl + u Delete the sentence before the cursor")
fmt.Fprintln(os.Stderr, " Ctrl + w Delete the word before the cursor")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, " Ctrl + l Clear the screen")
fmt.Fprintln(os.Stderr, " Ctrl + c Stop the model from responding")
fmt.Fprintln(os.Stderr, " Ctrl + d Exit (/bye)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Fprintln(os.Stderr, " Ctrl + d Exit (/bye)")
fmt.Fprintln(os.Stderr, " Ctrl + d Delete the character under the cursor. If the input line is empty, exit the chat (/bye)")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried and it's actually the same as /bye

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense! Ctrl-d is exit prompt in all shells, I guess you can't type /bye mid-prompt which this is highlighting.

fmt.Fprintln(os.Stderr, "")
}

scanner, err := readline.New(readline.Prompt{
Prompt: "> ",
AltPrompt: "... ",
Placeholder: "Send a message (/? for help)",
AltPlaceholder: `Use """ to end multi-line input`,
})
if err != nil {
// Fall back to basic input mode if readline initialization fails
return generateInteractiveBasic(cmd, desktopClient, backend, model, apiKey)
}

// Disable history if the environment variable is set
if os.Getenv("DOCKER_MODEL_NOHISTORY") != "" {
scanner.HistoryDisable()
}

fmt.Print(readline.StartBracketedPaste)
defer fmt.Printf(readline.EndBracketedPaste)

var sb strings.Builder
var multiline bool

for {
line, err := scanner.Readline()
switch {
case errors.Is(err, io.EOF):
fmt.Println()
return nil
case errors.Is(err, readline.ErrInterrupt):
if line == "" {
fmt.Println("\nUse Ctrl + d or /bye to exit.")
}

scanner.Prompt.UseAlt = false
sb.Reset()

continue
case err != nil:
return err
}

switch {
case multiline:
// check if there's a multiline terminating string
before, ok := strings.CutSuffix(line, `"""`)
sb.WriteString(before)
if !ok {
fmt.Fprintln(&sb)
continue
}

multiline = false
scanner.Prompt.UseAlt = false
case strings.HasPrefix(line, `"""`):
line := strings.TrimPrefix(line, `"""`)
line, ok := strings.CutSuffix(line, `"""`)
sb.WriteString(line)
if !ok {
// no multiline terminating string; need more input
fmt.Fprintln(&sb)
multiline = true
scanner.Prompt.UseAlt = true
}
case scanner.Pasting:
fmt.Fprintln(&sb, line)
continue
case strings.HasPrefix(line, "/help"), strings.HasPrefix(line, "/?"):
args := strings.Fields(line)
if len(args) > 1 {
switch args[1] {
case "shortcut", "shortcuts":
usageShortcuts()
default:
usage()
}
} else {
usage()
}
continue
case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"):
return nil
case strings.HasPrefix(line, "/"):
fmt.Printf("Unknown command '%s'. Type /? for help\n", strings.Fields(line)[0])
continue
default:
sb.WriteString(line)
}

if sb.Len() > 0 && !multiline {
userInput := sb.String()

if err := chatWithMarkdown(cmd, desktopClient, backend, model, userInput, apiKey); err != nil {
cmd.PrintErr(handleClientError(err, "Failed to generate a response"))
sb.Reset()
continue
}

cmd.Println()
sb.Reset()
}
}
}

// generateInteractiveBasic provides a basic interactive mode (fallback)
func generateInteractiveBasic(cmd *cobra.Command, desktopClient *desktop.Client, backend, model, apiKey string) error {
scanner := bufio.NewScanner(os.Stdin)
for {
userInput, err := readMultilineInput(cmd, scanner)
if err != nil {
if err.Error() == "EOF" {
break
}
return fmt.Errorf("Error reading input: %v", err)
}

if strings.ToLower(strings.TrimSpace(userInput)) == "/bye" {
break
}

if strings.TrimSpace(userInput) == "" {
continue
}

if err := chatWithMarkdown(cmd, desktopClient, backend, model, userInput, apiKey); err != nil {
cmd.PrintErr(handleClientError(err, "Failed to generate a response"))
continue
}

cmd.Println()
}
return nil
}

var (
markdownRenderer *glamour.TermRenderer
lastWidth int
Expand Down Expand Up @@ -389,36 +551,13 @@ func newRunCmd() *cobra.Command {
return nil
}

scanner := bufio.NewScanner(os.Stdin)
cmd.Println("Interactive chat mode started. Type '/bye' to exit.")

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(strings.TrimSpace(userInput)) == "/bye" {
cmd.Println("Chat session ended.")
break
}

if strings.TrimSpace(userInput) == "" {
continue
}

if err := chatWithMarkdown(cmd, desktopClient, backend, model, userInput, apiKey); err != nil {
cmd.PrintErr(handleClientError(err, "Failed to generate a response"))
continue
}

cmd.Println()
// Use enhanced readline-based interactive mode when terminal is available
if term.IsTerminal(int(os.Stdin.Fd())) {
return generateInteractiveWithReadline(cmd, desktopClient, backend, model, apiKey)
}
return nil

// Fall back to basic mode if not a terminal
return generateInteractiveBasic(cmd, desktopClient, backend, model, apiKey)
},
ValidArgsFunction: completion.ModelNames(getDesktopClient, 1),
}
Expand Down
2 changes: 0 additions & 2 deletions cmd/cli/docs/reference/docker_model_run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,9 @@ examples: |-
Output:

```console
Interactive chat mode started. Type '/bye' to exit.
> Hi
Hi there! It's SmolLM, AI assistant. How can I help you today?
> /bye
Chat session ended.
```
deprecated: false
hidden: false
Expand Down
2 changes: 0 additions & 2 deletions cmd/cli/docs/reference/model_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ docker model run ai/smollm2
Output:

```console
Interactive chat mode started. Type '/bye' to exit.
> Hi
Hi there! It's SmolLM, AI assistant. How can I help you today?
> /bye
Chat session ended.
```
5 changes: 3 additions & 2 deletions cmd/cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ require (
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/docker/model-runner v0.0.0
github.com/emirpasic/gods/v2 v2.0.0-alpha
github.com/fatih/color v1.18.0
github.com/google/go-containerregistry v0.20.6
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.16
github.com/nxadm/tail v1.4.8
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
Expand All @@ -23,6 +25,7 @@ require (
go.opentelemetry.io/otel v1.37.0
go.uber.org/mock v0.5.0
golang.org/x/sync v0.15.0
golang.org/x/sys v0.35.0
golang.org/x/term v0.32.0
)

Expand Down Expand Up @@ -76,7 +79,6 @@ require (
github.com/kolesnikovae/go-winjob v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
Expand Down Expand Up @@ -120,7 +122,6 @@ require (
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.34.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions cmd/cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn
github.com/elastic/go-sysinfo v1.15.3/go.mod h1:K/cNrqYTDrSoMh2oDkYEMS2+a72GRxMvNP+GC+vRIlo=
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
Expand Down
Loading
Loading