Add interactive chat UI with ratatui
- Implements a full-screen terminal chat interface in src/chat.rs - Updates Cargo.toml with documentation link - Updates dependencies in Cargo.lock - Updates main.rs to describe interactive and ACP modes
Seto Elkahfi committed
Apr 12, 2026 at 11:30 UTC
c5b17d837e7a2761d00f5a7e1cc2b365c91c3afb
4 files changed
+899
-74
Cargo.lock
+223
-56
@@ -113,13 +113,12 @@ dependencies = [
113
114
[[package]]
115
name = "annotate-snippets"
116
-version = "0.12.15"
116
+version = "0.12.5"
117
source = "registry+https://github.com/rust-lang/crates.io-index"
118
-checksum = "92570a3f9c98e7e84df84b71d0965ac99b1871fcd75a3773a3bd1bad13f64cf7"
118
+checksum = "96401ca08501972288ecbcde33902fce858bf73fbcbdf91dab8c3a9544e106bb"
119
dependencies = [
120
"anstyle",
121
- "memchr",
122
- "unicode-width 0.2.2",
121
+ "unicode-width 0.2.0",
122
]
123
124
[[package]]
@@ -725,6 +724,12 @@ dependencies = [
724
"thiserror 2.0.18",
725
]
726
727
+[[package]]
728
+name = "cassowary"
729
+version = "0.3.0"
730
+source = "registry+https://github.com/rust-lang/crates.io-index"
731
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
732
+
733
[[package]]
734
name = "castaway"
735
version = "0.2.4"
@@ -879,6 +884,20 @@ dependencies = [
884
"memchr",
885
]
886
887
+[[package]]
888
+name = "compact_str"
889
+version = "0.8.1"
890
+source = "registry+https://github.com/rust-lang/crates.io-index"
891
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
892
+dependencies = [
893
+ "castaway",
894
+ "cfg-if",
895
+ "itoa",
896
+ "rustversion",
897
+ "ryu",
898
+ "static_assertions",
899
+]
900
+
901
[[package]]
902
name = "compact_str"
903
version = "0.9.0"
@@ -912,7 +931,7 @@ dependencies = [
931
"encode_unicode",
932
"libc",
933
"once_cell",
915
- "unicode-width 0.2.2",
934
+ "unicode-width 0.2.0",
935
"windows-sys 0.59.0",
936
]
937
@@ -924,7 +943,7 @@ checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
943
dependencies = [
944
"encode_unicode",
945
"libc",
927
- "unicode-width 0.2.2",
946
+ "unicode-width 0.2.0",
947
"windows-sys 0.61.2",
948
]
949
@@ -1037,17 +1056,32 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
1056
1057
[[package]]
1058
name = "crossterm"
1040
-version = "0.29.0"
1059
+version = "0.25.0"
1060
source = "registry+https://github.com/rust-lang/crates.io-index"
1042
-checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
1061
+checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
1062
+dependencies = [
1063
+ "bitflags 1.3.2",
1064
+ "crossterm_winapi",
1065
+ "libc",
1066
+ "mio 0.8.11",
1067
+ "parking_lot",
1068
+ "signal-hook",
1069
+ "signal-hook-mio",
1070
+ "winapi",
1071
+]
1072
+
1073
+[[package]]
1074
+name = "crossterm"
1075
+version = "0.28.1"
1076
+source = "registry+https://github.com/rust-lang/crates.io-index"
1077
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
1078
dependencies = [
1079
"bitflags 2.11.0",
1080
"crossterm_winapi",
1046
- "derive_more",
1047
- "document-features",
1048
- "mio",
1081
+ "futures-core",
1082
+ "mio 1.2.0",
1083
"parking_lot",
1050
- "rustix",
1084
+ "rustix 0.38.44",
1085
"signal-hook",
1086
"signal-hook-mio",
1087
"winapi",
@@ -1401,15 +1435,6 @@ version = "1.1.1"
1435
source = "registry+https://github.com/rust-lang/crates.io-index"
1436
checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359"
1437
1404
-[[package]]
1405
-name = "document-features"
1406
-version = "0.2.12"
1407
-source = "registry+https://github.com/rust-lang/crates.io-index"
1408
-checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
1409
-dependencies = [
1410
- "litrs",
1411
-]
1412
-
1438
[[package]]
1439
name = "dtoa"
1440
version = "1.0.11"
@@ -2155,7 +2180,7 @@ version = "0.2.24"
2180
source = "registry+https://github.com/rust-lang/crates.io-index"
2181
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
2182
dependencies = [
2158
- "unicode-width 0.2.2",
2183
+ "unicode-width 0.2.0",
2184
]
2185
2186
[[package]]
@@ -2378,7 +2403,7 @@ dependencies = [
2403
"html5ever 0.38.0",
2404
"tendril 0.5.0",
2405
"thiserror 2.0.18",
2381
- "unicode-width 0.2.2",
2406
+ "unicode-width 0.2.0",
2407
]
2408
2409
[[package]]
@@ -2714,7 +2739,7 @@ dependencies = [
2739
"console 0.15.11",
2740
"number_prefix",
2741
"portable-atomic",
2717
- "unicode-width 0.2.2",
2742
+ "unicode-width 0.2.0",
2743
"web-time",
2744
]
2745
@@ -2727,11 +2752,33 @@ dependencies = [
2752
"console 0.16.3",
2753
"portable-atomic",
2754
"rayon",
2730
- "unicode-width 0.2.2",
2755
+ "unicode-width 0.2.0",
2756
"unit-prefix",
2757
"web-time",
2758
]
2759
2760
+[[package]]
2761
+name = "indoc"
2762
+version = "2.0.7"
2763
+source = "registry+https://github.com/rust-lang/crates.io-index"
2764
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
2765
+dependencies = [
2766
+ "rustversion",
2767
+]
2768
+
2769
+[[package]]
2770
+name = "instability"
2771
+version = "0.3.12"
2772
+source = "registry+https://github.com/rust-lang/crates.io-index"
2773
+checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
2774
+dependencies = [
2775
+ "darling 0.23.0",
2776
+ "indoc",
2777
+ "proc-macro2",
2778
+ "quote",
2779
+ "syn 2.0.117",
2780
+]
2781
+
2782
[[package]]
2783
name = "interpolate_name"
2784
version = "0.2.4"
@@ -2778,6 +2825,15 @@ version = "1.70.2"
2825
source = "registry+https://github.com/rust-lang/crates.io-index"
2826
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
2827
2828
+[[package]]
2829
+name = "itertools"
2830
+version = "0.13.0"
2831
+source = "registry+https://github.com/rust-lang/crates.io-index"
2832
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
2833
+dependencies = [
2834
+ "either",
2835
+]
2836
+
2837
[[package]]
2838
name = "itertools"
2839
version = "0.14.0"
@@ -2942,6 +2998,12 @@ dependencies = [
2998
"libc",
2999
]
3000
3001
+[[package]]
3002
+name = "linux-raw-sys"
3003
+version = "0.4.15"
3004
+source = "registry+https://github.com/rust-lang/crates.io-index"
3005
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
3006
+
3007
[[package]]
3008
name = "linux-raw-sys"
3009
version = "0.12.1"
@@ -2954,12 +3016,6 @@ version = "0.8.2"
3016
source = "registry+https://github.com/rust-lang/crates.io-index"
3017
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
3018
2957
-[[package]]
2958
-name = "litrs"
2959
-version = "1.0.0"
2960
-source = "registry+https://github.com/rust-lang/crates.io-index"
2961
-checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
2962
-
3019
[[package]]
3020
name = "llguidance"
3021
version = "1.7.2"
@@ -3027,6 +3083,15 @@ dependencies = [
3083
"vob",
3084
]
3085
3086
+[[package]]
3087
+name = "lru"
3088
+version = "0.12.5"
3089
+source = "registry+https://github.com/rust-lang/crates.io-index"
3090
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
3091
+dependencies = [
3092
+ "hashbrown 0.15.5",
3093
+]
3094
+
3095
[[package]]
3096
name = "lru-slab"
3097
version = "0.1.2"
@@ -3205,6 +3270,18 @@ dependencies = [
3270
"simd-adler32",
3271
]
3272
3273
+[[package]]
3274
+name = "mio"
3275
+version = "0.8.11"
3276
+source = "registry+https://github.com/rust-lang/crates.io-index"
3277
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
3278
+dependencies = [
3279
+ "libc",
3280
+ "log",
3281
+ "wasi",
3282
+ "windows-sys 0.48.0",
3283
+]
3284
+
3285
[[package]]
3286
name = "mio"
3287
version = "1.2.0"
@@ -3220,7 +3297,8 @@ dependencies = [
3297
[[package]]
3298
name = "mistralrs"
3299
version = "0.8.1"
3223
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3300
+source = "registry+https://github.com/rust-lang/crates.io-index"
3301
+checksum = "9bb0a83340b4492ebba9760ba6845364de369c7e10c1f91af8733d574c7405fa"
3302
dependencies = [
3303
"anyhow",
3304
"candle-core",
@@ -3247,7 +3325,8 @@ dependencies = [
3325
[[package]]
3326
name = "mistralrs-audio"
3327
version = "0.8.1"
3250
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3328
+source = "registry+https://github.com/rust-lang/crates.io-index"
3329
+checksum = "5ac5d36f634c7c20c45845bc995be3b6387ab01048410386a5b180cfeaf89c72"
3330
dependencies = [
3331
"anyhow",
3332
"apodize",
@@ -3258,7 +3337,8 @@ dependencies = [
3337
[[package]]
3338
name = "mistralrs-core"
3339
version = "0.8.1"
3261
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3340
+source = "registry+https://github.com/rust-lang/crates.io-index"
3341
+checksum = "a2b8b9e5c94491d9ceeded3a30291cb6af6ae74810a5776dd55e3bbb8b3429d4"
3342
dependencies = [
3343
"ahash",
3344
"akin",
@@ -3294,7 +3374,7 @@ dependencies = [
3374
"indexmap 2.14.0",
3375
"indicatif 0.18.4",
3376
"interprocess",
3297
- "itertools",
3377
+ "itertools 0.14.0",
3378
"libc",
3379
"llguidance",
3380
"lrtable",
@@ -3355,7 +3435,8 @@ dependencies = [
3435
[[package]]
3436
name = "mistralrs-macros"
3437
version = "0.8.1"
3358
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3438
+source = "registry+https://github.com/rust-lang/crates.io-index"
3439
+checksum = "5aa9b4794322d3f89fe61d21f33f67be494c16d5bd869604b111218f32544d32"
3440
dependencies = [
3441
"darling 0.23.0",
3442
"proc-macro2",
@@ -3366,7 +3447,8 @@ dependencies = [
3447
[[package]]
3448
name = "mistralrs-mcp"
3449
version = "0.8.1"
3369
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3450
+source = "registry+https://github.com/rust-lang/crates.io-index"
3451
+checksum = "4fa97d4e3189ed80ebbc730b7da5b2356df0ae004ab489066249340c33c089ab"
3452
dependencies = [
3453
"anyhow",
3454
"async-trait",
@@ -3386,7 +3468,8 @@ dependencies = [
3468
[[package]]
3469
name = "mistralrs-paged-attn"
3470
version = "0.8.1"
3389
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3471
+source = "registry+https://github.com/rust-lang/crates.io-index"
3472
+checksum = "6e53ddf1537426997b46abdadbe6ca8a7ce7668004ed2b7cf00115016558bae8"
3473
dependencies = [
3474
"anyhow",
3475
"candle-core",
@@ -3402,7 +3485,8 @@ dependencies = [
3485
[[package]]
3486
name = "mistralrs-quant"
3487
version = "0.8.1"
3405
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3488
+source = "registry+https://github.com/rust-lang/crates.io-index"
3489
+checksum = "980715493d252e9aaf0779c4c101576d67ef4dd9812bfd77a54987f6f17526f4"
3490
dependencies = [
3491
"byteorder",
3492
"candle-core",
@@ -3431,7 +3515,8 @@ dependencies = [
3515
[[package]]
3516
name = "mistralrs-vision"
3517
version = "0.8.1"
3434
-source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006"
3518
+source = "registry+https://github.com/rust-lang/crates.io-index"
3519
+checksum = "10046a3da2de5b702d3e829b5ddef7aa829ad454819dcc4876dea481a6cb5456"
3520
dependencies = [
3521
"candle-core",
3522
"image",
@@ -3745,7 +3830,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
3830
3831
[[package]]
3832
name = "onde"
3748
-version = "0.1.3"
3833
+version = "0.1.1"
3834
+source = "registry+https://github.com/rust-lang/crates.io-index"
3835
+checksum = "5436633863ffc31311f529439353ba06a436dd661d2a5d93c8793c3500123a13"
3836
dependencies = [
3837
"anyhow",
3838
"cc",
@@ -4317,6 +4404,27 @@ dependencies = [
4404
"rand_core 0.9.5",
4405
]
4406
4407
+[[package]]
4408
+name = "ratatui"
4409
+version = "0.29.0"
4410
+source = "registry+https://github.com/rust-lang/crates.io-index"
4411
+checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
4412
+dependencies = [
4413
+ "bitflags 2.11.0",
4414
+ "cassowary",
4415
+ "compact_str 0.8.1",
4416
+ "crossterm 0.28.1",
4417
+ "indoc",
4418
+ "instability",
4419
+ "itertools 0.13.0",
4420
+ "lru",
4421
+ "paste",
4422
+ "strum 0.26.3",
4423
+ "unicode-segmentation",
4424
+ "unicode-truncate",
4425
+ "unicode-width 0.2.0",
4426
+]
4427
+
4428
[[package]]
4429
name = "rav1e"
4430
version = "0.8.1"
@@ -4333,7 +4441,7 @@ dependencies = [
4441
"built",
4442
"cfg-if",
4443
"interpolate_name",
4336
- "itertools",
4444
+ "itertools 0.14.0",
4445
"libc",
4446
"libfuzzer-sys",
4447
"log",
@@ -4399,7 +4507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4507
checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f"
4508
dependencies = [
4509
"either",
4402
- "itertools",
4510
+ "itertools 0.14.0",
4511
"rayon",
4512
]
4513
@@ -4680,6 +4788,19 @@ dependencies = [
4788
"transpose",
4789
]
4790
4791
+[[package]]
4792
+name = "rustix"
4793
+version = "0.38.44"
4794
+source = "registry+https://github.com/rust-lang/crates.io-index"
4795
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
4796
+dependencies = [
4797
+ "bitflags 2.11.0",
4798
+ "errno",
4799
+ "libc",
4800
+ "linux-raw-sys 0.4.15",
4801
+ "windows-sys 0.59.0",
4802
+]
4803
+
4804
[[package]]
4805
name = "rustix"
4806
version = "1.1.4"
@@ -4689,7 +4810,7 @@ dependencies = [
4810
"bitflags 2.11.0",
4811
"errno",
4812
"libc",
4692
- "linux-raw-sys",
4813
+ "linux-raw-sys 0.12.1",
4814
"windows-sys 0.61.2",
4815
]
4816
@@ -5181,10 +5302,12 @@ dependencies = [
5302
"agent-client-protocol",
5303
"anyhow",
5304
"async-trait",
5305
+ "crossterm 0.28.1",
5306
"env_logger",
5307
"futures",
5308
"log",
5309
"onde",
5310
+ "ratatui",
5311
"tokio",
5312
"tokio-util",
5313
"uuid 1.23.0",
@@ -5207,7 +5330,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
5330
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
5331
dependencies = [
5332
"libc",
5210
- "mio",
5333
+ "mio 0.8.11",
5334
+ "mio 1.2.0",
5335
"signal-hook",
5336
]
5337
@@ -5437,6 +5561,15 @@ dependencies = [
5561
"syn 1.0.109",
5562
]
5563
5564
+[[package]]
5565
+name = "strum"
5566
+version = "0.26.3"
5567
+source = "registry+https://github.com/rust-lang/crates.io-index"
5568
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
5569
+dependencies = [
5570
+ "strum_macros 0.26.4",
5571
+]
5572
+
5573
[[package]]
5574
name = "strum"
5575
version = "0.27.2"
@@ -5455,6 +5588,19 @@ dependencies = [
5588
"strum_macros 0.28.0",
5589
]
5590
5591
+[[package]]
5592
+name = "strum_macros"
5593
+version = "0.26.4"
5594
+source = "registry+https://github.com/rust-lang/crates.io-index"
5595
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
5596
+dependencies = [
5597
+ "heck 0.5.0",
5598
+ "proc-macro2",
5599
+ "quote",
5600
+ "rustversion",
5601
+ "syn 2.0.117",
5602
+]
5603
+
5604
[[package]]
5605
name = "strum_macros"
5606
version = "0.27.2"
@@ -5720,7 +5866,7 @@ dependencies = [
5866
"fastrand",
5867
"getrandom 0.4.2",
5868
"once_cell",
5723
- "rustix",
5869
+ "rustix 1.1.4",
5870
"windows-sys 0.61.2",
5871
]
5872
@@ -5751,7 +5897,7 @@ version = "0.4.4"
5897
source = "registry+https://github.com/rust-lang/crates.io-index"
5898
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
5899
dependencies = [
5754
- "rustix",
5900
+ "rustix 1.1.4",
5901
"windows-sys 0.61.2",
5902
]
5903
@@ -5900,13 +6046,13 @@ checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476"
6046
dependencies = [
6047
"ahash",
6048
"aho-corasick",
5903
- "compact_str",
6049
+ "compact_str 0.9.0",
6050
"dary_heap",
6051
"derive_builder",
6052
"esaxx-rs",
6053
"fancy-regex 0.14.0",
6054
"getrandom 0.3.4",
5909
- "itertools",
6055
+ "itertools 0.14.0",
6056
"log",
6057
"macro_rules_attribute",
6058
"monostate",
@@ -5933,12 +6079,12 @@ checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223"
6079
dependencies = [
6080
"ahash",
6081
"aho-corasick",
5936
- "compact_str",
6082
+ "compact_str 0.9.0",
6083
"dary_heap",
6084
"derive_builder",
6085
"esaxx-rs",
6086
"getrandom 0.3.4",
5941
- "itertools",
6087
+ "itertools 0.14.0",
6088
"log",
6089
"macro_rules_attribute",
6090
"monostate",
@@ -5966,7 +6112,7 @@ checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
6112
dependencies = [
6113
"bytes",
6114
"libc",
5969
- "mio",
6115
+ "mio 1.2.0",
6116
"parking_lot",
6117
"pin-project-lite",
6118
"signal-hook-registry",
@@ -6146,10 +6292,11 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
6292
[[package]]
6293
name = "tqdm"
6294
version = "0.8.0"
6149
-source = "git+https://github.com/setoelkahfi/tqdm?branch=deps%2Fbump-crossterm#41e4182829136e6dadbdd1c36af824d19219147a"
6295
+source = "registry+https://github.com/rust-lang/crates.io-index"
6296
+checksum = "b316d5c2ac649ca856dacd487d0ebb94f3b746bada51355d93dd2c007ab62a2e"
6297
dependencies = [
6298
"anyhow",
6152
- "crossterm",
6299
+ "crossterm 0.25.0",
6300
"once_cell",
6301
]
6302
@@ -6349,6 +6496,17 @@ version = "1.13.2"
6496
source = "registry+https://github.com/rust-lang/crates.io-index"
6497
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
6498
6499
+[[package]]
6500
+name = "unicode-truncate"
6501
+version = "1.1.0"
6502
+source = "registry+https://github.com/rust-lang/crates.io-index"
6503
+checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
6504
+dependencies = [
6505
+ "itertools 0.13.0",
6506
+ "unicode-segmentation",
6507
+ "unicode-width 0.1.14",
6508
+]
6509
+
6510
[[package]]
6511
name = "unicode-width"
6512
version = "0.1.14"
@@ -6357,9 +6515,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
6515
6516
[[package]]
6517
name = "unicode-width"
6360
-version = "0.2.2"
6518
+version = "0.2.0"
6519
source = "registry+https://github.com/rust-lang/crates.io-index"
6362
-checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
6520
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
6521
6522
[[package]]
6523
name = "unicode-xid"
@@ -7115,6 +7273,15 @@ dependencies = [
7273
"windows-targets 0.42.2",
7274
]
7275
7276
+[[package]]
7277
+name = "windows-sys"
7278
+version = "0.48.0"
7279
+source = "registry+https://github.com/rust-lang/crates.io-index"
7280
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
7281
+dependencies = [
7282
+ "windows-targets 0.48.5",
7283
+]
7284
+
7285
[[package]]
7286
name = "windows-sys"
7287
version = "0.52.0"
Cargo.toml
+6
-1
@@ -3,6 +3,7 @@ name = "sigit"
3
version = "0.1.0"
4
edition = "2024"
5
description = "siGit Code — ACP-compatible AI coding agent for smbCloud platform."
6
+documentation = "https://getsigit.5mb.app/"
7
license = "MIT OR Apache-2.0"
8
9
[[bin]]
@@ -14,7 +15,7 @@ path = "src/main.rs"
15
agent-client-protocol = "0.10.4"
16
17
# Onde Inference engine (local LLM)
17
-onde = { path = "../onde" }
18
+onde = "0.1.1"
19
20
# Async runtime
21
async-trait = "0.1"
@@ -22,6 +23,10 @@ tokio = { version = "1", features = ["rt", "macros", "io-std", "io-util", "sync"
23
tokio-util = { version = "0.7", features = ["compat"] }
24
futures = "0.3"
25
26
+# Terminal UI
27
+crossterm = { version = "0.28", features = ["event-stream"] }
28
+ratatui = { version = "0.29", default-features = false, features = ["crossterm"] }
29
+
30
# Utilities
31
anyhow = "1"
32
log = "0.4"
src/chat.rs
new
+615
@@ -0,0 +1,615 @@
1
+//! Full-screen terminal chat UI for siGit Code.
2
+//!
3
+//! Takes over the alternate screen and multiplexes terminal events with
4
+//! streaming LLM tokens via `tokio::select!`.
5
+
6
+use std::future::pending;
7
+
8
+use anyhow::Result;
9
+use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
10
+use futures::StreamExt;
11
+use onde::inference::{ChatEngine, StreamChunk};
12
+use ratatui::{
13
+ Frame,
14
+ layout::{Constraint, Layout, Position},
15
+ style::{Color, Modifier, Style},
16
+ text::{Line, Span},
17
+ widgets::{Block, Borders, Paragraph, Wrap},
18
+};
19
+use tokio::sync::mpsc;
20
+
21
+// ── Message types ────────────────────────────────────────────────────────────
22
+
23
+#[derive(Clone, Copy, PartialEq, Eq)]
24
+enum Role {
25
+ User,
26
+ Assistant,
27
+ System,
28
+}
29
+
30
+struct ChatMessage {
31
+ role: Role,
32
+ text: String,
33
+}
34
+
35
+impl ChatMessage {
36
+ fn user(text: impl Into<String>) -> Self {
37
+ Self {
38
+ role: Role::User,
39
+ text: text.into(),
40
+ }
41
+ }
42
+
43
+ fn assistant(text: impl Into<String>) -> Self {
44
+ Self {
45
+ role: Role::Assistant,
46
+ text: text.into(),
47
+ }
48
+ }
49
+
50
+ fn system(text: impl Into<String>) -> Self {
51
+ Self {
52
+ role: Role::System,
53
+ text: text.into(),
54
+ }
55
+ }
56
+}
57
+
58
+// ── App state ────────────────────────────────────────────────────────────────
59
+
60
+struct App {
61
+ messages: Vec<ChatMessage>,
62
+ input: String,
63
+ cursor: usize,
64
+ scroll_offset: u16,
65
+ stream_rx: Option<mpsc::Receiver<StreamChunk>>,
66
+ stream_buf: String,
67
+ quit: bool,
68
+ /// Toggled every other tick while streaming — drives the blinking cursor.
69
+ blink_on: bool,
70
+ blink_counter: u8,
71
+}
72
+
73
+const BANNER_ART: &str = "\
74
+77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
75
+77777777322222222222222222222222222222223777389969902208431358831999699051111177777777777777
76
+1111111125555555555555555555555511113222311159 5002 088 3081771691111111111111
77
+1111111111111111111111111111131136841 1482853332007 05 9043332891 400811111111111
78
+1111111111111111111111111111111201 109 304 40 00 79 100041111111111
79
+333333255555555555555555555552392 102 503 90 7000000005 903 0000023333333333
80
+333333245454545454545454545433381 7600000 302 61 780 109 20009533333333333
81
+3333333333333333333333333333333402 7001 08 761 202 902 90003333333333333
82
+2222255555555555555555555555250899901 49 304 403 08 108 300042222222222222
83
+2222222222222222222222222222269 106 03 901 06 505 402 000052222222222222
84
+2222255555555555555555555555299 708 1002 80 00 90852222222222222
85
+55555555555555555555555555555560953258000866660000051140866908666600008966900065555555555555
86
+88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888";
87
+
88
+impl App {
89
+ fn new() -> Self {
90
+ let mut messages = Vec::new();
91
+ for line in BANNER_ART.lines() {
92
+ messages.push(ChatMessage::system(line));
93
+ }
94
+ messages.push(ChatMessage::system(""));
95
+ messages.push(ChatMessage::system(format!(
96
+ "siGit Code v{}",
97
+ env!("CARGO_PKG_VERSION"),
98
+ )));
99
+ messages.push(ChatMessage::system(
100
+ "AI coding agent, powered by local LLM.",
101
+ ));
102
+ messages.push(ChatMessage::system("Type /help for commands."));
103
+
104
+ Self {
105
+ messages,
106
+ input: String::new(),
107
+ cursor: 0,
108
+ scroll_offset: 0,
109
+ stream_rx: None,
110
+ stream_buf: String::new(),
111
+ quit: false,
112
+ blink_on: true,
113
+ blink_counter: 0,
114
+ }
115
+ }
116
+
117
+ fn is_streaming(&self) -> bool {
118
+ self.stream_rx.is_some()
119
+ }
120
+
121
+ fn finalize_stream(&mut self) {
122
+ self.stream_rx = None;
123
+ if !self.stream_buf.is_empty() {
124
+ let text = std::mem::take(&mut self.stream_buf);
125
+ self.messages.push(ChatMessage::assistant(text));
126
+ }
127
+ self.blink_on = false;
128
+ }
129
+
130
+ fn push_stream_delta(&mut self, delta: &str) {
131
+ self.stream_buf.push_str(delta);
132
+ // tick the blink
133
+ self.blink_counter = self.blink_counter.wrapping_add(1);
134
+ self.blink_on = self.blink_counter % 4 < 2;
135
+ }
136
+
137
+ /// Total lines the messages area would need (rough estimate for scrolling).
138
+ fn total_message_lines(&self, width: u16) -> u16 {
139
+ if width == 0 {
140
+ return 0;
141
+ }
142
+ let w = width.saturating_sub(2) as usize; // account for block borders
143
+ let mut lines: u16 = 0;
144
+ for msg in &self.messages {
145
+ lines += wrapped_line_count(&msg.text, msg.role, w);
146
+ }
147
+ // streaming buffer
148
+ if !self.stream_buf.is_empty() {
149
+ lines += wrapped_line_count(&self.stream_buf, Role::Assistant, w);
150
+ }
151
+ lines
152
+ }
153
+
154
+ fn auto_scroll(&mut self, visible_height: u16, width: u16) {
155
+ let total = self.total_message_lines(width);
156
+ if total > visible_height {
157
+ self.scroll_offset = total - visible_height;
158
+ } else {
159
+ self.scroll_offset = 0;
160
+ }
161
+ }
162
+}
163
+
164
+/// Estimate how many terminal rows a message takes once wrapped.
165
+fn wrapped_line_count(text: &str, role: Role, width: usize) -> u16 {
166
+ let prefix_len = match role {
167
+ Role::User => 6, // "you > "
168
+ Role::Assistant => 7, // "siGit > " — wait, that's 8. Let's just use 7 for "siGit> "
169
+ Role::System => 0,
170
+ };
171
+ let effective = if width > prefix_len {
172
+ width - prefix_len
173
+ } else {
174
+ 1
175
+ };
176
+
177
+ let mut count: u16 = 0;
178
+ for line in text.split('\n') {
179
+ if line.is_empty() {
180
+ count += 1;
181
+ } else {
182
+ count += ((line.len() as f64) / (effective as f64)).ceil() as u16;
183
+ }
184
+ }
185
+ count.max(1)
186
+}
187
+
188
+// ── Slash commands ───────────────────────────────────────────────────────────
189
+
190
+enum SlashCommand {
191
+ Help,
192
+ Clear,
193
+ Status,
194
+ Exit,
195
+ Unknown(String),
196
+}
197
+
198
+fn parse_slash(input: &str) -> Option<SlashCommand> {
199
+ let trimmed = input.trim();
200
+ if !trimmed.starts_with('/') {
201
+ return None;
202
+ }
203
+ let cmd = trimmed.split_whitespace().next().unwrap_or("");
204
+ Some(match cmd {
205
+ "/help" => SlashCommand::Help,
206
+ "/clear" => SlashCommand::Clear,
207
+ "/status" => SlashCommand::Status,
208
+ "/exit" | "/quit" | "/q" => SlashCommand::Exit,
209
+ other => SlashCommand::Unknown(other.to_string()),
210
+ })
211
+}
212
+
213
+// ── Rendering ────────────────────────────────────────────────────────────────
214
+
215
+fn render(frame: &mut Frame, app: &mut App) {
216
+ let area = frame.area();
217
+
218
+ // Layout: title(1) | messages(flex) | input(3) | footer(1)
219
+ let zones = Layout::vertical([
220
+ Constraint::Length(1),
221
+ Constraint::Min(1),
222
+ Constraint::Length(3),
223
+ Constraint::Length(1),
224
+ ])
225
+ .split(area);
226
+
227
+ render_title(frame, zones[0]);
228
+ render_messages(frame, app, zones[1]);
229
+ render_input(frame, app, zones[2]);
230
+ render_footer(frame, app, zones[3]);
231
+}
232
+
233
+fn render_title(frame: &mut Frame, area: ratatui::layout::Rect) {
234
+ let title = Line::from(vec![
235
+ Span::styled(
236
+ "siGit",
237
+ Style::default()
238
+ .fg(Color::Green)
239
+ .add_modifier(Modifier::BOLD),
240
+ ),
241
+ Span::styled(" Code", Style::default().fg(Color::White)),
242
+ Span::styled(" — AI Chat", Style::default().fg(Color::DarkGray)),
243
+ ]);
244
+ frame.render_widget(
245
+ Paragraph::new(title).style(Style::default().bg(Color::Black)),
246
+ area,
247
+ );
248
+}
249
+
250
+fn render_messages(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect) {
251
+ let mut lines: Vec<Line<'_>> = Vec::new();
252
+
253
+ for msg in &app.messages {
254
+ render_chat_message(&mut lines, msg);
255
+ }
256
+
257
+ // streaming partial response
258
+ if !app.stream_buf.is_empty() || app.is_streaming() {
259
+ let mut spans = vec![Span::styled(
260
+ "siGit > ",
261
+ Style::default()
262
+ .fg(Color::Green)
263
+ .add_modifier(Modifier::BOLD),
264
+ )];
265
+
266
+ // split on newlines so multi-line streaming renders correctly
267
+ let buf_lines: Vec<&str> = app.stream_buf.split('\n').collect();
268
+ for (i, segment) in buf_lines.iter().enumerate() {
269
+ if i > 0 {
270
+ lines.push(Line::from(spans.drain(..).collect::<Vec<_>>()));
271
+ // continuation lines get no prefix
272
+ }
273
+ spans.push(Span::raw(segment.to_string()));
274
+ }
275
+
276
+ // blinking cursor while streaming
277
+ if app.is_streaming() && app.blink_on {
278
+ spans.push(Span::styled("█", Style::default().fg(Color::Green)));
279
+ }
280
+
281
+ lines.push(Line::from(spans));
282
+ }
283
+
284
+ // auto-scroll
285
+ app.auto_scroll(area.height, area.width);
286
+
287
+ let paragraph = Paragraph::new(lines)
288
+ .wrap(Wrap { trim: false })
289
+ .scroll((app.scroll_offset, 0))
290
+ .style(Style::default());
291
+
292
+ frame.render_widget(paragraph, area);
293
+}
294
+
295
+fn render_chat_message<'a>(lines: &mut Vec<Line<'a>>, msg: &ChatMessage) {
296
+ let text_lines: Vec<&str> = msg.text.split('\n').collect();
297
+
298
+ match msg.role {
299
+ Role::User => {
300
+ for (i, segment) in text_lines.iter().enumerate() {
301
+ let mut spans = Vec::new();
302
+ if i == 0 {
303
+ spans.push(Span::styled(
304
+ "you > ",
305
+ Style::default()
306
+ .fg(Color::Cyan)
307
+ .add_modifier(Modifier::BOLD),
308
+ ));
309
+ }
310
+ spans.push(Span::styled(
311
+ segment.to_string(),
312
+ Style::default().fg(Color::White),
313
+ ));
314
+ lines.push(Line::from(spans));
315
+ }
316
+ }
317
+ Role::Assistant => {
318
+ for (i, segment) in text_lines.iter().enumerate() {
319
+ let mut spans = Vec::new();
320
+ if i == 0 {
321
+ spans.push(Span::styled(
322
+ "siGit > ",
323
+ Style::default()
324
+ .fg(Color::Green)
325
+ .add_modifier(Modifier::BOLD),
326
+ ));
327
+ }
328
+ spans.push(Span::styled(
329
+ segment.to_string(),
330
+ Style::default().fg(Color::White),
331
+ ));
332
+ lines.push(Line::from(spans));
333
+ }
334
+ }
335
+ Role::System => {
336
+ for segment in &text_lines {
337
+ lines.push(Line::from(Span::styled(
338
+ segment.to_string(),
339
+ Style::default().fg(Color::DarkGray),
340
+ )));
341
+ }
342
+ }
343
+ }
344
+}
345
+
346
+fn render_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
347
+ let block = Block::default()
348
+ .borders(Borders::TOP)
349
+ .border_style(Style::default().fg(Color::DarkGray))
350
+ .title(if app.is_streaming() {
351
+ " streaming… "
352
+ } else {
353
+ " message "
354
+ })
355
+ .title_style(Style::default().fg(if app.is_streaming() {
356
+ Color::Yellow
357
+ } else {
358
+ Color::DarkGray
359
+ }));
360
+
361
+ let input_text = Paragraph::new(app.input.as_str())
362
+ .style(Style::default().fg(if app.is_streaming() {
363
+ Color::DarkGray
364
+ } else {
365
+ Color::White
366
+ }))
367
+ .block(block);
368
+
369
+ frame.render_widget(input_text, area);
370
+
371
+ // place cursor inside the input block (1 for border padding)
372
+ if !app.is_streaming() {
373
+ let x = area.x + app.cursor as u16 + 1;
374
+ let y = area.y + 1;
375
+ frame.set_cursor_position(Position::new(x.min(area.right().saturating_sub(1)), y));
376
+ }
377
+}
378
+
379
+fn render_footer(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
380
+ let hints: &[(&str, &str)] = if app.is_streaming() {
381
+ &[("Ctrl+C", "cancel")]
382
+ } else {
383
+ &[("Enter", "send"), ("/help", "commands"), ("Ctrl+C", "quit")]
384
+ };
385
+
386
+ let mut spans: Vec<Span<'_>> = Vec::new();
387
+ for (i, (key, label)) in hints.iter().enumerate() {
388
+ if i > 0 {
389
+ spans.push(Span::styled(" ", Style::default()));
390
+ }
391
+ spans.push(Span::styled(
392
+ format!(" {key} "),
393
+ Style::default()
394
+ .fg(Color::Black)
395
+ .bg(Color::DarkGray)
396
+ .add_modifier(Modifier::BOLD),
397
+ ));
398
+ spans.push(Span::styled(
399
+ format!(" {label}"),
400
+ Style::default().fg(Color::DarkGray),
401
+ ));
402
+ }
403
+
404
+ frame.render_widget(
405
+ Paragraph::new(Line::from(spans)).style(Style::default().bg(Color::Black)),
406
+ area,
407
+ );
408
+}
409
+
410
+// ── Input handling ───────────────────────────────────────────────────────────
411
+
412
+fn handle_key(app: &mut App, key: KeyEvent) -> Option<String> {
413
+ if key.kind != KeyEventKind::Press {
414
+ return None;
415
+ }
416
+
417
+ match key.code {
418
+ KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
419
+ app.quit = true;
420
+ None
421
+ }
422
+ KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
423
+ app.quit = true;
424
+ None
425
+ }
426
+ KeyCode::Enter => {
427
+ if app.input.trim().is_empty() {
428
+ return None;
429
+ }
430
+ let text = app.input.drain(..).collect::<String>();
431
+ app.cursor = 0;
432
+ Some(text)
433
+ }
434
+ KeyCode::Backspace => {
435
+ if app.cursor > 0 {
436
+ app.cursor -= 1;
437
+ app.input.remove(app.cursor);
438
+ }
439
+ None
440
+ }
441
+ KeyCode::Delete => {
442
+ if app.cursor < app.input.len() {
443
+ app.input.remove(app.cursor);
444
+ }
445
+ None
446
+ }
447
+ KeyCode::Left => {
448
+ app.cursor = app.cursor.saturating_sub(1);
449
+ None
450
+ }
451
+ KeyCode::Right => {
452
+ if app.cursor < app.input.len() {
453
+ app.cursor += 1;
454
+ }
455
+ None
456
+ }
457
+ KeyCode::Home => {
458
+ app.cursor = 0;
459
+ None
460
+ }
461
+ KeyCode::End => {
462
+ app.cursor = app.input.len();
463
+ None
464
+ }
465
+ KeyCode::Char(ch) => {
466
+ app.input.insert(app.cursor, ch);
467
+ app.cursor += 1;
468
+ None
469
+ }
470
+ _ => None,
471
+ }
472
+}
473
+
474
+// ── Slash command execution ──────────────────────────────────────────────────
475
+
476
+async fn exec_slash(app: &mut App, cmd: SlashCommand, engine: &ChatEngine) {
477
+ match cmd {
478
+ SlashCommand::Help => {
479
+ app.messages.push(ChatMessage::system(
480
+ "/help — show this message\n\
481
+ /clear — wipe conversation history\n\
482
+ /status — show engine status\n\
483
+ /exit — quit chat",
484
+ ));
485
+ }
486
+ SlashCommand::Clear => {
487
+ let cleared = engine.clear_history().await;
488
+ app.messages.clear();
489
+ app.scroll_offset = 0;
490
+ app.messages.push(ChatMessage::system(format!(
491
+ "Cleared {cleared} turn(s). History is empty.",
492
+ )));
493
+ }
494
+ SlashCommand::Status => {
495
+ let info = engine.info().await;
496
+ let model = info.model_name.as_deref().unwrap_or("(none)");
497
+ let mem = info.approx_memory.as_deref().unwrap_or("unknown");
498
+ app.messages.push(ChatMessage::system(format!(
499
+ "status: {:?} model: {} memory: {} history: {} turns",
500
+ info.status, model, mem, info.history_length,
501
+ )));
502
+ }
503
+ SlashCommand::Exit => {
504
+ app.quit = true;
505
+ }
506
+ SlashCommand::Unknown(cmd) => {
507
+ app.messages
508
+ .push(ChatMessage::system(format!("unknown command: {cmd}")));
509
+ }
510
+ }
511
+}
512
+
513
+// ── Main loop ────────────────────────────────────────────────────────────────
514
+
515
+/// Run the interactive chat UI. Blocks until the user quits.
516
+///
517
+/// The caller must have already loaded a model into `engine`.
518
+pub async fn run(engine: &ChatEngine) -> Result<()> {
519
+ let mut terminal = ratatui::init();
520
+ let result = event_loop(&mut terminal, engine).await;
521
+ ratatui::restore();
522
+ result
523
+}
524
+
525
+async fn event_loop(terminal: &mut ratatui::DefaultTerminal, engine: &ChatEngine) -> Result<()> {
526
+ let mut app = App::new();
527
+ let mut event_stream = EventStream::new();
528
+
529
+ loop {
530
+ // draw
531
+ terminal.draw(|frame| render(frame, &mut app))?;
532
+
533
+ if app.quit {
534
+ break;
535
+ }
536
+
537
+ // multiplex terminal events and streaming tokens
538
+ tokio::select! {
539
+ biased;
540
+
541
+ // streaming chunks — only active when we have a receiver
542
+ chunk = async {
543
+ match app.stream_rx.as_mut() {
544
+ Some(rx) => rx.recv().await,
545
+ None => pending().await,
546
+ }
547
+ } => {
548
+ match chunk {
549
+ Some(chunk) => {
550
+ if !chunk.delta.is_empty() {
551
+ app.push_stream_delta(&chunk.delta);
552
+ }
553
+ if chunk.done {
554
+ app.finalize_stream();
555
+ }
556
+ }
557
+ // sender dropped without done=true
558
+ None => {
559
+ app.finalize_stream();
560
+ }
561
+ }
562
+ }
563
+
564
+ // terminal events
565
+ maybe_event = event_stream.next() => {
566
+ let Some(Ok(event)) = maybe_event else {
567
+ // stream ended or error — bail
568
+ break;
569
+ };
570
+
571
+ if let Event::Key(key) = event {
572
+ // while streaming, only ctrl+c/d work
573
+ if app.is_streaming() {
574
+ if key.kind == KeyEventKind::Press {
575
+ let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
576
+ if ctrl && (key.code == KeyCode::Char('c') || key.code == KeyCode::Char('d')) {
577
+ // drop the receiver to stop reading
578
+ app.finalize_stream();
579
+ app.messages.push(ChatMessage::system("(cancelled)"));
580
+ }
581
+ }
582
+ continue;
583
+ }
584
+
585
+ if let Some(text) = handle_key(&mut app, key) {
586
+ // check for slash command first
587
+ if let Some(cmd) = parse_slash(&text) {
588
+ exec_slash(&mut app, cmd, engine).await;
589
+ continue;
590
+ }
591
+
592
+ // regular message — send to engine
593
+ app.messages.push(ChatMessage::user(&text));
594
+
595
+ match engine.stream_message(text).await {
596
+ Ok(rx) => {
597
+ app.stream_rx = Some(rx);
598
+ app.stream_buf.clear();
599
+ app.blink_counter = 0;
600
+ app.blink_on = true;
601
+ }
602
+ Err(err) => {
603
+ app.messages.push(ChatMessage::system(format!(
604
+ "error: {err}"
605
+ )));
606
+ }
607
+ }
608
+ }
609
+ }
610
+ }
611
+ }
612
+ }
613
+
614
+ Ok(())
615
+}
src/main.rs
+55
-17
@@ -1,10 +1,14 @@
1
-//! siGit — AI coding agent that runs a local LLM via Onde Inference and
2
-//! speaks ACP over stdio so editors like Zed can talk to it.
1
+//! siGit Code — AI coding agent powered by a local LLM via Onde Inference.
2
+//!
3
+//! Two modes of operation:
4
+//!
5
+//! - **Interactive** (stdin is a TTY): full-screen chat UI built on ratatui.
6
+//! - **ACP server** (stdin is piped): JSON-RPC over stdio for editors like Zed.
7
//!
8
//! On macOS the model cache is shared with the siGit desktop app through an
9
//! App Group container. See [`setup`].
10
//!
7
-//! # Zed setup
11
+//! # Zed setup (ACP mode)
12
//!
13
//! Add to `~/.config/zed/settings.json`:
14
//! ```json
@@ -18,8 +22,10 @@
22
//! }
23
//! ```
24
25
+mod chat;
26
mod setup;
27
28
+use std::io::IsTerminal;
29
use std::sync::Arc;
30
31
use agent_client_protocol::{
@@ -230,24 +236,33 @@ fn print_banner() {
236
eprintln!("{art}");
237
}
238
233
-// ── Entry point ──────────────────────────────────────────────────────────────
239
+// ── Interactive mode ─────────────────────────────────────────────────────────
240
235
-#[tokio::main]
236
-async fn main() -> anyhow::Result<()> {
237
- // stdout is the ACP wire, so logs go to stderr.
238
- env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
239
- .target(env_logger::Target::Stderr)
240
- .init();
241
+/// Load the model, then hand off to the ratatui chat TUI.
242
+async fn run_interactive() -> anyhow::Result<()> {
243
+ println!(" Loading model...");
244
242
- print_banner();
245
+ let engine = ChatEngine::new();
246
+ let config = GgufModelConfig::platform_default();
247
+ engine
248
+ .load_gguf_model(config, Some(SYSTEM_PROMPT.to_string()), None)
249
+ .await
250
+ .map_err(|e| anyhow::anyhow!("model load failed: {e}"))?;
251
244
- log::info!("siGit v{} starting", env!("CARGO_PKG_VERSION"));
252
+ let info = engine.info().await;
253
+ println!(
254
+ " \x1b[32m✓\x1b[0m {} ({})\n",
255
+ info.model_name.as_deref().unwrap_or("unknown"),
256
+ info.approx_memory.as_deref().unwrap_or("?"),
257
+ );
258
246
- // Point HF_HOME at the shared App Group container (macOS) so we pick up
247
- // models the desktop app already downloaded. Must happen before anything
248
- // touches hf-hub.
249
- setup::setup_shared_model_cache();
259
+ chat::run(&engine).await
260
+}
261
+
262
+// ── ACP server mode ──────────────────────────────────────────────────────────
263
264
+/// Speak ACP JSON-RPC over stdio. The editor spawns us as a subprocess.
265
+async fn run_acp_server() -> anyhow::Result<()> {
266
// Agent::prompt sends chunks here; the forwarder task writes them out.
267
let (notification_tx, mut notification_rx) = mpsc::channel::<SessionNotification>(256);
268
@@ -288,6 +303,29 @@ async fn main() -> anyhow::Result<()> {
303
})
304
.await;
305
291
- log::info!("siGit shutting down");
306
Ok(())
307
}
308
+
309
+// ── Entry point ──────────────────────────────────────────────────────────────
310
+
311
+#[tokio::main]
312
+async fn main() -> anyhow::Result<()> {
313
+ // Logs always go to stderr (stdout is either the TUI or the ACP wire).
314
+ env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
315
+ .target(env_logger::Target::Stderr)
316
+ .init();
317
+
318
+ // Shared model cache (macOS App Group) — must run before anything
319
+ // touches hf-hub or ChatEngine.
320
+ setup::setup_shared_model_cache();
321
+
322
+ if std::io::stdin().is_terminal() {
323
+ // Interactive mode — full-screen chat TUI.
324
+ print_banner();
325
+ run_interactive().await
326
+ } else {
327
+ // Editor spawned us — speak ACP over stdio.
328
+ log::info!("siGit v{} starting (ACP mode)", env!("CARGO_PKG_VERSION"));
329
+ run_acp_server().await
330
+ }
331
+}