@@ -7,18 +7,12 @@ import (
7
7
"context"
8
8
_ "embed"
9
9
"fmt"
10
- "log"
11
- "os"
12
- "path/filepath"
13
10
"strings"
14
11
"sync"
15
- "time"
16
12
17
13
"github.com/azure/azure-dev/cli/azd/internal/agent/tools/common"
18
- "github.com/azure/azure-dev/cli/azd/pkg/input"
19
- "github.com/azure/azure-dev/cli/azd/pkg/output"
20
- uxlib "github.com/azure/azure-dev/cli/azd/pkg/ux"
21
- "github.com/fatih/color"
14
+ "github.com/azure/azure-dev/cli/azd/internal/agent/tools/ux"
15
+ "github.com/azure/azure-dev/cli/azd/pkg/watch"
22
16
"github.com/fsnotify/fsnotify"
23
17
"github.com/tmc/langchaingo/agents"
24
18
"github.com/tmc/langchaingo/chains"
@@ -94,225 +88,44 @@ func NewConversationalAzdAiAgent(llm llms.Model, opts ...AgentCreateOption) (Age
94
88
return azdAgent , nil
95
89
}
96
90
97
- type FileChanges struct {
98
- Created map [string ]bool
99
- Modified map [string ]bool
100
- Deleted map [string ]bool
101
- }
102
-
103
91
// SendMessage processes a single message through the agent and returns the response
104
- func (aai * ConversationalAzdAiAgent ) SendMessage (ctx context.Context , console input. Console , args ... string ) (string , error ) {
92
+ func (aai * ConversationalAzdAiAgent ) SendMessage (ctx context.Context , useWatch bool , args ... string ) (string , error ) {
105
93
thoughtsCtx , cancelCtx := context .WithCancel (ctx )
94
+
106
95
var watcher * fsnotify.Watcher
107
96
var done chan bool
108
97
var mu sync.Mutex
109
- var err error
110
- fileChanges := & FileChanges {
111
- Created : make (map [string ]bool ),
112
- Modified : make (map [string ]bool ),
113
- Deleted : make (map [string ]bool ),
114
- }
98
+ var fileChanges * watch.FileChanges
115
99
116
- if console != nil {
117
- watcher , done , err = startWatcher (ctx , fileChanges , & mu )
100
+ if useWatch {
101
+ var err error
102
+ watcher , done , fileChanges , err = watch .StartWatcher (ctx , & mu )
118
103
if err != nil {
119
104
return "" , fmt .Errorf ("failed to start watcher: %w" , err )
120
105
}
121
106
}
122
107
123
- cleanup , err := aai . renderThoughts (thoughtsCtx )
108
+ cleanup , err := ux . RenderThoughts (thoughtsCtx , aai . thoughtChan )
124
109
if err != nil {
125
110
cancelCtx ()
126
111
return "" , err
127
112
}
128
113
129
114
defer func () {
130
- if console != nil {
115
+ cleanup ()
116
+ cancelCtx ()
117
+
118
+ if useWatch {
119
+ watch .PrintChangedFiles (ctx , fileChanges , & mu )
131
120
close (done )
132
121
watcher .Close ()
133
122
}
134
- cleanup ()
135
- cancelCtx ()
136
123
}()
137
124
138
125
output , err := chains .Run (ctx , aai .executor , strings .Join (args , "\n " ))
139
126
if err != nil {
140
127
return "" , err
141
128
}
142
129
143
- if console != nil {
144
- printChangedFiles (ctx , console , fileChanges , & mu )
145
- }
146
-
147
130
return output , nil
148
131
}
149
-
150
- func (aai * ConversationalAzdAiAgent ) renderThoughts (ctx context.Context ) (func (), error ) {
151
- var latestThought string
152
-
153
- spinner := uxlib .NewSpinner (& uxlib.SpinnerOptions {
154
- Text : "Processing..." ,
155
- })
156
-
157
- canvas := uxlib .NewCanvas (
158
- spinner ,
159
- uxlib .NewVisualElement (func (printer uxlib.Printer ) error {
160
- printer .Fprintln ()
161
- printer .Fprintln ()
162
-
163
- if latestThought != "" {
164
- printer .Fprintln (color .HiBlackString (latestThought ))
165
- printer .Fprintln ()
166
- printer .Fprintln ()
167
- }
168
-
169
- return nil
170
- }))
171
-
172
- go func () {
173
- defer canvas .Clear ()
174
-
175
- var latestAction string
176
- var latestActionInput string
177
- var spinnerText string
178
-
179
- for {
180
-
181
- select {
182
- case thought := <- aai .thoughtChan :
183
- if thought .Action != "" {
184
- latestAction = thought .Action
185
- latestActionInput = thought .ActionInput
186
- }
187
- if thought .Thought != "" {
188
- latestThought = thought .Thought
189
- }
190
- case <- ctx .Done ():
191
- return
192
- case <- time .After (200 * time .Millisecond ):
193
- }
194
-
195
- // Update spinner text
196
- if latestAction == "" {
197
- spinnerText = "Processing..."
198
- } else {
199
- spinnerText = fmt .Sprintf ("Running %s tool" , color .BlueString (latestAction ))
200
- if latestActionInput != "" {
201
- spinnerText += " with " + color .BlueString (latestActionInput )
202
- }
203
-
204
- spinnerText += "..."
205
- }
206
-
207
- spinner .UpdateText (spinnerText )
208
- canvas .Update ()
209
- }
210
- }()
211
-
212
- cleanup := func () {
213
- canvas .Clear ()
214
- canvas .Close ()
215
- }
216
-
217
- return cleanup , canvas .Run ()
218
- }
219
-
220
- func startWatcher (ctx context.Context , fileChanges * FileChanges , mu * sync.Mutex ) (* fsnotify.Watcher , chan bool , error ) {
221
- watcher , err := fsnotify .NewWatcher ()
222
- if err != nil {
223
- return nil , nil , fmt .Errorf ("failed to create watcher: %w" , err )
224
- }
225
-
226
- done := make (chan bool )
227
-
228
- go func () {
229
- for {
230
- select {
231
- case event := <- watcher .Events :
232
- mu .Lock ()
233
- name := event .Name
234
- switch {
235
- case event .Has (fsnotify .Create ):
236
- fileChanges .Created [name ] = true
237
- case event .Has (fsnotify .Write ) || event .Has (fsnotify .Rename ):
238
- if ! fileChanges .Created [name ] && ! fileChanges .Deleted [name ] {
239
- fileChanges .Modified [name ] = true
240
- }
241
- case event .Has (fsnotify .Remove ):
242
- if fileChanges .Created [name ] {
243
- delete (fileChanges .Created , name )
244
- } else {
245
- fileChanges .Deleted [name ] = true
246
- delete (fileChanges .Modified , name )
247
- }
248
- }
249
- mu .Unlock ()
250
- case err := <- watcher .Errors :
251
- log .Printf ("watcher error: %v" , err )
252
- case <- done :
253
- return
254
- case <- ctx .Done ():
255
- return
256
- }
257
- }
258
- }()
259
-
260
- cwd , err := os .Getwd ()
261
- if err != nil {
262
- return nil , nil , fmt .Errorf ("failed to get current working directory: %w" , err )
263
- }
264
-
265
- if err := watchRecursive (cwd , watcher ); err != nil {
266
- return nil , nil , fmt .Errorf ("watcher failed: %w" , err )
267
- }
268
-
269
- return watcher , done , nil
270
- }
271
-
272
- func watchRecursive (root string , watcher * fsnotify.Watcher ) error {
273
- return filepath .Walk (root , func (path string , info os.FileInfo , err error ) error {
274
- if err != nil {
275
- return err
276
- }
277
- if info .IsDir () {
278
- err = watcher .Add (path )
279
- if err != nil {
280
- return fmt .Errorf ("failed to watch directory %s: %w" , path , err )
281
- }
282
- }
283
-
284
- return nil
285
- })
286
- }
287
-
288
- func printChangedFiles (ctx context.Context , console input.Console , fileChanges * FileChanges , mu * sync.Mutex ) {
289
- mu .Lock ()
290
- defer mu .Unlock ()
291
- createdFileLength := len (fileChanges .Created )
292
- modifiedFileLength := len (fileChanges .Modified )
293
- deletedFileLength := len (fileChanges .Deleted )
294
-
295
- if createdFileLength == 0 && modifiedFileLength == 0 && deletedFileLength == 0 {
296
- return
297
- }
298
-
299
- console .Message (ctx , output .WithHintFormat ("| Files changed:" ))
300
-
301
- if createdFileLength > 0 {
302
- for file := range fileChanges .Created {
303
- console .Message (ctx , fmt .Sprintf ("%s %s %s" , output .WithHintFormat ("|" ), color .GreenString ("+ Created" ), file ))
304
- }
305
- }
306
-
307
- if modifiedFileLength > 0 {
308
- for file := range fileChanges .Modified {
309
- console .Message (ctx , fmt .Sprintf ("%s %s %s" , output .WithHintFormat ("|" ), color .YellowString ("+/- Modified" ), file ))
310
- }
311
- }
312
-
313
- if deletedFileLength > 0 {
314
- for file := range fileChanges .Deleted {
315
- console .Message (ctx , fmt .Sprintf ("%s %s %s" , output .WithHintFormat ("|" ), color .RedString ("- Deleted" ), file ))
316
- }
317
- }
318
- }
0 commit comments