added streaming support
This commit is contained in:
parent
6d0592bda5
commit
0faecbf657
|
|
@ -17,6 +17,15 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
|
|
@ -115,6 +124,21 @@ version = "0.21.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
|
@ -253,6 +277,24 @@ version = "0.8.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.11.0"
|
||||
|
|
@ -358,6 +400,16 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
|
|
@ -373,6 +425,21 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
|
|
@ -380,6 +447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -388,6 +456,34 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
|
|
@ -406,10 +502,16 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -451,12 +553,16 @@ dependencies = [
|
|||
"console",
|
||||
"dialoguer",
|
||||
"dirs",
|
||||
"futures",
|
||||
"indicatif",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rustyline",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syntect",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"toml",
|
||||
]
|
||||
|
||||
|
|
@ -781,6 +887,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
|
|
@ -861,6 +973,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -897,6 +1015,28 @@ version = "1.70.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "onig"
|
||||
version = "6.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"onig_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onig_sys"
|
||||
version = "69.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
|
|
@ -944,6 +1084,25 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
|
|
@ -959,6 +1118,12 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.97"
|
||||
|
|
@ -968,6 +1133,15 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
|
|
@ -1013,13 +1187,42 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
|
|
@ -1045,10 +1248,12 @@ dependencies = [
|
|||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"winreg",
|
||||
|
|
@ -1105,7 +1310,7 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1152,6 +1357,15 @@ version = "1.0.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
|
|
@ -1314,6 +1528,28 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syntect"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags 1.3.2",
|
||||
"flate2",
|
||||
"fnv",
|
||||
"once_cell",
|
||||
"onig",
|
||||
"plist",
|
||||
"regex-syntax",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
|
|
@ -1368,6 +1604,37 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
|
|
@ -1419,6 +1686,17 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.16"
|
||||
|
|
@ -1557,6 +1835,16 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
|
@ -1652,6 +1940,19 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
|
|
@ -1694,6 +1995,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
|
@ -2015,6 +2325,15 @@ version = "0.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ clap = { version = "4.0", features = ["derive"] }
|
|||
tokio = { version = "1.0", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false }
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls", "stream"], default-features = false }
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
dialoguer = "0.11"
|
||||
|
|
@ -19,3 +19,7 @@ indicatif = "0.17"
|
|||
dirs = "5.0"
|
||||
rustyline = "13.0"
|
||||
toml = "0.8"
|
||||
syntect = "5.1"
|
||||
regex = "1.0"
|
||||
futures = "0.3"
|
||||
tokio-stream = "0.1"
|
||||
|
|
|
|||
141
src/cli.rs
141
src/cli.rs
|
|
@ -6,6 +6,8 @@ use crate::core::{
|
|||
ChatClient, Session,
|
||||
};
|
||||
use crate::utils::{Display, InputHandler, SessionAction};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
pub struct ChatCLI {
|
||||
session: Session,
|
||||
|
|
@ -82,8 +84,6 @@ impl ChatCLI {
|
|||
self.session.add_user_message(message.to_string());
|
||||
self.session.save()?;
|
||||
|
||||
let spinner = self.display.show_spinner("Thinking");
|
||||
|
||||
// Clone data needed for the API call before getting mutable client reference
|
||||
let model = self.session.model.clone();
|
||||
let messages = self.session.messages.clone();
|
||||
|
|
@ -91,27 +91,75 @@ impl ChatCLI {
|
|||
let enable_reasoning_summary = self.session.enable_reasoning_summary;
|
||||
let reasoning_effort = self.session.reasoning_effort.clone();
|
||||
|
||||
let client = self.get_client()?;
|
||||
// Check if we should use streaming before getting client
|
||||
let should_use_streaming = {
|
||||
let client = self.get_client()?;
|
||||
client.supports_streaming()
|
||||
};
|
||||
|
||||
match client
|
||||
.chat_completion(
|
||||
&model,
|
||||
&messages,
|
||||
enable_web_search,
|
||||
enable_reasoning_summary,
|
||||
&reasoning_effort,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
spinner.finish("Done");
|
||||
self.display.print_assistant_response(&response);
|
||||
self.session.add_assistant_message(response);
|
||||
self.session.save()?;
|
||||
if should_use_streaming {
|
||||
print!("{} ", console::style("🤖").magenta());
|
||||
use std::io::{self, Write};
|
||||
io::stdout().flush().ok();
|
||||
|
||||
let stream_callback = {
|
||||
use crate::core::StreamCallback;
|
||||
Box::new(move |chunk: &str| {
|
||||
print!("{}", chunk);
|
||||
use std::io::{self, Write};
|
||||
io::stdout().flush().ok();
|
||||
Box::pin(async move {}) as Pin<Box<dyn Future<Output = ()> + Send>>
|
||||
}) as StreamCallback
|
||||
};
|
||||
|
||||
let client = self.get_client()?;
|
||||
match client
|
||||
.chat_completion_stream(
|
||||
&model,
|
||||
&messages,
|
||||
enable_web_search,
|
||||
enable_reasoning_summary,
|
||||
&reasoning_effort,
|
||||
stream_callback,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
println!(); // Add newline after streaming
|
||||
self.session.add_assistant_message(response);
|
||||
self.session.save()?;
|
||||
}
|
||||
Err(e) => {
|
||||
println!(); // Add newline after failed streaming
|
||||
self.display.print_error(&format!("Streaming failed: {}", e));
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
spinner.finish_with_error("Failed");
|
||||
return Err(e);
|
||||
} else {
|
||||
// Fallback to non-streaming
|
||||
let spinner = self.display.show_spinner("Thinking");
|
||||
let client = self.get_client()?;
|
||||
|
||||
match client
|
||||
.chat_completion(
|
||||
&model,
|
||||
&messages,
|
||||
enable_web_search,
|
||||
enable_reasoning_summary,
|
||||
&reasoning_effort,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
spinner.finish("Done");
|
||||
self.display.print_assistant_response(&response);
|
||||
self.session.add_assistant_message(response);
|
||||
self.session.save()?;
|
||||
}
|
||||
Err(e) => {
|
||||
spinner.finish_with_error("Failed");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +198,9 @@ impl ChatCLI {
|
|||
"/tools" => {
|
||||
self.tools_manager().await?;
|
||||
}
|
||||
"/history" => {
|
||||
self.handle_history_command(&parts)?;
|
||||
}
|
||||
_ => {
|
||||
self.display.print_error(&format!("Unknown command: {} (see /help)", parts[0]));
|
||||
}
|
||||
|
|
@ -391,4 +442,52 @@ impl ChatCLI {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_history_command(&mut self, parts: &[&str]) -> Result<()> {
|
||||
let mut filter_role: Option<&str> = None;
|
||||
let mut limit: Option<usize> = None;
|
||||
|
||||
// Parse parameters
|
||||
for &part in parts.iter().skip(1) {
|
||||
match part {
|
||||
"user" | "assistant" => filter_role = Some(part),
|
||||
_ => {
|
||||
if let Ok(num) = part.parse::<usize>() {
|
||||
limit = Some(num);
|
||||
} else {
|
||||
self.display.print_error(&format!("Invalid parameter: {}", part));
|
||||
self.display.print_info("Usage: /history [user|assistant] [number]");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter messages (skip system prompt at index 0)
|
||||
let mut messages: Vec<(usize, &crate::core::Message)> = self.session.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(1) // Skip system prompt
|
||||
.collect();
|
||||
|
||||
// Apply role filter
|
||||
if let Some(role) = filter_role {
|
||||
messages.retain(|(_, msg)| msg.role == role);
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
if let Some(limit_count) = limit {
|
||||
let start_index = messages.len().saturating_sub(limit_count);
|
||||
messages = messages[start_index..].to_vec();
|
||||
}
|
||||
|
||||
if messages.is_empty() {
|
||||
self.display.print_info("No messages to display");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Format and display
|
||||
self.display.print_conversation_history(&messages);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,10 +4,15 @@ use serde::Deserialize;
|
|||
use serde_json::{json, Value};
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
use futures::stream::StreamExt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::config::Config;
|
||||
use super::{provider::Provider, session::Message};
|
||||
|
||||
pub type StreamCallback = Box<dyn Fn(&str) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ChatClient {
|
||||
OpenAI(OpenAIClient),
|
||||
|
|
@ -32,6 +37,30 @@ impl ChatClient {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn chat_completion_stream(
|
||||
&self,
|
||||
model: &str,
|
||||
messages: &[Message],
|
||||
enable_web_search: bool,
|
||||
enable_reasoning_summary: bool,
|
||||
reasoning_effort: &str,
|
||||
stream_callback: StreamCallback,
|
||||
) -> Result<String> {
|
||||
match self {
|
||||
ChatClient::OpenAI(client) => {
|
||||
client.chat_completion_stream(model, messages, enable_web_search, enable_reasoning_summary, reasoning_effort, stream_callback).await
|
||||
}
|
||||
ChatClient::Anthropic(_) => {
|
||||
// Fallback to non-streaming for Anthropic
|
||||
self.chat_completion(model, messages, enable_web_search, enable_reasoning_summary, reasoning_effort).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_streaming(&self) -> bool {
|
||||
matches!(self, ChatClient::OpenAI(_))
|
||||
}
|
||||
|
||||
pub fn supports_feature(&self, feature: &str) -> bool {
|
||||
match self {
|
||||
|
|
@ -96,6 +125,40 @@ struct FunctionCall {
|
|||
arguments: String,
|
||||
}
|
||||
|
||||
// Streaming response structures
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct StreamingChoice {
|
||||
delta: StreamingDelta,
|
||||
#[allow(dead_code)]
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct StreamingDelta {
|
||||
content: Option<String>,
|
||||
tool_calls: Option<Vec<StreamingToolCall>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct StreamingToolCall {
|
||||
index: usize,
|
||||
id: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
tool_type: Option<String>,
|
||||
function: Option<StreamingFunction>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct StreamingFunction {
|
||||
name: Option<String>,
|
||||
arguments: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct StreamingResponse {
|
||||
choices: Vec<StreamingChoice>,
|
||||
}
|
||||
|
||||
// Responses API structures
|
||||
#[derive(Deserialize)]
|
||||
struct ResponsesApiResponse {
|
||||
|
|
@ -428,6 +491,263 @@ impl OpenAIClient {
|
|||
Ok(final_content)
|
||||
}
|
||||
|
||||
pub async fn chat_completion_stream(
|
||||
&self,
|
||||
model: &str,
|
||||
messages: &[Message],
|
||||
enable_web_search: bool,
|
||||
_enable_reasoning_summary: bool,
|
||||
reasoning_effort: &str,
|
||||
stream_callback: StreamCallback,
|
||||
) -> Result<String> {
|
||||
|
||||
// Use Responses API for GPT-5 with web search, otherwise use Chat Completions API
|
||||
if enable_web_search && model.starts_with("gpt-5") {
|
||||
return self.responses_api_stream(model, messages, reasoning_effort, stream_callback).await;
|
||||
}
|
||||
|
||||
let url = format!("{}/chat/completions", self.base_url);
|
||||
|
||||
let mut payload = json!({
|
||||
"model": model,
|
||||
"messages": Self::convert_messages(messages),
|
||||
"stream": true
|
||||
});
|
||||
|
||||
// Add tools if web search is enabled
|
||||
if enable_web_search {
|
||||
payload["tools"] = json!([{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Search the web for current information on any topic",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query to find relevant information"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
}]);
|
||||
payload["tool_choice"] = json!("auto");
|
||||
}
|
||||
|
||||
// Add reasoning effort for GPT-5 models
|
||||
if model.starts_with("gpt-5") && ["low", "medium", "high"].contains(&reasoning_effort) {
|
||||
payload["reasoning_effort"] = json!(reasoning_effort);
|
||||
}
|
||||
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send request to OpenAI API")?;
|
||||
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(anyhow::anyhow!("OpenAI API error: {}", error_text));
|
||||
}
|
||||
|
||||
let mut full_response = String::new();
|
||||
let mut tool_calls_buffer: std::collections::HashMap<usize, (String, String, String)> = std::collections::HashMap::new();
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.context("Failed to read chunk from stream")?;
|
||||
let chunk_str = std::str::from_utf8(&chunk)
|
||||
.context("Failed to parse chunk as UTF-8")?;
|
||||
|
||||
// Temporary debug output
|
||||
|
||||
// Parse server-sent events
|
||||
for line in chunk_str.lines() {
|
||||
if line.starts_with("data: ") {
|
||||
let data = &line[6..];
|
||||
|
||||
if data == "[DONE]" {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip empty data lines
|
||||
if data.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match serde_json::from_str::<StreamingResponse>(data) {
|
||||
Ok(streaming_response) => {
|
||||
if let Some(choice) = streaming_response.choices.first() {
|
||||
// Handle streaming content
|
||||
if let Some(content) = &choice.delta.content {
|
||||
if !content.is_empty() {
|
||||
full_response.push_str(content);
|
||||
stream_callback(content).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle streaming tool calls
|
||||
if let Some(tool_calls) = &choice.delta.tool_calls {
|
||||
for tool_call in tool_calls {
|
||||
let entry = tool_calls_buffer.entry(tool_call.index).or_insert((String::new(), String::new(), String::new()));
|
||||
|
||||
if let Some(id) = &tool_call.id {
|
||||
entry.0.push_str(id);
|
||||
}
|
||||
|
||||
if let Some(function) = &tool_call.function {
|
||||
if let Some(name) = &function.name {
|
||||
entry.1.push_str(name);
|
||||
}
|
||||
if let Some(args) = &function.arguments {
|
||||
entry.2.push_str(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if !line.trim().is_empty() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any tool calls that were collected
|
||||
if !tool_calls_buffer.is_empty() && enable_web_search {
|
||||
for (_, (_id, name, args)) in tool_calls_buffer {
|
||||
if name == "web_search" {
|
||||
if let Ok(parsed_args) = serde_json::from_str::<serde_json::Value>(&args) {
|
||||
if let Some(query) = parsed_args.get("query").and_then(|q| q.as_str()) {
|
||||
let tool_message = format!(
|
||||
"\n\n[Web Search Request: \"{}\"]\nNote: Web search functionality is not implemented in this CLI. The AI wanted to search for: {}",
|
||||
query, query
|
||||
);
|
||||
full_response.push_str(&tool_message);
|
||||
stream_callback(&tool_message).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(full_response)
|
||||
}
|
||||
|
||||
async fn responses_api_stream(
|
||||
&self,
|
||||
model: &str,
|
||||
messages: &[Message],
|
||||
reasoning_effort: &str,
|
||||
stream_callback: StreamCallback,
|
||||
) -> Result<String> {
|
||||
let url = format!("{}/responses", self.base_url);
|
||||
|
||||
// Convert messages to input text (simple approach for now)
|
||||
let input_text = messages
|
||||
.iter()
|
||||
.filter(|msg| msg.role != "system")
|
||||
.map(|msg| msg.content.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let mut payload = json!({
|
||||
"model": model,
|
||||
"tools": [{"type": "web_search_preview"}],
|
||||
"input": input_text,
|
||||
"stream": true
|
||||
});
|
||||
|
||||
// Add reasoning effort for GPT-5 models
|
||||
if ["low", "medium", "high"].contains(&reasoning_effort) {
|
||||
payload["reasoning"] = json!({
|
||||
"effort": reasoning_effort
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send request to OpenAI Responses API")?;
|
||||
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(anyhow::anyhow!("OpenAI Responses API error: {}", error_text));
|
||||
}
|
||||
|
||||
let mut full_response = String::new();
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.context("Failed to read chunk from Responses API stream")?;
|
||||
let chunk_str = std::str::from_utf8(&chunk)
|
||||
.context("Failed to parse Responses API chunk as UTF-8")?;
|
||||
|
||||
|
||||
// Parse server-sent events for Responses API
|
||||
for line in chunk_str.lines() {
|
||||
if line.starts_with("data: ") {
|
||||
let data = &line[6..];
|
||||
|
||||
if data == "[DONE]" {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip empty data lines
|
||||
if data.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to parse streaming events
|
||||
match serde_json::from_str::<serde_json::Value>(data) {
|
||||
Ok(response_data) => {
|
||||
|
||||
// Check for streaming delta events
|
||||
if let Some(event_type) = response_data.get("type").and_then(|t| t.as_str()) {
|
||||
if event_type == "response.output_text.delta" {
|
||||
if let Some(delta) = response_data.get("delta").and_then(|d| d.as_str()) {
|
||||
if !delta.is_empty() {
|
||||
full_response.push_str(delta);
|
||||
stream_callback(delta).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if !line.trim().is_empty() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if full_response.is_empty() {
|
||||
return Err(anyhow::anyhow!("No content found in Responses API stream response"));
|
||||
}
|
||||
|
||||
Ok(full_response)
|
||||
}
|
||||
|
||||
pub fn supports_feature(&self, feature: &str) -> bool {
|
||||
match feature {
|
||||
"web_search" | "reasoning_summary" | "reasoning_effort" => true,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ pub mod session;
|
|||
pub mod client;
|
||||
pub mod provider;
|
||||
|
||||
pub use session::Session;
|
||||
pub use client::{ChatClient, create_client};
|
||||
pub use session::{Session, Message};
|
||||
pub use client::{ChatClient, create_client, StreamCallback};
|
||||
pub use provider::get_provider_for_model;
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
use console::{style, Term};
|
||||
use std::io::{self, Write};
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::highlighting::{ThemeSet, Style};
|
||||
use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
|
||||
use regex::Regex;
|
||||
|
||||
pub struct Display {
|
||||
term: Term,
|
||||
syntax_set: SyntaxSet,
|
||||
theme_set: ThemeSet,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
term: Term::stdout(),
|
||||
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||
theme_set: ThemeSet::load_defaults(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +50,115 @@ impl Display {
|
|||
}
|
||||
|
||||
pub fn print_assistant_response(&self, content: &str) {
|
||||
println!("{} {}", style("🤖").magenta(), content);
|
||||
print!("{} ", style("🤖").magenta());
|
||||
self.print_formatted_content_with_pagination(content);
|
||||
}
|
||||
|
||||
fn print_formatted_content_with_pagination(&self, content: &str) {
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let terminal_height = self.term.size().0 as usize;
|
||||
let lines_per_page = terminal_height.saturating_sub(5); // Leave space for prompts
|
||||
|
||||
if lines.len() <= lines_per_page {
|
||||
// Short content, no pagination needed
|
||||
self.print_formatted_content(content);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut current_line = 0;
|
||||
|
||||
while current_line < lines.len() {
|
||||
let end_line = (current_line + lines_per_page).min(lines.len());
|
||||
let page_content = lines[current_line..end_line].join("\n");
|
||||
|
||||
self.print_formatted_content(&page_content);
|
||||
|
||||
if end_line < lines.len() {
|
||||
print!("\n{} ", style("Press Enter to continue, 'q' to finish...").dim());
|
||||
io::stdout().flush().ok();
|
||||
|
||||
let mut input = String::new();
|
||||
if io::stdin().read_line(&mut input).is_ok() {
|
||||
if input.trim().to_lowercase() == "q" {
|
||||
println!("{}", style("(response truncated)").dim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current_line = end_line;
|
||||
}
|
||||
}
|
||||
|
||||
fn print_formatted_content(&self, content: &str) {
|
||||
// Regex to match code blocks with optional language specifier
|
||||
let code_block_regex = Regex::new(r"```(\w+)?\n([\s\S]*?)\n```").unwrap();
|
||||
|
||||
let mut last_end = 0;
|
||||
|
||||
// Process code blocks
|
||||
for captures in code_block_regex.captures_iter(content) {
|
||||
let full_match = captures.get(0).unwrap();
|
||||
let lang = captures.get(1).map(|m| m.as_str()).unwrap_or("text");
|
||||
let code = captures.get(2).unwrap().as_str();
|
||||
|
||||
// Print text before code block
|
||||
let before_text = &content[last_end..full_match.start()];
|
||||
self.print_text_with_inline_code(before_text);
|
||||
|
||||
// Print code block with syntax highlighting
|
||||
self.print_code_block(code, lang);
|
||||
|
||||
last_end = full_match.end();
|
||||
}
|
||||
|
||||
// Print remaining text after last code block
|
||||
let remaining_text = &content[last_end..];
|
||||
self.print_text_with_inline_code(remaining_text);
|
||||
|
||||
println!(); // Add newline at the end
|
||||
}
|
||||
|
||||
fn print_text_with_inline_code(&self, text: &str) {
|
||||
let inline_code_regex = Regex::new(r"`([^`]+)`").unwrap();
|
||||
let mut last_end = 0;
|
||||
|
||||
for captures in inline_code_regex.captures_iter(text) {
|
||||
let full_match = captures.get(0).unwrap();
|
||||
let code = captures.get(1).unwrap().as_str();
|
||||
|
||||
// Print text before inline code
|
||||
print!("{}", &text[last_end..full_match.start()]);
|
||||
|
||||
// Print inline code with background highlighting
|
||||
print!("{}", style(code).on_black().white());
|
||||
|
||||
last_end = full_match.end();
|
||||
}
|
||||
|
||||
// Print remaining text
|
||||
print!("{}", &text[last_end..]);
|
||||
}
|
||||
|
||||
fn print_code_block(&self, code: &str, language: &str) {
|
||||
// Find the appropriate syntax definition
|
||||
let syntax = self.syntax_set.find_syntax_by_extension(language)
|
||||
.or_else(|| self.syntax_set.find_syntax_by_name(language))
|
||||
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
|
||||
|
||||
// Use a dark theme for code highlighting
|
||||
let theme = &self.theme_set.themes["base16-ocean.dark"];
|
||||
|
||||
println!("{}", style("```").dim());
|
||||
|
||||
let mut highlighter = HighlightLines::new(syntax, theme);
|
||||
for line in LinesWithEndings::from(code) {
|
||||
let ranges: Vec<(Style, &str)> = highlighter.highlight_line(line, &self.syntax_set).unwrap();
|
||||
let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
|
||||
print!("{}", escaped);
|
||||
}
|
||||
|
||||
println!("{}", style("```").dim());
|
||||
}
|
||||
|
||||
pub fn print_command_result(&self, message: &str) {
|
||||
|
|
@ -74,8 +191,15 @@ Available Commands:
|
|||
/new <session_name> - Create a new session
|
||||
/switch - Interactive session manager (switch/delete)
|
||||
/clear - Clear current conversation
|
||||
/history [user|assistant] [number] - View conversation history
|
||||
/tools - Interactive tool and feature manager
|
||||
|
||||
Input Features:
|
||||
Multi-line input - End your message with '\' to enter multi-line mode
|
||||
Keyboard shortcuts - Ctrl+C: Cancel, Ctrl+D: Exit, Arrow keys: History
|
||||
Syntax highlighting - Code blocks are automatically highlighted
|
||||
Pagination - Long responses are paginated (press Enter or 'q')
|
||||
|
||||
Environment Variables:
|
||||
OPENAI_API_KEY - Required for OpenAI models
|
||||
ANTHROPIC_API_KEY - Required for Anthropic models
|
||||
|
|
@ -96,6 +220,41 @@ Supported Models:
|
|||
io::stdout().flush().ok();
|
||||
SpinnerHandle::new()
|
||||
}
|
||||
|
||||
pub fn print_conversation_history(&self, messages: &[(usize, &crate::core::Message)]) {
|
||||
println!("{}", style("📜 Conversation History").bold().cyan());
|
||||
println!("{}", style("─".repeat(50)).dim());
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
for (original_index, message) in messages {
|
||||
let role_icon = match message.role.as_str() {
|
||||
"user" => "👤",
|
||||
"assistant" => "🤖",
|
||||
_ => "💬",
|
||||
};
|
||||
|
||||
let role_name = match message.role.as_str() {
|
||||
"user" => "User",
|
||||
"assistant" => "Assistant",
|
||||
_ => &message.role,
|
||||
};
|
||||
|
||||
content.push_str(&format!(
|
||||
"\n{} {} {} [Message #{}]\n{}\n",
|
||||
style("•").dim(),
|
||||
role_icon,
|
||||
style(role_name).bold(),
|
||||
original_index,
|
||||
style("─".repeat(40)).dim()
|
||||
));
|
||||
|
||||
content.push_str(&message.content);
|
||||
content.push_str("\n\n");
|
||||
}
|
||||
|
||||
self.print_formatted_content_with_pagination(&content);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Display {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::{theme::ColorfulTheme, Select};
|
||||
use rustyline::{error::ReadlineError, DefaultEditor};
|
||||
use rustyline::{error::ReadlineError, DefaultEditor, KeyEvent, Cmd};
|
||||
|
||||
pub struct InputHandler {
|
||||
editor: DefaultEditor,
|
||||
|
|
@ -8,8 +8,13 @@ pub struct InputHandler {
|
|||
|
||||
impl InputHandler {
|
||||
pub fn new() -> Result<Self> {
|
||||
// Use a simpler configuration approach
|
||||
let mut editor = DefaultEditor::new()?;
|
||||
|
||||
// Configure key bindings for better UX
|
||||
editor.bind_sequence(KeyEvent::ctrl('C'), Cmd::Interrupt);
|
||||
editor.bind_sequence(KeyEvent::ctrl('D'), Cmd::EndOfFile);
|
||||
|
||||
// Try to load history file
|
||||
let history_file = dirs::home_dir()
|
||||
.map(|home| home.join(".chat_cli_history"))
|
||||
|
|
@ -23,8 +28,13 @@ impl InputHandler {
|
|||
pub fn read_line(&mut self, prompt: &str) -> Result<Option<String>> {
|
||||
match self.editor.readline(prompt) {
|
||||
Ok(line) => {
|
||||
let _ = self.editor.add_history_entry(&line);
|
||||
Ok(Some(line))
|
||||
// Check if user wants to enter multi-line mode
|
||||
if line.trim().ends_with("\\") {
|
||||
self.read_multiline_input(line.trim_end_matches("\\").to_string())
|
||||
} else {
|
||||
let _ = self.editor.add_history_entry(&line);
|
||||
Ok(Some(line))
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
println!("^C");
|
||||
|
|
@ -38,6 +48,34 @@ impl InputHandler {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read_multiline_input(&mut self, initial_line: String) -> Result<Option<String>> {
|
||||
let mut lines = vec![initial_line];
|
||||
println!("Multi-line mode: Type your message. End with a line containing only '.' or press Ctrl+D");
|
||||
|
||||
loop {
|
||||
match self.editor.readline("... ") {
|
||||
Ok(line) => {
|
||||
if line.trim() == "." {
|
||||
break;
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
println!("^C");
|
||||
return Ok(None);
|
||||
}
|
||||
Err(ReadlineError::Eof) => {
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(anyhow::anyhow!("Error reading input: {}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
let full_message = lines.join("\n");
|
||||
let _ = self.editor.add_history_entry(&full_message);
|
||||
Ok(Some(full_message))
|
||||
}
|
||||
|
||||
pub fn save_history(&mut self) -> Result<()> {
|
||||
let history_file = dirs::home_dir()
|
||||
.map(|home| home.join(".chat_cli_history"))
|
||||
|
|
|
|||
Loading…
Reference in New Issue