@@ -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 ("\n Use 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+
84246var (
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 ("\n Chat 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 }
0 commit comments