Skip to content

Commit 9bfb0ed

Browse files
committed
New run prompt
Much more fully featured readline-like implementation which advanced keyboard usage. Signed-off-by: Eric Curtin <[email protected]>
1 parent 38a800e commit 9bfb0ed

File tree

17 files changed

+1417
-36
lines changed

17 files changed

+1417
-36
lines changed

cmd/cli/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ Run `./model --help` to see all commands and options.
5252
Or enter chat mode:
5353
```bash
5454
./model run llama.cpp
55-
Interactive chat mode started. Type '/bye' to exit.
5655
> """
5756
Tell me a joke.
5857
"""

cmd/cli/commands/run.go

Lines changed: 168 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/charmbracelet/glamour"
1212
"github.com/docker/model-runner/cmd/cli/commands/completion"
1313
"github.com/docker/model-runner/cmd/cli/desktop"
14+
"github.com/docker/model-runner/cmd/cli/readline"
1415
"github.com/fatih/color"
1516
"github.com/spf13/cobra"
1617
"golang.org/x/term"
@@ -81,6 +82,167 @@ func readMultilineInput(cmd *cobra.Command, scanner *bufio.Scanner) (string, err
8182
return multilineInput.String(), nil
8283
}
8384

85+
// generateInteractiveWithReadline provides an enhanced interactive mode with readline support
86+
func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.Client, backend, model, apiKey string) error {
87+
usage := func() {
88+
fmt.Fprintln(os.Stderr, "Available Commands:")
89+
fmt.Fprintln(os.Stderr, " /bye Exit")
90+
fmt.Fprintln(os.Stderr, " /?, /help Help for a command")
91+
fmt.Fprintln(os.Stderr, " /? shortcuts Help for keyboard shortcuts")
92+
fmt.Fprintln(os.Stderr, "")
93+
fmt.Fprintln(os.Stderr, `Use """ to begin a multi-line message.`)
94+
fmt.Fprintln(os.Stderr, "")
95+
}
96+
97+
usageShortcuts := func() {
98+
fmt.Fprintln(os.Stderr, "Available keyboard shortcuts:")
99+
fmt.Fprintln(os.Stderr, " Ctrl + a Move to the beginning of the line (Home)")
100+
fmt.Fprintln(os.Stderr, " Ctrl + e Move to the end of the line (End)")
101+
fmt.Fprintln(os.Stderr, " Alt + b Move back (left) one word")
102+
fmt.Fprintln(os.Stderr, " Alt + f Move forward (right) one word")
103+
fmt.Fprintln(os.Stderr, " Ctrl + k Delete the sentence after the cursor")
104+
fmt.Fprintln(os.Stderr, " Ctrl + u Delete the sentence before the cursor")
105+
fmt.Fprintln(os.Stderr, " Ctrl + w Delete the word before the cursor")
106+
fmt.Fprintln(os.Stderr, "")
107+
fmt.Fprintln(os.Stderr, " Ctrl + l Clear the screen")
108+
fmt.Fprintln(os.Stderr, " Ctrl + c Stop the model from responding")
109+
fmt.Fprintln(os.Stderr, " Ctrl + d Exit (/bye)")
110+
fmt.Fprintln(os.Stderr, "")
111+
}
112+
113+
scanner, err := readline.New(readline.Prompt{
114+
Prompt: "> ",
115+
AltPrompt: "... ",
116+
Placeholder: "Send a message (/? for help)",
117+
AltPlaceholder: `Use """ to end multi-line input`,
118+
})
119+
if err != nil {
120+
// Fall back to basic input mode if readline initialization fails
121+
return generateInteractiveBasic(cmd, desktopClient, backend, model, apiKey)
122+
}
123+
124+
// Disable history if the environment variable is set
125+
if os.Getenv("DOCKER_MODEL_NOHISTORY") != "" {
126+
scanner.HistoryDisable()
127+
}
128+
129+
fmt.Print(readline.StartBracketedPaste)
130+
defer fmt.Printf(readline.EndBracketedPaste)
131+
132+
var sb strings.Builder
133+
var multiline bool
134+
135+
for {
136+
line, err := scanner.Readline()
137+
switch {
138+
case errors.Is(err, io.EOF):
139+
fmt.Println()
140+
return nil
141+
case errors.Is(err, readline.ErrInterrupt):
142+
if line == "" {
143+
fmt.Println("\nUse Ctrl + d or /bye to exit.")
144+
}
145+
146+
scanner.Prompt.UseAlt = false
147+
sb.Reset()
148+
149+
continue
150+
case err != nil:
151+
return err
152+
}
153+
154+
switch {
155+
case multiline:
156+
// check if there's a multiline terminating string
157+
before, ok := strings.CutSuffix(line, `"""`)
158+
sb.WriteString(before)
159+
if !ok {
160+
fmt.Fprintln(&sb)
161+
continue
162+
}
163+
164+
multiline = false
165+
scanner.Prompt.UseAlt = false
166+
case strings.HasPrefix(line, `"""`):
167+
line := strings.TrimPrefix(line, `"""`)
168+
line, ok := strings.CutSuffix(line, `"""`)
169+
sb.WriteString(line)
170+
if !ok {
171+
// no multiline terminating string; need more input
172+
fmt.Fprintln(&sb)
173+
multiline = true
174+
scanner.Prompt.UseAlt = true
175+
}
176+
case scanner.Pasting:
177+
fmt.Fprintln(&sb, line)
178+
continue
179+
case strings.HasPrefix(line, "/help"), strings.HasPrefix(line, "/?"):
180+
args := strings.Fields(line)
181+
if len(args) > 1 {
182+
switch args[1] {
183+
case "shortcut", "shortcuts":
184+
usageShortcuts()
185+
default:
186+
usage()
187+
}
188+
} else {
189+
usage()
190+
}
191+
continue
192+
case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"):
193+
return nil
194+
case strings.HasPrefix(line, "/"):
195+
fmt.Printf("Unknown command '%s'. Type /? for help\n", strings.Fields(line)[0])
196+
continue
197+
default:
198+
sb.WriteString(line)
199+
}
200+
201+
if sb.Len() > 0 && !multiline {
202+
userInput := sb.String()
203+
204+
if err := chatWithMarkdown(cmd, desktopClient, backend, model, userInput, apiKey); err != nil {
205+
cmd.PrintErr(handleClientError(err, "Failed to generate a response"))
206+
sb.Reset()
207+
continue
208+
}
209+
210+
cmd.Println()
211+
sb.Reset()
212+
}
213+
}
214+
}
215+
216+
// generateInteractiveBasic provides a basic interactive mode (fallback)
217+
func generateInteractiveBasic(cmd *cobra.Command, desktopClient *desktop.Client, backend, model, apiKey string) error {
218+
scanner := bufio.NewScanner(os.Stdin)
219+
for {
220+
userInput, err := readMultilineInput(cmd, scanner)
221+
if err != nil {
222+
if err.Error() == "EOF" {
223+
break
224+
}
225+
return fmt.Errorf("Error reading input: %v", err)
226+
}
227+
228+
if strings.ToLower(strings.TrimSpace(userInput)) == "/bye" {
229+
break
230+
}
231+
232+
if strings.TrimSpace(userInput) == "" {
233+
continue
234+
}
235+
236+
if err := chatWithMarkdown(cmd, desktopClient, backend, model, userInput, apiKey); err != nil {
237+
cmd.PrintErr(handleClientError(err, "Failed to generate a response"))
238+
continue
239+
}
240+
241+
cmd.Println()
242+
}
243+
return nil
244+
}
245+
84246
var (
85247
markdownRenderer *glamour.TermRenderer
86248
lastWidth int
@@ -389,36 +551,13 @@ func newRunCmd() *cobra.Command {
389551
return nil
390552
}
391553

392-
scanner := bufio.NewScanner(os.Stdin)
393-
cmd.Println("Interactive chat mode started. Type '/bye' to exit.")
394-
395-
for {
396-
userInput, err := readMultilineInput(cmd, scanner)
397-
if err != nil {
398-
if err.Error() == "EOF" {
399-
cmd.Println("\nChat session ended.")
400-
break
401-
}
402-
return fmt.Errorf("Error reading input: %v", err)
403-
}
404-
405-
if strings.ToLower(strings.TrimSpace(userInput)) == "/bye" {
406-
cmd.Println("Chat session ended.")
407-
break
408-
}
409-
410-
if strings.TrimSpace(userInput) == "" {
411-
continue
412-
}
413-
414-
if err := chatWithMarkdown(cmd, desktopClient, backend, model, userInput, apiKey); err != nil {
415-
cmd.PrintErr(handleClientError(err, "Failed to generate a response"))
416-
continue
417-
}
418-
419-
cmd.Println()
554+
// Use enhanced readline-based interactive mode when terminal is available
555+
if term.IsTerminal(int(os.Stdin.Fd())) {
556+
return generateInteractiveWithReadline(cmd, desktopClient, backend, model, apiKey)
420557
}
421-
return nil
558+
559+
// Fall back to basic mode if not a terminal
560+
return generateInteractiveBasic(cmd, desktopClient, backend, model, apiKey)
422561
},
423562
ValidArgsFunction: completion.ModelNames(getDesktopClient, 1),
424563
}

cmd/cli/docs/reference/docker_model_run.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,9 @@ examples: |-
7272
Output:
7373
7474
```console
75-
Interactive chat mode started. Type '/bye' to exit.
7675
> Hi
7776
Hi there! It's SmolLM, AI assistant. How can I help you today?
7877
> /bye
79-
Chat session ended.
8078
```
8179
deprecated: false
8280
hidden: false

cmd/cli/docs/reference/model_run.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ docker model run ai/smollm2
4545
Output:
4646

4747
```console
48-
Interactive chat mode started. Type '/bye' to exit.
4948
> Hi
5049
Hi there! It's SmolLM, AI assistant. How can I help you today?
5150
> /bye
52-
Chat session ended.
5351
```

cmd/cli/go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ require (
1111
github.com/docker/go-connections v0.5.0
1212
github.com/docker/go-units v0.5.0
1313
github.com/docker/model-runner v0.0.0
14+
github.com/emirpasic/gods/v2 v2.0.0-alpha
1415
github.com/fatih/color v1.18.0
1516
github.com/google/go-containerregistry v0.20.6
1617
github.com/mattn/go-isatty v0.0.20
18+
github.com/mattn/go-runewidth v0.0.16
1719
github.com/nxadm/tail v1.4.8
1820
github.com/olekukonko/tablewriter v0.0.5
1921
github.com/pkg/errors v0.9.1
@@ -23,6 +25,7 @@ require (
2325
go.opentelemetry.io/otel v1.37.0
2426
go.uber.org/mock v0.5.0
2527
golang.org/x/sync v0.15.0
28+
golang.org/x/sys v0.35.0
2629
golang.org/x/term v0.32.0
2730
)
2831

@@ -76,7 +79,6 @@ require (
7679
github.com/kolesnikovae/go-winjob v1.0.0 // indirect
7780
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
7881
github.com/mattn/go-colorable v0.1.13 // indirect
79-
github.com/mattn/go-runewidth v0.0.16 // indirect
8082
github.com/mattn/go-shellwords v1.0.12 // indirect
8183
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
8284
github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -120,7 +122,6 @@ require (
120122
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
121123
golang.org/x/mod v0.25.0 // indirect
122124
golang.org/x/net v0.41.0 // indirect
123-
golang.org/x/sys v0.35.0 // indirect
124125
golang.org/x/text v0.26.0 // indirect
125126
golang.org/x/time v0.9.0 // indirect
126127
golang.org/x/tools v0.34.0 // indirect

cmd/cli/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn
115115
github.com/elastic/go-sysinfo v1.15.3/go.mod h1:K/cNrqYTDrSoMh2oDkYEMS2+a72GRxMvNP+GC+vRIlo=
116116
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
117117
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
118+
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
119+
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
118120
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
119121
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
120122
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=

0 commit comments

Comments
 (0)