11package cmd
22
33import (
4- "bufio"
54 "context"
65 "fmt"
76 "os"
8- "os/signal"
97 "strings"
108 "time"
119
@@ -15,7 +13,7 @@ import (
1513 "docsgpt-cli/internal/display"
1614 "docsgpt-cli/internal/tools"
1715
18- "github.com/fatih/color "
16+ prompt "github.com/c-bata/go-prompt "
1917 "github.com/spf13/cobra"
2018)
2119
@@ -27,7 +25,10 @@ var chatCmd = &cobra.Command{
2725Special commands:
2826 /quit - Exit the chat session
2927 /clear - Clear conversation history
30- /copy - Copy the last code block to clipboard` ,
28+ /copy - Copy the last code block to clipboard
29+ /think - Toggle reasoning visibility
30+
31+ Type "/" to see available commands with live autocomplete.` ,
3132 RunE : func (cmd * cobra.Command , args []string ) error {
3233 cfg , err := config .Load ()
3334 if err != nil {
@@ -42,10 +43,11 @@ Special commands:
4243 baseURL := cfg .ResolveURL (globalURL )
4344 client := api .NewClient (baseURL , apiKey )
4445
45- green := color .New (color .FgGreen ).SprintFunc ()
46- fmt .Printf (green ("Connected with key: %s\n " ), keyName )
47- fmt .Printf (green ("Server: %s\n " ), baseURL )
48- fmt .Println ("Type /quit to exit, /clear to reset history, /copy to copy last code block." )
46+ cwd , _ := os .Getwd ()
47+ fmt .Println (display .RenderHeader (keyName , baseURL , cwd ))
48+ if hints := display .RenderHints ("chat" ); hints != "" {
49+ fmt .Println (hints )
50+ }
4951 fmt .Println ()
5052
5153 var history []api.Message
@@ -65,96 +67,138 @@ Special commands:
6567 },
6668}
6769
68- func runChatLoop (client * api.Client , history []api.Message ) error {
69- reader := bufio .NewReader (os .Stdin )
70- var lastAnswer string
71-
72- // Handle Ctrl+C gracefully
73- sigCh := make (chan os.Signal , 1 )
74- signal .Notify (sigCh , os .Interrupt )
75- go func () {
76- <- sigCh
77- fmt .Println ("\n Goodbye!" )
70+ // chatSession holds the mutable state for an interactive chat.
71+ type chatSession struct {
72+ client * api.Client
73+ history []api.Message
74+ lastAnswer string
75+ showReasoning bool
76+ toolDefs []api.Tool
77+ timeout time.Duration
78+ }
79+
80+ func (s * chatSession ) executor (input string ) {
81+ input = strings .TrimSpace (input )
82+ if input == "" {
83+ return
84+ }
85+
86+ switch input {
87+ case "/quit" :
88+ fmt .Println ("Goodbye!" )
7889 os .Exit (0 )
79- }()
90+ case "/clear" :
91+ var newHistory []api.Message
92+ if len (s .history ) > 0 && s .history [0 ].Role == "system" {
93+ newHistory = append (newHistory , s .history [0 ])
94+ }
95+ s .history = newHistory
96+ s .lastAnswer = ""
97+ fmt .Println ("History cleared." )
98+ return
99+ case "/copy" :
100+ if s .lastAnswer == "" {
101+ printError ("No previous response to copy from." )
102+ return
103+ }
104+ command := extractCommand (s .lastAnswer )
105+ if command != "" {
106+ copyToClipboard (command )
107+ } else {
108+ printError ("No code block found in last response." )
109+ }
110+ return
111+ case "/think" :
112+ s .showReasoning = ! s .showReasoning
113+ if s .showReasoning {
114+ fmt .Println (display .Muted ("Reasoning: visible" ))
115+ } else {
116+ fmt .Println (display .Muted ("Reasoning: hidden" ))
117+ }
118+ return
119+ }
80120
81- toolDefs := tools . ToolDefinitions ( )
82- timeout := time . Duration ( globalTimeout ) * time . Second
121+ s . history = append ( s . history , api. Message { Role : "user" , Content : input } )
122+ ctx := context . Background ()
83123
84- for {
85- green := color .New (color .FgGreen ).SprintFunc ()
86- fmt .Print (green ("> " ))
124+ renderer := display .NewStreamRenderer ()
125+ renderer .ShowReasoning = s .showReasoning
87126
88- input , err := reader .ReadString ('\n' )
89- if err != nil {
90- return nil // EOF
91- }
92- input = strings .TrimSpace (input )
127+ onDelta := func (delta api.Delta , finishReason string ) {
128+ renderer .Delta (delta )
129+ }
93130
94- if input == "" {
95- continue
96- }
131+ onToolCall := func ( tc api. ToolCall ) string {
132+ return handleToolCall ( tc , s . timeout )
133+ }
97134
98- switch input {
99- case "/quit" :
100- fmt .Println ("Goodbye!" )
101- return nil
102- case "/clear" :
103- // Keep system message if present
104- var newHistory []api.Message
105- if len (history ) > 0 && history [0 ].Role == "system" {
106- newHistory = append (newHistory , history [0 ])
107- }
108- history = newHistory
109- lastAnswer = ""
110- fmt .Println ("History cleared." )
111- continue
112- case "/copy" :
113- if lastAnswer == "" {
114- printError ("No previous response to copy from." )
115- continue
116- }
117- command := extractCommand (lastAnswer )
118- if command != "" {
119- copyToClipboard (command )
120- } else {
121- printError ("No code block found in last response." )
122- }
123- continue
124- }
135+ updatedHistory , err := s .client .RunWithTools (
136+ ctx , s .history , s .toolDefs , ! globalNoStream , onDelta , onToolCall ,
137+ )
138+ if err != nil {
139+ printError (err .Error ())
140+ return
141+ }
142+ fmt .Println ()
125143
126- history = append (history , api.Message {Role : "user" , Content : input })
127- ctx := context .Background ()
144+ if rendered := renderer .Finish (); rendered != "" {
145+ fmt .Print (rendered )
146+ }
128147
129- onDelta := func (delta api.Delta , finishReason string ) {
130- display .StreamDelta (delta )
131- }
148+ s .history = updatedHistory
149+ s .lastAnswer = renderer .Content ()
132150
133- onToolCall := func (tc api.ToolCall ) string {
134- return handleToolCall (tc , timeout )
135- }
151+ fmt .Println ()
152+ }
136153
137- updatedHistory , err := client .RunWithTools (
138- ctx , history , toolDefs , ! globalNoStream , onDelta , onToolCall ,
139- )
140- if err != nil {
141- printError (err .Error ())
142- continue
143- }
144- fmt .Println ()
154+ func (s * chatSession ) completer (d prompt.Document ) []prompt.Suggest {
155+ text := d .TextBeforeCursor ()
156+ if ! strings .HasPrefix (text , "/" ) {
157+ return nil
158+ }
145159
146- history = updatedHistory
160+ suggestions := []prompt.Suggest {
161+ {Text : "/quit" , Description : "Exit the chat session" },
162+ {Text : "/clear" , Description : "Clear conversation history" },
163+ {Text : "/copy" , Description : "Copy last code block to clipboard" },
164+ {Text : "/think" , Description : "Toggle reasoning visibility" },
165+ }
147166
148- // Find the last assistant message for lastAnswer
149- for i := len (history ) - 1 ; i >= 0 ; i -- {
150- if history [i ].Role == "assistant" {
151- lastAnswer = history [i ].Content
152- break
153- }
154- }
167+ return prompt .FilterHasPrefix (suggestions , text , true )
168+ }
155169
156- fmt .Println ()
170+ func runChatLoop (client * api.Client , history []api.Message ) error {
171+ var toolDefs []api.Tool
172+ if ! globalNoContext {
173+ toolDefs = tools .ToolDefinitions ()
174+ }
175+
176+ session := & chatSession {
177+ client : client ,
178+ history : history ,
179+ toolDefs : toolDefs ,
180+ timeout : time .Duration (globalTimeout ) * time .Second ,
157181 }
182+
183+ p := prompt .New (
184+ session .executor ,
185+ session .completer ,
186+ prompt .OptionPrefix ("> " ),
187+ prompt .OptionPrefixTextColor (prompt .Purple ),
188+ prompt .OptionSuggestionBGColor (prompt .DarkGray ),
189+ prompt .OptionSuggestionTextColor (prompt .White ),
190+ prompt .OptionSelectedSuggestionBGColor (prompt .Purple ),
191+ prompt .OptionSelectedSuggestionTextColor (prompt .White ),
192+ prompt .OptionDescriptionBGColor (prompt .DarkGray ),
193+ prompt .OptionDescriptionTextColor (prompt .White ),
194+ prompt .OptionSelectedDescriptionBGColor (prompt .Purple ),
195+ prompt .OptionSelectedDescriptionTextColor (prompt .White ),
196+ prompt .OptionScrollbarBGColor (prompt .DarkGray ),
197+ prompt .OptionScrollbarThumbColor (prompt .Purple ),
198+ prompt .OptionShowCompletionAtStart (),
199+ )
200+ p .Run ()
201+ return nil
158202}
159203
160204func handleToolCall (tc api.ToolCall , timeout time.Duration ) string {
@@ -164,8 +208,7 @@ func handleToolCall(tc api.ToolCall, timeout time.Duration) string {
164208 if normalizedName == "run_command" {
165209 safe , reason := tools .IsSafe (tc .Function .Arguments )
166210 if ! safe {
167- red := color .New (color .FgRed ).SprintFunc ()
168- fmt .Printf ("\n %s Command blocked: %s\n " , red ("✗" ), reason )
211+ fmt .Printf ("\n %s Command blocked: %s\n " , display .Danger ("✗" ), reason )
169212 return fmt .Sprintf ("Command was blocked for safety: %s" , reason )
170213 }
171214 }
0 commit comments