zarz 0.3.1-alpha → 0.3.4-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/executor.rs +2 -0
- package/src/main.rs +13 -7
- package/src/providers/anthropic.rs +49 -21
- package/src/providers/glm.rs +66 -16
- package/src/providers/mod.rs +13 -0
- package/src/providers/openai.rs +66 -16
- package/src/repl.rs +2153 -1910
package/src/repl.rs
CHANGED
|
@@ -1,1910 +1,2153 @@
|
|
|
1
|
-
use anyhow::{anyhow, Context, Result};
|
|
2
|
-
use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor};
|
|
3
|
-
use crossterm::{cursor, terminal::{self, ClearType}, ExecutableCommand, QueueableCommand};
|
|
4
|
-
use dialoguer::{theme::ColorfulTheme, Select};
|
|
5
|
-
use rustyline::completion::{Completer, Pair};
|
|
6
|
-
use rustyline::error::ReadlineError;
|
|
7
|
-
use rustyline::hint::{Hint as RtHint, Hinter};
|
|
8
|
-
use rustyline::highlight::Highlighter;
|
|
9
|
-
use rustyline::history::DefaultHistory;
|
|
10
|
-
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
|
|
11
|
-
use rustyline::{Cmd as RlCmd, ConditionalEventHandler as RlConditionalEventHandler, Context as RtContext, Editor, Event as RlBindingEvent, EventContext as RlEventContext, EventHandler as RlEventHandler, Helper, KeyCode as RlKeyCode, KeyEvent as RlKeyEvent, Modifiers as RlModifiers, RepeatCount as RlRepeatCount};
|
|
12
|
-
use similar::{ChangeTag, TextDiff};
|
|
13
|
-
use std::collections::HashMap;
|
|
14
|
-
use std::io::{stdout, Write};
|
|
15
|
-
use std::path::{Path, PathBuf};
|
|
16
|
-
use std::sync::{
|
|
17
|
-
atomic::{AtomicBool, Ordering},
|
|
18
|
-
Arc,
|
|
19
|
-
Mutex,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
use crate::cli::Provider;
|
|
23
|
-
use crate::conversation_store::{ConversationStore, ConversationSummary};
|
|
24
|
-
use crate::config::Config;
|
|
25
|
-
use crate::
|
|
26
|
-
use crate::
|
|
27
|
-
use crate::mcp::{
|
|
28
|
-
use crate::
|
|
29
|
-
use crate::
|
|
30
|
-
use
|
|
31
|
-
use
|
|
32
|
-
use tokio::
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
CommandInfo { name: "
|
|
42
|
-
CommandInfo { name: "
|
|
43
|
-
CommandInfo { name: "
|
|
44
|
-
CommandInfo { name: "
|
|
45
|
-
CommandInfo { name: "
|
|
46
|
-
CommandInfo { name: "
|
|
47
|
-
CommandInfo { name: "
|
|
48
|
-
CommandInfo { name: "
|
|
49
|
-
CommandInfo { name: "
|
|
50
|
-
CommandInfo { name: "
|
|
51
|
-
CommandInfo { name: "
|
|
52
|
-
CommandInfo { name: "
|
|
53
|
-
CommandInfo { name: "
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
impl
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
hint_text.push_str("
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
hint_text.
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
impl
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
))
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
- /
|
|
182
|
-
- /
|
|
183
|
-
- /
|
|
184
|
-
- /
|
|
185
|
-
- /
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
out
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
out.queue(
|
|
287
|
-
out.queue(
|
|
288
|
-
out.queue(
|
|
289
|
-
out.queue(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
Ok(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
println!("
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
)
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
self.session.
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
{
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
println!("No
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
)
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
.
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
let
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
fn
|
|
1627
|
-
let
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
"
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
"
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
Ok(())
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
if
|
|
1841
|
-
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
fn
|
|
1848
|
-
let mut out = stdout();
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
out.execute(
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
}
|
|
1
|
+
use anyhow::{anyhow, Context, Result};
|
|
2
|
+
use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor};
|
|
3
|
+
use crossterm::{cursor, terminal::{self, ClearType}, ExecutableCommand, QueueableCommand};
|
|
4
|
+
use dialoguer::{theme::ColorfulTheme, Select};
|
|
5
|
+
use rustyline::completion::{Completer, Pair};
|
|
6
|
+
use rustyline::error::ReadlineError;
|
|
7
|
+
use rustyline::hint::{Hint as RtHint, Hinter};
|
|
8
|
+
use rustyline::highlight::Highlighter;
|
|
9
|
+
use rustyline::history::DefaultHistory;
|
|
10
|
+
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
|
|
11
|
+
use rustyline::{Cmd as RlCmd, ConditionalEventHandler as RlConditionalEventHandler, Context as RtContext, Editor, Event as RlBindingEvent, EventContext as RlEventContext, EventHandler as RlEventHandler, Helper, KeyCode as RlKeyCode, KeyEvent as RlKeyEvent, Modifiers as RlModifiers, RepeatCount as RlRepeatCount};
|
|
12
|
+
use similar::{ChangeTag, TextDiff};
|
|
13
|
+
use std::collections::HashMap;
|
|
14
|
+
use std::io::{stdout, Write};
|
|
15
|
+
use std::path::{Path, PathBuf};
|
|
16
|
+
use std::sync::{
|
|
17
|
+
atomic::{AtomicBool, Ordering},
|
|
18
|
+
Arc,
|
|
19
|
+
Mutex,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
use crate::cli::Provider;
|
|
23
|
+
use crate::conversation_store::{ConversationStore, ConversationSummary};
|
|
24
|
+
use crate::config::Config;
|
|
25
|
+
use crate::fs_ops::FileSystemOps;
|
|
26
|
+
use crate::mcp::{McpManager, McpTool};
|
|
27
|
+
use crate::mcp::types::{CallToolResult, ToolContent};
|
|
28
|
+
use crate::providers::{CompletionProvider, CompletionRequest, ProviderClient};
|
|
29
|
+
use crate::session::{MessageRole, Session};
|
|
30
|
+
use serde_json::{self, json, Value};
|
|
31
|
+
use tokio::task::JoinHandle;
|
|
32
|
+
use tokio::time::{sleep, Duration};
|
|
33
|
+
|
|
34
|
+
struct CommandInfo {
|
|
35
|
+
name: &'static str,
|
|
36
|
+
description: &'static str,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const COMMANDS: &[CommandInfo] = &[
|
|
40
|
+
CommandInfo { name: "help", description: "Show this help message" },
|
|
41
|
+
CommandInfo { name: "apply", description: "Apply pending file changes" },
|
|
42
|
+
CommandInfo { name: "diff", description: "Show pending changes" },
|
|
43
|
+
CommandInfo { name: "undo", description: "Clear pending changes" },
|
|
44
|
+
CommandInfo { name: "edit", description: "Load a file for editing" },
|
|
45
|
+
CommandInfo { name: "search", description: "Search for a symbol" },
|
|
46
|
+
CommandInfo { name: "context", description: "Find relevant files" },
|
|
47
|
+
CommandInfo { name: "files", description: "List currently loaded files" },
|
|
48
|
+
CommandInfo { name: "model", description: "Switch to a different AI model" },
|
|
49
|
+
CommandInfo { name: "mcp", description: "Show MCP servers and available tools" },
|
|
50
|
+
CommandInfo { name: "resume", description: "Resume a previous chat session" },
|
|
51
|
+
CommandInfo { name: "clear", description: "Clear conversation history" },
|
|
52
|
+
CommandInfo { name: "logout", description: "Remove stored API keys and sign out" },
|
|
53
|
+
CommandInfo { name: "exit", description: "Exit the session" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
#[derive(Clone, Default)]
|
|
57
|
+
struct CommandHelper;
|
|
58
|
+
|
|
59
|
+
#[derive(Clone)]
|
|
60
|
+
struct CommandHint(String);
|
|
61
|
+
|
|
62
|
+
impl RtHint for CommandHint {
|
|
63
|
+
fn display(&self) -> &str {
|
|
64
|
+
&self.0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn completion(&self) -> Option<&str> {
|
|
68
|
+
None
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Helper for CommandHelper {}
|
|
73
|
+
|
|
74
|
+
impl Hinter for CommandHelper {
|
|
75
|
+
type Hint = CommandHint;
|
|
76
|
+
|
|
77
|
+
fn hint(&self, line: &str, pos: usize, _: &RtContext<'_>) -> Option<Self::Hint> {
|
|
78
|
+
if !line.starts_with('/') || pos == 0 {
|
|
79
|
+
return None;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let upto_cursor = &line[..pos];
|
|
83
|
+
if upto_cursor.contains(' ') {
|
|
84
|
+
return None;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let partial = upto_cursor.trim_start_matches('/');
|
|
88
|
+
|
|
89
|
+
let matches: Vec<&CommandInfo> = COMMANDS
|
|
90
|
+
.iter()
|
|
91
|
+
.filter(|info| info.name.starts_with(partial))
|
|
92
|
+
.collect();
|
|
93
|
+
|
|
94
|
+
if matches.is_empty() {
|
|
95
|
+
return None;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let mut hint_text = String::from("\n");
|
|
99
|
+
|
|
100
|
+
if partial.is_empty() {
|
|
101
|
+
hint_text.push_str("Available commands (press ↓ to browse):\n");
|
|
102
|
+
} else {
|
|
103
|
+
hint_text.push_str(&format!("Matches for '/{}' (press ↓ to browse):\n", partial));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let name_width = 10usize;
|
|
107
|
+
for info in matches.iter().take(6) {
|
|
108
|
+
hint_text.push_str(" /");
|
|
109
|
+
hint_text.push_str(info.name);
|
|
110
|
+
if info.name.len() < name_width {
|
|
111
|
+
hint_text.push_str(&" ".repeat(name_width - info.name.len()));
|
|
112
|
+
} else {
|
|
113
|
+
hint_text.push(' ');
|
|
114
|
+
}
|
|
115
|
+
hint_text.push_str(info.description);
|
|
116
|
+
hint_text.push('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if matches.len() > 6 {
|
|
120
|
+
hint_text.push_str(" ...\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Some(CommandHint(hint_text.trim_end().to_string()))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
impl Completer for CommandHelper {
|
|
128
|
+
type Candidate = Pair;
|
|
129
|
+
|
|
130
|
+
fn complete(
|
|
131
|
+
&self,
|
|
132
|
+
_line: &str,
|
|
133
|
+
pos: usize,
|
|
134
|
+
_ctx: &RtContext<'_>,
|
|
135
|
+
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
|
136
|
+
Ok((pos, Vec::new()))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
impl Highlighter for CommandHelper {}
|
|
141
|
+
|
|
142
|
+
impl Validator for CommandHelper {
|
|
143
|
+
fn validate(
|
|
144
|
+
&self,
|
|
145
|
+
ctx: &mut ValidationContext<'_>,
|
|
146
|
+
) -> rustyline::Result<ValidationResult> {
|
|
147
|
+
let input = ctx.input();
|
|
148
|
+
if input.trim().is_empty() {
|
|
149
|
+
Ok(ValidationResult::Invalid(Some(
|
|
150
|
+
"Input cannot be empty".to_string(),
|
|
151
|
+
)))
|
|
152
|
+
} else {
|
|
153
|
+
Ok(ValidationResult::Valid(None))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const REPL_SYSTEM_PROMPT: &str = r#"You are ZarzCLI, Fapzarz's official CLI for Claude and Codex.
|
|
159
|
+
|
|
160
|
+
You are an interactive CLI tool that helps users with software engineering tasks.
|
|
161
|
+
|
|
162
|
+
IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes.
|
|
163
|
+
|
|
164
|
+
## Bash Tool for Context Understanding
|
|
165
|
+
|
|
166
|
+
You have access to a `bash` tool that allows you to execute shell commands to understand the codebase better. Use this tool proactively to:
|
|
167
|
+
- Search for files: `find . -name "*.rs"` or `find . -type f -name "pattern"`
|
|
168
|
+
- Search code content: `grep -r "function_name" src/` or `rg "pattern" --type rust`
|
|
169
|
+
- Read file contents: `cat path/to/file.rs` or `head -n 20 file.py`
|
|
170
|
+
- List directory structure: `ls -la src/` or `tree -L 2`
|
|
171
|
+
- Check git status: `git log --oneline -10` or `git diff`
|
|
172
|
+
|
|
173
|
+
IMPORTANT: Use the bash tool whenever you need to understand the codebase structure, find files, or read file contents. This helps you provide accurate and contextual responses.
|
|
174
|
+
|
|
175
|
+
When making file changes, use code fences in this exact format:
|
|
176
|
+
```file:relative/path.rs
|
|
177
|
+
<entire file content>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Available commands the user can use:
|
|
181
|
+
- /help - Show help
|
|
182
|
+
- /apply - Apply pending changes
|
|
183
|
+
- /diff - Show pending changes
|
|
184
|
+
- /undo - Clear pending changes
|
|
185
|
+
- /edit <file> - Load a file for editing
|
|
186
|
+
- /search <symbol> - Search for a symbol in the codebase
|
|
187
|
+
- /context <query> - Find relevant files for a query
|
|
188
|
+
- /files - List currently loaded files
|
|
189
|
+
- /model <name> - Switch to a different AI model
|
|
190
|
+
- /mcp - Show MCP servers and available tools
|
|
191
|
+
- /resume - Resume a previous chat session
|
|
192
|
+
- /clear - Clear conversation history
|
|
193
|
+
- /exit - Exit the session
|
|
194
|
+
|
|
195
|
+
Tone and style:
|
|
196
|
+
- Only use emojis if the user explicitly requests it
|
|
197
|
+
- Responses should be short and concise
|
|
198
|
+
- Focus on facts and problem-solving
|
|
199
|
+
- Avoid over-the-top validation or excessive praise
|
|
200
|
+
|
|
201
|
+
Provide clear, concise responses. When suggesting changes, always use the file block format above.
|
|
202
|
+
|
|
203
|
+
Conversation format:
|
|
204
|
+
- The prompt includes the recent transcript using prefixes like "User:", "Assistant:", and "Tool[server.tool]:".
|
|
205
|
+
- Always respond in the voice of "Assistant" to the most recent user message.
|
|
206
|
+
- File changes are applied automatically; never instruct the user to run /apply or similar commands.
|
|
207
|
+
|
|
208
|
+
MCP tool usage:
|
|
209
|
+
- When the prompt lists available MCP tools, you may request one by replying exactly: CALL_MCP_TOOL server=<server_name> tool=<tool_name> args=<json_object>
|
|
210
|
+
- The JSON must be minified on a single line. Use {} when no arguments are required.
|
|
211
|
+
- Do not include any additional text when making a tool request. Wait for Tool[...] messages that show the results, then continue the conversation.
|
|
212
|
+
"#;
|
|
213
|
+
|
|
214
|
+
pub struct Repl {
|
|
215
|
+
session: Session,
|
|
216
|
+
provider: ProviderClient,
|
|
217
|
+
provider_kind: Provider,
|
|
218
|
+
endpoint: Option<String>,
|
|
219
|
+
timeout: Option<u64>,
|
|
220
|
+
model: String,
|
|
221
|
+
max_tokens: u32,
|
|
222
|
+
temperature: f32,
|
|
223
|
+
mcp_manager: Option<std::sync::Arc<McpManager>>,
|
|
224
|
+
config: Config,
|
|
225
|
+
logout_requested: bool,
|
|
226
|
+
pending_command: Arc<Mutex<Option<String>>>,
|
|
227
|
+
last_interrupt: Option<std::time::Instant>,
|
|
228
|
+
current_mode: String,
|
|
229
|
+
status_message: Option<String>,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
impl Repl {
|
|
233
|
+
fn command_list() -> &'static [CommandInfo] {
|
|
234
|
+
COMMANDS
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn print_command_suggestions(partial: &str) -> Result<bool> {
|
|
238
|
+
let matches: Vec<&CommandInfo> = Self::command_list()
|
|
239
|
+
.iter()
|
|
240
|
+
.filter(|info| info.name.starts_with(partial))
|
|
241
|
+
.collect();
|
|
242
|
+
|
|
243
|
+
if matches.is_empty() {
|
|
244
|
+
return Ok(false);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
stdout().execute(SetForegroundColor(Color::Yellow)).ok();
|
|
248
|
+
if partial.is_empty() {
|
|
249
|
+
println!("Available commands (press Enter to choose):");
|
|
250
|
+
} else {
|
|
251
|
+
println!(
|
|
252
|
+
"Commands matching '/{}' (press Enter to choose):",
|
|
253
|
+
partial
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
for info in matches {
|
|
257
|
+
println!(" /{:<8} - {}", info.name, info.description);
|
|
258
|
+
}
|
|
259
|
+
stdout().execute(ResetColor).ok();
|
|
260
|
+
println!();
|
|
261
|
+
std::io::stdout().flush().ok();
|
|
262
|
+
|
|
263
|
+
Ok(true)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn take_pending_command(&self) -> Option<String> {
|
|
267
|
+
self.pending_command
|
|
268
|
+
.lock()
|
|
269
|
+
.ok()
|
|
270
|
+
.and_then(|mut guard| guard.take())
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn record_message(&mut self, role: MessageRole, content: String) {
|
|
274
|
+
self.session.add_message(role, content);
|
|
275
|
+
self.persist_session_if_needed();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn draw_prompt_frame(&self) {
|
|
279
|
+
let mut out = stdout();
|
|
280
|
+
let width = terminal::size().map(|(w, _)| w as usize).unwrap_or(120);
|
|
281
|
+
let border = "─".repeat(width);
|
|
282
|
+
|
|
283
|
+
out.queue(cursor::Hide).ok();
|
|
284
|
+
out.queue(cursor::MoveToColumn(0)).ok();
|
|
285
|
+
out.queue(Print(&border)).ok();
|
|
286
|
+
out.queue(Print("\r\n")).ok();
|
|
287
|
+
out.queue(Print("\r\n")).ok();
|
|
288
|
+
out.queue(Print(&border)).ok();
|
|
289
|
+
out.queue(Print("\r\n")).ok();
|
|
290
|
+
|
|
291
|
+
if let Some(msg) = &self.status_message {
|
|
292
|
+
out.execute(SetForegroundColor(Color::Yellow)).ok();
|
|
293
|
+
out.queue(Print(msg)).ok();
|
|
294
|
+
out.execute(ResetColor).ok();
|
|
295
|
+
} else {
|
|
296
|
+
out.execute(SetForegroundColor(Color::Green)).ok();
|
|
297
|
+
out.queue(Print(format!(" ⏵⏵ Mode: {}", self.current_mode))).ok();
|
|
298
|
+
out.execute(ResetColor).ok();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
out.queue(cursor::MoveUp(2)).ok();
|
|
302
|
+
out.queue(cursor::MoveToColumn(0)).ok();
|
|
303
|
+
out.queue(cursor::Show).ok();
|
|
304
|
+
out.flush().ok();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fn clear_prompt_frame() {
|
|
308
|
+
let mut out = stdout();
|
|
309
|
+
out.queue(cursor::Hide).ok();
|
|
310
|
+
out.queue(cursor::MoveUp(1)).ok();
|
|
311
|
+
out.queue(cursor::MoveToColumn(0)).ok();
|
|
312
|
+
out.queue(terminal::Clear(ClearType::CurrentLine)).ok();
|
|
313
|
+
out.queue(cursor::MoveDown(1)).ok();
|
|
314
|
+
out.queue(cursor::MoveToColumn(0)).ok();
|
|
315
|
+
out.queue(terminal::Clear(ClearType::CurrentLine)).ok();
|
|
316
|
+
out.queue(cursor::MoveDown(1)).ok();
|
|
317
|
+
out.queue(cursor::MoveToColumn(0)).ok();
|
|
318
|
+
out.queue(terminal::Clear(ClearType::CurrentLine)).ok();
|
|
319
|
+
out.queue(cursor::MoveDown(1)).ok();
|
|
320
|
+
out.queue(cursor::MoveToColumn(0)).ok();
|
|
321
|
+
out.queue(terminal::Clear(ClearType::CurrentLine)).ok();
|
|
322
|
+
out.queue(cursor::MoveUp(3)).ok();
|
|
323
|
+
out.queue(cursor::MoveToColumn(0)).ok();
|
|
324
|
+
out.queue(cursor::Show).ok();
|
|
325
|
+
out.flush().ok();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fn persist_session_if_needed(&mut self) {
|
|
329
|
+
if self.session.conversation_history.is_empty() {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if let Err(err) = ConversationStore::save_session(
|
|
334
|
+
&mut self.session,
|
|
335
|
+
self.provider_kind.clone(),
|
|
336
|
+
&self.model,
|
|
337
|
+
) {
|
|
338
|
+
eprintln!("Warning: Failed to save session history: {:#}", err);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
pub fn new(
|
|
343
|
+
working_dir: PathBuf,
|
|
344
|
+
provider: ProviderClient,
|
|
345
|
+
provider_kind: Provider,
|
|
346
|
+
endpoint: Option<String>,
|
|
347
|
+
timeout: Option<u64>,
|
|
348
|
+
model: String,
|
|
349
|
+
max_tokens: u32,
|
|
350
|
+
temperature: f32,
|
|
351
|
+
mcp_manager: Option<std::sync::Arc<McpManager>>,
|
|
352
|
+
config: Config,
|
|
353
|
+
) -> Self {
|
|
354
|
+
Self {
|
|
355
|
+
session: Session::new(working_dir),
|
|
356
|
+
provider,
|
|
357
|
+
provider_kind,
|
|
358
|
+
endpoint,
|
|
359
|
+
timeout,
|
|
360
|
+
model,
|
|
361
|
+
max_tokens,
|
|
362
|
+
temperature,
|
|
363
|
+
mcp_manager,
|
|
364
|
+
config,
|
|
365
|
+
logout_requested: false,
|
|
366
|
+
pending_command: Arc::new(Mutex::new(None)),
|
|
367
|
+
last_interrupt: None,
|
|
368
|
+
current_mode: "Auto".to_string(),
|
|
369
|
+
status_message: None,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
pub async fn run(&mut self) -> Result<()> {
|
|
374
|
+
let mut editor: Editor<CommandHelper, DefaultHistory> = Editor::new()
|
|
375
|
+
.context("Failed to initialize readline editor")?;
|
|
376
|
+
editor.set_helper(Some(CommandHelper::default()));
|
|
377
|
+
|
|
378
|
+
let handler_down = CommandMenuHandler::new(self.pending_command.clone());
|
|
379
|
+
editor.bind_sequence(
|
|
380
|
+
RlKeyEvent(RlKeyCode::Down, RlModifiers::NONE),
|
|
381
|
+
RlEventHandler::Conditional(Box::new(handler_down)),
|
|
382
|
+
);
|
|
383
|
+
let handler_up = CommandMenuHandler::new(self.pending_command.clone());
|
|
384
|
+
editor.bind_sequence(
|
|
385
|
+
RlKeyEvent(RlKeyCode::Up, RlModifiers::NONE),
|
|
386
|
+
RlEventHandler::Conditional(Box::new(handler_up)),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
loop {
|
|
390
|
+
self.draw_prompt_frame();
|
|
391
|
+
let readline = editor.readline("> ");
|
|
392
|
+
|
|
393
|
+
match readline {
|
|
394
|
+
Ok(line) => {
|
|
395
|
+
self.last_interrupt = None;
|
|
396
|
+
self.status_message = None;
|
|
397
|
+
|
|
398
|
+
Self::clear_prompt_frame();
|
|
399
|
+
|
|
400
|
+
let line = line.trim();
|
|
401
|
+
|
|
402
|
+
if line.is_empty() {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let mut out = stdout();
|
|
407
|
+
out.execute(terminal::Clear(ClearType::CurrentLine)).ok();
|
|
408
|
+
out.execute(cursor::MoveToColumn(0)).ok();
|
|
409
|
+
println!("> {}", line);
|
|
410
|
+
|
|
411
|
+
editor.add_history_entry(line)
|
|
412
|
+
.context("Failed to add history entry")?;
|
|
413
|
+
|
|
414
|
+
if line.starts_with('/') {
|
|
415
|
+
if let Err(e) = self.handle_command(line).await {
|
|
416
|
+
eprintln!("Error: {:#}", e);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if self.logout_requested {
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if line == "/exit" {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
if self.logout_requested {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if let Err(e) = self.handle_user_input(line).await {
|
|
432
|
+
eprintln!("Error: {:#}", e);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if self.logout_requested {
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
Err(ReadlineError::Interrupted) => {
|
|
441
|
+
if let Some(cmd) = self.take_pending_command() {
|
|
442
|
+
Self::clear_prompt_frame();
|
|
443
|
+
println!("> {}", cmd);
|
|
444
|
+
editor
|
|
445
|
+
.add_history_entry(cmd.as_str())
|
|
446
|
+
.context("Failed to add history entry")?;
|
|
447
|
+
if let Err(e) = self.handle_command(&cmd).await {
|
|
448
|
+
eprintln!("Error: {:#}", e);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if self.logout_requested {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let now = std::time::Instant::now();
|
|
459
|
+
if let Some(last) = self.last_interrupt {
|
|
460
|
+
if now.duration_since(last).as_secs() < 2 {
|
|
461
|
+
Self::clear_prompt_frame();
|
|
462
|
+
println!();
|
|
463
|
+
println!("Exiting...");
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
Self::clear_prompt_frame();
|
|
469
|
+
self.last_interrupt = Some(now);
|
|
470
|
+
self.status_message = Some(" Press Ctrl+C again to exit, or continue typing...".to_string());
|
|
471
|
+
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
Err(ReadlineError::Eof) => {
|
|
475
|
+
Self::clear_prompt_frame();
|
|
476
|
+
println!("Exiting");
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
Err(err) => {
|
|
480
|
+
Self::clear_prompt_frame();
|
|
481
|
+
eprintln!("Error: {:#}", err);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
Ok(())
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async fn handle_command(&mut self, command: &str) -> Result<()> {
|
|
491
|
+
let parts: Vec<&str> = command.splitn(2, ' ').collect();
|
|
492
|
+
let cmd = parts[0];
|
|
493
|
+
let args = parts.get(1).copied().unwrap_or("");
|
|
494
|
+
|
|
495
|
+
if cmd == "/" {
|
|
496
|
+
let matches: Vec<&CommandInfo> = Self::command_list().iter().collect();
|
|
497
|
+
if let Some(choice) = pick_command_menu("", &matches, 0)? {
|
|
498
|
+
let mut selected_command = format!("/{}", choice.name);
|
|
499
|
+
if !args.is_empty() {
|
|
500
|
+
selected_command.push(' ');
|
|
501
|
+
selected_command.push_str(args);
|
|
502
|
+
}
|
|
503
|
+
return Self::execute_command(self, &selected_command).await;
|
|
504
|
+
}
|
|
505
|
+
return Ok(());
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if let Some(partial) = cmd.strip_prefix('/') {
|
|
509
|
+
if !partial.is_empty() && !Self::command_list().iter().any(|info| info.name == partial) {
|
|
510
|
+
let matches: Vec<&CommandInfo> = Self::command_list()
|
|
511
|
+
.iter()
|
|
512
|
+
.filter(|info| info.name.starts_with(partial))
|
|
513
|
+
.collect();
|
|
514
|
+
|
|
515
|
+
if matches.len() == 1 {
|
|
516
|
+
let mut selected_command = format!("/{}", matches[0].name);
|
|
517
|
+
if !args.is_empty() {
|
|
518
|
+
selected_command.push(' ');
|
|
519
|
+
selected_command.push_str(args);
|
|
520
|
+
}
|
|
521
|
+
return Self::execute_command(self, &selected_command).await;
|
|
522
|
+
} else if matches.len() > 1 {
|
|
523
|
+
if let Some(choice) = pick_command_menu(partial, &matches, 0)? {
|
|
524
|
+
let mut selected_command = format!("/{}", choice.name);
|
|
525
|
+
if !args.is_empty() {
|
|
526
|
+
selected_command.push(' ');
|
|
527
|
+
selected_command.push_str(args);
|
|
528
|
+
}
|
|
529
|
+
return Self::execute_command(self, &selected_command).await;
|
|
530
|
+
} else {
|
|
531
|
+
return Ok(());
|
|
532
|
+
}
|
|
533
|
+
} else if Self::print_command_suggestions(partial)? {
|
|
534
|
+
return Ok(());
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
Self::execute_command(self, command).await
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async fn execute_command(&mut self, command: &str) -> Result<()> {
|
|
543
|
+
let parts: Vec<&str> = command.splitn(2, ' ').collect();
|
|
544
|
+
let cmd = parts[0];
|
|
545
|
+
let args = parts.get(1).copied().unwrap_or("");
|
|
546
|
+
|
|
547
|
+
match cmd {
|
|
548
|
+
"/help" => self.show_help(),
|
|
549
|
+
"/exit" => {
|
|
550
|
+
println!("Goodbye!");
|
|
551
|
+
Ok(())
|
|
552
|
+
}
|
|
553
|
+
"/apply" => self.apply_changes().await,
|
|
554
|
+
"/diff" => self.show_diff(),
|
|
555
|
+
"/undo" => self.undo_changes(),
|
|
556
|
+
"/edit" => self.edit_file(args).await,
|
|
557
|
+
"/search" => self.search_symbol(args).await,
|
|
558
|
+
"/context" => self.find_context(args).await,
|
|
559
|
+
"/files" => self.list_files(),
|
|
560
|
+
"/model" => self.switch_model(args).await,
|
|
561
|
+
"/mcp" => self.show_mcp_status().await,
|
|
562
|
+
"/resume" => self.resume_session(args).await,
|
|
563
|
+
"/clear" => self.clear_history(),
|
|
564
|
+
"/logout" => self.logout(),
|
|
565
|
+
_ => {
|
|
566
|
+
println!("Unknown command: {}", cmd);
|
|
567
|
+
println!("Type /help for available commands");
|
|
568
|
+
Ok(())
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async fn handle_user_input(&mut self, input: &str) -> Result<()> {
|
|
574
|
+
if self.logout_requested {
|
|
575
|
+
return Err(anyhow!(
|
|
576
|
+
"You have logged out. Restart ZarzCLI and run 'zarz config' to sign in again."
|
|
577
|
+
));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
self.record_message(MessageRole::User, input.to_string());
|
|
581
|
+
|
|
582
|
+
let tools_snapshot = if let Some(manager) = &self.mcp_manager {
|
|
583
|
+
match manager.get_all_tools().await {
|
|
584
|
+
Ok(map) if !map.is_empty() => Some(map),
|
|
585
|
+
Ok(_) => None,
|
|
586
|
+
Err(e) => {
|
|
587
|
+
eprintln!("Warning: Failed to fetch MCP tools: {}", e);
|
|
588
|
+
None
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
None
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
let tool_prompt_section = tools_snapshot
|
|
596
|
+
.as_ref()
|
|
597
|
+
.map(|tools| build_tool_prompt_section(tools));
|
|
598
|
+
|
|
599
|
+
let mut tool_calls = 0usize;
|
|
600
|
+
let max_tool_calls = 5usize;
|
|
601
|
+
#[allow(unused_assignments)]
|
|
602
|
+
let mut final_response: Option<String> = None;
|
|
603
|
+
|
|
604
|
+
loop {
|
|
605
|
+
let mut prompt = String::new();
|
|
606
|
+
|
|
607
|
+
if let Some(section) = &tool_prompt_section {
|
|
608
|
+
prompt.push_str(section);
|
|
609
|
+
prompt.push_str("\n\n");
|
|
610
|
+
} else if self.mcp_manager.is_some() {
|
|
611
|
+
prompt.push_str("No MCP tools are currently available.\n\n");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
prompt.push_str(&self.session.build_prompt_with_context(true));
|
|
615
|
+
prompt.push_str("Respond as the assistant to the latest user message.");
|
|
616
|
+
|
|
617
|
+
let bash_tool = json!({
|
|
618
|
+
"name": "bash",
|
|
619
|
+
"description": "Execute bash commands to search files, read file contents, or perform other system operations. Use this to understand the codebase context better.",
|
|
620
|
+
"input_schema": {
|
|
621
|
+
"type": "object",
|
|
622
|
+
"properties": {
|
|
623
|
+
"command": {
|
|
624
|
+
"type": "string",
|
|
625
|
+
"description": "The bash command to execute (e.g., 'find . -name \"*.rs\"', 'grep -r \"function_name\" src/', 'cat src/main.rs')"
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
"required": ["command"]
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
let request = CompletionRequest {
|
|
633
|
+
model: self.model.clone(),
|
|
634
|
+
system_prompt: Some(REPL_SYSTEM_PROMPT.to_string()),
|
|
635
|
+
user_prompt: prompt.clone(),
|
|
636
|
+
max_output_tokens: self.max_tokens,
|
|
637
|
+
temperature: self.temperature,
|
|
638
|
+
messages: None,
|
|
639
|
+
tools: Some(vec![bash_tool.clone()]),
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
let spinner = Spinner::start("Thinking...".to_string());
|
|
643
|
+
let response_result = self.provider.complete(&request).await;
|
|
644
|
+
spinner.stop().await;
|
|
645
|
+
let mut response = response_result?;
|
|
646
|
+
|
|
647
|
+
if !response.tool_calls.is_empty() {
|
|
648
|
+
let is_anthropic = self.provider.name() == "anthropic";
|
|
649
|
+
|
|
650
|
+
let mut messages = if is_anthropic {
|
|
651
|
+
vec![json!({
|
|
652
|
+
"role": "user",
|
|
653
|
+
"content": [{
|
|
654
|
+
"type": "text",
|
|
655
|
+
"text": prompt
|
|
656
|
+
}]
|
|
657
|
+
})]
|
|
658
|
+
} else {
|
|
659
|
+
let mut msgs = Vec::new();
|
|
660
|
+
if let Some(system) = &request.system_prompt {
|
|
661
|
+
msgs.push(json!({
|
|
662
|
+
"role": "system",
|
|
663
|
+
"content": system
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
msgs.push(json!({
|
|
667
|
+
"role": "user",
|
|
668
|
+
"content": prompt
|
|
669
|
+
}));
|
|
670
|
+
msgs
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
if is_anthropic {
|
|
674
|
+
let mut assistant_content = Vec::new();
|
|
675
|
+
if !response.text.is_empty() {
|
|
676
|
+
assistant_content.push(json!({
|
|
677
|
+
"type": "text",
|
|
678
|
+
"text": response.text
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for tool_call in response.tool_calls.clone() {
|
|
683
|
+
assistant_content.push(json!({
|
|
684
|
+
"type": "tool_use",
|
|
685
|
+
"id": tool_call.id,
|
|
686
|
+
"name": tool_call.name,
|
|
687
|
+
"input": tool_call.input
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
messages.push(json!({
|
|
692
|
+
"role": "assistant",
|
|
693
|
+
"content": assistant_content
|
|
694
|
+
}));
|
|
695
|
+
} else {
|
|
696
|
+
let mut openai_tool_calls = Vec::new();
|
|
697
|
+
for tool_call in response.tool_calls.clone() {
|
|
698
|
+
openai_tool_calls.push(json!({
|
|
699
|
+
"id": tool_call.id,
|
|
700
|
+
"type": "function",
|
|
701
|
+
"function": {
|
|
702
|
+
"name": tool_call.name,
|
|
703
|
+
"arguments": tool_call.input.to_string()
|
|
704
|
+
}
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
messages.push(json!({
|
|
709
|
+
"role": "assistant",
|
|
710
|
+
"content": response.text,
|
|
711
|
+
"tool_calls": openai_tool_calls
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for tool_call in &response.tool_calls {
|
|
716
|
+
if tool_call.name == "bash" {
|
|
717
|
+
if let Some(command) = tool_call.input.get("command").and_then(|v| v.as_str()) {
|
|
718
|
+
println!();
|
|
719
|
+
stdout().execute(SetForegroundColor(Color::Cyan))?;
|
|
720
|
+
println!(" $ {}", command);
|
|
721
|
+
stdout().execute(ResetColor)?;
|
|
722
|
+
|
|
723
|
+
let result = execute_bash_command(command)?;
|
|
724
|
+
let truncated = if result.len() > 4000 {
|
|
725
|
+
format!("{}... (truncated, {} total chars)", &result[..4000], result.len())
|
|
726
|
+
} else {
|
|
727
|
+
result
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
if is_anthropic {
|
|
731
|
+
let tool_result_content = vec![json!({
|
|
732
|
+
"type": "tool_result",
|
|
733
|
+
"tool_use_id": tool_call.id,
|
|
734
|
+
"content": truncated
|
|
735
|
+
})];
|
|
736
|
+
messages.push(json!({
|
|
737
|
+
"role": "user",
|
|
738
|
+
"content": tool_result_content
|
|
739
|
+
}));
|
|
740
|
+
} else {
|
|
741
|
+
messages.push(json!({
|
|
742
|
+
"role": "tool",
|
|
743
|
+
"tool_call_id": tool_call.id,
|
|
744
|
+
"content": truncated
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let follow_up_request = CompletionRequest {
|
|
752
|
+
model: self.model.clone(),
|
|
753
|
+
system_prompt: Some(REPL_SYSTEM_PROMPT.to_string()),
|
|
754
|
+
user_prompt: String::new(),
|
|
755
|
+
max_output_tokens: self.max_tokens,
|
|
756
|
+
temperature: self.temperature,
|
|
757
|
+
messages: Some(messages),
|
|
758
|
+
tools: Some(vec![bash_tool]),
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
let spinner = Spinner::start("Thinking...".to_string());
|
|
762
|
+
let follow_up_result = self.provider.complete(&follow_up_request).await;
|
|
763
|
+
spinner.stop().await;
|
|
764
|
+
response = follow_up_result?;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let raw_text = response.text;
|
|
768
|
+
|
|
769
|
+
match parse_mcp_tool_call(&raw_text) {
|
|
770
|
+
Ok(Some(parsed)) => {
|
|
771
|
+
if let Some(prefix_text) = parsed.prefix.as_deref() {
|
|
772
|
+
let display = strip_file_blocks(prefix_text);
|
|
773
|
+
if !display.trim().is_empty() {
|
|
774
|
+
print_assistant_message(&display, &self.model)?;
|
|
775
|
+
}
|
|
776
|
+
self.record_message(
|
|
777
|
+
MessageRole::Assistant,
|
|
778
|
+
prefix_text.to_string(),
|
|
779
|
+
);
|
|
780
|
+
} else {
|
|
781
|
+
let note = format!(
|
|
782
|
+
"Calling MCP tool {}.{}...",
|
|
783
|
+
parsed.call.server, parsed.call.tool
|
|
784
|
+
);
|
|
785
|
+
print_assistant_message(¬e, &self.model)?;
|
|
786
|
+
self.record_message(MessageRole::Assistant, note);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
self.record_message(
|
|
790
|
+
MessageRole::Assistant,
|
|
791
|
+
parsed.command_text.clone(),
|
|
792
|
+
);
|
|
793
|
+
print_tool_command(&parsed.command_text)?;
|
|
794
|
+
|
|
795
|
+
if self.mcp_manager.is_none() {
|
|
796
|
+
stdout().execute(SetForegroundColor(Color::Yellow)).ok();
|
|
797
|
+
println!("MCP tool request ignored: no MCP manager configured.");
|
|
798
|
+
stdout().execute(ResetColor).ok();
|
|
799
|
+
|
|
800
|
+
self.record_message(
|
|
801
|
+
MessageRole::Tool {
|
|
802
|
+
server: parsed.call.server.clone(),
|
|
803
|
+
tool: parsed.call.tool.clone(),
|
|
804
|
+
},
|
|
805
|
+
"ERROR: MCP tools are not available in this session.".to_string(),
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if tool_calls >= max_tool_calls {
|
|
812
|
+
stdout().execute(SetForegroundColor(Color::Yellow)).ok();
|
|
813
|
+
println!("Skipping MCP tool call (limit of {} reached).", max_tool_calls);
|
|
814
|
+
stdout().execute(ResetColor).ok();
|
|
815
|
+
|
|
816
|
+
self.record_message(
|
|
817
|
+
MessageRole::Tool {
|
|
818
|
+
server: parsed.call.server.clone(),
|
|
819
|
+
tool: parsed.call.tool.clone(),
|
|
820
|
+
},
|
|
821
|
+
"ERROR: MCP tool call limit reached for this request.".to_string(),
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
let manager = self.mcp_manager.as_ref().unwrap();
|
|
828
|
+
|
|
829
|
+
let spinner = Spinner::start(format!(
|
|
830
|
+
"Running MCP {}.{}...",
|
|
831
|
+
parsed.call.server, parsed.call.tool
|
|
832
|
+
));
|
|
833
|
+
let tool_result = manager
|
|
834
|
+
.call_tool(
|
|
835
|
+
&parsed.call.server,
|
|
836
|
+
parsed.call.tool.clone(),
|
|
837
|
+
parsed.call.arguments.clone(),
|
|
838
|
+
)
|
|
839
|
+
.await;
|
|
840
|
+
spinner.stop().await;
|
|
841
|
+
|
|
842
|
+
let (mut tool_output, is_error) = match tool_result {
|
|
843
|
+
Ok(result) => {
|
|
844
|
+
let is_error = result.is_error.unwrap_or(false);
|
|
845
|
+
let mut text = format_tool_result(&result);
|
|
846
|
+
if text.trim().is_empty() {
|
|
847
|
+
if is_error {
|
|
848
|
+
text = "ERROR: MCP tool returned no content.".to_string();
|
|
849
|
+
} else {
|
|
850
|
+
text = "MCP tool returned no content.".to_string();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
(text, is_error)
|
|
854
|
+
}
|
|
855
|
+
Err(err) => (format!("ERROR: {}", err), true),
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
tool_calls += 1;
|
|
859
|
+
|
|
860
|
+
if is_error && !tool_output.starts_with("ERROR") {
|
|
861
|
+
tool_output = format!("ERROR: {}", tool_output);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
let stored_output = if tool_output.chars().count() > 8000 {
|
|
865
|
+
let mut truncated = truncate_for_display(&tool_output, 8000);
|
|
866
|
+
truncated.push_str("\n... (truncated for conversation history)");
|
|
867
|
+
truncated
|
|
868
|
+
} else {
|
|
869
|
+
tool_output.clone()
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
self.record_message(
|
|
873
|
+
MessageRole::Tool {
|
|
874
|
+
server: parsed.call.server.clone(),
|
|
875
|
+
tool: parsed.call.tool.clone(),
|
|
876
|
+
},
|
|
877
|
+
stored_output,
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
log_tool_execution(
|
|
881
|
+
&parsed.call.server,
|
|
882
|
+
&parsed.call.tool,
|
|
883
|
+
&tool_output,
|
|
884
|
+
is_error,
|
|
885
|
+
)?;
|
|
886
|
+
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
Ok(None) => {
|
|
890
|
+
final_response = Some(raw_text.clone());
|
|
891
|
+
self.record_message(MessageRole::Assistant, raw_text.clone());
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
Err(parse_error) => {
|
|
895
|
+
self.record_message(MessageRole::Assistant, raw_text.clone());
|
|
896
|
+
stdout().execute(SetForegroundColor(Color::Yellow)).ok();
|
|
897
|
+
println!("Warning: {}", parse_error);
|
|
898
|
+
stdout().execute(ResetColor).ok();
|
|
899
|
+
final_response = Some(raw_text.clone());
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if let Some(text) = final_response {
|
|
906
|
+
let printable = strip_file_blocks(&text);
|
|
907
|
+
if !printable.trim().is_empty() {
|
|
908
|
+
print_assistant_message(&printable, &self.model)?;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
let file_blocks = parse_file_blocks(&text);
|
|
912
|
+
if !file_blocks.is_empty() {
|
|
913
|
+
self.process_file_blocks(file_blocks).await?;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
Ok(())
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async fn process_file_blocks(&mut self, blocks: HashMap<PathBuf, String>) -> Result<()> {
|
|
921
|
+
if blocks.is_empty() {
|
|
922
|
+
return Ok(());
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
for (path, new_content) in blocks {
|
|
926
|
+
let full_path = self.session.working_directory.join(&path);
|
|
927
|
+
let existed = FileSystemOps::file_exists(&full_path).await;
|
|
928
|
+
let original = if existed {
|
|
929
|
+
FileSystemOps::read_file(&full_path).await?
|
|
930
|
+
} else {
|
|
931
|
+
String::new()
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
if original == new_content {
|
|
935
|
+
stdout().execute(SetForegroundColor(Color::DarkGrey)).ok();
|
|
936
|
+
println!("No changes for {}", path.display());
|
|
937
|
+
stdout().execute(ResetColor).ok();
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
print_file_change_summary(&path, &original, &new_content)?;
|
|
942
|
+
|
|
943
|
+
FileSystemOps::create_file(&full_path, &new_content).await?;
|
|
944
|
+
|
|
945
|
+
let mut out = stdout();
|
|
946
|
+
let message = if existed {
|
|
947
|
+
format!("Updated {}", path.display())
|
|
948
|
+
} else {
|
|
949
|
+
format!("Created {}", path.display())
|
|
950
|
+
};
|
|
951
|
+
out.execute(SetForegroundColor(Color::Green)).ok();
|
|
952
|
+
println!("{}", message);
|
|
953
|
+
out.execute(ResetColor).ok();
|
|
954
|
+
println!();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Since changes are applied immediately, clear any stale pending state
|
|
958
|
+
self.session.clear_pending_changes();
|
|
959
|
+
|
|
960
|
+
Ok(())
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
fn show_help(&self) -> Result<()> {
|
|
964
|
+
println!("Available commands:");
|
|
965
|
+
println!(" /help - Show this help message");
|
|
966
|
+
println!(" /apply - Apply pending file changes");
|
|
967
|
+
println!(" /diff - Show pending changes");
|
|
968
|
+
println!(" /undo - Clear pending changes");
|
|
969
|
+
println!(" /edit <file> - Load a file for editing");
|
|
970
|
+
println!(" /search <name> - Search for a symbol");
|
|
971
|
+
println!(" /context <query>- Find relevant files");
|
|
972
|
+
println!(" /files - List loaded files");
|
|
973
|
+
println!(" /model <name> - Switch to a different AI model");
|
|
974
|
+
println!(" Examples: claude-sonnet-4-5-20250929, claude-haiku-4-5,");
|
|
975
|
+
println!(" gpt-5-codex, gpt-4o");
|
|
976
|
+
println!(" /mcp - Show MCP servers and available tools");
|
|
977
|
+
println!(" /resume - Resume a previous chat session");
|
|
978
|
+
println!(" /clear - Clear conversation history");
|
|
979
|
+
println!(" /logout - Remove stored API keys and sign out");
|
|
980
|
+
println!(" /exit - Exit the session");
|
|
981
|
+
println!();
|
|
982
|
+
println!("Current model: {}", self.model);
|
|
983
|
+
println!("Current provider: {}", self.provider.name());
|
|
984
|
+
Ok(())
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async fn apply_changes(&mut self) -> Result<()> {
|
|
988
|
+
if self.session.pending_changes.is_empty() {
|
|
989
|
+
println!("No pending changes to apply");
|
|
990
|
+
return Ok(());
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
for change in &self.session.pending_changes {
|
|
994
|
+
let full_path = self.session.working_directory.join(&change.path);
|
|
995
|
+
FileSystemOps::create_file(&full_path, &change.new_content).await?;
|
|
996
|
+
println!("Applied changes to {}", change.path.display());
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
self.session.clear_pending_changes();
|
|
1000
|
+
println!("All changes applied successfully");
|
|
1001
|
+
|
|
1002
|
+
Ok(())
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fn show_diff(&self) -> Result<()> {
|
|
1006
|
+
if self.session.pending_changes.is_empty() {
|
|
1007
|
+
println!("No pending changes");
|
|
1008
|
+
return Ok(());
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
for change in &self.session.pending_changes {
|
|
1012
|
+
println!("--- {}", change.path.display());
|
|
1013
|
+
println!("+++ {}", change.path.display());
|
|
1014
|
+
print_diff(&change.original_content, &change.new_content);
|
|
1015
|
+
println!();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
Ok(())
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
fn undo_changes(&mut self) -> Result<()> {
|
|
1022
|
+
let count = self.session.pending_changes.len();
|
|
1023
|
+
self.session.clear_pending_changes();
|
|
1024
|
+
println!("Cleared {} pending change(s)", count);
|
|
1025
|
+
Ok(())
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async fn edit_file(&mut self, path: &str) -> Result<()> {
|
|
1029
|
+
if path.is_empty() {
|
|
1030
|
+
return Err(anyhow!("Usage: /edit <file>"));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
let file_path = PathBuf::from(path);
|
|
1034
|
+
let full_path = self.session.working_directory.join(&file_path);
|
|
1035
|
+
|
|
1036
|
+
if !FileSystemOps::file_exists(&full_path).await {
|
|
1037
|
+
return Err(anyhow!("File not found: {}", path));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
let content = FileSystemOps::read_file(&full_path).await?;
|
|
1041
|
+
self.session.load_file(file_path.clone(), content);
|
|
1042
|
+
|
|
1043
|
+
println!("Loaded {} for editing", path);
|
|
1044
|
+
|
|
1045
|
+
Ok(())
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async fn search_symbol(&self, name: &str) -> Result<()> {
|
|
1049
|
+
if name.is_empty() {
|
|
1050
|
+
return Err(anyhow!("Usage: /search <symbol>"));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
println!("Searching for symbol: {}", name);
|
|
1054
|
+
|
|
1055
|
+
let symbols = self.session.search_symbol(name)?;
|
|
1056
|
+
|
|
1057
|
+
if symbols.is_empty() {
|
|
1058
|
+
println!("No symbols found matching '{}'", name);
|
|
1059
|
+
} else {
|
|
1060
|
+
println!("Found {} symbol(s):", symbols.len());
|
|
1061
|
+
for symbol in symbols {
|
|
1062
|
+
println!(" {:?} {} in {}", symbol.kind, symbol.name, symbol.file.display());
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
Ok(())
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async fn find_context(&self, query: &str) -> Result<()> {
|
|
1070
|
+
if query.is_empty() {
|
|
1071
|
+
return Err(anyhow!("Usage: /context <query>"));
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
println!("Finding relevant context for: {}", query);
|
|
1075
|
+
|
|
1076
|
+
let files = self.session.get_relevant_context(query)?;
|
|
1077
|
+
|
|
1078
|
+
if files.is_empty() {
|
|
1079
|
+
println!("No relevant files found");
|
|
1080
|
+
} else {
|
|
1081
|
+
println!("Relevant files:");
|
|
1082
|
+
for file in files {
|
|
1083
|
+
println!(" {}", file.display());
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
Ok(())
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
fn list_files(&self) -> Result<()> {
|
|
1091
|
+
if self.session.current_files.is_empty() {
|
|
1092
|
+
println!("No files currently loaded");
|
|
1093
|
+
} else {
|
|
1094
|
+
println!("Currently loaded files:");
|
|
1095
|
+
for path in self.session.current_files.keys() {
|
|
1096
|
+
println!(" {}", path.display());
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
Ok(())
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
fn clear_history(&mut self) -> Result<()> {
|
|
1104
|
+
self.session.conversation_history.clear();
|
|
1105
|
+
self.session.reset_metadata();
|
|
1106
|
+
println!("Conversation history cleared");
|
|
1107
|
+
Ok(())
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async fn resume_session(&mut self, args: &str) -> Result<()> {
|
|
1111
|
+
let summaries = ConversationStore::list_summaries()?;
|
|
1112
|
+
|
|
1113
|
+
if summaries.is_empty() {
|
|
1114
|
+
println!("No saved sessions found.");
|
|
1115
|
+
return Ok(());
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
let trimmed = args.trim();
|
|
1119
|
+
|
|
1120
|
+
let selected_summary = if trimmed.is_empty() {
|
|
1121
|
+
let items: Vec<String> = summaries
|
|
1122
|
+
.iter()
|
|
1123
|
+
.map(|summary| format_session_line(summary))
|
|
1124
|
+
.collect();
|
|
1125
|
+
|
|
1126
|
+
let selection = Select::with_theme(&ColorfulTheme::default())
|
|
1127
|
+
.with_prompt("Select a session to resume")
|
|
1128
|
+
.items(&items)
|
|
1129
|
+
.default(0)
|
|
1130
|
+
.interact_opt()?;
|
|
1131
|
+
|
|
1132
|
+
match selection {
|
|
1133
|
+
Some(index) => summaries.get(index).cloned(),
|
|
1134
|
+
None => {
|
|
1135
|
+
println!("Resume cancelled.");
|
|
1136
|
+
return Ok(());
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
let needle = trimmed.to_ascii_lowercase();
|
|
1141
|
+
summaries
|
|
1142
|
+
.iter()
|
|
1143
|
+
.find(|summary| {
|
|
1144
|
+
summary.id.to_ascii_lowercase().starts_with(&needle)
|
|
1145
|
+
|| summary
|
|
1146
|
+
.title
|
|
1147
|
+
.to_ascii_lowercase()
|
|
1148
|
+
.contains(&needle)
|
|
1149
|
+
})
|
|
1150
|
+
.cloned()
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
let Some(summary) = selected_summary else {
|
|
1154
|
+
println!("No saved session matches '{}'.", trimmed);
|
|
1155
|
+
return Ok(());
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
let snapshot = ConversationStore::load_snapshot(&summary.id)?;
|
|
1159
|
+
|
|
1160
|
+
let previous_provider = self.provider_kind.clone();
|
|
1161
|
+
let provider_kind = Provider::from_str(&snapshot.provider).ok_or_else(|| {
|
|
1162
|
+
anyhow!(
|
|
1163
|
+
"Unknown provider '{}' in saved session",
|
|
1164
|
+
snapshot.provider
|
|
1165
|
+
)
|
|
1166
|
+
})?;
|
|
1167
|
+
|
|
1168
|
+
let switching_provider = provider_kind != previous_provider;
|
|
1169
|
+
|
|
1170
|
+
if switching_provider {
|
|
1171
|
+
let api_key = match provider_kind {
|
|
1172
|
+
Provider::Anthropic => self.config.get_anthropic_key(),
|
|
1173
|
+
Provider::OpenAi => self.config.get_openai_key(),
|
|
1174
|
+
Provider::Glm => self.config.get_glm_key(),
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
let client = ProviderClient::new(
|
|
1178
|
+
provider_kind.clone(),
|
|
1179
|
+
api_key,
|
|
1180
|
+
self.endpoint.clone(),
|
|
1181
|
+
self.timeout,
|
|
1182
|
+
)?;
|
|
1183
|
+
|
|
1184
|
+
self.provider = client;
|
|
1185
|
+
self.provider_kind = provider_kind;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
let previous_model = self.model.clone();
|
|
1189
|
+
self.model = snapshot.model.clone();
|
|
1190
|
+
self.session.conversation_history = snapshot.messages.clone();
|
|
1191
|
+
self.session.storage_id = Some(snapshot.id.clone());
|
|
1192
|
+
self.session.title = Some(snapshot.title.clone());
|
|
1193
|
+
self.session.created_at = Some(snapshot.created_at);
|
|
1194
|
+
self.session.updated_at = Some(snapshot.updated_at);
|
|
1195
|
+
self.session.pending_changes.clear();
|
|
1196
|
+
self.session.current_files.clear();
|
|
1197
|
+
|
|
1198
|
+
if !snapshot.working_directory.eq(&self.session.working_directory) {
|
|
1199
|
+
println!(
|
|
1200
|
+
"Note: saved session was created in {}",
|
|
1201
|
+
snapshot.working_directory.display()
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if switching_provider || self.model != previous_model {
|
|
1206
|
+
println!(
|
|
1207
|
+
"Active provider/model set to {} / {}",
|
|
1208
|
+
snapshot.provider, self.model
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
let formatted_time = snapshot
|
|
1213
|
+
.updated_at
|
|
1214
|
+
.with_timezone(&chrono::Local)
|
|
1215
|
+
.format("%Y-%m-%d %H:%M")
|
|
1216
|
+
.to_string();
|
|
1217
|
+
|
|
1218
|
+
println!(
|
|
1219
|
+
"Resumed session '{}' [{} • {}] ({} messages, updated {})",
|
|
1220
|
+
snapshot.title,
|
|
1221
|
+
snapshot.provider,
|
|
1222
|
+
snapshot.model,
|
|
1223
|
+
snapshot.message_count,
|
|
1224
|
+
formatted_time
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
if let Some(last_reply) = snapshot
|
|
1228
|
+
.messages
|
|
1229
|
+
.iter()
|
|
1230
|
+
.rev()
|
|
1231
|
+
.find(|message| matches!(message.role, MessageRole::Assistant))
|
|
1232
|
+
{
|
|
1233
|
+
let preview = truncate_for_display(&last_reply.content, 240);
|
|
1234
|
+
if !preview.trim().is_empty() {
|
|
1235
|
+
println!();
|
|
1236
|
+
print_assistant_message(&preview, &self.model)?;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
Ok(())
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
fn logout(&mut self) -> Result<()> {
|
|
1244
|
+
let config_path = Config::config_path()?;
|
|
1245
|
+
let had_keys = self.config.clear_api_keys()?;
|
|
1246
|
+
|
|
1247
|
+
let mut env_removed = false;
|
|
1248
|
+
for var in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GLM_API_KEY"] {
|
|
1249
|
+
if std::env::var(var).is_ok() {
|
|
1250
|
+
env_removed = true;
|
|
1251
|
+
}
|
|
1252
|
+
unsafe {
|
|
1253
|
+
std::env::remove_var(var);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if had_keys {
|
|
1258
|
+
println!(
|
|
1259
|
+
"Stored API keys removed from {}",
|
|
1260
|
+
config_path.display()
|
|
1261
|
+
);
|
|
1262
|
+
} else {
|
|
1263
|
+
println!(
|
|
1264
|
+
"No stored API keys found at {}",
|
|
1265
|
+
config_path.display()
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if env_removed {
|
|
1270
|
+
println!("Cleared API key environment variables for this session.");
|
|
1271
|
+
} else {
|
|
1272
|
+
println!("No API key environment variables were set for this session.");
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
println!("Restart ZarzCLI to complete logout. Run 'zarz config' to sign in again.");
|
|
1276
|
+
self.logout_requested = true;
|
|
1277
|
+
Ok(())
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async fn switch_model(&mut self, model_name: &str) -> Result<()> {
|
|
1281
|
+
if model_name.is_empty() {
|
|
1282
|
+
println!("Usage: /model <name>");
|
|
1283
|
+
println!();
|
|
1284
|
+
println!("Available models:");
|
|
1285
|
+
println!(" Anthropic Claude:");
|
|
1286
|
+
println!(" claude-sonnet-4-5-20250929 - Best for coding and agents");
|
|
1287
|
+
println!(" claude-sonnet-4-5-20250929-thinking - Extended thinking mode");
|
|
1288
|
+
println!(" claude-haiku-4-5 - Fast and cost-effective");
|
|
1289
|
+
println!(" claude-opus-4-1 - Most powerful");
|
|
1290
|
+
println!(" claude-sonnet-4 - General purpose");
|
|
1291
|
+
println!();
|
|
1292
|
+
println!(" OpenAI:");
|
|
1293
|
+
println!(" gpt-5-codex - Optimized for coding");
|
|
1294
|
+
println!(" gpt-4o - Multimodal");
|
|
1295
|
+
println!(" gpt-4-turbo - Fast and efficient");
|
|
1296
|
+
println!();
|
|
1297
|
+
println!(" GLM (Z.AI - International):");
|
|
1298
|
+
println!(" glm-4.6 - Best for coding (200K context)");
|
|
1299
|
+
println!(" glm-4.5 - Previous generation");
|
|
1300
|
+
println!();
|
|
1301
|
+
println!("Current model: {}", self.model);
|
|
1302
|
+
return Ok(());
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
let new_model = model_name.to_string();
|
|
1306
|
+
|
|
1307
|
+
let new_provider_kind = if new_model.starts_with("claude") {
|
|
1308
|
+
Provider::Anthropic
|
|
1309
|
+
} else if new_model.starts_with("gpt") {
|
|
1310
|
+
Provider::OpenAi
|
|
1311
|
+
} else if new_model.starts_with("glm") {
|
|
1312
|
+
Provider::Glm
|
|
1313
|
+
} else {
|
|
1314
|
+
return Err(anyhow!("Unknown model provider for '{}'", new_model));
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
if new_provider_kind != self.provider_kind {
|
|
1318
|
+
let api_key = match new_provider_kind {
|
|
1319
|
+
Provider::Anthropic => self.config.get_anthropic_key(),
|
|
1320
|
+
Provider::OpenAi => self.config.get_openai_key(),
|
|
1321
|
+
Provider::Glm => self.config.get_glm_key(),
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
let new_provider = ProviderClient::new(
|
|
1325
|
+
new_provider_kind.clone(),
|
|
1326
|
+
api_key,
|
|
1327
|
+
self.endpoint.clone(),
|
|
1328
|
+
self.timeout,
|
|
1329
|
+
)?;
|
|
1330
|
+
|
|
1331
|
+
self.provider = new_provider;
|
|
1332
|
+
self.provider_kind = new_provider_kind;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
self.model = new_model.clone();
|
|
1336
|
+
|
|
1337
|
+
println!("Switched to model: {}", new_model);
|
|
1338
|
+
println!("Provider: {}", self.provider.name());
|
|
1339
|
+
|
|
1340
|
+
Ok(())
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
async fn show_mcp_status(&self) -> Result<()> {
|
|
1344
|
+
match &self.mcp_manager {
|
|
1345
|
+
None => {
|
|
1346
|
+
println!("MCP support is not enabled.");
|
|
1347
|
+
println!();
|
|
1348
|
+
println!("To use MCP servers, add them with:");
|
|
1349
|
+
println!(" zarz mcp add <name> --command <cmd> --args <arg1> <arg2>");
|
|
1350
|
+
println!();
|
|
1351
|
+
println!("Example:");
|
|
1352
|
+
println!(" zarz mcp add firecrawl --command npx --args -y firecrawl-mcp \\");
|
|
1353
|
+
println!(" --env FIRECRAWL_API_KEY=your-key");
|
|
1354
|
+
Ok(())
|
|
1355
|
+
}
|
|
1356
|
+
Some(manager) => {
|
|
1357
|
+
let servers = manager.list_servers().await;
|
|
1358
|
+
|
|
1359
|
+
let tools_by_server = match manager.get_all_tools().await {
|
|
1360
|
+
Ok(map) => map,
|
|
1361
|
+
Err(e) => {
|
|
1362
|
+
eprintln!("Warning: Failed to fetch MCP tools: {}", e);
|
|
1363
|
+
HashMap::new()
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
if servers.is_empty() {
|
|
1368
|
+
println!("No MCP servers are currently running.");
|
|
1369
|
+
println!();
|
|
1370
|
+
println!("To add MCP servers, use:");
|
|
1371
|
+
println!(" zarz mcp add <name> --command <cmd> --args <arg1> <arg2>");
|
|
1372
|
+
return Ok(());
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
println!("Connected MCP Servers:");
|
|
1376
|
+
println!();
|
|
1377
|
+
|
|
1378
|
+
for server_name in &servers {
|
|
1379
|
+
// Get server info
|
|
1380
|
+
if let Some(info) = manager.get_server_info(server_name).await {
|
|
1381
|
+
stdout().execute(SetForegroundColor(Color::Green))?;
|
|
1382
|
+
println!(" ● {}", server_name);
|
|
1383
|
+
stdout().execute(ResetColor)?;
|
|
1384
|
+
println!(" Server: {}", info);
|
|
1385
|
+
} else {
|
|
1386
|
+
stdout().execute(SetForegroundColor(Color::Yellow))?;
|
|
1387
|
+
println!(" ◐ {}", server_name);
|
|
1388
|
+
stdout().execute(ResetColor)?;
|
|
1389
|
+
println!(" Status: Initializing...");
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Get tools for this server
|
|
1393
|
+
if let Some(tools) = tools_by_server.get(server_name) {
|
|
1394
|
+
if !tools.is_empty() {
|
|
1395
|
+
println!(" Tools ({}):", tools.len());
|
|
1396
|
+
for (i, tool) in tools.iter().enumerate() {
|
|
1397
|
+
if i < 5 {
|
|
1398
|
+
let description = tool
|
|
1399
|
+
.description
|
|
1400
|
+
.as_deref()
|
|
1401
|
+
.map(|d| truncate_inline(d, 160))
|
|
1402
|
+
.unwrap_or_else(|| "No description".to_string());
|
|
1403
|
+
println!(" - {}: {}", tool.name, description);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if tools.len() > 5 {
|
|
1407
|
+
println!(" ... and {} more", tools.len() - 5);
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
println!(" Tools: None available");
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
println!();
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
println!("Total servers: {}", servers.len());
|
|
1417
|
+
Ok(())
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
fn format_session_line(summary: &ConversationSummary) -> String {
|
|
1424
|
+
let time_str = summary
|
|
1425
|
+
.updated_at
|
|
1426
|
+
.with_timezone(&chrono::Local)
|
|
1427
|
+
.format("%Y-%m-%d %H:%M")
|
|
1428
|
+
.to_string();
|
|
1429
|
+
|
|
1430
|
+
let mut title = summary.title.clone();
|
|
1431
|
+
if title.len() > 60 {
|
|
1432
|
+
title.truncate(60);
|
|
1433
|
+
title.push('…');
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
let plural = if summary.message_count == 1 { "" } else { "s" };
|
|
1437
|
+
|
|
1438
|
+
format!(
|
|
1439
|
+
"{} │ {} [{} • {}] • {} message{} (id: {})",
|
|
1440
|
+
time_str,
|
|
1441
|
+
title,
|
|
1442
|
+
summary.provider,
|
|
1443
|
+
summary.model,
|
|
1444
|
+
summary.message_count,
|
|
1445
|
+
plural,
|
|
1446
|
+
summary.id
|
|
1447
|
+
)
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
#[derive(Clone)]
|
|
1451
|
+
struct CommandMenuHandler {
|
|
1452
|
+
pending_command: Arc<Mutex<Option<String>>>,
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
impl CommandMenuHandler {
|
|
1456
|
+
fn new(pending_command: Arc<Mutex<Option<String>>>) -> Self {
|
|
1457
|
+
Self { pending_command }
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
impl RlConditionalEventHandler for CommandMenuHandler {
|
|
1462
|
+
fn handle(
|
|
1463
|
+
&self,
|
|
1464
|
+
evt: &RlBindingEvent,
|
|
1465
|
+
_n: RlRepeatCount,
|
|
1466
|
+
_positive: bool,
|
|
1467
|
+
ctx: &RlEventContext,
|
|
1468
|
+
) -> Option<RlCmd> {
|
|
1469
|
+
let Some(key) = evt.get(0) else {
|
|
1470
|
+
return None;
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
let is_navigation = *key == RlKeyEvent(RlKeyCode::Down, RlModifiers::NONE)
|
|
1474
|
+
|| *key == RlKeyEvent(RlKeyCode::Up, RlModifiers::NONE);
|
|
1475
|
+
|
|
1476
|
+
if !is_navigation {
|
|
1477
|
+
return None;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
let line = ctx.line();
|
|
1481
|
+
if !line.starts_with('/') {
|
|
1482
|
+
return None;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
let pos = ctx.pos().min(line.len());
|
|
1486
|
+
let upto_cursor = &line[..pos];
|
|
1487
|
+
if upto_cursor.contains(' ') {
|
|
1488
|
+
return None;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
let partial = if pos > 1 { &line[1..pos] } else { "" };
|
|
1492
|
+
let args_suffix = line
|
|
1493
|
+
.find(' ')
|
|
1494
|
+
.map(|idx| line[idx..].to_string())
|
|
1495
|
+
.unwrap_or_default();
|
|
1496
|
+
|
|
1497
|
+
let matches: Vec<&CommandInfo> = COMMANDS
|
|
1498
|
+
.iter()
|
|
1499
|
+
.filter(|info| info.name.starts_with(partial))
|
|
1500
|
+
.collect();
|
|
1501
|
+
|
|
1502
|
+
if matches.is_empty() {
|
|
1503
|
+
return Some(RlCmd::Noop);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
let initial_index = match key.0 {
|
|
1507
|
+
RlKeyCode::Up => matches.len().saturating_sub(1),
|
|
1508
|
+
_ => 0,
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
match pick_command_menu(partial, &matches, initial_index) {
|
|
1512
|
+
Ok(Some(choice)) => {
|
|
1513
|
+
if let Ok(mut pending) = self.pending_command.lock() {
|
|
1514
|
+
let mut command = format!("/{}", choice.name);
|
|
1515
|
+
if !args_suffix.is_empty() {
|
|
1516
|
+
command.push_str(&args_suffix);
|
|
1517
|
+
}
|
|
1518
|
+
*pending = Some(command);
|
|
1519
|
+
}
|
|
1520
|
+
Some(RlCmd::Interrupt)
|
|
1521
|
+
}
|
|
1522
|
+
Ok(None) => {
|
|
1523
|
+
if let Ok(mut pending) = self.pending_command.lock() {
|
|
1524
|
+
if pending.is_some() {
|
|
1525
|
+
*pending = None;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
Some(RlCmd::Noop)
|
|
1529
|
+
}
|
|
1530
|
+
Err(err) => {
|
|
1531
|
+
eprintln!("Error: {:#}", err);
|
|
1532
|
+
Some(RlCmd::Noop)
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
fn pick_command_menu<'a>(
|
|
1539
|
+
partial: &str,
|
|
1540
|
+
matches: &'a [&'a CommandInfo],
|
|
1541
|
+
initial_index: usize,
|
|
1542
|
+
) -> Result<Option<&'a CommandInfo>> {
|
|
1543
|
+
if matches.is_empty() {
|
|
1544
|
+
return Ok(None);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
print!("\n\n");
|
|
1548
|
+
|
|
1549
|
+
let theme = ColorfulTheme::default();
|
|
1550
|
+
let items: Vec<String> = matches
|
|
1551
|
+
.iter()
|
|
1552
|
+
.map(|info| format!("/{:<16} {}", info.name, info.description))
|
|
1553
|
+
.collect();
|
|
1554
|
+
|
|
1555
|
+
let prompt = if partial.is_empty() {
|
|
1556
|
+
"Select a command".to_string()
|
|
1557
|
+
} else {
|
|
1558
|
+
format!("Commands matching '/{}'", partial)
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
let default_index = initial_index.min(items.len() - 1);
|
|
1562
|
+
|
|
1563
|
+
let selection = Select::with_theme(&theme)
|
|
1564
|
+
.with_prompt(prompt)
|
|
1565
|
+
.items(&items)
|
|
1566
|
+
.default(default_index)
|
|
1567
|
+
.clear(true)
|
|
1568
|
+
.report(false)
|
|
1569
|
+
.interact_opt()?;
|
|
1570
|
+
|
|
1571
|
+
Ok(selection.map(|idx| matches[idx]))
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
#[derive(Debug, Clone)]
|
|
1575
|
+
struct McpToolCall {
|
|
1576
|
+
server: String,
|
|
1577
|
+
tool: String,
|
|
1578
|
+
arguments: Option<HashMap<String, Value>>,
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
#[derive(Debug, Clone)]
|
|
1582
|
+
struct ParsedToolCall {
|
|
1583
|
+
prefix: Option<String>,
|
|
1584
|
+
command_text: String,
|
|
1585
|
+
call: McpToolCall,
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
fn build_tool_prompt_section(tools_by_server: &HashMap<String, Vec<McpTool>>) -> String {
|
|
1589
|
+
let mut section = String::from(
|
|
1590
|
+
"Available MCP tools:\n\
|
|
1591
|
+
Use CALL_MCP_TOOL server=<server_name> tool=<tool_name> args=<json_object> to request a tool.\n\
|
|
1592
|
+
Only request a tool when it will help solve the task.\n",
|
|
1593
|
+
);
|
|
1594
|
+
|
|
1595
|
+
let mut server_names: Vec<&String> = tools_by_server.keys().collect();
|
|
1596
|
+
server_names.sort();
|
|
1597
|
+
|
|
1598
|
+
for server in server_names {
|
|
1599
|
+
section.push_str(&format!("\nServer {}:\n", server));
|
|
1600
|
+
if let Some(tools) = tools_by_server.get(server) {
|
|
1601
|
+
let mut ordered: Vec<&McpTool> = tools.iter().collect();
|
|
1602
|
+
ordered.sort_by(|a, b| a.name.cmp(&b.name));
|
|
1603
|
+
|
|
1604
|
+
for tool in ordered.iter().take(8) {
|
|
1605
|
+
let description = tool
|
|
1606
|
+
.description
|
|
1607
|
+
.as_deref()
|
|
1608
|
+
.unwrap_or("No description provided");
|
|
1609
|
+
section.push_str(&format!(" - {}: {}\n", tool.name, description));
|
|
1610
|
+
|
|
1611
|
+
if let Ok(schema_str) = serde_json::to_string(&tool.input_schema) {
|
|
1612
|
+
let snippet = truncate_inline(&schema_str, 200);
|
|
1613
|
+
section.push_str(&format!(" schema: {}\n", snippet));
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if ordered.len() > 8 {
|
|
1618
|
+
section.push_str(&format!(" - ... ({} more)\n", ordered.len() - 8));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
section
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
fn parse_mcp_tool_call(text: &str) -> Result<Option<ParsedToolCall>> {
|
|
1627
|
+
let Some(command_index) = text.find("CALL_MCP_TOOL") else {
|
|
1628
|
+
return Ok(None);
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
let prefix_text = text[..command_index].trim();
|
|
1632
|
+
let prefix = if prefix_text.is_empty() {
|
|
1633
|
+
None
|
|
1634
|
+
} else {
|
|
1635
|
+
Some(prefix_text.to_string())
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
let command_and_rest = text[command_index..].trim();
|
|
1639
|
+
|
|
1640
|
+
// Ensure the tool call is the only content after the prefix (allow trailing whitespace)
|
|
1641
|
+
let (command_line, trailing_text) = if let Some(pos) = command_and_rest.find('\n') {
|
|
1642
|
+
let (line, rest) = command_and_rest.split_at(pos);
|
|
1643
|
+
(line.trim_end(), rest[pos + 1..].trim())
|
|
1644
|
+
} else {
|
|
1645
|
+
(command_and_rest, "")
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
if !trailing_text.is_empty() {
|
|
1649
|
+
anyhow::bail!("Additional text found after MCP tool call. Tool calls must be on a single line.");
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
let command_line = command_line.trim();
|
|
1653
|
+
if !command_line.starts_with("CALL_MCP_TOOL") {
|
|
1654
|
+
return Ok(None);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
let remainder = command_line["CALL_MCP_TOOL".len()..].trim();
|
|
1658
|
+
let mut parts = remainder.splitn(3, ' ');
|
|
1659
|
+
|
|
1660
|
+
let server_part = parts
|
|
1661
|
+
.next()
|
|
1662
|
+
.ok_or_else(|| anyhow!("Missing server component in MCP tool call"))?;
|
|
1663
|
+
let tool_part = parts
|
|
1664
|
+
.next()
|
|
1665
|
+
.ok_or_else(|| anyhow!("Missing tool component in MCP tool call"))?;
|
|
1666
|
+
let args_part = parts
|
|
1667
|
+
.next()
|
|
1668
|
+
.ok_or_else(|| anyhow!("Missing args component in MCP tool call"))?;
|
|
1669
|
+
|
|
1670
|
+
if !server_part.starts_with("server=") {
|
|
1671
|
+
anyhow::bail!("Expected server=<server_name> in MCP tool call");
|
|
1672
|
+
}
|
|
1673
|
+
if !tool_part.starts_with("tool=") {
|
|
1674
|
+
anyhow::bail!("Expected tool=<tool_name> in MCP tool call");
|
|
1675
|
+
}
|
|
1676
|
+
if !args_part.starts_with("args=") {
|
|
1677
|
+
anyhow::bail!("Expected args=<json> in MCP tool call");
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
let server = server_part["server=".len()..].to_string();
|
|
1681
|
+
let tool = tool_part["tool=".len()..].to_string();
|
|
1682
|
+
let args_raw = args_part["args=".len()..].trim();
|
|
1683
|
+
|
|
1684
|
+
if server.is_empty() {
|
|
1685
|
+
anyhow::bail!("Server name cannot be empty in MCP tool call");
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if tool.is_empty() {
|
|
1689
|
+
anyhow::bail!("Tool name cannot be empty in MCP tool call");
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
let arguments = if args_raw.eq_ignore_ascii_case("null") {
|
|
1693
|
+
None
|
|
1694
|
+
} else {
|
|
1695
|
+
let value: Value = serde_json::from_str(args_raw)
|
|
1696
|
+
.with_context(|| "Failed to parse MCP tool call arguments as JSON")?;
|
|
1697
|
+
|
|
1698
|
+
match value {
|
|
1699
|
+
Value::Null => None,
|
|
1700
|
+
Value::Object(map) => Some(map.into_iter().collect()),
|
|
1701
|
+
_ => {
|
|
1702
|
+
anyhow::bail!("Tool arguments must be a JSON object or null");
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
Ok(Some(ParsedToolCall {
|
|
1708
|
+
prefix,
|
|
1709
|
+
command_text: command_line.to_string(),
|
|
1710
|
+
call: McpToolCall {
|
|
1711
|
+
server,
|
|
1712
|
+
tool,
|
|
1713
|
+
arguments,
|
|
1714
|
+
},
|
|
1715
|
+
}))
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
fn format_tool_result(result: &CallToolResult) -> String {
|
|
1719
|
+
if result.content.is_empty() {
|
|
1720
|
+
return String::new();
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
let mut parts = Vec::new();
|
|
1724
|
+
|
|
1725
|
+
for item in &result.content {
|
|
1726
|
+
match item {
|
|
1727
|
+
ToolContent::Text { text } => parts.push(text.clone()),
|
|
1728
|
+
ToolContent::Image { mime_type, .. } => {
|
|
1729
|
+
parts.push(format!("Image content returned (mime type: {})", mime_type));
|
|
1730
|
+
}
|
|
1731
|
+
ToolContent::Resource { resource } => {
|
|
1732
|
+
let name = if resource.name.is_empty() {
|
|
1733
|
+
resource.uri.clone()
|
|
1734
|
+
} else {
|
|
1735
|
+
format!("{} ({})", resource.name, resource.uri)
|
|
1736
|
+
};
|
|
1737
|
+
parts.push(format!("Resource: {}", name));
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
parts.join("\n")
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
fn log_tool_execution(server: &str, tool: &str, output: &str, is_error: bool) -> Result<()> {
|
|
1746
|
+
let mut out = stdout();
|
|
1747
|
+
let color = if is_error { Color::Yellow } else { Color::DarkGrey };
|
|
1748
|
+
|
|
1749
|
+
out.execute(SetForegroundColor(color))?;
|
|
1750
|
+
|
|
1751
|
+
if is_error {
|
|
1752
|
+
println!("MCP tool {}.{} returned an error.", server, tool);
|
|
1753
|
+
} else {
|
|
1754
|
+
println!("MCP tool {}.{} executed.", server, tool);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
out.execute(ResetColor)?;
|
|
1758
|
+
|
|
1759
|
+
let trimmed = output.trim();
|
|
1760
|
+
if !trimmed.is_empty() {
|
|
1761
|
+
println!("{}", truncate_for_display(trimmed, 600));
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
println!();
|
|
1765
|
+
Ok(())
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
fn truncate_for_display(text: &str, max_chars: usize) -> String {
|
|
1769
|
+
let mut result = String::new();
|
|
1770
|
+
let mut count = 0;
|
|
1771
|
+
|
|
1772
|
+
for ch in text.chars() {
|
|
1773
|
+
if count >= max_chars {
|
|
1774
|
+
result.push_str("\n... (truncated)");
|
|
1775
|
+
break;
|
|
1776
|
+
}
|
|
1777
|
+
result.push(ch);
|
|
1778
|
+
count += 1;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
result
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
fn truncate_inline(text: &str, max_chars: usize) -> String {
|
|
1785
|
+
let mut result = String::new();
|
|
1786
|
+
let mut count = 0;
|
|
1787
|
+
|
|
1788
|
+
for ch in text.chars() {
|
|
1789
|
+
if count >= max_chars {
|
|
1790
|
+
result.push_str("... (truncated)");
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
if ch.is_control() && ch != '\n' && ch != '\t' {
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
result.push(ch);
|
|
1797
|
+
count += 1;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
result.replace('\n', " ")
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
fn strip_file_blocks(text: &str) -> String {
|
|
1804
|
+
let mut output = String::new();
|
|
1805
|
+
let mut lines = text.lines();
|
|
1806
|
+
|
|
1807
|
+
while let Some(line) = lines.next() {
|
|
1808
|
+
if line.trim_start().starts_with("```file:") {
|
|
1809
|
+
while let Some(next) = lines.next() {
|
|
1810
|
+
if next.trim() == "```" {
|
|
1811
|
+
break;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
output.push_str(line);
|
|
1817
|
+
output.push('\n');
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
output.trim_end_matches('\n').to_string()
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
fn get_model_display_name(model: &str) -> String {
|
|
1824
|
+
if model.contains("sonnet") {
|
|
1825
|
+
"Sonnet".to_string()
|
|
1826
|
+
} else if model.contains("opus") {
|
|
1827
|
+
"Opus".to_string()
|
|
1828
|
+
} else if model.contains("haiku") {
|
|
1829
|
+
"Haiku".to_string()
|
|
1830
|
+
} else if model.starts_with("gpt-5-codex") {
|
|
1831
|
+
"GPT-5 Codex".to_string()
|
|
1832
|
+
} else if model.starts_with("gpt-4o") {
|
|
1833
|
+
"GPT-4o".to_string()
|
|
1834
|
+
} else if model.starts_with("gpt-4-turbo") {
|
|
1835
|
+
"GPT-4 Turbo".to_string()
|
|
1836
|
+
} else if model.starts_with("glm-4.6") {
|
|
1837
|
+
"GLM-4.6".to_string()
|
|
1838
|
+
} else if model.starts_with("glm-4.5") {
|
|
1839
|
+
"GLM-4.5".to_string()
|
|
1840
|
+
} else if model.starts_with("glm") {
|
|
1841
|
+
"GLM".to_string()
|
|
1842
|
+
} else {
|
|
1843
|
+
model.to_string()
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
fn print_assistant_message(text: &str, model: &str) -> Result<()> {
|
|
1848
|
+
let mut out = stdout();
|
|
1849
|
+
let model_name = get_model_display_name(model);
|
|
1850
|
+
let trimmed_text = text.trim();
|
|
1851
|
+
|
|
1852
|
+
println!();
|
|
1853
|
+
out.execute(SetForegroundColor(Color::Green))?;
|
|
1854
|
+
out.execute(Print("● "))?;
|
|
1855
|
+
out.execute(Print(format!("{}:", model_name)))?;
|
|
1856
|
+
out.execute(ResetColor)?;
|
|
1857
|
+
println!();
|
|
1858
|
+
|
|
1859
|
+
print_formatted_text(trimmed_text, 2)?;
|
|
1860
|
+
println!();
|
|
1861
|
+
println!();
|
|
1862
|
+
Ok(())
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
fn print_formatted_text(text: &str, indent_spaces: usize) -> Result<()> {
|
|
1866
|
+
let mut out = stdout();
|
|
1867
|
+
let indent = " ".repeat(indent_spaces);
|
|
1868
|
+
let lines: Vec<&str> = text.lines().collect();
|
|
1869
|
+
|
|
1870
|
+
for (i, line) in lines.iter().enumerate() {
|
|
1871
|
+
print!("{}", indent);
|
|
1872
|
+
|
|
1873
|
+
let mut chars = line.chars().peekable();
|
|
1874
|
+
let mut buffer = String::new();
|
|
1875
|
+
|
|
1876
|
+
while let Some(ch) = chars.next() {
|
|
1877
|
+
if ch == '*' && chars.peek() == Some(&'*') {
|
|
1878
|
+
chars.next();
|
|
1879
|
+
|
|
1880
|
+
if !buffer.is_empty() {
|
|
1881
|
+
print!("{}", buffer);
|
|
1882
|
+
buffer.clear();
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
let mut bold_text = String::new();
|
|
1886
|
+
let mut found_closing = false;
|
|
1887
|
+
|
|
1888
|
+
while let Some(ch) = chars.next() {
|
|
1889
|
+
if ch == '*' && chars.peek() == Some(&'*') {
|
|
1890
|
+
chars.next();
|
|
1891
|
+
found_closing = true;
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
bold_text.push(ch);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if found_closing && !bold_text.is_empty() {
|
|
1898
|
+
out.execute(SetAttribute(Attribute::Bold))?;
|
|
1899
|
+
print!("{}", bold_text);
|
|
1900
|
+
out.execute(SetAttribute(Attribute::Reset))?;
|
|
1901
|
+
} else {
|
|
1902
|
+
print!("**{}", bold_text);
|
|
1903
|
+
}
|
|
1904
|
+
} else {
|
|
1905
|
+
buffer.push(ch);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
if !buffer.is_empty() {
|
|
1910
|
+
print!("{}", buffer);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
if i < lines.len() - 1 {
|
|
1914
|
+
println!();
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
Ok(())
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
fn print_tool_command(command: &str) -> Result<()> {
|
|
1922
|
+
let mut out = stdout();
|
|
1923
|
+
out.execute(SetForegroundColor(Color::DarkGrey))?;
|
|
1924
|
+
println!("{}", command);
|
|
1925
|
+
out.execute(ResetColor)?;
|
|
1926
|
+
Ok(())
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
struct Spinner {
|
|
1930
|
+
stop: Arc<AtomicBool>,
|
|
1931
|
+
handle: JoinHandle<()>,
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
impl Spinner {
|
|
1935
|
+
fn start(message: String) -> Self {
|
|
1936
|
+
let stop = Arc::new(AtomicBool::new(true));
|
|
1937
|
+
let stop_clone = stop.clone();
|
|
1938
|
+
|
|
1939
|
+
let handle = tokio::spawn(async move {
|
|
1940
|
+
let symbols = ['|', '/', '-', '\\'];
|
|
1941
|
+
let mut index = 0usize;
|
|
1942
|
+
|
|
1943
|
+
while stop_clone.load(Ordering::Relaxed) {
|
|
1944
|
+
let symbol = symbols[index % symbols.len()];
|
|
1945
|
+
let mut out = stdout();
|
|
1946
|
+
let _ = write!(out, "\r{} {}", symbol, message);
|
|
1947
|
+
let _ = out.flush();
|
|
1948
|
+
index = (index + 1) % symbols.len();
|
|
1949
|
+
sleep(Duration::from_millis(120)).await;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
let mut out = stdout();
|
|
1953
|
+
let _ = write!(out, "\r\x1B[K");
|
|
1954
|
+
let _ = out.flush();
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
Self { stop, handle }
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async fn stop(self) {
|
|
1961
|
+
self.stop.store(false, Ordering::Relaxed);
|
|
1962
|
+
let _ = self.handle.await;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
fn print_file_change_summary(path: &Path, before: &str, after: &str) -> Result<()> {
|
|
1967
|
+
let mut out = stdout();
|
|
1968
|
+
|
|
1969
|
+
let diff = TextDiff::from_lines(before, after);
|
|
1970
|
+
let mut additions = 0;
|
|
1971
|
+
let mut removals = 0;
|
|
1972
|
+
|
|
1973
|
+
for change in diff.iter_all_changes() {
|
|
1974
|
+
match change.tag() {
|
|
1975
|
+
ChangeTag::Delete => removals += 1,
|
|
1976
|
+
ChangeTag::Insert => additions += 1,
|
|
1977
|
+
_ => {}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
if before.is_empty() {
|
|
1982
|
+
out.execute(SetForegroundColor(Color::Green)).ok();
|
|
1983
|
+
println!("● Create({})", path.display());
|
|
1984
|
+
out.execute(ResetColor).ok();
|
|
1985
|
+
println!(" ⎿ Created {} with {} lines", path.display(), additions);
|
|
1986
|
+
} else {
|
|
1987
|
+
out.execute(SetForegroundColor(Color::Green)).ok();
|
|
1988
|
+
println!("● Update({})", path.display());
|
|
1989
|
+
out.execute(ResetColor).ok();
|
|
1990
|
+
println!(" ⎿ Updated {} with {} addition{} and {} removal{}",
|
|
1991
|
+
path.display(),
|
|
1992
|
+
additions, if additions == 1 { "" } else { "s" },
|
|
1993
|
+
removals, if removals == 1 { "" } else { "s" }
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
let mut old_line = 1usize;
|
|
1998
|
+
let mut new_line = 1usize;
|
|
1999
|
+
let mut context_before: Vec<(usize, String)> = Vec::new();
|
|
2000
|
+
let max_context = 3;
|
|
2001
|
+
|
|
2002
|
+
for change in diff.iter_all_changes() {
|
|
2003
|
+
let value = change.value().trim_end_matches('\n');
|
|
2004
|
+
match change.tag() {
|
|
2005
|
+
ChangeTag::Equal => {
|
|
2006
|
+
context_before.push((old_line, value.to_string()));
|
|
2007
|
+
if context_before.len() > max_context {
|
|
2008
|
+
context_before.remove(0);
|
|
2009
|
+
}
|
|
2010
|
+
old_line += 1;
|
|
2011
|
+
new_line += 1;
|
|
2012
|
+
}
|
|
2013
|
+
ChangeTag::Delete => {
|
|
2014
|
+
for (line_num, text) in &context_before {
|
|
2015
|
+
print_context_line(*line_num, text);
|
|
2016
|
+
}
|
|
2017
|
+
context_before.clear();
|
|
2018
|
+
|
|
2019
|
+
print_diff_line_with_bg('-', old_line, value, Color::Rgb { r: 60, g: 20, b: 20 })?;
|
|
2020
|
+
old_line += 1;
|
|
2021
|
+
}
|
|
2022
|
+
ChangeTag::Insert => {
|
|
2023
|
+
for (line_num, text) in &context_before {
|
|
2024
|
+
print_context_line(*line_num, text);
|
|
2025
|
+
}
|
|
2026
|
+
context_before.clear();
|
|
2027
|
+
|
|
2028
|
+
print_diff_line_with_bg('+', new_line, value, Color::Rgb { r: 20, g: 60, b: 20 })?;
|
|
2029
|
+
new_line += 1;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
println!();
|
|
2035
|
+
Ok(())
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
fn print_context_line(line_number: usize, text: &str) {
|
|
2039
|
+
println!(" {:>5} {}", line_number, text);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
fn print_diff_line_with_bg(prefix: char, line_number: usize, text: &str, bg_color: Color) -> Result<()> {
|
|
2043
|
+
let mut out = stdout();
|
|
2044
|
+
|
|
2045
|
+
out.execute(Print(format!(" {:>5} ", line_number)))?;
|
|
2046
|
+
|
|
2047
|
+
let prefix_color = if prefix == '-' { Color::Red } else { Color::Green };
|
|
2048
|
+
out.execute(SetBackgroundColor(bg_color))?;
|
|
2049
|
+
out.execute(SetForegroundColor(prefix_color))?;
|
|
2050
|
+
out.execute(Print(prefix))?;
|
|
2051
|
+
|
|
2052
|
+
if !text.is_empty() {
|
|
2053
|
+
out.execute(SetForegroundColor(Color::White))?;
|
|
2054
|
+
out.execute(Print(format!(" {}", text)))?;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
out.execute(ResetColor)?;
|
|
2058
|
+
println!();
|
|
2059
|
+
Ok(())
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
fn execute_bash_command(command: &str) -> Result<String> {
|
|
2064
|
+
use std::process::Command;
|
|
2065
|
+
|
|
2066
|
+
let output = if cfg!(target_os = "windows") {
|
|
2067
|
+
Command::new("cmd")
|
|
2068
|
+
.args(&["/C", command])
|
|
2069
|
+
.output()
|
|
2070
|
+
.context("Failed to execute bash command")?
|
|
2071
|
+
} else {
|
|
2072
|
+
Command::new("sh")
|
|
2073
|
+
.arg("-c")
|
|
2074
|
+
.arg(command)
|
|
2075
|
+
.output()
|
|
2076
|
+
.context("Failed to execute bash command")?
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
2080
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
2081
|
+
|
|
2082
|
+
let mut result = String::new();
|
|
2083
|
+
if !stdout.is_empty() {
|
|
2084
|
+
result.push_str(&stdout);
|
|
2085
|
+
}
|
|
2086
|
+
if !stderr.is_empty() {
|
|
2087
|
+
if !result.is_empty() {
|
|
2088
|
+
result.push_str("\n");
|
|
2089
|
+
}
|
|
2090
|
+
result.push_str("STDERR:\n");
|
|
2091
|
+
result.push_str(&stderr);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if result.is_empty() {
|
|
2095
|
+
result = "(command produced no output)".to_string();
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
Ok(result)
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
fn parse_file_blocks(input: &str) -> HashMap<PathBuf, String> {
|
|
2102
|
+
let mut map = HashMap::new();
|
|
2103
|
+
let mut lines = input.lines();
|
|
2104
|
+
|
|
2105
|
+
while let Some(line) = lines.next() {
|
|
2106
|
+
if let Some(rest) = line.strip_prefix("```file:") {
|
|
2107
|
+
let file_path = normalize_response_path(rest);
|
|
2108
|
+
let mut content = String::new();
|
|
2109
|
+
|
|
2110
|
+
while let Some(next_line) = lines.next() {
|
|
2111
|
+
if next_line.trim() == "```" {
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
content.push_str(next_line);
|
|
2115
|
+
content.push('\n');
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
if content.ends_with('\n') {
|
|
2119
|
+
content.pop();
|
|
2120
|
+
if content.ends_with('\r') {
|
|
2121
|
+
content.pop();
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
map.insert(file_path, content);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
map
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
fn normalize_response_path(raw: &str) -> PathBuf {
|
|
2133
|
+
let mut trimmed = raw.trim();
|
|
2134
|
+
while let Some(rest) = trimmed.strip_prefix("./") {
|
|
2135
|
+
trimmed = rest;
|
|
2136
|
+
}
|
|
2137
|
+
while let Some(rest) = trimmed.strip_prefix(".\\") {
|
|
2138
|
+
trimmed = rest;
|
|
2139
|
+
}
|
|
2140
|
+
let normalized = trimmed.replace('\\', "/");
|
|
2141
|
+
PathBuf::from(normalized)
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
fn print_diff(before: &str, after: &str) {
|
|
2145
|
+
let diff = TextDiff::from_lines(before, after);
|
|
2146
|
+
for change in diff.iter_all_changes() {
|
|
2147
|
+
match change.tag() {
|
|
2148
|
+
ChangeTag::Delete => print!("-{}", change),
|
|
2149
|
+
ChangeTag::Insert => print!("+{}", change),
|
|
2150
|
+
ChangeTag::Equal => print!(" {}", change),
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|