@hej / sigit / commits / c5b17d8

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 +}