From 66a4ddbd778793deb0f620ac815775057c95653d Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 10:59:20 +0800 Subject: [PATCH 1/9] Fix table cell formatting in README evaluation section Fix table cell formatting in README evaluation section --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 788b359..9110ea9 100644 --- a/README.md +++ b/README.md @@ -201,8 +201,8 @@ Cortex Memory has been rigorously evaluated against LangMem using the **LOCOMO d
- From 83cb018e4c333d235de95b822fdacc4a3c3afebc Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 15:29:36 +0800 Subject: [PATCH 2/9] refact the tars --- examples/cortex-mem-tars-new/.gitignore | 1 + examples/cortex-mem-tars-new/Cargo.lock | 2079 +++++++++++++++++ examples/cortex-mem-tars-new/Cargo.toml | 24 + .../docs/tui-implementation.md | 398 ++++ examples/cortex-mem-tars-new/src/agent.rs | 129 + examples/cortex-mem-tars-new/src/app.rs | 302 +++ examples/cortex-mem-tars-new/src/config.rs | 142 ++ examples/cortex-mem-tars-new/src/lib.rs | 5 + examples/cortex-mem-tars-new/src/logger.rs | 129 + examples/cortex-mem-tars-new/src/main.rs | 33 + examples/cortex-mem-tars-new/src/ui.rs | 1315 +++++++++++ examples/cortex-mem-tars-new/test.rs | Bin 0 -> 58378 bytes 12 files changed, 4557 insertions(+) create mode 100644 examples/cortex-mem-tars-new/.gitignore create mode 100644 examples/cortex-mem-tars-new/Cargo.lock create mode 100644 examples/cortex-mem-tars-new/Cargo.toml create mode 100644 examples/cortex-mem-tars-new/docs/tui-implementation.md create mode 100644 examples/cortex-mem-tars-new/src/agent.rs create mode 100644 examples/cortex-mem-tars-new/src/app.rs create mode 100644 examples/cortex-mem-tars-new/src/config.rs create mode 100644 examples/cortex-mem-tars-new/src/lib.rs create mode 100644 examples/cortex-mem-tars-new/src/logger.rs create mode 100644 examples/cortex-mem-tars-new/src/main.rs create mode 100644 examples/cortex-mem-tars-new/src/ui.rs create mode 100644 examples/cortex-mem-tars-new/test.rs diff --git a/examples/cortex-mem-tars-new/.gitignore b/examples/cortex-mem-tars-new/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/examples/cortex-mem-tars-new/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/cortex-mem-tars-new/Cargo.lock b/examples/cortex-mem-tars-new/Cargo.lock new file mode 100644 index 0000000..2a84d57 --- /dev/null +++ b/examples/cortex-mem-tars-new/Cargo.lock @@ -0,0 +1,2079 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi-to-tui" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdfd3cbf4843347ca072771a797484f1c3434a14d57f39d31c92dfb93a8799a8" +dependencies = [ + "nom", + "ratatui-core", + "simdutf8", + "smallvec", + "thiserror", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[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 = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cortex-mem-tars" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "clipboard", + "crossterm", + "directories", + "env_logger", + "log", + "ratatui", + "ratatui-core", + "serde", + "serde_json", + "tokio", + "toml", + "tui-markdown", + "tui-textarea", + "unicode-width 0.2.0", + "uuid", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.0", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "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.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str 0.8.1", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru 0.12.5", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate 1.1.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags", + "compact_str 0.9.0", + "hashbrown 0.16.1", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru 0.16.2", + "strum 0.27.2", + "thiserror", + "unicode-segmentation", + "unicode-truncate 2.0.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tui-markdown" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e766339aabad4528c3fccddf4acf03bc2b7ae6642def41e43c7af1a11f183122" +dependencies = [ + "ansi-to-tui", + "itertools 0.14.0", + "pretty_assertions", + "pulldown-cmark", + "ratatui-core", + "rstest", + "syntect", + "tracing", +] + +[[package]] +name = "tui-textarea" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +dependencies = [ + "crossterm", + "ratatui", + "unicode-width 0.2.0", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-truncate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.2.0", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + +[[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 = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zmij" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3280a1b827474fcd5dbef4b35a674deb52ba5c312363aef9135317df179d81b" diff --git a/examples/cortex-mem-tars-new/Cargo.toml b/examples/cortex-mem-tars-new/Cargo.toml new file mode 100644 index 0000000..ebca94d --- /dev/null +++ b/examples/cortex-mem-tars-new/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cortex-mem-tars" +version = "0.1.0" +edition = "2024" + +[dependencies] +ratatui = "0.29" +tui-markdown = "0.3.7" +ratatui-core = "0.1" +crossterm = "0.28" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.9" +anyhow = "1.0" +chrono = { version = "0.4", features = ["serde"] } +directories = "6.0" +log = "0.4" +env_logger = "0.11" +tui-textarea = "0.7" +unicode-width = "0.2" +uuid = { version = "1.10", features = ["v4"] } +async-trait = "0.1" +tokio = { version = "1.40", features = ["full"] } +clipboard = "0.5" diff --git a/examples/cortex-mem-tars-new/docs/tui-implementation.md b/examples/cortex-mem-tars-new/docs/tui-implementation.md new file mode 100644 index 0000000..b7eea3d --- /dev/null +++ b/examples/cortex-mem-tars-new/docs/tui-implementation.md @@ -0,0 +1,398 @@ +# TUI (Terminal User Interface) 实现总结 + +## 项目概述 + +本项目是一个基于 Rust 的终端聊天应用,使用 TUI 技术构建了一个功能完整的聊天界面,支持多机器人选择、消息显示、输入框、日志面板等功能。 + +## 技术选型 + +### 核心库 + +1. **ratatui** - TUI 框架 + - 提供了丰富的 UI 组件(Paragraph, List, Block, Scrollbar 等) + - 支持灵活的布局系统 + - 跨平台支持(Windows, Linux, macOS) + +2. **crossterm** - 终端控制库 + - 处理键盘和鼠标事件 + - 控制终端模式(原始模式、备用屏幕) + - 鼠标捕获和禁用 + +3. **tui-textarea** - 多行文本输入框 + - 支持多行输入 + - 自动换行处理 + - 光标移动和编辑 + +4. **tui-markdown** - Markdown 渲染 + - 将 Markdown 文本渲染为 TUI 组件 + - 支持基本的 Markdown 语法 + +5. **clipboard** - 剪贴板操作 + - 跨平台剪贴板访问 + - 支持复制功能 + +## 关键用法 + +### 1. 应用生命周期管理 + +```rust +// 启用原始模式和备用屏幕 +enable_raw_mode()?; +execute!( + stdout, + EnterAlternateScreen, + EnableMouseCapture, + DisableLineWrap +)?; + +// 创建终端 +let backend = CrosstermBackend::new(stdout); +let mut terminal = ratatui::Terminal::new(backend)?; + +// 主循环 +loop { + // 渲染 UI + terminal.draw(|f| self.ui.render(f))?; + + // 处理事件 + if event::poll(tick_rate)? { + match event::read()? { + Event::Key(key) => { /* 处理键盘事件 */ } + Event::Mouse(mouse) => { /* 处理鼠标事件 */ } + _ => {} + } + } +} + +// 恢复终端 +disable_raw_mode()?; +execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture +)?; +``` + +### 2. 布局系统 + +ratatui 使用约束布局系统,可以灵活地定义界面布局: + +```rust +let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), // 固定高度 + Constraint::Min(0), // 最小高度 + Constraint::Length(8), // 固定高度 + ]) + .split(area); +``` + +### 3. 状态管理 + +使用枚举来管理应用状态: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppState { + BotSelection, // 机器人选择界面 + Chat, // 聊天界面 +} +``` + +### 4. 事件处理 + +#### 键盘事件处理 + +```rust +match key.code { + KeyCode::Enter => { + // Enter 发送消息 + KeyAction::SendMessage + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+C 退出 + KeyAction::Quit + } + _ => KeyAction::Continue, +} +``` + +#### 鼠标事件处理 + +```rust +match event.kind { + MouseEventKind::ScrollUp => { + // 向上滚动 + self.scroll_offset = self.scroll_offset.saturating_sub(3); + } + MouseEventKind::Down(but) if but == MouseButton::Left => { + // 鼠标左键按下,开始选择 + self.selection_active = true; + self.selection_start = Some((line_idx, col_idx)); + } + MouseEventKind::Drag(but) if but == MouseButton::Left => { + // 鼠标拖拽,更新选择 + self.selection_end = Some((line_idx, col_idx)); + } + _ => {} +} +``` + +### 5. 文本选择实现 + +文本选择是本项目的核心功能之一,实现要点: + +#### 5.1 鼠标坐标转换 + +将鼠标的屏幕坐标转换为文本的行列索引: + +```rust +fn mouse_to_text_position(&self, event: MouseEvent, area: Rect) -> (usize, usize) { + let content_area = area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + // 检查鼠标是否在消息区域内 + if event.row < content_area.top() || event.row >= content_area.bottom() || + event.column < content_area.left() || event.column >= content_area.right() { + return (self.scroll_offset, 0); + } + + // 计算行索引(考虑滚动偏移) + let relative_row = event.row.saturating_sub(content_area.top()); + let line_idx = self.scroll_offset + relative_row as usize; + + // 计算列索引 + let relative_col = event.column.saturating_sub(content_area.left()); + let col_idx = relative_col as usize; + + // 确保列索引不超出范围 + let all_lines = self.get_all_rendered_lines(); + if line_idx < all_lines.len() { + let line_len = all_lines[line_idx].len(); + (line_idx, col_idx.min(line_len)) + } else { + (line_idx, 0) + } +} +``` + +#### 5.2 选择高亮渲染 + +使用字符索引而不是字节索引来处理多字节字符: + +```rust +fn apply_selection_highlight<'a>(&self, lines: Vec>, scroll_offset: usize, visible_lines: usize) -> Vec> { + let (start, end) = match (self.selection_start, self.selection_end) { + (Some(s), Some(e)) => (s, e), + _ => return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(), + }; + + let start_line = start.0.min(end.0); + let end_line = end.0.max(start.0); + let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; + let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; + + let highlight_style = Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD); + + lines + .into_iter() + .enumerate() + .skip(scroll_offset) + .take(visible_lines) + .map(|(original_idx, line)| { + if original_idx >= start_line && original_idx <= end_line { + let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); + let chars: Vec = line_text.chars().collect(); + let char_len = chars.len(); + + // 使用字符索引进行切片 + if original_idx == start_line && original_idx == end_line { + let safe_start_col = start_col.min(char_len); + let safe_end_col = end_col.min(char_len); + if safe_start_col < safe_end_col { + let before: String = chars[..safe_start_col].iter().collect(); + let selected: String = chars[safe_start_col..safe_end_col].iter().collect(); + let after: String = chars[safe_end_col..].iter().collect(); + + Line::from(vec![ + Span::raw(before), + Span::styled(selected, highlight_style), + Span::raw(after), + ]) + } else { + line + } + } else if original_idx == start_line { + // 起始行 + let safe_start_col = start_col.min(char_len); + let before: String = chars[..safe_start_col].iter().collect(); + let selected: String = chars[safe_start_col..].iter().collect(); + + Line::from(vec![ + Span::raw(before), + Span::styled(selected, highlight_style), + ]) + } else if original_idx == end_line { + // 结束行 + let safe_end_col = end_col.min(char_len); + let selected: String = chars[..safe_end_col].iter().collect(); + let after: String = chars[safe_end_col..].iter().collect(); + + Line::from(vec![ + Span::styled(selected, highlight_style), + Span::raw(after), + ]) + } else { + // 中间行,整行高亮 + Line::from(vec![Span::styled(line_text, highlight_style)]) + } + } else { + line + } + }) + .collect() +} +``` + +#### 5.3 关键注意事项 + +1. **字符索引 vs 字节索引**:在处理多字节字符(如中文、emoji)时,必须使用字符索引而不是字节索引,否则会出现 "byte index is not a char boundary" 错误。 + +2. **滚动偏移处理**:鼠标位置计算时需要考虑滚动偏移,确保选择范围正确。 + +3. **可见区域判断**:只渲染可见区域的行,避免性能问题。 + +4. **边界检查**:所有的索引访问都要进行边界检查,防止越界。 + +### 6. 滚动实现 + +```rust +// 计算滚动 +let total_lines = all_lines.len(); +let visible_lines = area.height.saturating_sub(2) as usize; +let max_scroll = total_lines.saturating_sub(visible_lines); + +// 限制 scroll_offset 在有效范围内 +if self.scroll_offset > max_scroll { + self.scroll_offset = max_scroll; +} + +// 滚动到底部 +self.scroll_offset = max_scroll; +``` + +### 7. 消息渲染 + +使用 Markdown 渲染来美化消息显示: + +```rust +let markdown_text = from_str(&message.content); +for line in markdown_text.lines { + all_lines.push(Line::from(line.spans.iter().map(|s| { + Span::raw(s.content.clone()) + }).collect::>())); +} +``` + +## 实践认知迭代 + +### 第一阶段:基础实现 + +1. **选择 ratatui**:相比其他 TUI 框架(如 tui-rs、termion),ratatui 更活跃且文档完善。 +2. **基本布局**:实现了机器人选择和聊天界面的基本布局。 +3. **键盘输入**:使用 tui-textarea 实现了多行输入框。 + +### 第二阶段:功能完善 + +1. **滚动功能**:实现了消息区域的滚动,支持查看历史消息。 +2. **日志面板**:添加了可切换的日志面板,方便调试。 +3. **Markdown 渲染**:使用 tui-markdown 美化消息显示。 + +### 第三阶段:交互优化 + +1. **鼠标支持**:添加了鼠标滚动和选择功能。 +2. **文本选择**:实现了鼠标拖拽选择文本,支持高亮显示。 +3. **多字节字符支持**:修复了中文等字符的显示和选择问题。 + +### 第四阶段:问题解决 + +1. **索引计算问题**: + - 问题:使用 `enumerate().skip(scroll_offset)` 后,索引计算混乱。 + - 解决:明确区分原始索引(original_idx)和可见索引(visible_idx)。 + +2. **字符边界问题**: + - 问题:直接使用字节索引导致多字节字符处理错误。 + - 解决:将字符串转换为字符数组,使用字符索引进行操作。 + +3. **渲染顺序问题**: + - 问题:先渲染内容再渲染边框,导致边框覆盖内容。 + - 解决:先渲染边框,再在边框内部渲染内容。 + +4. **选择范围计算**: + - 问题:滚动后选择范围计算不正确。 + - 解决:确保鼠标位置计算和渲染时使用相同的行数计算逻辑。 + +## 最佳实践 + +### 1. 状态管理 + +- 使用枚举来管理应用的不同状态 +- 将 UI 状态和业务逻辑分离 +- 使用 Option 来表示可选的状态 + +### 2. 事件处理 + +- 使用模式匹配来处理不同的事件类型 +- 返回明确的操作类型(Continue, Quit, SendMessage) +- 避免在事件处理中直接修改 UI + +### 3. 渲染优化 + +- 只渲染可见区域的内容 +- 使用滚动偏移来控制显示范围 +- 避免在每次渲染时重新计算所有内容 + +### 4. 错误处理 + +- 使用 Result 类型来处理可能的错误 +- 提供有意义的错误信息 +- 使用 context! 宏来添加上下文信息 + +### 5. 调试技巧 + +- 使用 log 宏记录关键操作 +- 添加详细的调试信息来追踪问题 +- 使用日志级别来控制输出量 + +## 性能考虑 + +1. **避免频繁渲染**:只在状态变化时重新渲染 +2. **限制渲染范围**:只渲染可见区域的内容 +3. **缓存计算结果**:避免重复计算相同的值 +4. **使用迭代器**:利用 Rust 的迭代器来优化性能 + +## 未来改进方向 + +1. **异步支持**:使用 tokio 来处理异步操作 +2. **主题系统**:支持自定义颜色和样式 +3. **插件系统**:支持扩展功能 +4. **国际化**:支持多语言 +5. **快捷键配置**:允许用户自定义快捷键 + +## 总结 + +本项目的 TUI 实现展示了如何使用 Rust 构建一个功能完整的终端应用。通过合理的技术选型、良好的架构设计和持续的迭代优化,我们实现了一个用户体验良好的聊天界面。关键的学习点包括: + +- 正确处理多字节字符 +- 准确计算鼠标位置和选择范围 +- 优化渲染性能 +- 良好的错误处理和调试技巧 + +这些经验可以应用到其他 TUI 项目的开发中。 \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/agent.rs b/examples/cortex-mem-tars-new/src/agent.rs new file mode 100644 index 0000000..40e5f1a --- /dev/null +++ b/examples/cortex-mem-tars-new/src/agent.rs @@ -0,0 +1,129 @@ +use anyhow::Result; +use async_trait::async_trait; +use chrono::{DateTime, Local}; + +/// 消息角色 +#[derive(Debug, Clone, PartialEq)] +pub enum MessageRole { + System, + User, + Assistant, +} + +/// 聊天消息 +#[derive(Debug, Clone)] +pub struct ChatMessage { + pub role: MessageRole, + pub content: String, + pub timestamp: DateTime, +} + +impl ChatMessage { + pub fn new(role: MessageRole, content: String) -> Self { + Self { + role, + content, + timestamp: Local::now(), + } + } + + pub fn system(content: impl Into) -> Self { + Self::new(MessageRole::System, content.into()) + } + + pub fn user(content: impl Into) -> Self { + Self::new(MessageRole::User, content.into()) + } + + pub fn assistant(content: impl Into) -> Self { + Self::new(MessageRole::Assistant, content.into()) + } +} + +/// Agent 抽象 trait +#[async_trait] +pub trait Agent: Send + Sync { + /// 发送消息并获取响应 + async fn chat(&self, messages: &[ChatMessage]) -> Result; + + /// 获取 Agent 名称 + fn name(&self) -> &str; + + /// 获取 Agent 描述 + fn description(&self) -> &str; +} + +/// Mock Agent 实现,用于模拟 AI 调用 +pub struct MockAgent { + name: String, + description: String, +} + +impl MockAgent { + pub fn new(name: impl Into, description: impl Into) -> Self { + Self { + name: name.into(), + description: description.into(), + } + } +} + +#[async_trait] +impl Agent for MockAgent { + async fn chat(&self, messages: &[ChatMessage]) -> Result { + // 模拟 AI 响应 + let last_message = messages.last().map(|m| m.content.as_str()).unwrap_or(""); + + // 简单的模拟响应逻辑 + let response = if last_message.contains("你好") || last_message.contains("hello") { + "你好!我是你的 AI 助手。很高兴为你服务!\n\n有什么我可以帮助你的吗?".to_string() + } else if last_message.contains("markdown") || last_message.contains("表格") { + "# Markdown 渲染演示\n\n这是一个 **Markdown** 渲染演示,包含各种格式。\n\n## 功能列表\n\n1. 支持多级标题\n2. 支持 **粗体** 和 *斜体*\n3. 支持有序列表\n4. 支持无序列表\n5. 支持 `代码`\n6. 支持引用\n\n## 数据表格\n\n| 名称 | 类型 | 描述 |\n|------|------|------|\n| String | 字符串 | 文本数据 |\n| Number | 数字 | 数值数据 |\n| Boolean | 布尔 | 真假值 |\n\n## 代码示例\n\n```rust\nfn main() {\n println!(\"Hello, world!\");\n let x = 42;\n println!(\"The answer is: {}\", x);\n}\n```\n\n```python\ndef hello():\n print(\"Hello, Python!\")\n return True\n```\n\n## 引用示例\n\n> 这是一段引用文本。\n> 可以有多行。\n\n希望这个演示对你有帮助!".to_string() + } else if last_message.contains("帮助") || last_message.contains("help") { + "# 帮助信息\n\n我可以演示以下 Markdown 功能:\n\n- 输入 \"markdown\" 或 \"表格\" 查看 Markdown 渲染\n- 输入 \"你好\" 查看简单问候\n\n## 快捷键\n\n- **Enter**: 发送消息\n- **Shift+Enter**: 换行\n- **l**: 打开/关闭日志面板\n- **Esc**: 关闭日志面板\n- **q**: 退出程序".to_string() + } else { + format!( + "# 响应\n\n我收到了你的消息:\n\n> {}\n\n这是一个模拟的 AI 响应。在实际使用中,这里会调用真实的 AI API。\n\n## 提示\n\n你可以尝试输入以下内容:\n\n1. \"你好\" - 查看问候\n2. \"markdown\" - 查看 Markdown 渲染效果\n3. \"帮助\" - 查看帮助信息", + last_message + ) + }; + + Ok(response) + } + + fn name(&self) -> &str { + &self.name + } + + fn description(&self) -> &str { + &self.description + } +} + +/// Agent 工厂 +pub struct AgentFactory; + +impl AgentFactory { + /// 创建 Mock Agent + pub fn create_mock_agent(name: impl Into, description: impl Into) -> Box { + Box::new(MockAgent::new(name, description)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_mock_agent() { + let agent = AgentFactory::create_mock_agent("TestBot", "A test agent"); + + let messages = vec![ + ChatMessage::system("你是一个有用的助手"), + ChatMessage::user("你好"), + ]; + + let response = agent.chat(&messages).await.unwrap(); + assert!(response.contains("你好")); + } +} \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/app.rs b/examples/cortex-mem-tars-new/src/app.rs new file mode 100644 index 0000000..d4f1bd2 --- /dev/null +++ b/examples/cortex-mem-tars-new/src/app.rs @@ -0,0 +1,302 @@ +use crate::agent::{Agent, AgentFactory, ChatMessage}; +use crate::config::{BotConfig, ConfigManager}; +use crate::logger::LogManager; +use crate::ui::{AppState, AppUi}; +use anyhow::{Context, Result}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ratatui::backend::CrosstermBackend; +use ratatui::layout::Rect; +use std::io; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +/// 应用程序 +pub struct App { + config_manager: ConfigManager, + log_manager: Arc, + ui: AppUi, + current_bot: Option, + agent: Option>, + should_quit: bool, +} + +impl App { + /// 创建新的应用 + pub fn new(config_manager: ConfigManager, log_manager: Arc) -> Result { + let mut ui = AppUi::new(); + + // 加载机器人列表 + let bots = config_manager.get_bots()?; + ui.set_bot_list(bots); + + log::info!("应用程序初始化完成"); + + Ok(Self { + config_manager, + log_manager, + ui, + current_bot: None, + agent: None, + should_quit: false, + }) + } + + /// 运行应用 + pub async fn run(&mut self) -> Result<()> { + enable_raw_mode().context("无法启用原始模式")?; + + let mut stdout = io::stdout(); + execute!( + stdout, + EnterAlternateScreen, + EnableMouseCapture, + crossterm::terminal::DisableLineWrap + ) + .context("无法设置终端")?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = ratatui::Terminal::new(backend).context("无法创建终端")?; + + let mut last_log_update = Instant::now(); + let tick_rate = Duration::from_millis(100); + + loop { + // 更新日志 + if last_log_update.elapsed() > Duration::from_secs(1) { + self.update_logs(); + last_log_update = Instant::now(); + } + + // 渲染 UI + terminal.draw(|f| self.ui.render(f)).context("渲染失败")?; + + // 处理事件 + if event::poll(tick_rate).context("事件轮询失败")? { + let event = event::read().context("读取事件失败")?; + log::trace!("收到事件: {:?}", event); + + match event { + Event::Key(key) => { + let action = self.ui.handle_key_event(key); + + match action { + crate::ui::KeyAction::Quit => { + self.should_quit = true; + break; + } + crate::ui::KeyAction::SendMessage => { + if self.ui.state == AppState::Chat { + self.send_message().await?; + } + } + crate::ui::KeyAction::ClearChat => { + if self.ui.state == AppState::Chat { + self.clear_chat(); + } + } + crate::ui::KeyAction::ShowHelp => { + if self.ui.state == AppState::Chat { + self.show_help(); + } + } + crate::ui::KeyAction::DumpChats => { + if self.ui.state == AppState::Chat { + self.dump_chats(); + } + } + crate::ui::KeyAction::Continue => {} + } + } + Event::Mouse(mouse) => { + let size = terminal.size()?; + self.ui + .handle_mouse_event(mouse, Rect::new(0, 0, size.width, size.height)); + } + _ => {} + } + } + + if self.should_quit { + break; + } + } + + disable_raw_mode().context("无法禁用原始模式")?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ) + .context("无法恢复终端")?; + + terminal.show_cursor().context("无法显示光标")?; + + log::info!("应用程序退出"); + Ok(()) + } + + /// 更新日志 + fn update_logs(&mut self) { + match self.log_manager.read_logs(1000) { + Ok(logs) => { + let log_count = logs.len(); + self.ui.log_lines = logs; + } + Err(e) => { + log::error!("读取日志失败: {}", e); + } + } + } + + /// 发送消息 + async fn send_message(&mut self) -> Result<()> { + let input_text = self.ui.get_input_text(); + let input_text = input_text.trim(); + + log::debug!("准备发送消息,长度: {}", input_text.len()); + + if input_text.is_empty() { + log::debug!("消息为空,忽略"); + return Ok(()); + } + + // 检查是否是命令 + if let Some(command_action) = self.ui.parse_and_execute_command(input_text) { + self.ui.clear_input(); + + match command_action { + crate::ui::KeyAction::Quit => { + self.should_quit = true; + } + crate::ui::KeyAction::ClearChat => { + self.clear_chat(); + } + crate::ui::KeyAction::ShowHelp => { + self.show_help(); + } + crate::ui::KeyAction::DumpChats => { + self.dump_chats(); + } + _ => {} + } + return Ok(()); + } + + // 检查是否刚进入聊天模式 + if self.current_bot.is_none() { + if let Some(bot) = self.ui.selected_bot() { + self.current_bot = Some(bot.clone()); + self.agent = Some(AgentFactory::create_mock_agent( + &bot.name, + &bot.system_prompt, + )); + log::info!("选择机器人: {}", bot.name); + } else { + log::warn!("没有选中的机器人"); + return Ok(()); + } + } + + // 添加用户消息 + let user_message = ChatMessage::user(input_text); + self.ui.messages.push(user_message.clone()); + self.ui.clear_input(); + + log::info!("用户发送消息: {}", input_text); + log::debug!("当前消息总数: {}", self.ui.messages.len()); + + // 获取 AI 响应 + if let Some(agent) = &self.agent { + let mut messages = vec![]; + if let Some(bot) = &self.current_bot { + messages.push(ChatMessage::system(&bot.system_prompt)); + log::debug!("添加系统提示词"); + } + messages.extend(self.ui.messages.iter().cloned()); + log::debug!("准备调用 Agent,消息数: {}", messages.len()); + + match agent.chat(&messages).await { + Ok(response) => { + log::info!("AI 响应成功,长度: {}", response.len()); + let assistant_message = ChatMessage::assistant(response); + self.ui.messages.push(assistant_message); + } + Err(e) => { + log::error!("AI 响应失败: {}", e); + let error_message = ChatMessage::assistant(format!("错误: {}", e)); + self.ui.messages.push(error_message); + } + } + } else { + log::warn!("Agent 未初始化"); + } + + // 滚动到底部 - 将在渲染时自动计算 + self.ui.scroll_offset = usize::MAX; + + Ok(()) + } + + /// 清空会话 + fn clear_chat(&mut self) { + log::info!("清空会话"); + self.ui.messages.clear(); + self.ui.scroll_offset = 0; + } + + /// 显示帮助信息 + fn show_help(&mut self) { + log::info!("显示帮助信息"); + let help_message = ChatMessage::assistant(AppUi::get_help_message()); + self.ui.messages.push(help_message); + self.ui.scroll_offset = usize::MAX; + } + + /// 导出会话到剪贴板 + fn dump_chats(&mut self) { + match self.ui.dump_chats_to_clipboard() { + Ok(msg) => { + log::info!("{}", msg); + let success_message = ChatMessage::assistant(msg); + self.ui.messages.push(success_message); + } + Err(e) => { + log::error!("{}", e); + let error_message = ChatMessage::assistant(format!("❌ {}", e)); + self.ui.messages.push(error_message); + } + } + self.ui.scroll_offset = usize::MAX; + } +} + +/// 创建默认机器人 +pub fn create_default_bots(config_manager: &ConfigManager) -> Result<()> { + let bots = config_manager.get_bots()?; + + if bots.is_empty() { + // 创建默认机器人 + let default_bot = BotConfig::new( + "助手", + "你是一个有用的 AI 助手,能够回答各种问题并提供帮助。", + "password", + ); + config_manager.add_bot(default_bot)?; + + let coder_bot = BotConfig::new( + "程序员", + "你是一个经验丰富的程序员,精通多种编程语言,能够帮助解决编程问题。", + "password", + ); + config_manager.add_bot(coder_bot)?; + + log::info!("已创建默认机器人"); + } + + Ok(()) +} + diff --git a/examples/cortex-mem-tars-new/src/config.rs b/examples/cortex-mem-tars-new/src/config.rs new file mode 100644 index 0000000..b33e7ba --- /dev/null +++ b/examples/cortex-mem-tars-new/src/config.rs @@ -0,0 +1,142 @@ +use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; + +/// 机器人配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BotConfig { + pub id: String, + pub name: String, + pub system_prompt: String, + pub access_password: String, + pub created_at: DateTime, +} + +impl BotConfig { + pub fn new(name: impl Into, system_prompt: impl Into, access_password: impl Into) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + name: name.into(), + system_prompt: system_prompt.into(), + access_password: access_password.into(), + created_at: Utc::now(), + } + } +} + +/// 配置管理器 +pub struct ConfigManager { + config_dir: PathBuf, + bots_file: PathBuf, +} + +impl ConfigManager { + /// 创建新的配置管理器 + pub fn new() -> Result { + let config_dir = directories::ProjectDirs::from("com", "cortex", "mem-tars") + .context("无法获取项目目录")? + .config_dir() + .to_path_buf(); + + fs::create_dir_all(&config_dir).context("无法创建配置目录")?; + + let bots_file = config_dir.join("bots.json"); + + Ok(Self { + config_dir, + bots_file, + }) + } + + /// 获取所有机器人配置 + pub fn get_bots(&self) -> Result> { + if !self.bots_file.exists() { + return Ok(vec![]); + } + + let content = fs::read_to_string(&self.bots_file).context("无法读取配置文件")?; + let bots: Vec = serde_json::from_str(&content).context("无法解析配置文件")?; + + Ok(bots) + } + + /// 保存所有机器人配置 + fn save_bots(&self, bots: &[BotConfig]) -> Result<()> { + let content = serde_json::to_string_pretty(bots).context("无法序列化配置")?; + fs::write(&self.bots_file, content).context("无法写入配置文件")?; + Ok(()) + } + + /// 添加机器人 + pub fn add_bot(&self, bot: BotConfig) -> Result<()> { + let bot_name = bot.name.clone(); + let bot_id = bot.id.clone(); + let mut bots = self.get_bots()?; + bots.push(bot); + self.save_bots(&bots)?; + log::info!("添加机器人: {} (ID: {})", bot_name, bot_id); + Ok(()) + } + + /// 删除机器人 + pub fn remove_bot(&self, bot_id: &str) -> Result { + let mut bots = self.get_bots()?; + let original_len = bots.len(); + bots.retain(|bot| bot.id != bot_id); + + if bots.len() < original_len { + self.save_bots(&bots)?; + log::info!("删除机器人 ID: {}", bot_id); + Ok(true) + } else { + Ok(false) + } + } + + /// 更新机器人 + pub fn update_bot(&self, bot_id: &str, updated_bot: BotConfig) -> Result { + let bot_name = updated_bot.name.clone(); + let mut bots = self.get_bots()?; + if let Some(bot) = bots.iter_mut().find(|b| b.id == bot_id) { + *bot = updated_bot; + self.save_bots(&bots)?; + log::info!("更新机器人: {} (ID: {})", bot_name, bot_id); + Ok(true) + } else { + Ok(false) + } + } + + /// 根据 ID 获取机器人 + pub fn get_bot(&self, bot_id: &str) -> Result> { + let bots = self.get_bots()?; + Ok(bots.into_iter().find(|bot| bot.id == bot_id)) + } + + /// 获取配置目录路径 + pub fn config_dir(&self) -> &Path { + &self.config_dir + } +} + +impl Default for ConfigManager { + fn default() -> Self { + Self::new().expect("无法初始化配置管理器") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bot_config_creation() { + let bot = BotConfig::new("TestBot", "You are a helpful assistant", "password123"); + assert_eq!(bot.name, "TestBot"); + assert_eq!(bot.system_prompt, "You are a helpful assistant"); + assert_eq!(bot.access_password, "password123"); + assert!(!bot.id.is_empty()); + } +} \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/lib.rs b/examples/cortex-mem-tars-new/src/lib.rs new file mode 100644 index 0000000..07a3c0d --- /dev/null +++ b/examples/cortex-mem-tars-new/src/lib.rs @@ -0,0 +1,5 @@ +pub mod agent; +pub mod config; +pub mod app; +pub mod ui; +pub mod logger; \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/logger.rs b/examples/cortex-mem-tars-new/src/logger.rs new file mode 100644 index 0000000..551032b --- /dev/null +++ b/examples/cortex-mem-tars-new/src/logger.rs @@ -0,0 +1,129 @@ +use anyhow::{Context, Result}; +use log::{Level, LevelFilter, Metadata, Record}; +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +/// 日志管理器 +pub struct LogManager { + log_file: PathBuf, + file: Arc>, + lines: Arc>>, +} + +impl LogManager { + /// 创建新的日志管理器 + pub fn new(log_dir: &Path) -> Result { + let log_file = log_dir.join("app.log"); + + // 确保日志目录存在 + if let Some(parent) = log_file.parent() { + std::fs::create_dir_all(parent).context("无法创建日志目录")?; + } + + // 打开或创建日志文件 + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_file) + .context("无法打开日志文件")?; + + Ok(Self { + log_file, + file: Arc::new(Mutex::new(file)), + lines: Arc::new(Mutex::new(Vec::new())), + }) + } + + /// 写入日志 + pub fn write(&self, level: Level, message: &str) -> Result<()> { + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + let log_line = format!("[{} {}] {}", timestamp, level, message); + + // 写入文件 + let mut file = self.file.lock().map_err(|e| anyhow::anyhow!("无法获取文件锁: {}", e))?; + writeln!(file, "{}", log_line) + .context("无法写入日志")?; + file.flush().context("无法刷新日志")?; + + // 添加到内存中的日志行 + let mut lines = self.lines.lock().map_err(|e| anyhow::anyhow!("无法获取日志行锁: {}", e))?; + lines.push(log_line.clone()); + + // 限制内存中的日志行数 + if lines.len() > 1000 { + let excess = lines.len() - 1000; + lines.drain(0..excess); + } + + Ok(()) + } + + /// 读取日志内容 + pub fn read_logs(&self, max_lines: usize) -> Result> { + let lines = self.lines.lock().map_err(|e| anyhow::anyhow!("无法获取日志行锁: {}", e))?; + + // 返回最后 max_lines 行 + if lines.len() > max_lines { + Ok(lines[lines.len() - max_lines..].to_vec()) + } else { + Ok(lines.clone()) + } + } + + /// 获取日志文件路径 + pub fn log_file_path(&self) -> &Path { + &self.log_file + } + + /// 清空日志 + pub fn clear(&self) -> Result<()> { + File::create(&self.log_file).context("无法清空日志文件")?; + let mut lines = self.lines.lock().map_err(|e| anyhow::anyhow!("无法获取日志行锁: {}", e))?; + lines.clear(); + Ok(()) + } +} + +/// 自定义 Logger +struct SimpleLogger { + manager: Arc, +} + +impl log::Log for SimpleLogger { + fn enabled(&self, _metadata: &Metadata) -> bool { + true + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let message = format!("{}", record.args()); + if let Err(e) = self.manager.write(record.level(), &message) { + eprintln!("日志写入失败: {}", e); + } + } + } + + fn flush(&self) {} +} + +/// 初始化日志系统 +pub fn init_logger(log_dir: &Path) -> Result> { + let manager = Arc::new(LogManager::new(log_dir)?); + + // 创建自定义 logger + let logger = SimpleLogger { + manager: Arc::clone(&manager), + }; + + // 设置全局 logger + log::set_logger(Box::leak(Box::new(logger))) + .map_err(|e| anyhow::anyhow!("无法设置 logger: {}", e))?; + log::set_max_level(LevelFilter::Debug); + + log::info!("日志系统初始化完成"); + log::info!("日志文件路径: {}", log_dir.display()); + + Ok(manager) +} \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/main.rs b/examples/cortex-mem-tars-new/src/main.rs new file mode 100644 index 0000000..3559f0b --- /dev/null +++ b/examples/cortex-mem-tars-new/src/main.rs @@ -0,0 +1,33 @@ +mod agent; +mod app; +mod config; +mod logger; +mod ui; + +use anyhow::{Context, Result}; +use app::{create_default_bots, App}; +use config::ConfigManager; +use logger::init_logger; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<()> { + // 初始化配置管理器 + let config_manager = ConfigManager::new().context("无法初始化配置管理器")?; + log::info!("配置管理器初始化成功"); + + // 初始化日志系统 + let log_manager = init_logger(config_manager.config_dir()).context("无法初始化日志系统")?; + log::info!("日志系统初始化成功"); + + // 创建默认机器人 + create_default_bots(&config_manager).context("无法创建默认机器人")?; + + // 创建并运行应用 + let mut app = App::new(config_manager, log_manager).context("无法创建应用")?; + log::info!("应用创建成功"); + + app.run().await.context("应用运行失败")?; + + Ok(()) +} diff --git a/examples/cortex-mem-tars-new/src/ui.rs b/examples/cortex-mem-tars-new/src/ui.rs new file mode 100644 index 0000000..55d40ee --- /dev/null +++ b/examples/cortex-mem-tars-new/src/ui.rs @@ -0,0 +1,1315 @@ +use crate::agent::ChatMessage; +use crate::config::BotConfig; +use clipboard::ClipboardProvider; +use ratatui::{ + crossterm::event::{KeyEvent, MouseEvent, MouseEventKind}, + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap}, + Frame, +}; +use tui_markdown::from_str; +use tui_textarea::TextArea; + +/// 应用状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppState { + BotSelection, + Chat, +} + +/// 聊天界面状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChatState { + Normal, + LogPanel, + Selection, +} + +/// 应用 UI 状态 +pub struct AppUi { + pub state: AppState, + pub chat_state: ChatState, + pub bot_list_state: ListState, + pub bot_list: Vec, + pub messages: Vec, + pub input_textarea: TextArea<'static>, + pub scroll_offset: usize, + pub log_panel_visible: bool, + pub log_lines: Vec, + pub log_scroll_offset: usize, + pub input_area_width: u16, + last_key_event: Option, + // 选择模式相关字段 + pub selection_active: bool, + pub selection_start: Option<(usize, usize)>, // (line_index, char_index) + pub selection_end: Option<(usize, usize)>, // (line_index, char_index) + pub cursor_position: (usize, usize), // 当前光标位置 (line_index, char_index) + // 消息显示区域位置 + pub messages_area: Option, +} + +/// 键盘事件处理结果 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyAction { + Continue, // 继续运行 + Quit, // 退出程序 + SendMessage, // 发送消息 + ClearChat, // 清空会话 + ShowHelp, // 显示帮助 + DumpChats, // 导出会话到剪贴板 +} + +impl AppUi { + pub fn new() -> Self { + let mut bot_list_state = ListState::default(); + bot_list_state.select(Some(0)); + + let mut input_textarea = TextArea::default(); + let _ = input_textarea.set_block(Block::default() + .borders(Borders::ALL) + .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); + let _ = input_textarea.set_cursor_line_style(Style::default()); + + Self { + state: AppState::BotSelection, + chat_state: ChatState::Normal, + bot_list_state, + bot_list: vec![], + messages: vec![], + input_textarea, + scroll_offset: 0, + log_panel_visible: false, + log_lines: vec![], + log_scroll_offset: 0, + input_area_width: 0, + last_key_event: None, + selection_active: false, + selection_start: None, + selection_end: None, + cursor_position: (0, 0), + messages_area: None, + } + } + + /// 设置机器人列表 + pub fn set_bot_list(&mut self, bots: Vec) { + self.bot_list = bots; + if !self.bot_list.is_empty() { + self.bot_list_state.select(Some(0)); + } else { + self.bot_list_state.select(None); + } + } + + /// 获取选中的机器人 + pub fn selected_bot(&self) -> Option<&BotConfig> { + if let Some(index) = self.bot_list_state.selected() { + self.bot_list.get(index) + } else { + None + } + } + + /// 处理键盘事件 + pub fn handle_key_event(&mut self, key: KeyEvent) -> KeyAction { + // 事件去重:如果和上一次事件完全相同,则忽略 + if let Some(last_key) = self.last_key_event { + if last_key.code == key.code && last_key.modifiers == key.modifiers { + // log::debug!("忽略重复事件: {:?}", key); + self.last_key_event = None; + return KeyAction::Continue; + } + } + + self.last_key_event = Some(key); + + match self.state { + AppState::BotSelection => { + if self.handle_bot_selection_key(key) { + KeyAction::Continue + } else { + KeyAction::Quit + } + } + AppState::Chat => self.handle_chat_key(key), + } + } + + /// 处理机器人选择界面的键盘事件 + fn handle_bot_selection_key(&mut self, key: KeyEvent) -> bool { + use ratatui::crossterm::event::KeyCode; + match key.code { + KeyCode::Up | KeyCode::Char('k') => { + if let Some(selected) = self.bot_list_state.selected() { + if selected > 0 { + self.bot_list_state.select(Some(selected - 1)); + log::debug!("选择上一个机器人"); + } + } + true + } + KeyCode::Down | KeyCode::Char('j') => { + if let Some(selected) = self.bot_list_state.selected() { + if selected < self.bot_list.len().saturating_sub(1) { + self.bot_list_state.select(Some(selected + 1)); + log::debug!("选择下一个机器人"); + } + } + true + } + KeyCode::Enter => { + if let Some(bot) = self.selected_bot() { + log::info!("选择机器人: {}", bot.name); + self.state = AppState::Chat; + } + true + } + KeyCode::Char('q') => { + log::info!("用户按 q 退出"); + false + } + KeyCode::Char('c') if key.modifiers.contains(ratatui::crossterm::event::KeyModifiers::CONTROL) => { + log::info!("用户按 Ctrl-C 退出"); + false + } + _ => true, + } + } + + /// 处理聊天界面的键盘事件 + fn handle_chat_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::{KeyCode, KeyModifiers}; + + if self.log_panel_visible { + log::debug!("日志面板打开,处理日志面板键盘事件"); + if self.handle_log_panel_key(key) { + KeyAction::Continue + } else { + KeyAction::Quit + } + } else { + match key.code { + KeyCode::Enter => { + if key.modifiers.is_empty() { + // Enter 发送消息 + log::debug!("Enter: 准备发送消息"); + let text = self.get_input_text(); + if !text.trim().is_empty() { + KeyAction::SendMessage + } else { + KeyAction::Continue + } + } else { + // Shift+Enter 换行 + log::debug!("Shift+Enter: 换行"); + self.input_textarea.input(key); + KeyAction::Continue + } + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + log::debug!("Ctrl-C: 退出"); + KeyAction::Quit + } + KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => { + log::debug!("Ctrl-L: 切换日志面板"); + self.log_panel_visible = !self.log_panel_visible; + KeyAction::Continue + } + KeyCode::Esc => { + log::debug!("关闭日志面板"); + self.log_panel_visible = false; + // 清除选择 + self.selection_active = false; + self.selection_start = None; + self.selection_end = None; + KeyAction::Continue + } + KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => { + // 先让 tui-textarea 处理输入 + self.input_textarea.input(key); + // 尝试自动换行 + self.handle_auto_wrap(); + KeyAction::Continue + } + _ => { + self.input_textarea.input(key); + KeyAction::Continue + } + } + } + } + + /// 处理自动换行 - 检查所有行的显示宽度,必要时插入换行 + fn handle_auto_wrap(&mut self) { + // 使用实际的输入框宽度(减去边框的2个字符) + let max_display_width = if self.input_area_width > 2 { + (self.input_area_width as usize).saturating_sub(2) + } else { + 74 // 默认宽度 + }; + + loop { + let lines = self.input_textarea.lines().to_vec(); + let (cursor_row, cursor_col) = self.input_textarea.cursor(); + + // 检查所有行,找到第一行超过宽度的行 + let mut wrap_line_idx = None; + let mut wrap_pos = 0; + + for (line_idx, line) in lines.iter().enumerate() { + let line_width: usize = line.chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum(); + + if line_width > max_display_width { + // 找到这一行中需要换行的位置 + let chars: Vec = line.chars().collect(); + let mut current_width = 0usize; + + for (char_idx, c) in chars.iter().enumerate() { + let char_width = unicode_width::UnicodeWidthChar::width(*c).unwrap_or(0); + current_width += char_width; + + if current_width > max_display_width { + // 找到前一个空格的位置 + wrap_pos = char_idx; + for j in (0..char_idx).rev() { + if chars[j].is_whitespace() { + wrap_pos = j; + break; + } + } + if wrap_pos == 0 { + wrap_pos = 1; + } + wrap_line_idx = Some(line_idx); + break; + } + } + break; + } + } + + // 如果没有需要换行的行,退出 + if wrap_line_idx.is_none() { + return; + } + + let line_idx = wrap_line_idx.unwrap(); + let line = &lines[line_idx]; + let chars: Vec = line.chars().collect(); + + log::debug!("[AUTO_WRAP] line {} needs wrap, pos {}", line_idx, wrap_pos); + + // 分割这一行 + let prefix: String = chars[..wrap_pos].iter().collect(); + let suffix: String = chars[wrap_pos..].iter().collect(); + + // 构建新的行列表 + let mut new_lines = lines[..line_idx].to_vec(); + new_lines.push(prefix.trim_end().to_string()); + new_lines.push(suffix.trim_start().to_string()); + if line_idx + 1 < lines.len() { + new_lines.extend_from_slice(&lines[line_idx + 1..]); + } + + // 重新创建 TextArea + let mut new_textarea = TextArea::from(new_lines.iter().cloned()); + let _ = new_textarea.set_block(Block::default() + .borders(Borders::ALL) + .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); + let _ = new_textarea.set_cursor_line_style(Style::default()); + + // 重新计算光标位置 + let new_cursor_row = if line_idx < cursor_row { + cursor_row + 1 + } else if line_idx == cursor_row { + if cursor_col > wrap_pos { + line_idx + 1 + } else { + line_idx + } + } else { + cursor_row + }; + + let new_cursor_col = if cursor_row == line_idx && cursor_col > wrap_pos { + let suffix_prefix: String = chars[wrap_pos..cursor_col].iter().collect(); + suffix_prefix.chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum() + } else if cursor_row == line_idx { + cursor_col + } else { + cursor_col + }; + + // 移动光标到正确位置 + for _ in 0..new_cursor_row { + new_textarea.move_cursor(tui_textarea::CursorMove::Down); + } + for _ in 0..new_cursor_col { + new_textarea.move_cursor(tui_textarea::CursorMove::Forward); + } + + self.input_textarea = new_textarea; + } + } + + /// 处理日志面板的键盘事件 + fn handle_log_panel_key(&mut self, key: KeyEvent) -> bool { + use ratatui::crossterm::event::KeyCode; + match key.code { + KeyCode::Esc => { + self.log_panel_visible = false; + true + } + KeyCode::Up | KeyCode::Char('k') => { + if self.log_scroll_offset > 0 { + self.log_scroll_offset -= 1; + } + true + } + KeyCode::Down | KeyCode::Char('j') => { + if self.log_scroll_offset < self.log_lines.len().saturating_sub(1) { + self.log_scroll_offset += 1; + } + true + } + KeyCode::PageUp => { + self.log_scroll_offset = self.log_scroll_offset.saturating_sub(10); + true + } + KeyCode::PageDown => { + self.log_scroll_offset = self + .log_scroll_offset + .saturating_add(10) + .min(self.log_lines.len().saturating_sub(1)); + true + } + KeyCode::Home => { + self.log_scroll_offset = 0; + true + } + KeyCode::End => { + self.log_scroll_offset = self.log_lines.len().saturating_sub(1); + true + } + KeyCode::Char('l') => { + self.log_panel_visible = false; + true + } + _ => true, + } + } + + /// 处理选择模式的键盘事件 + fn handle_selection_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::{KeyCode, KeyModifiers}; + + match key.code { + KeyCode::Esc => { + // 退出选择模式 + log::debug!("退出选择模式"); + self.chat_state = ChatState::Normal; + self.selection_active = false; + self.selection_start = None; + self.selection_end = None; + KeyAction::Continue + } + KeyCode::Up | KeyCode::Char('k') => { + // 向上移动光标 + if self.cursor_position.0 > 0 { + self.cursor_position.0 -= 1; + // 调整列位置到新行的长度范围内 + let all_lines = self.get_all_rendered_lines(); + if self.cursor_position.0 < all_lines.len() { + let line_len = all_lines[self.cursor_position.0].len(); + self.cursor_position.1 = self.cursor_position.1.min(line_len); + } + self.selection_end = Some(self.cursor_position); + } + KeyAction::Continue + } + KeyCode::Down | KeyCode::Char('j') => { + // 向下移动光标 + let total_lines = self.calculate_total_lines(); + if self.cursor_position.0 < total_lines.saturating_sub(1) { + self.cursor_position.0 += 1; + // 调整列位置到新行的长度范围内 + let all_lines = self.get_all_rendered_lines(); + if self.cursor_position.0 < all_lines.len() { + let line_len = all_lines[self.cursor_position.0].len(); + self.cursor_position.1 = self.cursor_position.1.min(line_len); + } + self.selection_end = Some(self.cursor_position); + } + KeyAction::Continue + } + KeyCode::Left | KeyCode::Char('h') => { + // 向左移动光标 + if self.cursor_position.1 > 0 { + self.cursor_position.1 -= 1; + self.selection_end = Some(self.cursor_position); + } + KeyAction::Continue + } + KeyCode::Right | KeyCode::Char('l') => { + // 向右移动光标 + let all_lines = self.get_all_rendered_lines(); + if self.cursor_position.0 < all_lines.len() { + let line_len = all_lines[self.cursor_position.0].len(); + if self.cursor_position.1 < line_len { + self.cursor_position.1 += 1; + self.selection_end = Some(self.cursor_position); + } + } + KeyAction::Continue + } + KeyCode::Char('y') => { + // 复制选中的内容 + log::debug!("复制选中的内容"); + self.copy_selection(); + KeyAction::Continue + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+C 复制选中的内容 + log::debug!("Ctrl+C 复制选中的内容"); + self.copy_selection(); + KeyAction::Continue + } + _ => KeyAction::Continue, + } + } + + /// 复制选中的内容到剪贴板 + fn copy_selection(&mut self) { + if let (Some(start), Some(end)) = (self.selection_start, self.selection_end) { + let selected_text = self.get_selected_text(start, end); + + if !selected_text.is_empty() { + match clipboard::ClipboardContext::new() { + Ok(mut ctx) => { + match ctx.set_contents(selected_text.clone()) { + Ok(_) => { + log::info!("已复制 {} 个字符到剪贴板", selected_text.len()); + } + Err(e) => { + log::error!("复制到剪贴板失败: {}", e); + } + } + } + Err(e) => { + log::error!("无法访问剪贴板: {}", e); + } + } + } + } + } + + /// 获取选中的文本 + fn get_selected_text(&self, start: (usize, usize), end: (usize, usize)) -> String { + let mut result = String::new(); + let all_lines = self.get_all_rendered_lines(); + + let start_line = start.0.min(end.0); + let end_line = end.0.max(start.0); + let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; + let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; + + for line_idx in start_line..=end_line { + if line_idx < all_lines.len() { + let line = &all_lines[line_idx]; + let chars: Vec = line.chars().collect(); + let char_len = chars.len(); + + if line_idx == start_line && line_idx == end_line { + // 单行选择 + if start_col < char_len && end_col <= char_len && start_col < end_col { + let selected: String = chars[start_col..end_col].iter().collect(); + result.push_str(&selected); + } + } else if line_idx == start_line { + // 起始行 + if start_col < char_len { + let selected: String = chars[start_col..].iter().collect(); + result.push_str(&selected); + } + result.push('\n'); + } else if line_idx == end_line { + // 结束行 + if end_col <= char_len { + let selected: String = chars[..end_col].iter().collect(); + result.push_str(&selected); + } + } else { + // 中间行 + result.push_str(line); + result.push('\n'); + } + } + } + + result + } + + /// 获取所有渲染的行文本 + fn get_all_rendered_lines(&self) -> Vec { + let mut all_lines: Vec = vec![]; + + for message in &self.messages { + // 角色标签行 + let role_label = match message.role { + crate::agent::MessageRole::System => "[System]", + crate::agent::MessageRole::User => "[You]", + crate::agent::MessageRole::Assistant => "[AI]", + }; + all_lines.push(role_label.to_string()); + + // 渲染 Markdown 内容(与 render_messages 保持一致) + let markdown_text = from_str(&message.content); + for line in markdown_text.lines { + let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); + all_lines.push(line_text); + } + + // 空行分隔 + all_lines.push(String::new()); + } + + all_lines + } + + /// 处理鼠标事件 + pub fn handle_mouse_event(&mut self, event: MouseEvent, _area: Rect) -> bool { + if self.state != AppState::Chat { + return true; + } + + // 使用保存的消息区域 + let messages_area = match self.messages_area { + Some(area) => area, + None => return true, + }; + + match event.kind { + MouseEventKind::ScrollUp => { + if self.log_panel_visible { + if self.log_scroll_offset > 0 { + self.log_scroll_offset = self.log_scroll_offset.saturating_sub(3); + } + } else if self.scroll_offset > 0 { + self.scroll_offset = self.scroll_offset.saturating_sub(3); + } + true + } + MouseEventKind::ScrollDown => { + if self.log_panel_visible { + self.log_scroll_offset = self.log_scroll_offset.saturating_add(3); + } else { + self.scroll_offset = self.scroll_offset.saturating_add(3); + } + true + } + MouseEventKind::Down(but) if but == ratatui::crossterm::event::MouseButton::Left => { + // 鼠标左键按下,开始选择 + let (line_idx, col_idx) = self.mouse_to_text_position(event, messages_area); + self.selection_active = true; + self.selection_start = Some((line_idx, col_idx)); + self.selection_end = Some((line_idx, col_idx)); + true + } + MouseEventKind::Drag(but) if but == ratatui::crossterm::event::MouseButton::Left => { + // 鼠标拖拽,更新选择 + let (line_idx, col_idx) = self.mouse_to_text_position(event, messages_area); + self.selection_end = Some((line_idx, col_idx)); + true + } + MouseEventKind::Up(but) if but == ratatui::crossterm::event::MouseButton::Left => { + + // 保持选择状态,用户可以继续操作 + true + } + MouseEventKind::Up(but) if but == ratatui::crossterm::event::MouseButton::Right => { + // 鼠标右键释放,复制选中的文本 + log::debug!("鼠标右键,复制选中的文本"); + if self.selection_active { + self.copy_selection(); + } + true + } + _ => true, + } + } + + /// 将鼠标坐标转换为文本位置 (line_index, char_index) + fn mouse_to_text_position(&self, event: MouseEvent, area: Rect) -> (usize, usize) { + // 计算相对于消息区域的坐标 + let content_area = area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + // 检查鼠标是否在消息区域内 + if event.row < content_area.top() || event.row >= content_area.bottom() || + event.column < content_area.left() || event.column >= content_area.right() { + log::debug!("鼠标不在消息区域内"); + return (self.scroll_offset, 0); + } + + // 计算行索引(考虑滚动偏移) + let relative_row = event.row.saturating_sub(content_area.top()); + let line_idx = self.scroll_offset + relative_row as usize; + + // 计算列索引 + let relative_col = event.column.saturating_sub(content_area.left()); + let col_idx = relative_col as usize; + + // 获取实际行的文本,确保列索引不超出范围 + let all_lines = self.get_all_rendered_lines(); + if line_idx < all_lines.len() { + let line_len = all_lines[line_idx].len(); + (line_idx, col_idx.min(line_len)) + } else { + log::debug!("行索引超出范围: {} >= {}", line_idx, all_lines.len()); + (line_idx, 0) + } + } + + /// 渲染 UI + pub fn render(&mut self, frame: &mut Frame) { + match self.state { + AppState::BotSelection => self.render_bot_selection(frame), + AppState::Chat => self.render_chat(frame), + } + } + + /// 渲染机器人选择界面 + fn render_bot_selection(&mut self, frame: &mut Frame) { + let area = frame.area(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(3)]) + .split(area); + + // 标题 + let title = Paragraph::new("选择机器人") + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Center) + .style(Style::default().add_modifier(Modifier::BOLD)); + + frame.render_widget(title, chunks[0]); + + // 机器人列表 + let items: Vec = self + .bot_list + .iter() + .map(|bot| { + ListItem::new(Line::from(vec![ + Span::styled( + bot.name.clone(), + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + Span::raw(" - "), + Span::styled( + format!("{}...", &bot.system_prompt.chars().take(40).collect::()), + Style::default().fg(Color::Gray), + ), + ])) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("可用机器人")) + .highlight_style( + Style::default() + .bg(Color::DarkGray) + .add_modifier(Modifier::REVERSED), + ); + + frame.render_stateful_widget(list, chunks[1], &mut self.bot_list_state); + + // 帮助提示 + let help = Paragraph::new("↑/↓ 或 j/k: 选择 | Enter: 进入 | q 或 Ctrl-C: 退出") + .alignment(Alignment::Center); + + frame.render_widget(help, chunks[2]); + } + + /// 渲染聊天界面 + fn render_chat(&mut self, frame: &mut Frame) { + let area = frame.area(); + + if self.log_panel_visible { + self.render_chat_with_log_panel(frame, area); + } else { + self.render_chat_normal(frame, area); + } + } + + /// 渲染普通聊天界面 + fn render_chat_normal(&mut self, frame: &mut Frame, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(8), + ]) + .split(area); + + // 创建简洁的标题文字 + let title_line = Line::from(vec![ + Span::styled( + "TARS AI Program", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ]); + + let title = Paragraph::new(title_line) + .block( + Block::default() + .borders(Borders::ALL) + .border_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + ) + .border_type(ratatui::widgets::BorderType::Double) + .title_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + ) + .title(" [ SYSTEM ACTIVE ] ") + ) + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::White) + .bg(Color::Rgb(20, 30, 40)) + ); + + frame.render_widget(title, chunks[0]); + + // 消息显示区域 + let messages_area = chunks[1]; + self.messages_area = Some(messages_area); + self.render_messages(frame, messages_area); + + // 输入区域 + self.render_input(frame, chunks[2]); + } + + /// 渲染带日志面板的聊天界面 + fn render_chat_with_log_panel(&mut self, frame: &mut Frame, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Percentage(50), + Constraint::Percentage(50), + ]) + .split(area); + + // 创建简洁的标题文字 + let title_line = Line::from(vec![ + Span::styled( + "TARS AI Program", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ]); + + let title = Paragraph::new(title_line) + .block( + Block::default() + .borders(Borders::ALL) + .border_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + ) + .border_type(ratatui::widgets::BorderType::Double) + .title_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + ) + .title(" [ SYSTEM ACTIVE ] ") + ) + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::White) + .bg(Color::Rgb(20, 30, 40)) + ); + + frame.render_widget(title, chunks[0]); + + // 消息显示区域 + let messages_area = chunks[1]; + self.messages_area = Some(messages_area); + self.render_messages(frame, messages_area); + + // 日志面板 + self.render_log_panel(frame, chunks[2]); + } + + /// 计算消息的总行数 + fn calculate_total_lines(&self) -> usize { + let mut total = 0; + for message in &self.messages { + // 角色标签占 1 行 + total += 1; + // 消息内容行数 + total += message.content.lines().count().max(1); + // 空行分隔 + total += 1; + } + total + } + + /// 滚动到底部 + pub fn scroll_to_bottom(&mut self, area_height: u16) { + let total_lines = self.calculate_total_lines(); + let visible_lines = area_height.saturating_sub(2) as usize; + let max_scroll = total_lines.saturating_sub(visible_lines); + self.scroll_offset = max_scroll; + } + + /// 渲染消息 + fn render_messages(&mut self, frame: &mut Frame, area: Rect) { + // 使用 tui-markdown 渲染每个消息 + let content_area = area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + // 收集所有消息的渲染行 + let mut all_lines: Vec = vec![]; + + for message in &self.messages { + let role_label = match message.role { + crate::agent::MessageRole::System => "System", + crate::agent::MessageRole::User => "You", + crate::agent::MessageRole::Assistant => "TARS AI", + }; + + let role_color = match message.role { + crate::agent::MessageRole::System => Color::Yellow, + crate::agent::MessageRole::User => Color::Green, + crate::agent::MessageRole::Assistant => Color::Cyan, + }; + + // 格式化时间戳 + let time_str = message.timestamp.format("%H:%M:%S").to_string(); + + // 添加角色标签和时间戳 + all_lines.push(Line::from(vec![ + Span::styled( + format!("[{}]", role_label), + Style::default() + .fg(role_color) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::styled( + format!("[{}]", time_str), + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::DIM), + ), + ])); + + // 渲染 Markdown 内容 + let markdown_text = from_str(&message.content); + // 将 tui_markdown 的 Text 转换为 ratatui 的 Text + for line in markdown_text.lines { + all_lines.push(Line::from(line.spans.iter().map(|s| { + Span::raw(s.content.clone()) + }).collect::>())); + } + + // 添加空行分隔 + all_lines.push(Line::from("")); + } + + // 计算滚动 + let total_lines = all_lines.len(); + let visible_lines = area.height.saturating_sub(2) as usize; // 减去边框 + let max_scroll = total_lines.saturating_sub(visible_lines); + + // 如果 scroll_offset 为 usize::MAX,表示需要滚动到底部 + if self.scroll_offset == usize::MAX { + self.scroll_offset = max_scroll; + } + + // 限制 scroll_offset 在有效范围内 + if self.scroll_offset > max_scroll { + self.scroll_offset = max_scroll; + } + + // 应用选择高亮 + let display_lines: Vec = if self.selection_active { + self.apply_selection_highlight(all_lines, self.scroll_offset, visible_lines) + } else { + all_lines + .into_iter() + .skip(self.scroll_offset) + .take(visible_lines) + .collect() + }; + + // 渲染边框 + let title = "聊天记录 (鼠标拖拽选择, Esc 清除选择)"; + let block = Block::default() + .borders(Borders::ALL) + .title(title); + frame.render_widget(block, area); + + // 渲染消息内容(在边框内部) + let paragraph = Paragraph::new(display_lines) + .wrap(Wrap { trim: false }); + frame.render_widget(paragraph, content_area); + + // 渲染滚动条 + if total_lines > visible_lines { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + let mut scrollbar_state = ScrollbarState::new(total_lines) + .position(self.scroll_offset); + + let scrollbar_area = area.inner(Margin { + vertical: 1, + horizontal: 0, + }); + + frame.render_stateful_widget( + scrollbar, + scrollbar_area, + &mut scrollbar_state, + ); + } + } + + /// 应用选择高亮 + fn apply_selection_highlight<'a>(&self, lines: Vec>, scroll_offset: usize, visible_lines: usize) -> Vec> { + let (start, end) = match (self.selection_start, self.selection_end) { + (Some(s), Some(e)) => (s, e), + _ => return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(), + }; + + // 如果选择范围完全在可见区域之外,直接返回 + let start_line = start.0.min(end.0); + let end_line = end.0.max(start.0); + + // 可见区域的行范围是 [scroll_offset, scroll_offset + visible_lines) + let visible_start = scroll_offset; + let visible_end = scroll_offset + visible_lines; + + // 如果选择区域和可见区域没有重叠,直接返回 + if end_line < visible_start || start_line >= visible_end { + log::debug!("选择区域和可见区域没有重叠,直接返回"); + return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(); + } + + let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; + let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; + + // 使用反色样式使高亮更明显 + let highlight_style = Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD); + + let mut highlighted_count = 0; + let mut total_processed = 0; + + let result = lines + .into_iter() + .enumerate() + .skip(scroll_offset) + .take(visible_lines) + .map(|(original_idx, line)| { + total_processed += 1; + // original_idx 是原始的行索引(从 0 开始) + // skip(scroll_offset) 后,visible_idx 从 0 开始 + let visible_idx = original_idx - scroll_offset; + let in_range = original_idx >= start_line && original_idx <= end_line; + + if in_range { + // 这一行在选择范围内 + highlighted_count += 1; + let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); + let chars: Vec = line_text.chars().collect(); + let char_len = chars.len(); + + if original_idx == start_line && original_idx == end_line { + // 单行选择 + let safe_start_col = start_col.min(char_len); + let safe_end_col = end_col.min(char_len); + if safe_start_col < char_len && safe_end_col <= char_len && safe_start_col < safe_end_col { + let before: String = chars[..safe_start_col].iter().collect(); + let selected: String = chars[safe_start_col..safe_end_col].iter().collect(); + let after: String = chars[safe_end_col..].iter().collect(); + + log::debug!("单行高亮: original_idx={}, before_len={}, selected_len={}, after_len={}", + original_idx, before.len(), selected.len(), after.len()); + + Line::from(vec![ + Span::raw(before), + Span::styled(selected, highlight_style), + Span::raw(after), + ]) + } else { + line + } + } else if original_idx == start_line { + // 起始行 + let safe_start_col = start_col.min(char_len); + if safe_start_col < char_len { + let before: String = chars[..safe_start_col].iter().collect(); + let selected: String = chars[safe_start_col..].iter().collect(); + + Line::from(vec![ + Span::raw(before), + Span::styled(selected, highlight_style), + ]) + } else { + line + } + } else if original_idx == end_line { + // 结束行 + let safe_end_col = end_col.min(char_len); + if safe_end_col <= char_len { + let selected: String = chars[..safe_end_col].iter().collect(); + let after: String = chars[safe_end_col..].iter().collect(); + + Line::from(vec![ + Span::styled(selected, highlight_style), + Span::raw(after), + ]) + } else { + line + } + } else { + // 中间行,整行高亮 + Line::from(vec![Span::styled( + line_text, + highlight_style, + )]) + } + } else { + line + } + }) + .collect(); + + result + } + + /// 渲染输入框 - 使用 tui-textarea + fn render_input(&mut self, frame: &mut Frame, area: Rect) { + // 保存输入框可用宽度(减去边框的2个字符) + self.input_area_width = area.width.saturating_sub(2); + frame.render_widget(&self.input_textarea, area); + } + + /// 渲染日志面板 + fn render_log_panel(&mut self, frame: &mut Frame, area: Rect) { + let visible_lines = area.height as usize; + let max_scroll = self.log_lines.len().saturating_sub(visible_lines); + self.log_scroll_offset = self.log_scroll_offset.min(max_scroll); + + let display_lines: Vec = self + .log_lines + .iter() + .skip(self.log_scroll_offset) + .take(visible_lines) + .map(|line| { + let style = if line.contains("ERROR") { + Style::default().fg(Color::Red) + } else if line.contains("WARN") { + Style::default().fg(Color::Yellow) + } else if line.contains("INFO") { + Style::default().fg(Color::Green) + } else { + Style::default() + }; + Line::from(Span::styled(line.clone(), style)) + }) + .collect(); + + let paragraph = Paragraph::new(display_lines) + .block(Block::default().borders(Borders::ALL).title("日志 (Esc 关闭)")) + .wrap(Wrap { trim: false }); + + frame.render_widget(paragraph, area); + + // 渲染滚动条 + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + let mut scrollbar_state = ScrollbarState::new(self.log_lines.len()) + .position(self.log_scroll_offset); + + let scrollbar_area = area.inner(Margin { + vertical: 1, + horizontal: 0, + }); + + frame.render_stateful_widget( + scrollbar, + scrollbar_area, + &mut scrollbar_state, + ); + } + + /// Get input text, filtering out auto-wrap newlines + /// Heuristic: next line starts with whitespace = user newline (Shift+Enter) + /// next line starts without whitespace = auto-wrap continuation + pub fn get_input_text(&self) -> String { + let lines = self.input_textarea.lines(); + if lines.len() <= 1 { + return lines.first().map(|s| s.as_str()).unwrap_or("").to_string(); + } + + let mut result = Vec::new(); + let mut current_line = lines[0].clone(); + + for i in 1..lines.len() { + let line = &lines[i]; + let starts_with_space = line.starts_with(' ') || line.starts_with('\t'); + + if starts_with_space { + result.push(current_line); + current_line = line.trim_start().to_string(); + } else { + if !current_line.is_empty() && !current_line.ends_with(' ') { + current_line.push(' '); + } + current_line.push_str(line); + } + } + + result.push(current_line); + + if result.len() <= 1 { + return result.first().map(|s| s.as_str()).unwrap_or("").to_string(); + } + + result.join("\n") + } + + /// Clear input box + pub fn clear_input(&mut self) { + self.input_textarea = TextArea::default(); + let _ = self.input_textarea.set_block(Block::default() + .borders(Borders::ALL) + .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); + let _ = self.input_textarea.set_cursor_line_style(Style::default()); + } + + /// 解析并执行命令 + pub fn parse_and_execute_command(&mut self, input: &str) -> Option { + let trimmed = input.trim(); + + // 检查是否是命令(以 / 开头) + if !trimmed.starts_with('/') { + return None; + } + + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + let command = parts.get(0).map(|s| s.to_lowercase()).unwrap_or_default(); + + match command.as_str() { + "/quit" => { + log::info!("执行命令: /quit"); + Some(KeyAction::Quit) + } + "/cls" | "/clear" => { + log::info!("执行命令: {}", command); + Some(KeyAction::ClearChat) + } + "/help" => { + log::info!("执行命令: /help"); + Some(KeyAction::ShowHelp) + } + "/dump-chats" => { + log::info!("执行命令: /dump-chats"); + Some(KeyAction::DumpChats) + } + _ => { + log::warn!("未知命令: {}", command); + None + } + } + } + + /// 获取帮助信息 + pub fn get_help_message() -> String { + "# TARS AI Program - 帮助信息\n\n## 版本\n\nv0.1.0\n\n## 可用命令\n\n| 命令 | 说明 |\n|------|------|\n| `/quit` | 退出程序 |\n| `/cls` 或 `/clear` | 清空会话区域 |\n| `/help` | 显示此帮助信息 |\n| `/dump-chats` | 复制会话区域的所有内容到剪贴板 |\n\n## 快捷键\n\n- **Enter**: 发送消息\n- **Shift+Enter**: 换行\n- **Ctrl+L**: 打开/关闭日志面板\n- **Esc**: 关闭日志面板\n- **q**: 退出程序\n\n## 使用说明\n\n在输入框中输入命令并按 Enter 即可执行。\n\n---\n\n*Powered by TARS AI*".to_string() + } + + /// 导出所有会话内容到剪贴板 + pub fn dump_chats_to_clipboard(&self) -> Result { + let mut content = String::new(); + + for message in &self.messages { + let role = match message.role { + crate::agent::MessageRole::System => "System", + crate::agent::MessageRole::User => "You", + crate::agent::MessageRole::Assistant => "TARS AI", + }; + + let time_str = message.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(); + + content.push_str(&format!("[{}] [{}]\n", role, time_str)); + content.push_str(&message.content); + content.push_str("\n\n"); + } + + if content.is_empty() { + return Err("没有会话内容可导出".to_string()); + } + + // 尝试复制到剪贴板 + match clipboard::ClipboardContext::new() { + Ok(mut ctx) => { + match ctx.set_contents(content.clone()) { + Ok(_) => { + log::info!("已导出 {} 个字符到剪贴板", content.len()); + Ok(format!("已导出 {} 条消息到剪贴板", self.messages.len())) + } + Err(e) => { + log::error!("复制到剪贴板失败: {}", e); + Err(format!("复制到剪贴板失败: {}", e)) + } + } + } + Err(e) => { + log::error!("无法访问剪贴板: {}", e); + Err(format!("无法访问剪贴板: {}", e)) + } + } + } +} + +impl Default for AppUi { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/cortex-mem-tars-new/test.rs b/examples/cortex-mem-tars-new/test.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ab789e4528085038ed3991242796d1ee4682db8 GIT binary patch literal 58378 zcmeHQYm8mhbv~4&QbLpzAw?wVld43H4iiPJUyPdp7G2W zF!qcg526-9L(Yt#O`AXc5w$pK1te;kM^i$11uC`uL83^F9R&kTLQsU1D3zS9@2vT} z_c>?peb2e`Fd=AUpL@?ed++sLYp=cbKEFA0I2w=oqDs^gjYa#S3Hkgn`P&x_$ftwy z9p5%a`;*^pl6&Lwr@vkmy*qNZ?ukaEZGSWBi{!T>~VLi)vLm}Po5e` z+TnZ4Y>PB3=>3R{uKYQgj5#cQjwK@j*IwxXGzXQ?PwUwh>7GExJLNZ}^wwnDzS$O| z0^7k174DEHdJ}n=5I$(D>jaV!!D}dK2mX%9GZ=Y1xr1H-vEr+rx5)QCp__dVFsjty z?k(~e6dXwY9~G%nJX8jTWz6p63HRM?(hAsv`smY8yfu;T5oymJ(&O%$9WPC%>yx|S zGh|{ivSn6fwJX60v;^FJ@;|7vT|VL7hS_^)hrcV5ze=>O_ldsVzCWD)$R|%!)YLk7-|8}ee%ra=umPO*HzIT8E;hj=$0Np zk;6jwia<0fe+KtTO4C$JTjhC3nJu$M8)*rYA~sfsrmt(?(!6~7)19Lu=K?oE=ELsh zL`JI#t!y3ND)$Ex$pejtXL)U7ru?czJEAQ@L*hp2jti&9gub8`DZE2?lBTWwG_5S3 zHW1rzu|?0U?d?kR$+XSm>uTLC*iA%Tf|u%GwioJY7=~;w9~11hiOqG52z@a=0`|iH zEG#AP1cbCoMw2QzO}aW-d6o);Lh);z5R6;e-X-|=%O89kEM7%M8y8yKCEs1p!>SDk zG-?MS4_%^_pxvP8l3L%2d>WBIl@UBkGwI@V+6zet7 zpQJE`Z-eZ1i4BME?2`YDPX*6LM!1h77|}RTI5aS@v0o zN_34pnYR(}a`5*3B14B|tchfN_&JWkYu0ku_NPhY&-j`~fqO)t>XW{j1)Eigzs>iU zh2NI`tPN54v;Md^^c<^8@B$@5V<|SEBWQz#%hz7`66ANI{5B@Nu9opD(RbpPnzv7_lK#mzV!>;Dl;=EM)V5a}Zwo+pvF!T^mlUQwR?BmG<;Sv1jYK zU}ICz=lN^VBOC=l_H^!%w=wWnlmqo${KlEiJre^h_a1&GescSN9q6n+x&0q@HB1=q z$cRb(7<_?R&veAv7_B(#tJGigCtg^k)8#&6$1Uv`kR(%q2z_&s=AQQyE#69VVb%i7Agy_h{ z8Rav5l!-&2U+^e*ieFP{;i!3O^79Um&qXSlWJUtrXiVY{!eeM==wFokizG;!>9&(1DAL#0uW%x8(=Oge!*{q)7b*(6$G|@O)=Onx1_Vzn>Hq2Z${POAV z#@{*kdUssi(Ei31fSK;{qneoYdiD)sF9D@0o=!i8+hYSI$8XwGmv*N5u~ zm;Mk&Ue&iT;?-Q%QjyXYFa_3iy0fq8!YBmL-;5F>u=$j=-zY z$1Hwn$|8EF)VnM_`{NbaJmmPsM1m=Y_M8&9WoMG1er?;xG1B_QyJavkmdd7?(MR@) ztk;A{%6`Gc^lxI@u;i7AdLe~AD@$m2RV^~`r0oTxQvKbItZ7RcXEPVjvLaH+ar3sr z#mzhw*`C+p-j*$Rbwj`$Ly?jF&$uLwJR&*g2_ryR3(;w+gLo*F!R&?i=#GL*mm)bWHx`e^VJ46AQgkbZX^nv{O!x^U;&7 zxrl-C-jF(u8I*bR+cKr+Et=8 z-pq*)4&L|s+DB%fQ5xgK)y*dvZ6izLk~a33z1q5*{f*y?JfrI*eb%gzt$7Xgl9$>| zqB(aY{<*ePANU=R>Z?VL{LGTrtnVs>it8>6RxTjc$uaPWi6> z@%{6ouy(9oVC`7d@y1`XzSYLe&oB`XKz?3$5P<8^eFABvNTSk90gqvp)FHMD*Bq0-jQNBw2fE7tsA z*FVeZ)FGj~u5zs}=s!#rwZ3RIZ}Dv$duDM|A3r1!)RCx3q##`fGcNZqL)MxQNY=R6 zx4Fb?*-9I&h}kIXr8moZb&FiFBEx7p6r)iRYSW&pEtkujbFsP5bru&&>|Ek~yGo=? zl%Jn{)F0EUT-mdEx38O9bkCYJ#`jn?(~z@mzDMfh%mFkNT*k5hSTYhbXeP^$l z$F<>2*G^=gul3#7+^i;;F_0O!HGXl&WvA0)h2`EiGGi*%C}^Z%Vi3*ocO|hn^5|Hp z;W)xZ$bRZ&b52&Xs@i!8eMh8aOqlE^>KFnMC~?Z^w}*H<*j&q(y>|Q zl08Dx;@K5SqhDLL#oz3HWB=mv$IR1zduVU$VplTTc4vUEle`f2sEmtG!6&SeyPw=7 zziEV~E9phoKFCq^#M|QKyYD_#kq83S!k3?oyXe6xk*<1a6=reIY;?|eKocFZ_qqGW zSu94M8@b~4JEk|Ce6>yedOeV)=Xw2#~ zvt;sy2S&G_KK-5eBcI&ZwN0K}yT`5+bglckBr2a1@@1hF-k$0!_xJW)Ir=Z7|9GHr z`n8syO=9c|Ub=Gh+0Q)I(J-@Q_gy=8$M5U^;av~4eSgP;Lu;eBy6NQ06E99(edYs` z27g)s&hao;zf)u#JMv(AP@94s7G_U|dd6&y#MW+Vb-ESo(pvc!lFLRJQ+EK^7NJXE z&^>DD9#Iu!?}}L^d8Y8{&|0y$dEAT7W<( zm=5HIsn5;MxovuLu1zW-+lJqS^w`>qHtRP`mL*rRK zBDVrakclO4VIRO{a|bI1*fD`U5z2SuF0g+Rcd%NDofJO1!qIspE*#I+J;dW?H=5Fi z&tcU99u&JDFq)sM9En$~fvA5yCVe2(#Xa{YdVV9-H_$vIJ~OHd?kK)@AJC`2i`mM z#$62)$2xDl=V{sPwPWJQHh8JKJ0BeCseX3Py@!iqPN^_FhAd!5WmpPyEepGtwVH)G zPk3uxIWDzT?FYO9vR-L=LR*_H`7FjELSVFyINI6=8KU0t_xaU3!oe#s=V*Cc=T}QE zt)VBNCSfaj4RLFAVqIO>mE#*>QB4bth)86<^^K$RYU^KX{OLWY)VTiS`!9JR{`L1mIbD`W^ur}}*uWrYSrouh@dwnZnq|@AFrVPeqo-GGesAv4<_b_T# zPmO%xmMx?2X+MAIhJ)FNreOj*V3?~X)w0*D%w~B+TXRriPt2r>+ZTKPMMyToc+%@< z;nFC}FMH|uwpNst9YuRzadwU|F1u*65bOzR9DAgfFuNS3rUi35)oMSyD!DDP zD*xPWM&xFHKvq6U$I-+?V4u^S$tp;zM7vlAN7buR)KI!3@y}Vf)Nk6eLY=W=1nf>M zDkGTj;5?FIkYu4i^;Jx+B~#J)hg1l zS!t+mWVNVSs#WBi1$gO7gc@)DiWw8+Piwl1#Ro3T^t39aS(nHra#U5}?RYX0R!y)| z(~O$8W`s}WGfdmC#a8`(bmrbY&keN14_7Zbo<9Z1FWJ=htdv4;?Nho`=eN(^-5?nA z>@H&8nAo+ngu|PqEdZ=1D5|m+#+$I|+5*pQajoJY`=>kM(LdI&=5me*HFP_Uel@2blGl#q1(6rqGj`A z&6a8Gi(tZ**)fZZ<$tC;9BEprbcOmyJnsKJEW6~?H6kl6P8!+96Ises#1COFdm3W) ziHuZz+$YSoXGcT|zc80r%>`%C_>I!xyZT`@*`?o&wFAE_rgfnnc`KsFE-n{^X(9R# z>!YrvjauRx$3A%K%Tr%J^_j`@kNam+%w{5j(KR#8Wrn>}5e(TwHcr$1 zJXmqRK$uHF#y_HZq}Ei+kZR-SDX?qHt>tiVE_ zgOI0(|Ae>tP9i8b!sW@zV6hB;h?1o2eDi}=*;kplakdTuXH(R>i@zMjFn7wgU@V%> zy40$bD!-FHKXOQZblvyO%Uzu4j-5@ab;T>C%TWPyg)%K_a}6iZy8IKhfY-e*HZ+l5nH=EYzGK zhALW(UPcErHq$CPa@TjAZzZL3-C0TLQfv}d#z)o7Q4^R*L1+cSN z``Ie(>^B8Mf)P=hx8vk{W3Npj@7ebR;w^C5dx(~HCYk|R>5<;CV=UA!jKM{nwde8GRfHGfpYEKr-tRObCp0WdrA$ zXHO{%^QFuig38qGghz$E;EkN--Ia|})tS8qyf!9$)mo=;pOA^7)iTg9q=z8tCo`dJgXH;d>JdHz9Yn_I8LgLb@;;^2rJSeuLEG9;lBTRipK`AU{0BmDg5*4ey6QPezK6^()>7`^ugJiV6tChkz8;+~wEU z)^}|^E7^cBZ_k?q{{iV8)(&HhC75g!Z2(rNJgryS%RpNWH=E|LW_WMhGLZ_LK#g-? z%8YFHHCO)Wy1GVZdg?2x#!*e$H2Y476|*vo%>9u>ULcuTqXQYlyLYm6FQ~x9t{}+3 zy2R_?3C%QUb_SfiI(dT_YlE*%1>y)>=S6?Gu9e4Ayh`g^=4(d2Q7&U6V;_x?4{cqdX&3URa zcHA$rH7pjXSme0+^Xnu0dtA;~J|Gn?L!vD>B@Jiy>zX)LOEeb4FM|UQSD$LtUvMTf z+WF5|%R_0Tr`~wb9V_{{sHX=o79(}?JX?D<5+jVE`KEo6u))$b%QSP{yfgp`1Npg_ zQ_gp;kI`l}A|@47hcIJ9)qD1B$96sdwBc-7^@4oiHJvFA#g?_9!XCjf@#K)ovQQZQ zha~3T<8PljQTg!jKi&M*k&TxO^*=LXY8^vz(x`fVTmwvX0T`fN_dL}qypQhTkXh>!O9v8>n3deywo4trC3 zk5S&n_@Jmgr%uCBF6*K6G0?c?t$8{uwx8tr3&_tQ^P61}(=$|;%F43FO$HzO7w)u6 zN^ddTyER7r(Sx>l=UwHFe8UcQIGVQ;ehXTHok9go5z3 zX5==3?QpURvruDhNcg8~1z0;X)ho+neR@lcv3>8(A|3J^*EJwybl$uiM_grA72AX- zsE^gNgBOYqS>C5%z~M<`KfEHnSBP))>#_|-K}p$d_GoE^BIngB)_pI(VmL%PA!*h@4~A!)E{2wZ{3PL zas}PMW;8PtU9o-ky09zm#i(WT(q3V&wYsh=wO2W-P>82WEUPGQ5|4s)zTU(t-~{Vi z;_CL#1?3m#3PBd2#TvaKpQl#P7tg>S+T-XhiL_q?-*k-%|GU=;hy^`6wTV7G4^`Z}DjhI#UE65lWeQ$CY6>3yt_JdV#RdrR-(HfJTV_K|BuO%gNp z&909_&a!V72#o5Yu=d03mI>l&kKZL!#94jN4AwLlE%85z zm=rW`Ok&9_R=zn0*8$CLnC$qdz;|;m9FG#W?As_zUs#PY^=jsRUM46Tyy8qW=sezx zP^>P^v>X@fH7Ht(-I_kEi}-=2MAo&=C&c$6*r*2r#jrBxuXrIAq%CUM()FP#z2Ul1 zKSzq;y-@29pr~CXqnX=#CXNr>KdE-B2rYRQQ=^<83fk!VI&=pb-a!eeGrJBeQt{=} z4N}{z)Mr<>(=`TpZrE$DwMMc1tXkS7e?g~JgZ0&c8Jcn8&c-$iVz$|w_+8j*m>=EGr*1NFUZcbTVmeitz*S$!A zrUXi?2cjk~+*>%ZAq|-~$kWoU(ecq05vrMQlfrKQQH)}GCM`69doygv()PKQR1rtJ z2bBl2x}%sGe+pg!53HSPyr=vN+k5f{Sr(i|%Dn>6w>5=kZ~H|qAcc3y71kY*1X?!- zThB#*vHqyMb60xRmhU`WL$gEDDBsF(qey!@vu1o4`D(BV^`(+Sh*ovR)i@2G4vrYRB>2r_tT4V;bGU^l^Unx%)KV5)WYs|V-JRIWi zJa>5hJ8HF2p^a0ve0%fQr|(LRr)PRw1yfkaboRv$PkLW~ZwBUrf%n7xWA~9(ySQdN z>?3o^?u#MXlBobV-dOI?mB~wQ(rJCAR0aESK4?j3mt`#VU#X)I+hk zV4C+vq(k;fSu?XQjeB*6oq818;@fQ?uT~$ivyK8wT6DKli|7H)kzCl!YnDoQ z|K(huwpUXY8}<*+5q7f*v)GWEQvhnqO1WJmW}jy$O3q3_t@WMk^VBUyq_&PR`cn-9 ze%udc&(+;=dUhOspAWmGC;ylTtsWhB;c4f}vr%I?EQ#%6*+-4`io}5SN(^Q$YTdBE z)I|}U?J(zMTV2G6#b{E*tF4SKorf6awW(_d?{mC=nB2%-HBMI^nfHho&Zp-auDFBe z;A2+7_(L(NM<$K5tSxlPViupQE^8hOnW%-(WR9NIX#1TkVOnBOt9c?D$0;4lP<@qf zp&q&ulRUlV-y(1_7$E#ntkg*Yrg38Unf)vt7%TwJ!ycv;z~$G+CKtHsVeV13!fWR(*u zTA0n*Gw3qVmG#AC`CEjqD`mZlOuQMp(hYF2J*+f>(S zR^|+uvX*lGGA2DjmGUc-Wj!Aw%O1&PYc(5uA+vy3wLF*h-{l){FK@#aV&+>$Lwt1i zQO}Kq8PQ;vev&m1TIHboo*!ykpPXdYQ+?(B-ab?>;2mL?C)U`0`>W~GRTF^>BX>jF z)g4&z>zDeMBIm{6DOT~Ib^_}pp_4w;J3n20YUGs(;_~8-v94v)m#mpSwXXN-GZ#uomb8 zLey9rX(26)2HJD3S`y*MBF#5?r~J5fJyyEDPU%plMSXSHuP-*@+P9yRZviOvl57m;LB4c0Wh~p?byl!>Yx!Jip3ht4-nni)fM?)=X)mvWMY1HKr~4| z*g;wU&Ii(p)hM>V&R387=m+bQ-|@hR#6$zM2I)P2VcB%;eJB%Uz^dgXiFmv4WxTK0 z@|(_J%I|_Gzx$!wt_S~a2fr@D^47P@Ay2PRMkfEXnfry>*|VOkuG%|rTz#Y?eD}<3 zYWnrg8@f2d{r-*zhn|RE**SW!VP^U6jZ+_QzqdKM?T+b9Ctq#*TD*Mig)cSCOm+@* zJzaffA6x*b=J-g&z{_r)x?TkNmS@$|e|>`LT(-pnkqhnZS} zctqdg3~ge@#~q_kdW}A*tg^54Y@4+xMv_fC?kuE#=QSN2ru7xO1g;TiQMDP0QBa4k zJ7cjc+>D9!5WG_aQi$>GSbaP>yAk6RKd}+9oG)AEifPg^L5$V6z-Lzb*0w)7dfn{} z*14?dr*KZaEg#3Kmkl2{*dAZ9`$XlhZo%8@pE`B%H=6P1RU zW1a6j8dv4h#8l_627lbKq2;Q>zTNT)PjVuDB5{<&)w^)^cfZhXwRn2WKd`&1VdmX! z5AAK3xx2Hs@A3FQ9{yagmY z4<=A4JJ!37?ybJu{QgT`h;KfW9b?Il{$}U)c*Powad+o~L&}MepP+w)Opl9x4-4H@ z=P(1e4V23RM9wSk7>%>|}6d`83otqbce z;bmKeYlYR9R*$(MA=WAhkeKJ=A2-{w-yW|&Lur6+Kn592Wh<5;}f+K^s# zbMM1)>jrz{wU}u(uj$N3J=Dxw6VlW61BeHf4V`y<|EVRD?eUV`Km26Fgn!32 zp;U<|zk5w9HAC%mc3|s*E!jorKp1^IvAIs2f&8pa z4~2cwACqkKR{68}!`8fF9>Cob;HdIQPpj127$fG2ymMzo_lXL)*D9-w`Ix#AsUJjU zif=YqE#p@r&9yPJwh%N`+c%z2!0a+tdsp;5cWGM)NmgmbnJ1&dvx>Ya$9_8`W~`ui pMd&;#e>;RG=_pZWw0WsAGgr;bq+5|Utuh+cuG7)7kqCwG{{aB^&=UXv literal 0 HcmV?d00001 From 200771fc27c7f5a7fde669592a041b9d931522e2 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 18:57:38 +0800 Subject: [PATCH 3/9] refact tars --- Cargo.lock | 633 +++++++++++++++++- Cargo.toml | 3 +- examples/cortex-mem-tars-new/Cargo.toml | 36 +- examples/cortex-mem-tars-new/README.md | 184 +++++ .../cortex-mem-tars-new/config.example.toml | 72 ++ examples/cortex-mem-tars-new/src/agent.rs | 295 +++++++- examples/cortex-mem-tars-new/src/app.rs | 216 +++++- examples/cortex-mem-tars-new/src/config.rs | 65 ++ .../cortex-mem-tars-new/src/infrastructure.rs | 56 ++ examples/cortex-mem-tars-new/src/lib.rs | 1 + examples/cortex-mem-tars-new/src/main.rs | 31 +- 11 files changed, 1562 insertions(+), 30 deletions(-) create mode 100644 examples/cortex-mem-tars-new/README.md create mode 100644 examples/cortex-mem-tars-new/config.example.toml create mode 100644 examples/cortex-mem-tars-new/src/infrastructure.rs diff --git a/Cargo.lock b/Cargo.lock index 55f8459..fa3283f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,19 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi-to-tui" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdfd3cbf4843347ca072771a797484f1c3434a14d57f39d31c92dfb93a8799a8" +dependencies = [ + "nom 8.0.0", + "ratatui-core", + "simdutf8", + "smallvec", + "thiserror 2.0.17", +] + [[package]] name = "anstream" version = "0.6.21" @@ -256,6 +269,15 @@ 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 = "2.10.0" @@ -265,6 +287,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -383,6 +411,28 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -413,6 +463,20 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "config" version = "0.15.18" @@ -661,6 +725,39 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "cortex-mem-tars-new" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "chrono", + "clipboard", + "cortex-mem-config", + "cortex-mem-core", + "cortex-mem-rig", + "crossterm 0.28.1", + "directories", + "env_logger", + "futures", + "log", + "once_cell", + "ratatui", + "ratatui-core", + "rig-core", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "tui-markdown", + "tui-textarea", + "unicode-width 0.2.0", + "uuid", +] + [[package]] name = "cortex-mem-tools" version = "1.0.0" @@ -915,6 +1012,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -925,13 +1028,22 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", ] [[package]] @@ -942,10 +1054,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1002,6 +1126,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1036,7 +1183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" dependencies = [ "futures-core", - "nom", + "nom 7.1.3", "pin-project-lite", ] @@ -1074,6 +1221,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1203,6 +1356,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.0", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1275,7 +1437,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1283,6 +1445,11 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -1666,6 +1833,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "js-sys" version = "0.3.82" @@ -1687,6 +1878,16 @@ dependencies = [ "serde", ] +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.0", + "thiserror 2.0.17", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1709,6 +1910,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.4.15" @@ -1757,12 +1964,30 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.0", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1867,6 +2092,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1891,6 +2125,35 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1903,6 +2166,28 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags", + "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 = "openssl" version = "0.10.74" @@ -2111,6 +2396,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.12.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -2135,6 +2448,25 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2200,6 +2532,25 @@ dependencies = [ "prost", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "qdrant-client" version = "1.15.0" @@ -2221,6 +2572,15 @@ dependencies = [ "tonic", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2358,16 +2718,36 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags", "cassowary", - "compact_str", + "compact_str 0.8.1", "crossterm 0.28.1", "indoc", "instability", "itertools 0.13.0", - "lru", + "lru 0.12.5", "paste", - "strum", + "strum 0.26.3", "unicode-segmentation", - "unicode-truncate", + "unicode-truncate 1.1.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags", + "compact_str 0.9.0", + "hashbrown 0.16.0", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru 0.16.2", + "strum 0.27.2", + "thiserror 2.0.17", + "unicode-segmentation", + "unicode-truncate 2.0.0", "unicode-width 0.2.0", ] @@ -2391,6 +2771,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -2440,6 +2831,12 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.24" @@ -2578,6 +2975,35 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.108", + "unicode-ident", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -2594,6 +3020,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2689,6 +3124,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 = "schannel" version = "0.1.28" @@ -2938,6 +3382,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.11" @@ -2994,7 +3444,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -3010,6 +3469,18 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3058,6 +3529,27 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.17", + "walkdir", + "yaml-rust", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -3319,18 +3811,30 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -3444,9 +3948,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3468,9 +3972,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3479,9 +3983,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3535,6 +4039,33 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-markdown" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e766339aabad4528c3fccddf4acf03bc2b7ae6642def41e43c7af1a11f183122" +dependencies = [ + "ansi-to-tui", + "itertools 0.14.0", + "pretty_assertions", + "pulldown-cmark", + "ratatui-core", + "rstest", + "syntect", + "tracing", +] + +[[package]] +name = "tui-textarea" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +dependencies = [ + "crossterm 0.28.1", + "ratatui", + "unicode-width 0.2.0", +] + [[package]] name = "typeid" version = "1.0.3" @@ -3582,6 +4113,17 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "unicode-truncate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.2.0", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -3654,6 +4196,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[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" @@ -3794,6 +4346,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -4146,6 +4707,34 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + +[[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 = "yaml-rust2" version = "0.10.4" @@ -4157,6 +4746,12 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 27e31e6..5ee5212 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ members = [ "cortex-mem-config", "cortex-mem-mcp", "cortex-mem-tools", - "examples/cortex-mem-tars" + "examples/cortex-mem-tars", + "examples/cortex-mem-tars-new" ] [dependencies] diff --git a/examples/cortex-mem-tars-new/Cargo.toml b/examples/cortex-mem-tars-new/Cargo.toml index ebca94d..a7d5949 100644 --- a/examples/cortex-mem-tars-new/Cargo.toml +++ b/examples/cortex-mem-tars-new/Cargo.toml @@ -1,24 +1,52 @@ [package] -name = "cortex-mem-tars" +name = "cortex-mem-tars-new" version = "0.1.0" edition = "2024" [dependencies] +# Cortex Memory dependencies +cortex-mem-config = { path = "../../cortex-mem-config" } +cortex-mem-core = { path = "../../cortex-mem-core" } +cortex-mem-rig = { path = "../../cortex-mem-rig" } + +# LLM framework +rig-core = "0.23" + +# TUI ratatui = "0.29" tui-markdown = "0.3.7" ratatui-core = "0.1" crossterm = "0.28" +tui-textarea = "0.7" +unicode-width = "0.2" + +# Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.9" + +# Error handling anyhow = "1.0" + +# Time handling chrono = { version = "0.4", features = ["serde"] } + +# File system directories = "6.0" + +# Logging log = "0.4" env_logger = "0.11" -tui-textarea = "0.7" -unicode-width = "0.2" -uuid = { version = "1.10", features = ["v4"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +once_cell = "1.21" + +# Async async-trait = "0.1" tokio = { version = "1.40", features = ["full"] } +futures = "0.3" +async-stream = "0.3" + +# Utilities +uuid = { version = "1.10", features = ["v4"] } clipboard = "0.5" diff --git a/examples/cortex-mem-tars-new/README.md b/examples/cortex-mem-tars-new/README.md new file mode 100644 index 0000000..0572345 --- /dev/null +++ b/examples/cortex-mem-tars-new/README.md @@ -0,0 +1,184 @@ +# Cortex Memory TARS New + +这是一个基于 Cortex Memory 的 TUI(终端用户界面)聊天应用,具有记忆功能。它能够记住用户的对话历史和个人信息,提供更智能的对话体验。 + +## 功能特性 + +- 🧠 **记忆功能**:自动记忆用户的对话历史和个人信息 +- 🤖 **智能 AI 助手**:支持多个机器人配置,每个机器人可以有不同的系统提示词 +- 📝 **Markdown 渲染**:支持 Markdown 格式的消息显示 +- 💾 **对话导出**:可以将对话导出到剪贴板 +- 🔧 **灵活配置**:支持自定义 LLM API、向量存储等配置 +- 🎨 **现代化 TUI**:基于 ratatui 的美观终端界面 + +## 安装 + +### 前置要求 + +- Rust 1.70 或更高版本 +- Qdrant 向量数据库(可选,用于记忆功能) +- OpenAI API 密钥或其他兼容的 LLM API + +### 构建项目 + +```bash +cd examples/cortex-mem-tars-new +cargo build --release +``` + +## 配置 + +### 1. 创建配置文件 + +将 `config.example.toml` 复制为 `config.toml` 并修改相应的配置: + +```bash +cp config.example.toml config.toml +``` + +### 2. 修改配置 + +编辑 `config.toml` 文件,至少需要配置以下内容: + +```toml +[llm] +api_base_url = "https://api.openai.com/v1" +api_key = "your-actual-api-key" +model_efficient = "gpt-4o-mini" + +[embedding] +api_base_url = "https://api.openai.com/v1" +api_key = "your-actual-api-key" +model_name = "text-embedding-3-small" + +[qdrant] +url = "http://localhost:6334" +``` + +### 3. 启动 Qdrant(可选,用于记忆功能) + +如果你使用记忆功能,需要启动 Qdrant 向量数据库: + +```bash +# 使用 Docker +docker run -p 6334:6334 qdrant/qdrant + +# 或使用本地安装 +qdrant +``` + +## 使用方法 + +### 运行应用 + +```bash +cargo run --release +``` + +### 基本操作 + +- **Enter**:发送消息 +- **Shift+Enter**:换行 +- **Ctrl+L**:打开/关闭日志面板 +- **Esc**:关闭日志面板 +- **Ctrl+H**:显示帮助信息 +- **Ctrl+C**:清空会话 +- **Ctrl+D**:导出对话到剪贴板 +- **q**:退出程序 + +### 命令 + +在输入框中输入以下命令: + +- `/quit`:退出程序 +- `/clear`:清空会话 +- `/help`:显示帮助信息 +- `/dump`:导出对话到剪贴板 + +## 项目结构 + +``` +cortex-mem-tars-new/ +├── src/ +│ ├── main.rs # 主程序入口 +│ ├── app.rs # 应用程序主逻辑 +│ ├── agent.rs # Agent 实现(包括记忆功能) +│ ├── config.rs # 配置管理 +│ ├── infrastructure.rs # 基础设施(LLM、向量存储、记忆管理器) +│ ├── logger.rs # 日志系统 +│ └── ui.rs # TUI 界面 +├── config.example.toml # 配置文件示例 +└── README.md # 本文件 +``` + +## 核心功能 + +### 1. 记忆功能 + +应用会自动: + +- 在启动时加载用户的基本信息(个人特征、事实信息等) +- 在对话过程中使用记忆工具检索相关信息 +- 在退出时将对话历史保存到记忆系统 + +### 2. 多机器人支持 + +可以在配置目录中创建多个机器人配置,每个机器人可以有: + +- 不同的名称 +- 不同的系统提示词 +- 不同的访问密码 + +### 3. 流式响应 + +支持实时的流式 AI 响应,提供更流畅的对话体验。 + +## 故障排除 + +### 1. 无法连接到 Qdrant + +确保 Qdrant 正在运行并且 URL 配置正确: + +```bash +curl http://localhost:6334/health +``` + +### 2. API 密钥错误 + +检查 `config.toml` 中的 API 密钥是否正确。 + +### 3. 记忆功能不工作 + +- 确保 Qdrant 正在运行 +- 检查 API 密钥是否正确 +- 查看日志面板获取详细错误信息 + +## 开发 + +### 运行测试 + +```bash +cargo test +``` + +### 检查代码 + +```bash +cargo check +``` + +### 格式化代码 + +```bash +cargo fmt +``` + +## 许可证 + +MIT + +## 致谢 + +- [Cortex Memory](https://github.com/sopaco/cortex-mem) - 记忆管理系统 +- [RatATUI](https://github.com/ratatui-org/ratatui) - TUI 框架 +- [Rig](https://github.com/0xPlaygrounds/rig) - LLM Agent 框架 \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/config.example.toml b/examples/cortex-mem-tars-new/config.example.toml new file mode 100644 index 0000000..ce1475d --- /dev/null +++ b/examples/cortex-mem-tars-new/config.example.toml @@ -0,0 +1,72 @@ +# Cortex Memory TARS 配置文件示例 +# 将此文件复制为 config.toml 并修改相应的配置 + +[qdrant] +# Qdrant 向量数据库地址 +url = "http://localhost:6334" +# 集合名称 +collection_name = "cortex_mem" +# 嵌入维度(可选,默认为 1536) +embedding_dim = 1536 +# 超时时间(秒) +timeout_secs = 30 + +[llm] +# LLM API 基础 URL +api_base_url = "https://api.openai.com/v1" +# API 密钥 +api_key = "your-api-key-here" +# 使用的模型 +model_efficient = "gpt-4o-mini" +# 温度参数 +temperature = 0.7 +# 最大令牌数 +max_tokens = 2000 + +[server] +# 服务器主机 +host = "127.0.0.1" +# 服务器端口 +port = 8080 +# CORS 允许的来源 +cors_origins = ["*"] + +[embedding] +# 嵌入服务 API 基础 URL +api_base_url = "https://api.openai.com/v1" +# 嵌入模型名称 +model_name = "text-embedding-3-small" +# API 密钥 +api_key = "your-api-key-here" +# 批处理大小 +batch_size = 100 +# 超时时间(秒) +timeout_secs = 30 + +[memory] +# 最大记忆数量 +max_memories = 10000 +# 相似度阈值 +similarity_threshold = 0.65 +# 最大搜索结果数 +max_search_results = 50 +# 记忆 TTL(小时,可选) +memory_ttl_hours = null +# 自动摘要阈值 +auto_summary_threshold = 32768 +# 自动增强 +auto_enhance = true +# 去重 +deduplicate = true +# 合并阈值 +merge_threshold = 0.75 +# 搜索相似度阈值(可选) +search_similarity_threshold = 0.70 + +[logging] +# 是否启用日志 +enabled = false +# 日志目录 +log_directory = "logs" +# 日志级别 +level = "info" \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/agent.rs b/examples/cortex-mem-tars-new/src/agent.rs index 40e5f1a..9042ea6 100644 --- a/examples/cortex-mem-tars-new/src/agent.rs +++ b/examples/cortex-mem-tars-new/src/agent.rs @@ -1,6 +1,21 @@ use anyhow::Result; use async_trait::async_trait; use chrono::{DateTime, Local}; +use cortex_mem_config::Config; +use cortex_mem_core::memory::MemoryManager; +use cortex_mem_rig::{ListMemoriesArgs, create_memory_tools, tool::MemoryToolConfig}; +use futures::StreamExt; +use rig::{ + agent::Agent as RigAgent, + client::CompletionClient, + providers::openai::{Client, CompletionModel}, + tool::Tool, +}; +use rig::agent::MultiTurnStreamItem; +use rig::completion::Message; +use rig::streaming::{StreamedAssistantContent, StreamingChat}; +use std::sync::Arc; +use tokio::sync::mpsc; /// 消息角色 #[derive(Debug, Clone, PartialEq)] @@ -110,6 +125,284 @@ impl AgentFactory { } } +/// 创建带记忆功能的Agent +pub async fn create_memory_agent( + memory_manager: Arc, + memory_tool_config: MemoryToolConfig, + config: &Config, +) -> Result, Box> { + // 创建记忆工具 + let memory_tools = + create_memory_tools(memory_manager.clone(), &config, Some(memory_tool_config)); + + let llm_client = Client::builder(&config.llm.api_key) + .base_url(&config.llm.api_base_url) + .build(); + + // 构建带有记忆工具的agent,让agent能够自主决定何时调用记忆功能 + let completion_model = llm_client + .completion_model(&config.llm.model_efficient) + .completions_api() + .into_agent_builder() + // 注册四个独立的记忆工具,保持与MCP一致 + .tool(memory_tools.store_memory()) + .tool(memory_tools.query_memory()) + .tool(memory_tools.list_memories()) + .tool(memory_tools.get_memory()) + .preamble(&format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 + +此会话发生的初始时间:{current_time} + +你的工具: +- CortexMemoryTool: 可以存储、搜索和检索记忆。支持以下操作: + * store_memory: 存储新记忆 + * query_memory: 搜索相关记忆 + * list_memories: 获取一系列的记忆集合 + * get_memory: 获取特定记忆 + +重要指令: +- 对话历史将作为上下文提供,请使用这些信息来理解当前的对话流程 +- 用户基本信息将在上下文中提供一次,请不要再使用memory工具来创建或更新用户基本信息 +- 在需要时可以自主使用memory工具搜索其他相关记忆 +- 当用户提供新的重要信息时,可以主动使用memory工具存储 +- 保持对话的连贯性和一致性 +- 自然地融入记忆信息,避免刻意复述此前的记忆信息,关注当前的会话内容,记忆主要用于做隐式的逻辑与事实支撑 +- 专注于用户的需求和想要了解的信息,以及想要你做的事情 + +记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S"))) + .build(); + + Ok(completion_model) +} + +/// 从记忆中提取用户基本信息 +pub async fn extract_user_basic_info( + config: &Config, + memory_manager: Arc, + user_id: &str, +) -> Result, Box> { + let memory_tools = create_memory_tools( + memory_manager, + config, + Some(MemoryToolConfig { + default_user_id: Some(user_id.to_string()), + ..Default::default() + }), + ); + + let mut context = String::new(); + + let search_args_personal = ListMemoriesArgs { + limit: Some(20), + memory_type: Some("personal".to_string()), // 使用小写以匹配新API + user_id: Some(user_id.to_string()), + agent_id: None, + }; + + let search_args_factual = ListMemoriesArgs { + limit: Some(20), + memory_type: Some("factual".to_string()), // 使用小写以匹配新API + user_id: Some(user_id.to_string()), + agent_id: None, + }; + + if let Ok(search_result) = memory_tools + .list_memories() + .call(search_args_personal) + .await + { + if let Some(data) = search_result.data { + // 根据新的MCP格式调整数据结构访问 + if let Some(results) = data.get("memories").and_then(|r| r.as_array()) { + if !results.is_empty() { + context.push_str("用户基本信息 - 特征:\n"); + for (i, result) in results.iter().enumerate() { + if let Some(content) = result.get("content").and_then(|c| c.as_str()) { + context.push_str(&format!("{}. {}\n", i + 1, content)); + } + } + return Ok(Some(context)); + } + } + } + } + + if let Ok(search_result) = memory_tools.list_memories().call(search_args_factual).await { + if let Some(data) = search_result.data { + if let Some(results) = data.get("memories").and_then(|r| r.as_array()) { + if !results.is_empty() { + context.push_str("用户基本信息 - 事实:\n"); + for (i, result) in results.iter().enumerate() { + if let Some(content) = result.get("content").and_then(|c| c.as_str()) { + context.push_str(&format!("{}. {}\n", i + 1, content)); + } + } + return Ok(Some(context)); + } + } + } + } + + match context.len() > 0 { + true => Ok(Some(context)), + false => Ok(None), + } +} + +/// Agent回复函数 - 基于tool call的记忆引擎使用(真实流式版本) +pub async fn agent_reply_with_memory_retrieval_streaming( + agent: &RigAgent, + _memory_manager: Arc, + user_input: &str, + _user_id: &str, + user_info: Option<&str>, + conversations: &[(String, String)], + stream_sender: mpsc::UnboundedSender, +) -> Result> { + // 构建对话历史 - 转换为rig的Message格式 + let mut chat_history = Vec::new(); + for (user_msg, assistant_msg) in conversations { + chat_history.push(Message::user(user_msg)); + chat_history.push(Message::assistant(assistant_msg)); + } + + // 构建system prompt,包含明确的指令 + let system_prompt = r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 + +重要指令: +- 对话历史已提供在上下文中,请使用这些信息来理解当前的对话上下文 +- 用户基本信息已在下方提供一次,请不要再使用memory工具来创建或更新用户基本信息 +- 在需要时可以自主使用memory工具搜索其他相关记忆 +- 当用户提供新的重要信息时,可以主动使用memory工具存储 +- 保持对话的连贯性和一致性 +- 自然地融入记忆信息,避免显得刻意 +- 专注于用户的需求和想要了解的信息,以及想要你做的事情 + +记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#; + + // 构建完整的prompt + let prompt_content = if let Some(info) = user_info { + format!( + "{}\n\n用户基本信息:\n{}\n\n当前用户输入: {}", + system_prompt, info, user_input + ) + } else { + format!("{}\n\n当前用户输入: {}", system_prompt, user_input) + }; + + log::debug!("正在生成AI回复(真实流式模式)..."); + + // 使用rig的真实流式API + let prompt_message = Message::user(&prompt_content); + + // 获取流式响应 + let stream = agent + .stream_chat(prompt_message, chat_history) + .multi_turn(10); + + let mut full_response = String::new(); + + // 处理流式响应 + let mut stream = stream.await; + while let Some(item) = stream.next().await { + match item { + Ok(stream_item) => { + // 根据rig的流式响应类型处理 + match stream_item { + MultiTurnStreamItem::StreamItem(content) => { + match content { + StreamedAssistantContent::Text(text_content) => { + let text = text_content.text; + full_response.push_str(&text); + + // 发送流式内容到UI + if let Err(_) = stream_sender.send(text) { + // 如果发送失败,说明接收端已关闭,停止流式处理 + break; + } + } + StreamedAssistantContent::ToolCall(_) => { + // 处理工具调用(如果需要) + log::debug!("收到工具调用"); + } + StreamedAssistantContent::Reasoning(_) => { + // 处理推理过程(如果需要) + log::debug!("收到推理过程"); + } + StreamedAssistantContent::Final(_) => { + // 处理最终响应 + log::debug!("收到最终响应"); + } + StreamedAssistantContent::ToolCallDelta { .. } => { + // 处理工具调用增量 + log::debug!("收到工具调用增量"); + } + } + } + MultiTurnStreamItem::FinalResponse(final_response) => { + // 处理最终响应 + log::debug!("收到最终响应: {}", final_response.response()); + full_response = final_response.response().to_string(); + break; + } + _ => { + // 处理其他未知的流式项目类型 + log::debug!("收到未知的流式项目类型"); + } + } + } + Err(e) => { + log::error!("流式处理错误: {}", e); + return Err(format!("Streaming error: {}", e).into()); + } + } + } + + log::debug!("AI回复生成完成"); + Ok(full_response.trim().to_string()) +} + +/// 批量存储对话到记忆系统(优化版) +pub async fn store_conversations_batch( + memory_manager: Arc, + conversations: &[(String, String)], + user_id: &str, +) -> Result<(), Box> { + // 只创建一次ConversationProcessor实例 + let conversation_processor = + cortex_mem_rig::processor::ConversationProcessor::new(memory_manager); + + let metadata = cortex_mem_core::types::MemoryMetadata::new( + cortex_mem_core::types::MemoryType::Conversational, + ) + .with_user_id(user_id.to_string()); + + // 将对话历史转换为消息格式 + let mut messages = Vec::new(); + for (user_msg, assistant_msg) in conversations { + // 添加用户消息 + messages.push(cortex_mem_core::types::Message { + role: "user".to_string(), + content: user_msg.clone(), + name: None, + }); + + // 添加助手回复 + messages.push(cortex_mem_core::types::Message { + role: "assistant".to_string(), + content: assistant_msg.clone(), + name: None, + }); + } + + // 一次性处理所有消息 + conversation_processor + .process_turn(&messages, metadata) + .await?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -117,7 +410,7 @@ mod tests { #[tokio::test] async fn test_mock_agent() { let agent = AgentFactory::create_mock_agent("TestBot", "A test agent"); - + let messages = vec![ ChatMessage::system("你是一个有用的助手"), ChatMessage::user("你好"), diff --git a/examples/cortex-mem-tars-new/src/app.rs b/examples/cortex-mem-tars-new/src/app.rs index d4f1bd2..ca5af85 100644 --- a/examples/cortex-mem-tars-new/src/app.rs +++ b/examples/cortex-mem-tars-new/src/app.rs @@ -1,5 +1,6 @@ -use crate::agent::{Agent, AgentFactory, ChatMessage}; +use crate::agent::{Agent, AgentFactory, ChatMessage, create_memory_agent, extract_user_basic_info, store_conversations_batch, agent_reply_with_memory_retrieval_streaming}; use crate::config::{BotConfig, ConfigManager}; +use crate::infrastructure::Infrastructure; use crate::logger::LogManager; use crate::ui::{AppState, AppUi}; use anyhow::{Context, Result}; @@ -10,9 +11,12 @@ use crossterm::{ }; use ratatui::backend::CrosstermBackend; use ratatui::layout::Rect; +use rig::agent::Agent as RigAgent; +use rig::providers::openai::CompletionModel; use std::io; use std::sync::Arc; use std::time::{Duration, Instant}; +use tokio::sync::mpsc; /// 应用程序 pub struct App { @@ -21,18 +25,41 @@ pub struct App { ui: AppUi, current_bot: Option, agent: Option>, + rig_agent: Option>, + infrastructure: Option>, + user_id: String, + user_info: Option, should_quit: bool, + message_sender: mpsc::UnboundedSender, + message_receiver: mpsc::UnboundedReceiver, +} + +/// 应用消息类型 +#[derive(Debug, Clone)] +pub enum AppMessage { + Log(String), + StreamingChunk { + user: String, + chunk: String, + }, + StreamingComplete { + user: String, + full_response: String, + }, } impl App { /// 创建新的应用 - pub fn new(config_manager: ConfigManager, log_manager: Arc) -> Result { + pub fn new(config_manager: ConfigManager, log_manager: Arc, infrastructure: Option>) -> Result { let mut ui = AppUi::new(); // 加载机器人列表 let bots = config_manager.get_bots()?; ui.set_bot_list(bots); + // 创建消息通道 + let (msg_tx, msg_rx) = mpsc::unbounded_channel::(); + log::info!("应用程序初始化完成"); Ok(Self { @@ -41,10 +68,35 @@ impl App { ui, current_bot: None, agent: None, + rig_agent: None, + infrastructure, + user_id: "demo_user".to_string(), + user_info: None, should_quit: false, + message_sender: msg_tx, + message_receiver: msg_rx, }) } + /// 设置用户信息 + pub async fn load_user_info(&mut self) -> Result<()> { + if let Some(infrastructure) = &self.infrastructure { + let user_info = extract_user_basic_info( + infrastructure.config(), + infrastructure.memory_manager().clone(), + &self.user_id, + ).await.map_err(|e| anyhow::anyhow!("加载用户信息失败: {}", e))?; + + if let Some(info) = user_info { + log::info!("已加载用户基本信息"); + self.user_info = Some(info); + } else { + log::info!("未找到用户基本信息"); + } + } + Ok(()) + } + /// 运行应用 pub async fn run(&mut self) -> Result<()> { enable_raw_mode().context("无法启用原始模式")?; @@ -71,6 +123,41 @@ impl App { last_log_update = Instant::now(); } + // 处理流式消息 + if let Ok(msg) = self.message_receiver.try_recv() { + match msg { + AppMessage::StreamingChunk { user: _, chunk } => { + // 添加流式内容到当前正在生成的消息 + if let Some(last_msg) = self.ui.messages.last_mut() { + if last_msg.role == crate::agent::MessageRole::Assistant { + last_msg.content.push_str(&chunk); + } else { + // 如果最后一条不是助手消息,创建新的助手消息 + self.ui.messages.push(ChatMessage::assistant(chunk)); + } + } else { + // 如果没有消息,创建新的助手消息 + self.ui.messages.push(ChatMessage::assistant(chunk)); + } + } + AppMessage::StreamingComplete { user: _, full_response } => { + // 流式完成,确保完整响应已保存 + if let Some(last_msg) = self.ui.messages.last_mut() { + if last_msg.role == crate::agent::MessageRole::Assistant { + last_msg.content = full_response; + } else { + self.ui.messages.push(ChatMessage::assistant(full_response)); + } + } else { + self.ui.messages.push(ChatMessage::assistant(full_response)); + } + } + AppMessage::Log(_) => { + // 日志消息暂时忽略 + } + } + } + // 渲染 UI terminal.draw(|f| self.ui.render(f)).context("渲染失败")?; @@ -194,6 +281,29 @@ impl App { &bot.name, &bot.system_prompt, )); + + // 如果有基础设施,创建真实的带记忆的 Agent + if let Some(infrastructure) = &self.infrastructure { + let memory_tool_config = cortex_mem_rig::tool::MemoryToolConfig { + default_user_id: Some(self.user_id.clone()), + ..Default::default() + }; + + match create_memory_agent( + infrastructure.memory_manager().clone(), + memory_tool_config, + infrastructure.config(), + ).await { + Ok(rig_agent) => { + self.rig_agent = Some(rig_agent); + log::info!("已创建带记忆功能的真实 Agent"); + } + Err(e) => { + log::error!("创建真实 Agent 失败,使用 Mock Agent: {}", e); + } + } + } + log::info!("选择机器人: {}", bot.name); } else { log::warn!("没有选中的机器人"); @@ -209,8 +319,74 @@ impl App { log::info!("用户发送消息: {}", input_text); log::debug!("当前消息总数: {}", self.ui.messages.len()); - // 获取 AI 响应 - if let Some(agent) = &self.agent { + // 使用真实的带记忆的 Agent 或 Mock Agent + if let Some(rig_agent) = &self.rig_agent { + // 使用真实 Agent 进行流式响应 + let current_conversations: Vec<(String, String)> = self.ui.messages + .iter() + .filter_map(|msg| match msg.role { + crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), + crate::agent::MessageRole::Assistant => { + if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { + Some((last.content.clone(), msg.content.clone())) + } else { + None + } + } + _ => None, + }) + .collect(); + + let user_info_clone = self.user_info.clone(); + let infrastructure_clone = self.infrastructure.clone(); + let rig_agent_clone = rig_agent.clone(); + let msg_tx = self.message_sender.clone(); + let user_input = input_text.to_string(); + let user_id = self.user_id.clone(); + let user_input_for_stream = user_input.clone(); + + tokio::spawn(async move { + let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::(); + + let generation_task = tokio::spawn(async move { + agent_reply_with_memory_retrieval_streaming( + &rig_agent_clone, + infrastructure_clone.unwrap().memory_manager().clone(), + &user_input, + &user_id, + user_info_clone.as_deref(), + ¤t_conversations, + stream_tx, + ).await + }); + + while let Some(chunk) = stream_rx.recv().await { + if let Err(_) = msg_tx.send(AppMessage::StreamingChunk { + user: user_input_for_stream.clone(), + chunk, + }) { + break; + } + } + + match generation_task.await { + Ok(Ok(full_response)) => { + let _ = msg_tx.send(AppMessage::StreamingComplete { + user: user_input_for_stream.clone(), + full_response, + }); + } + Ok(Err(e)) => { + log::error!("生成回复失败: {}", e); + } + Err(e) => { + log::error!("任务执行失败: {}", e); + } + } + }); + + } else if let Some(agent) = &self.agent { + // 使用 Mock Agent let mut messages = vec![]; if let Some(bot) = &self.current_bot { messages.push(ChatMessage::system(&bot.system_prompt)); @@ -272,6 +448,38 @@ impl App { } self.ui.scroll_offset = usize::MAX; } + + /// 退出时保存对话到记忆系统 + pub async fn save_conversations_to_memory(&self) -> Result<()> { + if let Some(infrastructure) = &self.infrastructure { + let conversations: Vec<(String, String)> = self.ui.messages + .iter() + .filter_map(|msg| match msg.role { + crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), + crate::agent::MessageRole::Assistant => { + if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { + Some((last.content.clone(), msg.content.clone())) + } else { + None + } + } + _ => None, + }) + .filter(|(user, assistant)| !user.is_empty() && !assistant.is_empty()) + .collect(); + + if !conversations.is_empty() { + log::info!("正在保存 {} 条对话到记忆系统...", conversations.len()); + store_conversations_batch( + infrastructure.memory_manager().clone(), + &conversations, + &self.user_id, + ).await.map_err(|e| anyhow::anyhow!("保存对话到记忆系统失败: {}", e))?; + log::info!("对话保存完成"); + } + } + Ok(()) + } } /// 创建默认机器人 diff --git a/examples/cortex-mem-tars-new/src/config.rs b/examples/cortex-mem-tars-new/src/config.rs index b33e7ba..1c2bc30 100644 --- a/examples/cortex-mem-tars-new/src/config.rs +++ b/examples/cortex-mem-tars-new/src/config.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; +use cortex_mem_config::Config as CortexConfig; use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; @@ -30,11 +31,16 @@ impl BotConfig { pub struct ConfigManager { config_dir: PathBuf, bots_file: PathBuf, + cortex_config: CortexConfig, } impl ConfigManager { /// 创建新的配置管理器 pub fn new() -> Result { + // 获取当前工作目录 + let current_dir = std::env::current_dir().context("无法获取当前工作目录")?; + + // 系统配置目录(用于 bots.json) let config_dir = directories::ProjectDirs::from("com", "cortex", "mem-tars") .context("无法获取项目目录")? .config_dir() @@ -44,9 +50,63 @@ impl ConfigManager { let bots_file = config_dir.join("bots.json"); + // cortex-mem 配置文件:优先从当前目录读取 + let local_config_file = current_dir.join("config.toml"); + let system_config_file = config_dir.join("config.toml"); + + // 确定使用哪个配置文件 + let cortex_config_file = if local_config_file.exists() { + log::info!("使用当前目录的配置文件: {:?}", local_config_file); + local_config_file + } else { + log::info!("使用系统配置目录的配置文件: {:?}", system_config_file); + system_config_file + }; + + // 加载或创建 cortex-mem 配置 + let cortex_config = if cortex_config_file.exists() { + CortexConfig::load(&cortex_config_file).context("无法加载 cortex-mem 配置")? + } else { + // 创建默认配置 + let default_config = CortexConfig { + qdrant: cortex_mem_config::QdrantConfig { + url: "http://localhost:6334".to_string(), + collection_name: "cortex_mem".to_string(), + embedding_dim: Some(1536), + timeout_secs: 30, + }, + llm: cortex_mem_config::LLMConfig { + api_base_url: "https://api.openai.com/v1".to_string(), + api_key: "".to_string(), + model_efficient: "gpt-4o-mini".to_string(), + temperature: 0.7, + max_tokens: 2000, + }, + server: cortex_mem_config::ServerConfig { + host: "127.0.0.1".to_string(), + port: 8080, + cors_origins: vec!["*".to_string()], + }, + embedding: cortex_mem_config::EmbeddingConfig { + api_base_url: "https://api.openai.com/v1".to_string(), + model_name: "text-embedding-3-small".to_string(), + api_key: "".to_string(), + batch_size: 100, + timeout_secs: 30, + }, + memory: cortex_mem_config::MemoryConfig::default(), + logging: cortex_mem_config::LoggingConfig::default(), + }; + let content = toml::to_string_pretty(&default_config).context("无法序列化默认配置")?; + fs::write(&cortex_config_file, content).context("无法写入默认配置文件")?; + log::info!("已创建默认 cortex-mem 配置文件: {:?}", cortex_config_file); + default_config + }; + Ok(Self { config_dir, bots_file, + cortex_config, }) } @@ -119,6 +179,11 @@ impl ConfigManager { pub fn config_dir(&self) -> &Path { &self.config_dir } + + /// 获取 cortex-mem 配置 + pub fn cortex_config(&self) -> &CortexConfig { + &self.cortex_config + } } impl Default for ConfigManager { diff --git a/examples/cortex-mem-tars-new/src/infrastructure.rs b/examples/cortex-mem-tars-new/src/infrastructure.rs new file mode 100644 index 0000000..504105b --- /dev/null +++ b/examples/cortex-mem-tars-new/src/infrastructure.rs @@ -0,0 +1,56 @@ +use anyhow::{Context, Result}; +use cortex_mem_config::Config; +use cortex_mem_core::memory::MemoryManager; +use cortex_mem_rig::llm::OpenAILLMClient; +use cortex_mem_rig::vector_store::qdrant::QdrantVectorStore; +use std::sync::Arc; + +/// 基础设施管理器,负责初始化和管理 LLM 客户端、向量存储和记忆管理器 +pub struct Infrastructure { + pub memory_manager: Arc, + pub config: Config, +} + +impl Infrastructure { + /// 创建新的基础设施 + pub async fn new(config: Config) -> Result { + log::info!("正在初始化基础设施..."); + + // 初始化 LLM 客户端 + let llm_client = OpenAILLMClient::new(&config.llm, &config.embedding) + .context("无法初始化 LLM 客户端")?; + log::info!("LLM 客户端初始化成功"); + + // 初始化向量存储 + let vector_store = QdrantVectorStore::new(&config.qdrant) + .await + .context("无法连接到 Qdrant 向量存储")?; + log::info!("Qdrant 向量存储连接成功"); + + // 初始化记忆管理器 + let memory_config = config.memory.clone(); + let memory_manager = Arc::new(MemoryManager::new( + Box::new(vector_store), + Box::new(llm_client), + memory_config, + )); + log::info!("记忆管理器初始化成功"); + + log::info!("基础设施初始化完成"); + + Ok(Self { + memory_manager, + config, + }) + } + + /// 获取记忆管理器 + pub fn memory_manager(&self) -> &Arc { + &self.memory_manager + } + + /// 获取配置 + pub fn config(&self) -> &Config { + &self.config + } +} \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/lib.rs b/examples/cortex-mem-tars-new/src/lib.rs index 07a3c0d..5c681a7 100644 --- a/examples/cortex-mem-tars-new/src/lib.rs +++ b/examples/cortex-mem-tars-new/src/lib.rs @@ -1,5 +1,6 @@ pub mod agent; pub mod config; pub mod app; +pub mod infrastructure; pub mod ui; pub mod logger; \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/main.rs b/examples/cortex-mem-tars-new/src/main.rs index 3559f0b..398dbd7 100644 --- a/examples/cortex-mem-tars-new/src/main.rs +++ b/examples/cortex-mem-tars-new/src/main.rs @@ -1,12 +1,14 @@ mod agent; mod app; mod config; +mod infrastructure; mod logger; mod ui; use anyhow::{Context, Result}; use app::{create_default_bots, App}; use config::ConfigManager; +use infrastructure::Infrastructure; use logger::init_logger; use std::sync::Arc; @@ -23,11 +25,38 @@ async fn main() -> Result<()> { // 创建默认机器人 create_default_bots(&config_manager).context("无法创建默认机器人")?; + // 初始化基础设施(LLM 客户端、向量存储、记忆管理器) + let infrastructure = match Infrastructure::new(config_manager.cortex_config().clone()).await { + Ok(inf) => { + log::info!("基础设施初始化成功"); + Some(Arc::new(inf)) + } + Err(e) => { + log::warn!("基础设施初始化失败,将使用 Mock Agent: {}", e); + None + } + }; + // 创建并运行应用 - let mut app = App::new(config_manager, log_manager).context("无法创建应用")?; + let mut app = App::new( + config_manager, + log_manager, + infrastructure.clone(), + ).context("无法创建应用")?; log::info!("应用创建成功"); + // 加载用户基本信息 + app.load_user_info().await.context("无法加载用户信息")?; + + // 运行应用 app.run().await.context("应用运行失败")?; + // 退出时保存对话到记忆系统 + if let Some(inf) = infrastructure { + log::info!("正在保存对话到记忆系统..."); + app.save_conversations_to_memory().await.context("保存对话失败")?; + log::info!("对话保存完成"); + } + Ok(()) } From 7b2e006bf872eb5defee1f3d0292a1f1025b0aea Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 19:16:31 +0800 Subject: [PATCH 4/9] add auto scroll --- examples/cortex-mem-tars-new/src/app.rs | 14 +++++++++++--- examples/cortex-mem-tars-new/src/ui.rs | 20 +++++++++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/examples/cortex-mem-tars-new/src/app.rs b/examples/cortex-mem-tars-new/src/app.rs index ca5af85..5aa0856 100644 --- a/examples/cortex-mem-tars-new/src/app.rs +++ b/examples/cortex-mem-tars-new/src/app.rs @@ -139,6 +139,8 @@ impl App { // 如果没有消息,创建新的助手消息 self.ui.messages.push(ChatMessage::assistant(chunk)); } + // 确保自动滚动启用 + self.ui.auto_scroll = true; } AppMessage::StreamingComplete { user: _, full_response } => { // 流式完成,确保完整响应已保存 @@ -151,6 +153,8 @@ impl App { } else { self.ui.messages.push(ChatMessage::assistant(full_response)); } + // 确保自动滚动启用 + self.ui.auto_scroll = true; } AppMessage::Log(_) => { // 日志消息暂时忽略 @@ -316,6 +320,9 @@ impl App { self.ui.messages.push(user_message.clone()); self.ui.clear_input(); + // 用户发送新消息,重新启用自动滚动 + self.ui.auto_scroll = true; + log::info!("用户发送消息: {}", input_text); log::debug!("当前消息总数: {}", self.ui.messages.len()); @@ -412,7 +419,7 @@ impl App { } // 滚动到底部 - 将在渲染时自动计算 - self.ui.scroll_offset = usize::MAX; + self.ui.auto_scroll = true; Ok(()) } @@ -422,6 +429,7 @@ impl App { log::info!("清空会话"); self.ui.messages.clear(); self.ui.scroll_offset = 0; + self.ui.auto_scroll = true; } /// 显示帮助信息 @@ -429,7 +437,7 @@ impl App { log::info!("显示帮助信息"); let help_message = ChatMessage::assistant(AppUi::get_help_message()); self.ui.messages.push(help_message); - self.ui.scroll_offset = usize::MAX; + self.ui.auto_scroll = true; } /// 导出会话到剪贴板 @@ -446,7 +454,7 @@ impl App { self.ui.messages.push(error_message); } } - self.ui.scroll_offset = usize::MAX; + self.ui.auto_scroll = true; } /// 退出时保存对话到记忆系统 diff --git a/examples/cortex-mem-tars-new/src/ui.rs b/examples/cortex-mem-tars-new/src/ui.rs index 55d40ee..f1fdafb 100644 --- a/examples/cortex-mem-tars-new/src/ui.rs +++ b/examples/cortex-mem-tars-new/src/ui.rs @@ -36,6 +36,7 @@ pub struct AppUi { pub messages: Vec, pub input_textarea: TextArea<'static>, pub scroll_offset: usize, + pub auto_scroll: bool, // 是否自动滚动到底部 pub log_panel_visible: bool, pub log_lines: Vec, pub log_scroll_offset: usize, @@ -80,6 +81,7 @@ impl AppUi { messages: vec![], input_textarea, scroll_offset: 0, + auto_scroll: true, log_panel_visible: false, log_lines: vec![], log_scroll_offset: 0, @@ -602,6 +604,8 @@ impl AppUi { } } else if self.scroll_offset > 0 { self.scroll_offset = self.scroll_offset.saturating_sub(3); + // 用户手动滚动,禁用自动滚动 + self.auto_scroll = false; } true } @@ -610,6 +614,8 @@ impl AppUi { self.log_scroll_offset = self.log_scroll_offset.saturating_add(3); } else { self.scroll_offset = self.scroll_offset.saturating_add(3); + // 用户手动滚动,禁用自动滚动 + self.auto_scroll = false; } true } @@ -949,14 +955,14 @@ impl AppUi { let visible_lines = area.height.saturating_sub(2) as usize; // 减去边框 let max_scroll = total_lines.saturating_sub(visible_lines); - // 如果 scroll_offset 为 usize::MAX,表示需要滚动到底部 - if self.scroll_offset == usize::MAX { - self.scroll_offset = max_scroll; - } - - // 限制 scroll_offset 在有效范围内 - if self.scroll_offset > max_scroll { + // 如果启用了自动滚动,始终滚动到底部 + if self.auto_scroll { self.scroll_offset = max_scroll; + } else { + // 限制 scroll_offset 在有效范围内 + if self.scroll_offset > max_scroll { + self.scroll_offset = max_scroll; + } } // 应用选择高亮 From 201e8a065e428a6776feb10a20c63ad9f623d38e Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 19:20:01 +0800 Subject: [PATCH 5/9] refact tars --- examples/cortex-mem-tars-new/src/ui.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/cortex-mem-tars-new/src/ui.rs b/examples/cortex-mem-tars-new/src/ui.rs index f1fdafb..8626f58 100644 --- a/examples/cortex-mem-tars-new/src/ui.rs +++ b/examples/cortex-mem-tars-new/src/ui.rs @@ -1024,10 +1024,10 @@ impl AppUi { // 可见区域的行范围是 [scroll_offset, scroll_offset + visible_lines) let visible_start = scroll_offset; let visible_end = scroll_offset + visible_lines; - + // 如果选择区域和可见区域没有重叠,直接返回 if end_line < visible_start || start_line >= visible_end { - log::debug!("选择区域和可见区域没有重叠,直接返回"); + // log::debug!("选择区域和可见区域没有重叠,直接返回"); return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(); } @@ -1054,7 +1054,7 @@ impl AppUi { // skip(scroll_offset) 后,visible_idx 从 0 开始 let visible_idx = original_idx - scroll_offset; let in_range = original_idx >= start_line && original_idx <= end_line; - + if in_range { // 这一行在选择范围内 highlighted_count += 1; @@ -1267,7 +1267,7 @@ impl AppUi { /// 获取帮助信息 pub fn get_help_message() -> String { - "# TARS AI Program - 帮助信息\n\n## 版本\n\nv0.1.0\n\n## 可用命令\n\n| 命令 | 说明 |\n|------|------|\n| `/quit` | 退出程序 |\n| `/cls` 或 `/clear` | 清空会话区域 |\n| `/help` | 显示此帮助信息 |\n| `/dump-chats` | 复制会话区域的所有内容到剪贴板 |\n\n## 快捷键\n\n- **Enter**: 发送消息\n- **Shift+Enter**: 换行\n- **Ctrl+L**: 打开/关闭日志面板\n- **Esc**: 关闭日志面板\n- **q**: 退出程序\n\n## 使用说明\n\n在输入框中输入命令并按 Enter 即可执行。\n\n---\n\n*Powered by TARS AI*".to_string() + "# TARS AI Program - 帮助信息\n\n欢迎使用TARS演示程序,我是由Cortex Memory技术驱动的人工智能程序,作为你的第二大脑,我能够作为你的外脑与你的记忆深度链接。\n\n## 可用命令\n\n| 命令 | 说明 |\n|------|------|\n| `/quit` | 退出程序 |\n| `/cls` 或 `/clear` | 清空会话区域 |\n| `/help` | 显示此帮助信息 |\n| `/dump-chats` | 复制会话区域的所有内容到剪贴板 |\n\n## 快捷键\n\n- **Enter**: 发送消息\n- **Shift+Enter**: 换行\n- **Ctrl+L**: 打开/关闭日志面板\n- **Esc**: 关闭日志面板\n\n---\n\n*Powered by TARS AI*".to_string() } /// 导出所有会话内容到剪贴板 From c585d5c03f6a078ccd5e802a09b1a6c35b776687 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 19:43:03 +0800 Subject: [PATCH 6/9] add service indicator --- Cargo.lock | 1 + examples/cortex-mem-tars-new/Cargo.toml | 3 ++ examples/cortex-mem-tars-new/src/app.rs | 52 ++++++++++++++++++++++++ examples/cortex-mem-tars-new/src/main.rs | 3 ++ examples/cortex-mem-tars-new/src/ui.rs | 36 +++++++++++++--- 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa3283f..0e8b7bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,6 +745,7 @@ dependencies = [ "once_cell", "ratatui", "ratatui-core", + "reqwest", "rig-core", "serde", "serde_json", diff --git a/examples/cortex-mem-tars-new/Cargo.toml b/examples/cortex-mem-tars-new/Cargo.toml index a7d5949..6025ce0 100644 --- a/examples/cortex-mem-tars-new/Cargo.toml +++ b/examples/cortex-mem-tars-new/Cargo.toml @@ -47,6 +47,9 @@ tokio = { version = "1.40", features = ["full"] } futures = "0.3" async-stream = "0.3" +# HTTP client +reqwest = { version = "0.12", features = ["json"] } + # Utilities uuid = { version = "1.10", features = ["v4"] } clipboard = "0.5" diff --git a/examples/cortex-mem-tars-new/src/app.rs b/examples/cortex-mem-tars-new/src/app.rs index 5aa0856..7407878 100644 --- a/examples/cortex-mem-tars-new/src/app.rs +++ b/examples/cortex-mem-tars-new/src/app.rs @@ -97,6 +97,50 @@ impl App { Ok(()) } + /// 检查服务可用性 + pub async fn check_service_status(&mut self) -> Result<()> { + use reqwest::Method; + + if let Some(infrastructure) = &self.infrastructure { + let api_base_url = &infrastructure.config().llm.api_base_url; + // 拼接完整的 API 地址 + let check_url = format!("{}/chat/completions", api_base_url.trim_end_matches('/')); + + log::info!("检查服务可用性: {}", check_url); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .context("无法创建 HTTP 客户端")?; + + match client + .request(Method::OPTIONS, &check_url) + .send() + .await + { + Ok(response) => { + if response.status().is_success() || response.status().as_u16() == 405 { + // 200 OK 或 405 Method Not Allowed 都表示服务可用 + log::info!("服务可用,状态码: {}", response.status()); + self.ui.service_status = crate::ui::ServiceStatus::Active; + } else { + log::warn!("服务不可用,状态码: {}", response.status()); + self.ui.service_status = crate::ui::ServiceStatus::Inactive; + } + } + Err(e) => { + log::error!("服务检查失败: {}", e); + self.ui.service_status = crate::ui::ServiceStatus::Inactive; + } + } + } else { + log::warn!("基础设施未初始化,无法检查服务状态"); + self.ui.service_status = crate::ui::ServiceStatus::Inactive; + } + + Ok(()) + } + /// 运行应用 pub async fn run(&mut self) -> Result<()> { enable_raw_mode().context("无法启用原始模式")?; @@ -114,6 +158,7 @@ impl App { let mut terminal = ratatui::Terminal::new(backend).context("无法创建终端")?; let mut last_log_update = Instant::now(); + let mut last_service_check = Instant::now(); let tick_rate = Duration::from_millis(100); loop { @@ -123,6 +168,13 @@ impl App { last_log_update = Instant::now(); } + // 定期检查服务状态(每5秒) + if last_service_check.elapsed() > Duration::from_secs(5) { + // 在后台检查服务状态,不阻塞主循环 + let _ = self.check_service_status().await; + last_service_check = Instant::now(); + } + // 处理流式消息 if let Ok(msg) = self.message_receiver.try_recv() { match msg { diff --git a/examples/cortex-mem-tars-new/src/main.rs b/examples/cortex-mem-tars-new/src/main.rs index 398dbd7..2745141 100644 --- a/examples/cortex-mem-tars-new/src/main.rs +++ b/examples/cortex-mem-tars-new/src/main.rs @@ -45,6 +45,9 @@ async fn main() -> Result<()> { ).context("无法创建应用")?; log::info!("应用创建成功"); + // 检查服务可用性 + app.check_service_status().await.context("无法检查服务状态")?; + // 加载用户基本信息 app.load_user_info().await.context("无法加载用户信息")?; diff --git a/examples/cortex-mem-tars-new/src/ui.rs b/examples/cortex-mem-tars-new/src/ui.rs index 8626f58..fc6be80 100644 --- a/examples/cortex-mem-tars-new/src/ui.rs +++ b/examples/cortex-mem-tars-new/src/ui.rs @@ -19,6 +19,14 @@ pub enum AppState { Chat, } +/// 服务状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServiceStatus { + Initing, // 初始化中 + Active, // 服务可用 + Inactive, // 服务不可用 +} + /// 聊天界面状态 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ChatState { @@ -30,6 +38,7 @@ pub enum ChatState { /// 应用 UI 状态 pub struct AppUi { pub state: AppState, + pub service_status: ServiceStatus, pub chat_state: ChatState, pub bot_list_state: ListState, pub bot_list: Vec, @@ -75,6 +84,7 @@ impl AppUi { Self { state: AppState::BotSelection, + service_status: ServiceStatus::Initing, chat_state: ChatState::Normal, bot_list_state, bot_list: vec![], @@ -791,10 +801,18 @@ impl AppUi { .border_type(ratatui::widgets::BorderType::Double) .title_style( Style::default() - .fg(Color::Cyan) + .fg(match self.service_status { + ServiceStatus::Initing => Color::Blue, + ServiceStatus::Active => Color::Green, + ServiceStatus::Inactive => Color::Red, + }) .add_modifier(Modifier::BOLD) ) - .title(" [ SYSTEM ACTIVE ] ") + .title(match self.service_status { + ServiceStatus::Initing => " [ SYSTEM INITING ] ", + ServiceStatus::Active => " [ SYSTEM ACTIVE ] ", + ServiceStatus::Inactive => " [ SYSTEM INACTIVE ] ", + }) ) .alignment(Alignment::Center) .style( @@ -848,10 +866,18 @@ impl AppUi { .border_type(ratatui::widgets::BorderType::Double) .title_style( Style::default() - .fg(Color::Cyan) + .fg(match self.service_status { + ServiceStatus::Initing => Color::Blue, + ServiceStatus::Active => Color::Green, + ServiceStatus::Inactive => Color::Red, + }) .add_modifier(Modifier::BOLD) ) - .title(" [ SYSTEM ACTIVE ] ") + .title(match self.service_status { + ServiceStatus::Initing => " [ SYSTEM INITING ] ", + ServiceStatus::Active => " [ SYSTEM ACTIVE ] ", + ServiceStatus::Inactive => " [ SYSTEM INACTIVE ] ", + }) ) .alignment(Alignment::Center) .style( @@ -977,7 +1003,7 @@ impl AppUi { }; // 渲染边框 - let title = "聊天记录 (鼠标拖拽选择, Esc 清除选择)"; + let title = "交互信息 (鼠标拖拽选择, Esc 清除选择)"; let block = Block::default() .borders(Borders::ALL) .title(title); From 2845ef8c907a40c55ae558d4c74391225253650f Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 19:53:34 +0800 Subject: [PATCH 7/9] add enhance_memory_saver to enable enhanced memories saver --- examples/cortex-mem-tars-new/src/main.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/cortex-mem-tars-new/src/main.rs b/examples/cortex-mem-tars-new/src/main.rs index 2745141..62a20f1 100644 --- a/examples/cortex-mem-tars-new/src/main.rs +++ b/examples/cortex-mem-tars-new/src/main.rs @@ -14,6 +14,14 @@ use std::sync::Arc; #[tokio::main] async fn main() -> Result<()> { + // 解析命令行参数 + let args: Vec = std::env::args().collect(); + let enhance_memory_saver = args.contains(&"--enhance-memory-saver".to_string()); + + if enhance_memory_saver { + log::info!("已启用增强记忆保存功能"); + } + // 初始化配置管理器 let config_manager = ConfigManager::new().context("无法初始化配置管理器")?; log::info!("配置管理器初始化成功"); @@ -54,11 +62,15 @@ async fn main() -> Result<()> { // 运行应用 app.run().await.context("应用运行失败")?; - // 退出时保存对话到记忆系统 - if let Some(inf) = infrastructure { - log::info!("正在保存对话到记忆系统..."); - app.save_conversations_to_memory().await.context("保存对话失败")?; - log::info!("对话保存完成"); + // 退出时保存对话到记忆系统(仅在启用增强记忆保存功能时) + if enhance_memory_saver { + if let Some(_inf) = infrastructure { + log::info!("正在保存对话到记忆系统..."); + app.save_conversations_to_memory().await.context("保存对话失败")?; + log::info!("对话保存完成"); + } + } else { + log::info!("未启用增强记忆保存功能,跳过对话保存"); } Ok(()) From 23421a4ec100a6693f217caf6f411f2ffc4e4466 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 19:58:23 +0800 Subject: [PATCH 8/9] update --- examples/cortex-mem-tars-new/src/app.rs | 23 +++++++++++ examples/cortex-mem-tars-new/src/main.rs | 52 ++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/examples/cortex-mem-tars-new/src/app.rs b/examples/cortex-mem-tars-new/src/app.rs index 7407878..333b789 100644 --- a/examples/cortex-mem-tars-new/src/app.rs +++ b/examples/cortex-mem-tars-new/src/app.rs @@ -540,6 +540,29 @@ impl App { } Ok(()) } + + /// 获取所有对话 + pub fn get_conversations(&self) -> Vec<(String, String)> { + self.ui.messages + .iter() + .filter_map(|msg| match msg.role { + crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), + crate::agent::MessageRole::Assistant => { + if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { + Some((last.content.clone(), msg.content.clone())) + } else { + None + } + } + _ => None, + }) + .collect() + } + + /// 获取用户ID + pub fn get_user_id(&self) -> String { + self.user_id.clone() + } } /// 创建默认机器人 diff --git a/examples/cortex-mem-tars-new/src/main.rs b/examples/cortex-mem-tars-new/src/main.rs index 62a20f1..b42b4c8 100644 --- a/examples/cortex-mem-tars-new/src/main.rs +++ b/examples/cortex-mem-tars-new/src/main.rs @@ -65,12 +65,58 @@ async fn main() -> Result<()> { // 退出时保存对话到记忆系统(仅在启用增强记忆保存功能时) if enhance_memory_saver { if let Some(_inf) = infrastructure { - log::info!("正在保存对话到记忆系统..."); - app.save_conversations_to_memory().await.context("保存对话失败")?; - log::info!("对话保存完成"); + println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ 🧠 Cortex Memory - 退出流程 ║"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + + log::info!("🚀 开始退出流程,准备保存对话到记忆系统..."); + + let conversations = app.get_conversations(); + let user_id = app.get_user_id(); + + println!("📋 会话摘要:"); + println!(" • 对话轮次: {} 轮", conversations.len()); + println!(" • 用户ID: {}", user_id); + + if conversations.is_empty() { + println!("⚠️ 没有需要存储的内容"); + println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ ✅ 退出流程完成 ║"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + println!("👋 Cortex TARS powering down. Goodbye!"); + return Ok(()); + } + + println!("\n🧠 开始执行记忆化存储..."); + println!("📝 正在保存 {} 条对话记录到记忆库...", conversations.len()); + println!("🚀 开始存储对话到记忆系统..."); + + match app.save_conversations_to_memory().await { + Ok(_) => { + println!("✨ 记忆化完成!"); + println!("✅ 所有对话已成功存储到记忆系统"); + println!("🔍 存储详情:"); + println!(" • 对话轮次: {} 轮", conversations.len()); + println!(" • 用户消息: {} 条", conversations.len()); + println!(" • 助手消息: {} 条", conversations.len()); + } + Err(e) => { + println!("❌ 记忆存储失败: {}", e); + println!("⚠️ 虽然记忆化失败,但仍正常退出"); + } + } + + println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ 🎉 退出流程完成 ║"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + println!("👋 Cortex TARS powering down. Goodbye!"); + } else { + println!("\n⚠️ 基础设施未初始化,无法保存对话到记忆系统"); + println!("👋 Cortex TARS powering down. Goodbye!"); } } else { log::info!("未启用增强记忆保存功能,跳过对话保存"); + println!("\n👋 Cortex TARS powering down. Goodbye!"); } Ok(()) From fc89ac8cb889c0847f9ef5e377945489f8082782 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Thu, 1 Jan 2026 20:56:17 +0800 Subject: [PATCH 9/9] bugfix --- Cargo.lock | 96 +- Cargo.toml | 3 +- examples/cortex-mem-tars-new/Cargo.toml | 55 - .../docs/tui-implementation.md | 398 ----- examples/cortex-mem-tars-new/src/agent.rs | 422 ----- examples/cortex-mem-tars-new/src/app.rs | 593 ------- examples/cortex-mem-tars-new/src/main.rs | 123 -- examples/cortex-mem-tars-new/src/ui.rs | 1347 --------------- examples/cortex-mem-tars-new/test.rs | Bin 58378 -> 0 bytes .../.gitignore | 0 .../Cargo.lock | 0 examples/cortex-mem-tars/Cargo.toml | 49 +- .../README.md | 4 +- .../config.example.toml | 0 examples/cortex-mem-tars/src/agent.rs | 94 +- examples/cortex-mem-tars/src/app.rs | 797 +++++---- .../src/config.rs | 18 +- examples/cortex-mem-tars/src/events.rs | 197 --- .../src/infrastructure.rs | 0 .../src/lib.rs | 0 examples/cortex-mem-tars/src/log_monitor.rs | 152 -- .../src/logger.rs | 16 +- examples/cortex-mem-tars/src/main.rs | 630 ++----- examples/cortex-mem-tars/src/terminal.rs | 28 - examples/cortex-mem-tars/src/ui.rs | 1483 ++++++++++++++--- 25 files changed, 1904 insertions(+), 4601 deletions(-) delete mode 100644 examples/cortex-mem-tars-new/Cargo.toml delete mode 100644 examples/cortex-mem-tars-new/docs/tui-implementation.md delete mode 100644 examples/cortex-mem-tars-new/src/agent.rs delete mode 100644 examples/cortex-mem-tars-new/src/app.rs delete mode 100644 examples/cortex-mem-tars-new/src/main.rs delete mode 100644 examples/cortex-mem-tars-new/src/ui.rs delete mode 100644 examples/cortex-mem-tars-new/test.rs rename examples/{cortex-mem-tars-new => cortex-mem-tars}/.gitignore (100%) rename examples/{cortex-mem-tars-new => cortex-mem-tars}/Cargo.lock (100%) rename examples/{cortex-mem-tars-new => cortex-mem-tars}/README.md (99%) rename examples/{cortex-mem-tars-new => cortex-mem-tars}/config.example.toml (100%) rename examples/{cortex-mem-tars-new => cortex-mem-tars}/src/config.rs (94%) delete mode 100644 examples/cortex-mem-tars/src/events.rs rename examples/{cortex-mem-tars-new => cortex-mem-tars}/src/infrastructure.rs (100%) rename examples/{cortex-mem-tars-new => cortex-mem-tars}/src/lib.rs (100%) delete mode 100644 examples/cortex-mem-tars/src/log_monitor.rs rename examples/{cortex-mem-tars-new => cortex-mem-tars}/src/logger.rs (89%) delete mode 100644 examples/cortex-mem-tars/src/terminal.rs diff --git a/Cargo.lock b/Cargo.lock index 0e8b7bf..21d624a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -484,7 +484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" dependencies = [ "async-trait", - "convert_case 0.6.0", + "convert_case", "json5", "pathdiff", "ron", @@ -539,15 +539,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "convert_case" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -704,30 +695,7 @@ dependencies = [ [[package]] name = "cortex-mem-tars" -version = "1.0.0" -dependencies = [ - "async-stream", - "async-trait", - "chrono", - "clap", - "cortex-mem-config", - "cortex-mem-core", - "cortex-mem-rig", - "crossterm 0.29.0", - "futures", - "once_cell", - "ratatui", - "rig-core", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", - "unicode-width 0.1.14", -] - -[[package]] -name = "cortex-mem-tars-new" -version = "0.1.0" +version = "1.1.0" dependencies = [ "anyhow", "async-stream", @@ -737,7 +705,7 @@ dependencies = [ "cortex-mem-config", "cortex-mem-core", "cortex-mem-rig", - "crossterm 0.28.1", + "crossterm", "directories", "env_logger", "futures", @@ -826,24 +794,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix 1.1.2", - "signal-hook", - "signal-hook-mio", - "winapi", -] - [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -979,27 +929,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "convert_case 0.7.1", - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "dialoguer" version = "0.11.0" @@ -1091,15 +1020,6 @@ dependencies = [ "const-random", ] -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - [[package]] name = "dyn-clone" version = "1.0.20" @@ -1935,12 +1855,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "lock_api" version = "0.4.14" @@ -2720,7 +2634,7 @@ dependencies = [ "bitflags", "cassowary", "compact_str 0.8.1", - "crossterm 0.28.1", + "crossterm", "indoc", "instability", "itertools 0.13.0", @@ -4062,7 +3976,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" dependencies = [ - "crossterm 0.28.1", + "crossterm", "ratatui", "unicode-width 0.2.0", ] diff --git a/Cargo.toml b/Cargo.toml index 5ee5212..27e31e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,7 @@ members = [ "cortex-mem-config", "cortex-mem-mcp", "cortex-mem-tools", - "examples/cortex-mem-tars", - "examples/cortex-mem-tars-new" + "examples/cortex-mem-tars" ] [dependencies] diff --git a/examples/cortex-mem-tars-new/Cargo.toml b/examples/cortex-mem-tars-new/Cargo.toml deleted file mode 100644 index 6025ce0..0000000 --- a/examples/cortex-mem-tars-new/Cargo.toml +++ /dev/null @@ -1,55 +0,0 @@ -[package] -name = "cortex-mem-tars-new" -version = "0.1.0" -edition = "2024" - -[dependencies] -# Cortex Memory dependencies -cortex-mem-config = { path = "../../cortex-mem-config" } -cortex-mem-core = { path = "../../cortex-mem-core" } -cortex-mem-rig = { path = "../../cortex-mem-rig" } - -# LLM framework -rig-core = "0.23" - -# TUI -ratatui = "0.29" -tui-markdown = "0.3.7" -ratatui-core = "0.1" -crossterm = "0.28" -tui-textarea = "0.7" -unicode-width = "0.2" - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.9" - -# Error handling -anyhow = "1.0" - -# Time handling -chrono = { version = "0.4", features = ["serde"] } - -# File system -directories = "6.0" - -# Logging -log = "0.4" -env_logger = "0.11" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -once_cell = "1.21" - -# Async -async-trait = "0.1" -tokio = { version = "1.40", features = ["full"] } -futures = "0.3" -async-stream = "0.3" - -# HTTP client -reqwest = { version = "0.12", features = ["json"] } - -# Utilities -uuid = { version = "1.10", features = ["v4"] } -clipboard = "0.5" diff --git a/examples/cortex-mem-tars-new/docs/tui-implementation.md b/examples/cortex-mem-tars-new/docs/tui-implementation.md deleted file mode 100644 index b7eea3d..0000000 --- a/examples/cortex-mem-tars-new/docs/tui-implementation.md +++ /dev/null @@ -1,398 +0,0 @@ -# TUI (Terminal User Interface) 实现总结 - -## 项目概述 - -本项目是一个基于 Rust 的终端聊天应用,使用 TUI 技术构建了一个功能完整的聊天界面,支持多机器人选择、消息显示、输入框、日志面板等功能。 - -## 技术选型 - -### 核心库 - -1. **ratatui** - TUI 框架 - - 提供了丰富的 UI 组件(Paragraph, List, Block, Scrollbar 等) - - 支持灵活的布局系统 - - 跨平台支持(Windows, Linux, macOS) - -2. **crossterm** - 终端控制库 - - 处理键盘和鼠标事件 - - 控制终端模式(原始模式、备用屏幕) - - 鼠标捕获和禁用 - -3. **tui-textarea** - 多行文本输入框 - - 支持多行输入 - - 自动换行处理 - - 光标移动和编辑 - -4. **tui-markdown** - Markdown 渲染 - - 将 Markdown 文本渲染为 TUI 组件 - - 支持基本的 Markdown 语法 - -5. **clipboard** - 剪贴板操作 - - 跨平台剪贴板访问 - - 支持复制功能 - -## 关键用法 - -### 1. 应用生命周期管理 - -```rust -// 启用原始模式和备用屏幕 -enable_raw_mode()?; -execute!( - stdout, - EnterAlternateScreen, - EnableMouseCapture, - DisableLineWrap -)?; - -// 创建终端 -let backend = CrosstermBackend::new(stdout); -let mut terminal = ratatui::Terminal::new(backend)?; - -// 主循环 -loop { - // 渲染 UI - terminal.draw(|f| self.ui.render(f))?; - - // 处理事件 - if event::poll(tick_rate)? { - match event::read()? { - Event::Key(key) => { /* 处理键盘事件 */ } - Event::Mouse(mouse) => { /* 处理鼠标事件 */ } - _ => {} - } - } -} - -// 恢复终端 -disable_raw_mode()?; -execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture -)?; -``` - -### 2. 布局系统 - -ratatui 使用约束布局系统,可以灵活地定义界面布局: - -```rust -let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([ - Constraint::Length(3), // 固定高度 - Constraint::Min(0), // 最小高度 - Constraint::Length(8), // 固定高度 - ]) - .split(area); -``` - -### 3. 状态管理 - -使用枚举来管理应用状态: - -```rust -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AppState { - BotSelection, // 机器人选择界面 - Chat, // 聊天界面 -} -``` - -### 4. 事件处理 - -#### 键盘事件处理 - -```rust -match key.code { - KeyCode::Enter => { - // Enter 发送消息 - KeyAction::SendMessage - } - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+C 退出 - KeyAction::Quit - } - _ => KeyAction::Continue, -} -``` - -#### 鼠标事件处理 - -```rust -match event.kind { - MouseEventKind::ScrollUp => { - // 向上滚动 - self.scroll_offset = self.scroll_offset.saturating_sub(3); - } - MouseEventKind::Down(but) if but == MouseButton::Left => { - // 鼠标左键按下,开始选择 - self.selection_active = true; - self.selection_start = Some((line_idx, col_idx)); - } - MouseEventKind::Drag(but) if but == MouseButton::Left => { - // 鼠标拖拽,更新选择 - self.selection_end = Some((line_idx, col_idx)); - } - _ => {} -} -``` - -### 5. 文本选择实现 - -文本选择是本项目的核心功能之一,实现要点: - -#### 5.1 鼠标坐标转换 - -将鼠标的屏幕坐标转换为文本的行列索引: - -```rust -fn mouse_to_text_position(&self, event: MouseEvent, area: Rect) -> (usize, usize) { - let content_area = area.inner(Margin { - vertical: 1, - horizontal: 1, - }); - - // 检查鼠标是否在消息区域内 - if event.row < content_area.top() || event.row >= content_area.bottom() || - event.column < content_area.left() || event.column >= content_area.right() { - return (self.scroll_offset, 0); - } - - // 计算行索引(考虑滚动偏移) - let relative_row = event.row.saturating_sub(content_area.top()); - let line_idx = self.scroll_offset + relative_row as usize; - - // 计算列索引 - let relative_col = event.column.saturating_sub(content_area.left()); - let col_idx = relative_col as usize; - - // 确保列索引不超出范围 - let all_lines = self.get_all_rendered_lines(); - if line_idx < all_lines.len() { - let line_len = all_lines[line_idx].len(); - (line_idx, col_idx.min(line_len)) - } else { - (line_idx, 0) - } -} -``` - -#### 5.2 选择高亮渲染 - -使用字符索引而不是字节索引来处理多字节字符: - -```rust -fn apply_selection_highlight<'a>(&self, lines: Vec>, scroll_offset: usize, visible_lines: usize) -> Vec> { - let (start, end) = match (self.selection_start, self.selection_end) { - (Some(s), Some(e)) => (s, e), - _ => return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(), - }; - - let start_line = start.0.min(end.0); - let end_line = end.0.max(start.0); - let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; - let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; - - let highlight_style = Style::default() - .fg(Color::Black) - .bg(Color::White) - .add_modifier(Modifier::BOLD); - - lines - .into_iter() - .enumerate() - .skip(scroll_offset) - .take(visible_lines) - .map(|(original_idx, line)| { - if original_idx >= start_line && original_idx <= end_line { - let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); - let chars: Vec = line_text.chars().collect(); - let char_len = chars.len(); - - // 使用字符索引进行切片 - if original_idx == start_line && original_idx == end_line { - let safe_start_col = start_col.min(char_len); - let safe_end_col = end_col.min(char_len); - if safe_start_col < safe_end_col { - let before: String = chars[..safe_start_col].iter().collect(); - let selected: String = chars[safe_start_col..safe_end_col].iter().collect(); - let after: String = chars[safe_end_col..].iter().collect(); - - Line::from(vec![ - Span::raw(before), - Span::styled(selected, highlight_style), - Span::raw(after), - ]) - } else { - line - } - } else if original_idx == start_line { - // 起始行 - let safe_start_col = start_col.min(char_len); - let before: String = chars[..safe_start_col].iter().collect(); - let selected: String = chars[safe_start_col..].iter().collect(); - - Line::from(vec![ - Span::raw(before), - Span::styled(selected, highlight_style), - ]) - } else if original_idx == end_line { - // 结束行 - let safe_end_col = end_col.min(char_len); - let selected: String = chars[..safe_end_col].iter().collect(); - let after: String = chars[safe_end_col..].iter().collect(); - - Line::from(vec![ - Span::styled(selected, highlight_style), - Span::raw(after), - ]) - } else { - // 中间行,整行高亮 - Line::from(vec![Span::styled(line_text, highlight_style)]) - } - } else { - line - } - }) - .collect() -} -``` - -#### 5.3 关键注意事项 - -1. **字符索引 vs 字节索引**:在处理多字节字符(如中文、emoji)时,必须使用字符索引而不是字节索引,否则会出现 "byte index is not a char boundary" 错误。 - -2. **滚动偏移处理**:鼠标位置计算时需要考虑滚动偏移,确保选择范围正确。 - -3. **可见区域判断**:只渲染可见区域的行,避免性能问题。 - -4. **边界检查**:所有的索引访问都要进行边界检查,防止越界。 - -### 6. 滚动实现 - -```rust -// 计算滚动 -let total_lines = all_lines.len(); -let visible_lines = area.height.saturating_sub(2) as usize; -let max_scroll = total_lines.saturating_sub(visible_lines); - -// 限制 scroll_offset 在有效范围内 -if self.scroll_offset > max_scroll { - self.scroll_offset = max_scroll; -} - -// 滚动到底部 -self.scroll_offset = max_scroll; -``` - -### 7. 消息渲染 - -使用 Markdown 渲染来美化消息显示: - -```rust -let markdown_text = from_str(&message.content); -for line in markdown_text.lines { - all_lines.push(Line::from(line.spans.iter().map(|s| { - Span::raw(s.content.clone()) - }).collect::>())); -} -``` - -## 实践认知迭代 - -### 第一阶段:基础实现 - -1. **选择 ratatui**:相比其他 TUI 框架(如 tui-rs、termion),ratatui 更活跃且文档完善。 -2. **基本布局**:实现了机器人选择和聊天界面的基本布局。 -3. **键盘输入**:使用 tui-textarea 实现了多行输入框。 - -### 第二阶段:功能完善 - -1. **滚动功能**:实现了消息区域的滚动,支持查看历史消息。 -2. **日志面板**:添加了可切换的日志面板,方便调试。 -3. **Markdown 渲染**:使用 tui-markdown 美化消息显示。 - -### 第三阶段:交互优化 - -1. **鼠标支持**:添加了鼠标滚动和选择功能。 -2. **文本选择**:实现了鼠标拖拽选择文本,支持高亮显示。 -3. **多字节字符支持**:修复了中文等字符的显示和选择问题。 - -### 第四阶段:问题解决 - -1. **索引计算问题**: - - 问题:使用 `enumerate().skip(scroll_offset)` 后,索引计算混乱。 - - 解决:明确区分原始索引(original_idx)和可见索引(visible_idx)。 - -2. **字符边界问题**: - - 问题:直接使用字节索引导致多字节字符处理错误。 - - 解决:将字符串转换为字符数组,使用字符索引进行操作。 - -3. **渲染顺序问题**: - - 问题:先渲染内容再渲染边框,导致边框覆盖内容。 - - 解决:先渲染边框,再在边框内部渲染内容。 - -4. **选择范围计算**: - - 问题:滚动后选择范围计算不正确。 - - 解决:确保鼠标位置计算和渲染时使用相同的行数计算逻辑。 - -## 最佳实践 - -### 1. 状态管理 - -- 使用枚举来管理应用的不同状态 -- 将 UI 状态和业务逻辑分离 -- 使用 Option 来表示可选的状态 - -### 2. 事件处理 - -- 使用模式匹配来处理不同的事件类型 -- 返回明确的操作类型(Continue, Quit, SendMessage) -- 避免在事件处理中直接修改 UI - -### 3. 渲染优化 - -- 只渲染可见区域的内容 -- 使用滚动偏移来控制显示范围 -- 避免在每次渲染时重新计算所有内容 - -### 4. 错误处理 - -- 使用 Result 类型来处理可能的错误 -- 提供有意义的错误信息 -- 使用 context! 宏来添加上下文信息 - -### 5. 调试技巧 - -- 使用 log 宏记录关键操作 -- 添加详细的调试信息来追踪问题 -- 使用日志级别来控制输出量 - -## 性能考虑 - -1. **避免频繁渲染**:只在状态变化时重新渲染 -2. **限制渲染范围**:只渲染可见区域的内容 -3. **缓存计算结果**:避免重复计算相同的值 -4. **使用迭代器**:利用 Rust 的迭代器来优化性能 - -## 未来改进方向 - -1. **异步支持**:使用 tokio 来处理异步操作 -2. **主题系统**:支持自定义颜色和样式 -3. **插件系统**:支持扩展功能 -4. **国际化**:支持多语言 -5. **快捷键配置**:允许用户自定义快捷键 - -## 总结 - -本项目的 TUI 实现展示了如何使用 Rust 构建一个功能完整的终端应用。通过合理的技术选型、良好的架构设计和持续的迭代优化,我们实现了一个用户体验良好的聊天界面。关键的学习点包括: - -- 正确处理多字节字符 -- 准确计算鼠标位置和选择范围 -- 优化渲染性能 -- 良好的错误处理和调试技巧 - -这些经验可以应用到其他 TUI 项目的开发中。 \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/agent.rs b/examples/cortex-mem-tars-new/src/agent.rs deleted file mode 100644 index 9042ea6..0000000 --- a/examples/cortex-mem-tars-new/src/agent.rs +++ /dev/null @@ -1,422 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use chrono::{DateTime, Local}; -use cortex_mem_config::Config; -use cortex_mem_core::memory::MemoryManager; -use cortex_mem_rig::{ListMemoriesArgs, create_memory_tools, tool::MemoryToolConfig}; -use futures::StreamExt; -use rig::{ - agent::Agent as RigAgent, - client::CompletionClient, - providers::openai::{Client, CompletionModel}, - tool::Tool, -}; -use rig::agent::MultiTurnStreamItem; -use rig::completion::Message; -use rig::streaming::{StreamedAssistantContent, StreamingChat}; -use std::sync::Arc; -use tokio::sync::mpsc; - -/// 消息角色 -#[derive(Debug, Clone, PartialEq)] -pub enum MessageRole { - System, - User, - Assistant, -} - -/// 聊天消息 -#[derive(Debug, Clone)] -pub struct ChatMessage { - pub role: MessageRole, - pub content: String, - pub timestamp: DateTime, -} - -impl ChatMessage { - pub fn new(role: MessageRole, content: String) -> Self { - Self { - role, - content, - timestamp: Local::now(), - } - } - - pub fn system(content: impl Into) -> Self { - Self::new(MessageRole::System, content.into()) - } - - pub fn user(content: impl Into) -> Self { - Self::new(MessageRole::User, content.into()) - } - - pub fn assistant(content: impl Into) -> Self { - Self::new(MessageRole::Assistant, content.into()) - } -} - -/// Agent 抽象 trait -#[async_trait] -pub trait Agent: Send + Sync { - /// 发送消息并获取响应 - async fn chat(&self, messages: &[ChatMessage]) -> Result; - - /// 获取 Agent 名称 - fn name(&self) -> &str; - - /// 获取 Agent 描述 - fn description(&self) -> &str; -} - -/// Mock Agent 实现,用于模拟 AI 调用 -pub struct MockAgent { - name: String, - description: String, -} - -impl MockAgent { - pub fn new(name: impl Into, description: impl Into) -> Self { - Self { - name: name.into(), - description: description.into(), - } - } -} - -#[async_trait] -impl Agent for MockAgent { - async fn chat(&self, messages: &[ChatMessage]) -> Result { - // 模拟 AI 响应 - let last_message = messages.last().map(|m| m.content.as_str()).unwrap_or(""); - - // 简单的模拟响应逻辑 - let response = if last_message.contains("你好") || last_message.contains("hello") { - "你好!我是你的 AI 助手。很高兴为你服务!\n\n有什么我可以帮助你的吗?".to_string() - } else if last_message.contains("markdown") || last_message.contains("表格") { - "# Markdown 渲染演示\n\n这是一个 **Markdown** 渲染演示,包含各种格式。\n\n## 功能列表\n\n1. 支持多级标题\n2. 支持 **粗体** 和 *斜体*\n3. 支持有序列表\n4. 支持无序列表\n5. 支持 `代码`\n6. 支持引用\n\n## 数据表格\n\n| 名称 | 类型 | 描述 |\n|------|------|------|\n| String | 字符串 | 文本数据 |\n| Number | 数字 | 数值数据 |\n| Boolean | 布尔 | 真假值 |\n\n## 代码示例\n\n```rust\nfn main() {\n println!(\"Hello, world!\");\n let x = 42;\n println!(\"The answer is: {}\", x);\n}\n```\n\n```python\ndef hello():\n print(\"Hello, Python!\")\n return True\n```\n\n## 引用示例\n\n> 这是一段引用文本。\n> 可以有多行。\n\n希望这个演示对你有帮助!".to_string() - } else if last_message.contains("帮助") || last_message.contains("help") { - "# 帮助信息\n\n我可以演示以下 Markdown 功能:\n\n- 输入 \"markdown\" 或 \"表格\" 查看 Markdown 渲染\n- 输入 \"你好\" 查看简单问候\n\n## 快捷键\n\n- **Enter**: 发送消息\n- **Shift+Enter**: 换行\n- **l**: 打开/关闭日志面板\n- **Esc**: 关闭日志面板\n- **q**: 退出程序".to_string() - } else { - format!( - "# 响应\n\n我收到了你的消息:\n\n> {}\n\n这是一个模拟的 AI 响应。在实际使用中,这里会调用真实的 AI API。\n\n## 提示\n\n你可以尝试输入以下内容:\n\n1. \"你好\" - 查看问候\n2. \"markdown\" - 查看 Markdown 渲染效果\n3. \"帮助\" - 查看帮助信息", - last_message - ) - }; - - Ok(response) - } - - fn name(&self) -> &str { - &self.name - } - - fn description(&self) -> &str { - &self.description - } -} - -/// Agent 工厂 -pub struct AgentFactory; - -impl AgentFactory { - /// 创建 Mock Agent - pub fn create_mock_agent(name: impl Into, description: impl Into) -> Box { - Box::new(MockAgent::new(name, description)) - } -} - -/// 创建带记忆功能的Agent -pub async fn create_memory_agent( - memory_manager: Arc, - memory_tool_config: MemoryToolConfig, - config: &Config, -) -> Result, Box> { - // 创建记忆工具 - let memory_tools = - create_memory_tools(memory_manager.clone(), &config, Some(memory_tool_config)); - - let llm_client = Client::builder(&config.llm.api_key) - .base_url(&config.llm.api_base_url) - .build(); - - // 构建带有记忆工具的agent,让agent能够自主决定何时调用记忆功能 - let completion_model = llm_client - .completion_model(&config.llm.model_efficient) - .completions_api() - .into_agent_builder() - // 注册四个独立的记忆工具,保持与MCP一致 - .tool(memory_tools.store_memory()) - .tool(memory_tools.query_memory()) - .tool(memory_tools.list_memories()) - .tool(memory_tools.get_memory()) - .preamble(&format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 - -此会话发生的初始时间:{current_time} - -你的工具: -- CortexMemoryTool: 可以存储、搜索和检索记忆。支持以下操作: - * store_memory: 存储新记忆 - * query_memory: 搜索相关记忆 - * list_memories: 获取一系列的记忆集合 - * get_memory: 获取特定记忆 - -重要指令: -- 对话历史将作为上下文提供,请使用这些信息来理解当前的对话流程 -- 用户基本信息将在上下文中提供一次,请不要再使用memory工具来创建或更新用户基本信息 -- 在需要时可以自主使用memory工具搜索其他相关记忆 -- 当用户提供新的重要信息时,可以主动使用memory工具存储 -- 保持对话的连贯性和一致性 -- 自然地融入记忆信息,避免刻意复述此前的记忆信息,关注当前的会话内容,记忆主要用于做隐式的逻辑与事实支撑 -- 专注于用户的需求和想要了解的信息,以及想要你做的事情 - -记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S"))) - .build(); - - Ok(completion_model) -} - -/// 从记忆中提取用户基本信息 -pub async fn extract_user_basic_info( - config: &Config, - memory_manager: Arc, - user_id: &str, -) -> Result, Box> { - let memory_tools = create_memory_tools( - memory_manager, - config, - Some(MemoryToolConfig { - default_user_id: Some(user_id.to_string()), - ..Default::default() - }), - ); - - let mut context = String::new(); - - let search_args_personal = ListMemoriesArgs { - limit: Some(20), - memory_type: Some("personal".to_string()), // 使用小写以匹配新API - user_id: Some(user_id.to_string()), - agent_id: None, - }; - - let search_args_factual = ListMemoriesArgs { - limit: Some(20), - memory_type: Some("factual".to_string()), // 使用小写以匹配新API - user_id: Some(user_id.to_string()), - agent_id: None, - }; - - if let Ok(search_result) = memory_tools - .list_memories() - .call(search_args_personal) - .await - { - if let Some(data) = search_result.data { - // 根据新的MCP格式调整数据结构访问 - if let Some(results) = data.get("memories").and_then(|r| r.as_array()) { - if !results.is_empty() { - context.push_str("用户基本信息 - 特征:\n"); - for (i, result) in results.iter().enumerate() { - if let Some(content) = result.get("content").and_then(|c| c.as_str()) { - context.push_str(&format!("{}. {}\n", i + 1, content)); - } - } - return Ok(Some(context)); - } - } - } - } - - if let Ok(search_result) = memory_tools.list_memories().call(search_args_factual).await { - if let Some(data) = search_result.data { - if let Some(results) = data.get("memories").and_then(|r| r.as_array()) { - if !results.is_empty() { - context.push_str("用户基本信息 - 事实:\n"); - for (i, result) in results.iter().enumerate() { - if let Some(content) = result.get("content").and_then(|c| c.as_str()) { - context.push_str(&format!("{}. {}\n", i + 1, content)); - } - } - return Ok(Some(context)); - } - } - } - } - - match context.len() > 0 { - true => Ok(Some(context)), - false => Ok(None), - } -} - -/// Agent回复函数 - 基于tool call的记忆引擎使用(真实流式版本) -pub async fn agent_reply_with_memory_retrieval_streaming( - agent: &RigAgent, - _memory_manager: Arc, - user_input: &str, - _user_id: &str, - user_info: Option<&str>, - conversations: &[(String, String)], - stream_sender: mpsc::UnboundedSender, -) -> Result> { - // 构建对话历史 - 转换为rig的Message格式 - let mut chat_history = Vec::new(); - for (user_msg, assistant_msg) in conversations { - chat_history.push(Message::user(user_msg)); - chat_history.push(Message::assistant(assistant_msg)); - } - - // 构建system prompt,包含明确的指令 - let system_prompt = r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 - -重要指令: -- 对话历史已提供在上下文中,请使用这些信息来理解当前的对话上下文 -- 用户基本信息已在下方提供一次,请不要再使用memory工具来创建或更新用户基本信息 -- 在需要时可以自主使用memory工具搜索其他相关记忆 -- 当用户提供新的重要信息时,可以主动使用memory工具存储 -- 保持对话的连贯性和一致性 -- 自然地融入记忆信息,避免显得刻意 -- 专注于用户的需求和想要了解的信息,以及想要你做的事情 - -记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#; - - // 构建完整的prompt - let prompt_content = if let Some(info) = user_info { - format!( - "{}\n\n用户基本信息:\n{}\n\n当前用户输入: {}", - system_prompt, info, user_input - ) - } else { - format!("{}\n\n当前用户输入: {}", system_prompt, user_input) - }; - - log::debug!("正在生成AI回复(真实流式模式)..."); - - // 使用rig的真实流式API - let prompt_message = Message::user(&prompt_content); - - // 获取流式响应 - let stream = agent - .stream_chat(prompt_message, chat_history) - .multi_turn(10); - - let mut full_response = String::new(); - - // 处理流式响应 - let mut stream = stream.await; - while let Some(item) = stream.next().await { - match item { - Ok(stream_item) => { - // 根据rig的流式响应类型处理 - match stream_item { - MultiTurnStreamItem::StreamItem(content) => { - match content { - StreamedAssistantContent::Text(text_content) => { - let text = text_content.text; - full_response.push_str(&text); - - // 发送流式内容到UI - if let Err(_) = stream_sender.send(text) { - // 如果发送失败,说明接收端已关闭,停止流式处理 - break; - } - } - StreamedAssistantContent::ToolCall(_) => { - // 处理工具调用(如果需要) - log::debug!("收到工具调用"); - } - StreamedAssistantContent::Reasoning(_) => { - // 处理推理过程(如果需要) - log::debug!("收到推理过程"); - } - StreamedAssistantContent::Final(_) => { - // 处理最终响应 - log::debug!("收到最终响应"); - } - StreamedAssistantContent::ToolCallDelta { .. } => { - // 处理工具调用增量 - log::debug!("收到工具调用增量"); - } - } - } - MultiTurnStreamItem::FinalResponse(final_response) => { - // 处理最终响应 - log::debug!("收到最终响应: {}", final_response.response()); - full_response = final_response.response().to_string(); - break; - } - _ => { - // 处理其他未知的流式项目类型 - log::debug!("收到未知的流式项目类型"); - } - } - } - Err(e) => { - log::error!("流式处理错误: {}", e); - return Err(format!("Streaming error: {}", e).into()); - } - } - } - - log::debug!("AI回复生成完成"); - Ok(full_response.trim().to_string()) -} - -/// 批量存储对话到记忆系统(优化版) -pub async fn store_conversations_batch( - memory_manager: Arc, - conversations: &[(String, String)], - user_id: &str, -) -> Result<(), Box> { - // 只创建一次ConversationProcessor实例 - let conversation_processor = - cortex_mem_rig::processor::ConversationProcessor::new(memory_manager); - - let metadata = cortex_mem_core::types::MemoryMetadata::new( - cortex_mem_core::types::MemoryType::Conversational, - ) - .with_user_id(user_id.to_string()); - - // 将对话历史转换为消息格式 - let mut messages = Vec::new(); - for (user_msg, assistant_msg) in conversations { - // 添加用户消息 - messages.push(cortex_mem_core::types::Message { - role: "user".to_string(), - content: user_msg.clone(), - name: None, - }); - - // 添加助手回复 - messages.push(cortex_mem_core::types::Message { - role: "assistant".to_string(), - content: assistant_msg.clone(), - name: None, - }); - } - - // 一次性处理所有消息 - conversation_processor - .process_turn(&messages, metadata) - .await?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_mock_agent() { - let agent = AgentFactory::create_mock_agent("TestBot", "A test agent"); - - let messages = vec![ - ChatMessage::system("你是一个有用的助手"), - ChatMessage::user("你好"), - ]; - - let response = agent.chat(&messages).await.unwrap(); - assert!(response.contains("你好")); - } -} \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/app.rs b/examples/cortex-mem-tars-new/src/app.rs deleted file mode 100644 index 333b789..0000000 --- a/examples/cortex-mem-tars-new/src/app.rs +++ /dev/null @@ -1,593 +0,0 @@ -use crate::agent::{Agent, AgentFactory, ChatMessage, create_memory_agent, extract_user_basic_info, store_conversations_batch, agent_reply_with_memory_retrieval_streaming}; -use crate::config::{BotConfig, ConfigManager}; -use crate::infrastructure::Infrastructure; -use crate::logger::LogManager; -use crate::ui::{AppState, AppUi}; -use anyhow::{Context, Result}; -use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, -}; -use ratatui::backend::CrosstermBackend; -use ratatui::layout::Rect; -use rig::agent::Agent as RigAgent; -use rig::providers::openai::CompletionModel; -use std::io; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::mpsc; - -/// 应用程序 -pub struct App { - config_manager: ConfigManager, - log_manager: Arc, - ui: AppUi, - current_bot: Option, - agent: Option>, - rig_agent: Option>, - infrastructure: Option>, - user_id: String, - user_info: Option, - should_quit: bool, - message_sender: mpsc::UnboundedSender, - message_receiver: mpsc::UnboundedReceiver, -} - -/// 应用消息类型 -#[derive(Debug, Clone)] -pub enum AppMessage { - Log(String), - StreamingChunk { - user: String, - chunk: String, - }, - StreamingComplete { - user: String, - full_response: String, - }, -} - -impl App { - /// 创建新的应用 - pub fn new(config_manager: ConfigManager, log_manager: Arc, infrastructure: Option>) -> Result { - let mut ui = AppUi::new(); - - // 加载机器人列表 - let bots = config_manager.get_bots()?; - ui.set_bot_list(bots); - - // 创建消息通道 - let (msg_tx, msg_rx) = mpsc::unbounded_channel::(); - - log::info!("应用程序初始化完成"); - - Ok(Self { - config_manager, - log_manager, - ui, - current_bot: None, - agent: None, - rig_agent: None, - infrastructure, - user_id: "demo_user".to_string(), - user_info: None, - should_quit: false, - message_sender: msg_tx, - message_receiver: msg_rx, - }) - } - - /// 设置用户信息 - pub async fn load_user_info(&mut self) -> Result<()> { - if let Some(infrastructure) = &self.infrastructure { - let user_info = extract_user_basic_info( - infrastructure.config(), - infrastructure.memory_manager().clone(), - &self.user_id, - ).await.map_err(|e| anyhow::anyhow!("加载用户信息失败: {}", e))?; - - if let Some(info) = user_info { - log::info!("已加载用户基本信息"); - self.user_info = Some(info); - } else { - log::info!("未找到用户基本信息"); - } - } - Ok(()) - } - - /// 检查服务可用性 - pub async fn check_service_status(&mut self) -> Result<()> { - use reqwest::Method; - - if let Some(infrastructure) = &self.infrastructure { - let api_base_url = &infrastructure.config().llm.api_base_url; - // 拼接完整的 API 地址 - let check_url = format!("{}/chat/completions", api_base_url.trim_end_matches('/')); - - log::info!("检查服务可用性: {}", check_url); - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(5)) - .build() - .context("无法创建 HTTP 客户端")?; - - match client - .request(Method::OPTIONS, &check_url) - .send() - .await - { - Ok(response) => { - if response.status().is_success() || response.status().as_u16() == 405 { - // 200 OK 或 405 Method Not Allowed 都表示服务可用 - log::info!("服务可用,状态码: {}", response.status()); - self.ui.service_status = crate::ui::ServiceStatus::Active; - } else { - log::warn!("服务不可用,状态码: {}", response.status()); - self.ui.service_status = crate::ui::ServiceStatus::Inactive; - } - } - Err(e) => { - log::error!("服务检查失败: {}", e); - self.ui.service_status = crate::ui::ServiceStatus::Inactive; - } - } - } else { - log::warn!("基础设施未初始化,无法检查服务状态"); - self.ui.service_status = crate::ui::ServiceStatus::Inactive; - } - - Ok(()) - } - - /// 运行应用 - pub async fn run(&mut self) -> Result<()> { - enable_raw_mode().context("无法启用原始模式")?; - - let mut stdout = io::stdout(); - execute!( - stdout, - EnterAlternateScreen, - EnableMouseCapture, - crossterm::terminal::DisableLineWrap - ) - .context("无法设置终端")?; - - let backend = CrosstermBackend::new(stdout); - let mut terminal = ratatui::Terminal::new(backend).context("无法创建终端")?; - - let mut last_log_update = Instant::now(); - let mut last_service_check = Instant::now(); - let tick_rate = Duration::from_millis(100); - - loop { - // 更新日志 - if last_log_update.elapsed() > Duration::from_secs(1) { - self.update_logs(); - last_log_update = Instant::now(); - } - - // 定期检查服务状态(每5秒) - if last_service_check.elapsed() > Duration::from_secs(5) { - // 在后台检查服务状态,不阻塞主循环 - let _ = self.check_service_status().await; - last_service_check = Instant::now(); - } - - // 处理流式消息 - if let Ok(msg) = self.message_receiver.try_recv() { - match msg { - AppMessage::StreamingChunk { user: _, chunk } => { - // 添加流式内容到当前正在生成的消息 - if let Some(last_msg) = self.ui.messages.last_mut() { - if last_msg.role == crate::agent::MessageRole::Assistant { - last_msg.content.push_str(&chunk); - } else { - // 如果最后一条不是助手消息,创建新的助手消息 - self.ui.messages.push(ChatMessage::assistant(chunk)); - } - } else { - // 如果没有消息,创建新的助手消息 - self.ui.messages.push(ChatMessage::assistant(chunk)); - } - // 确保自动滚动启用 - self.ui.auto_scroll = true; - } - AppMessage::StreamingComplete { user: _, full_response } => { - // 流式完成,确保完整响应已保存 - if let Some(last_msg) = self.ui.messages.last_mut() { - if last_msg.role == crate::agent::MessageRole::Assistant { - last_msg.content = full_response; - } else { - self.ui.messages.push(ChatMessage::assistant(full_response)); - } - } else { - self.ui.messages.push(ChatMessage::assistant(full_response)); - } - // 确保自动滚动启用 - self.ui.auto_scroll = true; - } - AppMessage::Log(_) => { - // 日志消息暂时忽略 - } - } - } - - // 渲染 UI - terminal.draw(|f| self.ui.render(f)).context("渲染失败")?; - - // 处理事件 - if event::poll(tick_rate).context("事件轮询失败")? { - let event = event::read().context("读取事件失败")?; - log::trace!("收到事件: {:?}", event); - - match event { - Event::Key(key) => { - let action = self.ui.handle_key_event(key); - - match action { - crate::ui::KeyAction::Quit => { - self.should_quit = true; - break; - } - crate::ui::KeyAction::SendMessage => { - if self.ui.state == AppState::Chat { - self.send_message().await?; - } - } - crate::ui::KeyAction::ClearChat => { - if self.ui.state == AppState::Chat { - self.clear_chat(); - } - } - crate::ui::KeyAction::ShowHelp => { - if self.ui.state == AppState::Chat { - self.show_help(); - } - } - crate::ui::KeyAction::DumpChats => { - if self.ui.state == AppState::Chat { - self.dump_chats(); - } - } - crate::ui::KeyAction::Continue => {} - } - } - Event::Mouse(mouse) => { - let size = terminal.size()?; - self.ui - .handle_mouse_event(mouse, Rect::new(0, 0, size.width, size.height)); - } - _ => {} - } - } - - if self.should_quit { - break; - } - } - - disable_raw_mode().context("无法禁用原始模式")?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ) - .context("无法恢复终端")?; - - terminal.show_cursor().context("无法显示光标")?; - - log::info!("应用程序退出"); - Ok(()) - } - - /// 更新日志 - fn update_logs(&mut self) { - match self.log_manager.read_logs(1000) { - Ok(logs) => { - let log_count = logs.len(); - self.ui.log_lines = logs; - } - Err(e) => { - log::error!("读取日志失败: {}", e); - } - } - } - - /// 发送消息 - async fn send_message(&mut self) -> Result<()> { - let input_text = self.ui.get_input_text(); - let input_text = input_text.trim(); - - log::debug!("准备发送消息,长度: {}", input_text.len()); - - if input_text.is_empty() { - log::debug!("消息为空,忽略"); - return Ok(()); - } - - // 检查是否是命令 - if let Some(command_action) = self.ui.parse_and_execute_command(input_text) { - self.ui.clear_input(); - - match command_action { - crate::ui::KeyAction::Quit => { - self.should_quit = true; - } - crate::ui::KeyAction::ClearChat => { - self.clear_chat(); - } - crate::ui::KeyAction::ShowHelp => { - self.show_help(); - } - crate::ui::KeyAction::DumpChats => { - self.dump_chats(); - } - _ => {} - } - return Ok(()); - } - - // 检查是否刚进入聊天模式 - if self.current_bot.is_none() { - if let Some(bot) = self.ui.selected_bot() { - self.current_bot = Some(bot.clone()); - self.agent = Some(AgentFactory::create_mock_agent( - &bot.name, - &bot.system_prompt, - )); - - // 如果有基础设施,创建真实的带记忆的 Agent - if let Some(infrastructure) = &self.infrastructure { - let memory_tool_config = cortex_mem_rig::tool::MemoryToolConfig { - default_user_id: Some(self.user_id.clone()), - ..Default::default() - }; - - match create_memory_agent( - infrastructure.memory_manager().clone(), - memory_tool_config, - infrastructure.config(), - ).await { - Ok(rig_agent) => { - self.rig_agent = Some(rig_agent); - log::info!("已创建带记忆功能的真实 Agent"); - } - Err(e) => { - log::error!("创建真实 Agent 失败,使用 Mock Agent: {}", e); - } - } - } - - log::info!("选择机器人: {}", bot.name); - } else { - log::warn!("没有选中的机器人"); - return Ok(()); - } - } - - // 添加用户消息 - let user_message = ChatMessage::user(input_text); - self.ui.messages.push(user_message.clone()); - self.ui.clear_input(); - - // 用户发送新消息,重新启用自动滚动 - self.ui.auto_scroll = true; - - log::info!("用户发送消息: {}", input_text); - log::debug!("当前消息总数: {}", self.ui.messages.len()); - - // 使用真实的带记忆的 Agent 或 Mock Agent - if let Some(rig_agent) = &self.rig_agent { - // 使用真实 Agent 进行流式响应 - let current_conversations: Vec<(String, String)> = self.ui.messages - .iter() - .filter_map(|msg| match msg.role { - crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), - crate::agent::MessageRole::Assistant => { - if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { - Some((last.content.clone(), msg.content.clone())) - } else { - None - } - } - _ => None, - }) - .collect(); - - let user_info_clone = self.user_info.clone(); - let infrastructure_clone = self.infrastructure.clone(); - let rig_agent_clone = rig_agent.clone(); - let msg_tx = self.message_sender.clone(); - let user_input = input_text.to_string(); - let user_id = self.user_id.clone(); - let user_input_for_stream = user_input.clone(); - - tokio::spawn(async move { - let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::(); - - let generation_task = tokio::spawn(async move { - agent_reply_with_memory_retrieval_streaming( - &rig_agent_clone, - infrastructure_clone.unwrap().memory_manager().clone(), - &user_input, - &user_id, - user_info_clone.as_deref(), - ¤t_conversations, - stream_tx, - ).await - }); - - while let Some(chunk) = stream_rx.recv().await { - if let Err(_) = msg_tx.send(AppMessage::StreamingChunk { - user: user_input_for_stream.clone(), - chunk, - }) { - break; - } - } - - match generation_task.await { - Ok(Ok(full_response)) => { - let _ = msg_tx.send(AppMessage::StreamingComplete { - user: user_input_for_stream.clone(), - full_response, - }); - } - Ok(Err(e)) => { - log::error!("生成回复失败: {}", e); - } - Err(e) => { - log::error!("任务执行失败: {}", e); - } - } - }); - - } else if let Some(agent) = &self.agent { - // 使用 Mock Agent - let mut messages = vec![]; - if let Some(bot) = &self.current_bot { - messages.push(ChatMessage::system(&bot.system_prompt)); - log::debug!("添加系统提示词"); - } - messages.extend(self.ui.messages.iter().cloned()); - log::debug!("准备调用 Agent,消息数: {}", messages.len()); - - match agent.chat(&messages).await { - Ok(response) => { - log::info!("AI 响应成功,长度: {}", response.len()); - let assistant_message = ChatMessage::assistant(response); - self.ui.messages.push(assistant_message); - } - Err(e) => { - log::error!("AI 响应失败: {}", e); - let error_message = ChatMessage::assistant(format!("错误: {}", e)); - self.ui.messages.push(error_message); - } - } - } else { - log::warn!("Agent 未初始化"); - } - - // 滚动到底部 - 将在渲染时自动计算 - self.ui.auto_scroll = true; - - Ok(()) - } - - /// 清空会话 - fn clear_chat(&mut self) { - log::info!("清空会话"); - self.ui.messages.clear(); - self.ui.scroll_offset = 0; - self.ui.auto_scroll = true; - } - - /// 显示帮助信息 - fn show_help(&mut self) { - log::info!("显示帮助信息"); - let help_message = ChatMessage::assistant(AppUi::get_help_message()); - self.ui.messages.push(help_message); - self.ui.auto_scroll = true; - } - - /// 导出会话到剪贴板 - fn dump_chats(&mut self) { - match self.ui.dump_chats_to_clipboard() { - Ok(msg) => { - log::info!("{}", msg); - let success_message = ChatMessage::assistant(msg); - self.ui.messages.push(success_message); - } - Err(e) => { - log::error!("{}", e); - let error_message = ChatMessage::assistant(format!("❌ {}", e)); - self.ui.messages.push(error_message); - } - } - self.ui.auto_scroll = true; - } - - /// 退出时保存对话到记忆系统 - pub async fn save_conversations_to_memory(&self) -> Result<()> { - if let Some(infrastructure) = &self.infrastructure { - let conversations: Vec<(String, String)> = self.ui.messages - .iter() - .filter_map(|msg| match msg.role { - crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), - crate::agent::MessageRole::Assistant => { - if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { - Some((last.content.clone(), msg.content.clone())) - } else { - None - } - } - _ => None, - }) - .filter(|(user, assistant)| !user.is_empty() && !assistant.is_empty()) - .collect(); - - if !conversations.is_empty() { - log::info!("正在保存 {} 条对话到记忆系统...", conversations.len()); - store_conversations_batch( - infrastructure.memory_manager().clone(), - &conversations, - &self.user_id, - ).await.map_err(|e| anyhow::anyhow!("保存对话到记忆系统失败: {}", e))?; - log::info!("对话保存完成"); - } - } - Ok(()) - } - - /// 获取所有对话 - pub fn get_conversations(&self) -> Vec<(String, String)> { - self.ui.messages - .iter() - .filter_map(|msg| match msg.role { - crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), - crate::agent::MessageRole::Assistant => { - if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { - Some((last.content.clone(), msg.content.clone())) - } else { - None - } - } - _ => None, - }) - .collect() - } - - /// 获取用户ID - pub fn get_user_id(&self) -> String { - self.user_id.clone() - } -} - -/// 创建默认机器人 -pub fn create_default_bots(config_manager: &ConfigManager) -> Result<()> { - let bots = config_manager.get_bots()?; - - if bots.is_empty() { - // 创建默认机器人 - let default_bot = BotConfig::new( - "助手", - "你是一个有用的 AI 助手,能够回答各种问题并提供帮助。", - "password", - ); - config_manager.add_bot(default_bot)?; - - let coder_bot = BotConfig::new( - "程序员", - "你是一个经验丰富的程序员,精通多种编程语言,能够帮助解决编程问题。", - "password", - ); - config_manager.add_bot(coder_bot)?; - - log::info!("已创建默认机器人"); - } - - Ok(()) -} - diff --git a/examples/cortex-mem-tars-new/src/main.rs b/examples/cortex-mem-tars-new/src/main.rs deleted file mode 100644 index b42b4c8..0000000 --- a/examples/cortex-mem-tars-new/src/main.rs +++ /dev/null @@ -1,123 +0,0 @@ -mod agent; -mod app; -mod config; -mod infrastructure; -mod logger; -mod ui; - -use anyhow::{Context, Result}; -use app::{create_default_bots, App}; -use config::ConfigManager; -use infrastructure::Infrastructure; -use logger::init_logger; -use std::sync::Arc; - -#[tokio::main] -async fn main() -> Result<()> { - // 解析命令行参数 - let args: Vec = std::env::args().collect(); - let enhance_memory_saver = args.contains(&"--enhance-memory-saver".to_string()); - - if enhance_memory_saver { - log::info!("已启用增强记忆保存功能"); - } - - // 初始化配置管理器 - let config_manager = ConfigManager::new().context("无法初始化配置管理器")?; - log::info!("配置管理器初始化成功"); - - // 初始化日志系统 - let log_manager = init_logger(config_manager.config_dir()).context("无法初始化日志系统")?; - log::info!("日志系统初始化成功"); - - // 创建默认机器人 - create_default_bots(&config_manager).context("无法创建默认机器人")?; - - // 初始化基础设施(LLM 客户端、向量存储、记忆管理器) - let infrastructure = match Infrastructure::new(config_manager.cortex_config().clone()).await { - Ok(inf) => { - log::info!("基础设施初始化成功"); - Some(Arc::new(inf)) - } - Err(e) => { - log::warn!("基础设施初始化失败,将使用 Mock Agent: {}", e); - None - } - }; - - // 创建并运行应用 - let mut app = App::new( - config_manager, - log_manager, - infrastructure.clone(), - ).context("无法创建应用")?; - log::info!("应用创建成功"); - - // 检查服务可用性 - app.check_service_status().await.context("无法检查服务状态")?; - - // 加载用户基本信息 - app.load_user_info().await.context("无法加载用户信息")?; - - // 运行应用 - app.run().await.context("应用运行失败")?; - - // 退出时保存对话到记忆系统(仅在启用增强记忆保存功能时) - if enhance_memory_saver { - if let Some(_inf) = infrastructure { - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ 🧠 Cortex Memory - 退出流程 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); - - log::info!("🚀 开始退出流程,准备保存对话到记忆系统..."); - - let conversations = app.get_conversations(); - let user_id = app.get_user_id(); - - println!("📋 会话摘要:"); - println!(" • 对话轮次: {} 轮", conversations.len()); - println!(" • 用户ID: {}", user_id); - - if conversations.is_empty() { - println!("⚠️ 没有需要存储的内容"); - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ ✅ 退出流程完成 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); - println!("👋 Cortex TARS powering down. Goodbye!"); - return Ok(()); - } - - println!("\n🧠 开始执行记忆化存储..."); - println!("📝 正在保存 {} 条对话记录到记忆库...", conversations.len()); - println!("🚀 开始存储对话到记忆系统..."); - - match app.save_conversations_to_memory().await { - Ok(_) => { - println!("✨ 记忆化完成!"); - println!("✅ 所有对话已成功存储到记忆系统"); - println!("🔍 存储详情:"); - println!(" • 对话轮次: {} 轮", conversations.len()); - println!(" • 用户消息: {} 条", conversations.len()); - println!(" • 助手消息: {} 条", conversations.len()); - } - Err(e) => { - println!("❌ 记忆存储失败: {}", e); - println!("⚠️ 虽然记忆化失败,但仍正常退出"); - } - } - - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ 🎉 退出流程完成 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); - println!("👋 Cortex TARS powering down. Goodbye!"); - } else { - println!("\n⚠️ 基础设施未初始化,无法保存对话到记忆系统"); - println!("👋 Cortex TARS powering down. Goodbye!"); - } - } else { - log::info!("未启用增强记忆保存功能,跳过对话保存"); - println!("\n👋 Cortex TARS powering down. Goodbye!"); - } - - Ok(()) -} diff --git a/examples/cortex-mem-tars-new/src/ui.rs b/examples/cortex-mem-tars-new/src/ui.rs deleted file mode 100644 index fc6be80..0000000 --- a/examples/cortex-mem-tars-new/src/ui.rs +++ /dev/null @@ -1,1347 +0,0 @@ -use crate::agent::ChatMessage; -use crate::config::BotConfig; -use clipboard::ClipboardProvider; -use ratatui::{ - crossterm::event::{KeyEvent, MouseEvent, MouseEventKind}, - layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap}, - Frame, -}; -use tui_markdown::from_str; -use tui_textarea::TextArea; - -/// 应用状态 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AppState { - BotSelection, - Chat, -} - -/// 服务状态 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ServiceStatus { - Initing, // 初始化中 - Active, // 服务可用 - Inactive, // 服务不可用 -} - -/// 聊天界面状态 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ChatState { - Normal, - LogPanel, - Selection, -} - -/// 应用 UI 状态 -pub struct AppUi { - pub state: AppState, - pub service_status: ServiceStatus, - pub chat_state: ChatState, - pub bot_list_state: ListState, - pub bot_list: Vec, - pub messages: Vec, - pub input_textarea: TextArea<'static>, - pub scroll_offset: usize, - pub auto_scroll: bool, // 是否自动滚动到底部 - pub log_panel_visible: bool, - pub log_lines: Vec, - pub log_scroll_offset: usize, - pub input_area_width: u16, - last_key_event: Option, - // 选择模式相关字段 - pub selection_active: bool, - pub selection_start: Option<(usize, usize)>, // (line_index, char_index) - pub selection_end: Option<(usize, usize)>, // (line_index, char_index) - pub cursor_position: (usize, usize), // 当前光标位置 (line_index, char_index) - // 消息显示区域位置 - pub messages_area: Option, -} - -/// 键盘事件处理结果 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum KeyAction { - Continue, // 继续运行 - Quit, // 退出程序 - SendMessage, // 发送消息 - ClearChat, // 清空会话 - ShowHelp, // 显示帮助 - DumpChats, // 导出会话到剪贴板 -} - -impl AppUi { - pub fn new() -> Self { - let mut bot_list_state = ListState::default(); - bot_list_state.select(Some(0)); - - let mut input_textarea = TextArea::default(); - let _ = input_textarea.set_block(Block::default() - .borders(Borders::ALL) - .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); - let _ = input_textarea.set_cursor_line_style(Style::default()); - - Self { - state: AppState::BotSelection, - service_status: ServiceStatus::Initing, - chat_state: ChatState::Normal, - bot_list_state, - bot_list: vec![], - messages: vec![], - input_textarea, - scroll_offset: 0, - auto_scroll: true, - log_panel_visible: false, - log_lines: vec![], - log_scroll_offset: 0, - input_area_width: 0, - last_key_event: None, - selection_active: false, - selection_start: None, - selection_end: None, - cursor_position: (0, 0), - messages_area: None, - } - } - - /// 设置机器人列表 - pub fn set_bot_list(&mut self, bots: Vec) { - self.bot_list = bots; - if !self.bot_list.is_empty() { - self.bot_list_state.select(Some(0)); - } else { - self.bot_list_state.select(None); - } - } - - /// 获取选中的机器人 - pub fn selected_bot(&self) -> Option<&BotConfig> { - if let Some(index) = self.bot_list_state.selected() { - self.bot_list.get(index) - } else { - None - } - } - - /// 处理键盘事件 - pub fn handle_key_event(&mut self, key: KeyEvent) -> KeyAction { - // 事件去重:如果和上一次事件完全相同,则忽略 - if let Some(last_key) = self.last_key_event { - if last_key.code == key.code && last_key.modifiers == key.modifiers { - // log::debug!("忽略重复事件: {:?}", key); - self.last_key_event = None; - return KeyAction::Continue; - } - } - - self.last_key_event = Some(key); - - match self.state { - AppState::BotSelection => { - if self.handle_bot_selection_key(key) { - KeyAction::Continue - } else { - KeyAction::Quit - } - } - AppState::Chat => self.handle_chat_key(key), - } - } - - /// 处理机器人选择界面的键盘事件 - fn handle_bot_selection_key(&mut self, key: KeyEvent) -> bool { - use ratatui::crossterm::event::KeyCode; - match key.code { - KeyCode::Up | KeyCode::Char('k') => { - if let Some(selected) = self.bot_list_state.selected() { - if selected > 0 { - self.bot_list_state.select(Some(selected - 1)); - log::debug!("选择上一个机器人"); - } - } - true - } - KeyCode::Down | KeyCode::Char('j') => { - if let Some(selected) = self.bot_list_state.selected() { - if selected < self.bot_list.len().saturating_sub(1) { - self.bot_list_state.select(Some(selected + 1)); - log::debug!("选择下一个机器人"); - } - } - true - } - KeyCode::Enter => { - if let Some(bot) = self.selected_bot() { - log::info!("选择机器人: {}", bot.name); - self.state = AppState::Chat; - } - true - } - KeyCode::Char('q') => { - log::info!("用户按 q 退出"); - false - } - KeyCode::Char('c') if key.modifiers.contains(ratatui::crossterm::event::KeyModifiers::CONTROL) => { - log::info!("用户按 Ctrl-C 退出"); - false - } - _ => true, - } - } - - /// 处理聊天界面的键盘事件 - fn handle_chat_key(&mut self, key: KeyEvent) -> KeyAction { - use ratatui::crossterm::event::{KeyCode, KeyModifiers}; - - if self.log_panel_visible { - log::debug!("日志面板打开,处理日志面板键盘事件"); - if self.handle_log_panel_key(key) { - KeyAction::Continue - } else { - KeyAction::Quit - } - } else { - match key.code { - KeyCode::Enter => { - if key.modifiers.is_empty() { - // Enter 发送消息 - log::debug!("Enter: 准备发送消息"); - let text = self.get_input_text(); - if !text.trim().is_empty() { - KeyAction::SendMessage - } else { - KeyAction::Continue - } - } else { - // Shift+Enter 换行 - log::debug!("Shift+Enter: 换行"); - self.input_textarea.input(key); - KeyAction::Continue - } - } - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - log::debug!("Ctrl-C: 退出"); - KeyAction::Quit - } - KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => { - log::debug!("Ctrl-L: 切换日志面板"); - self.log_panel_visible = !self.log_panel_visible; - KeyAction::Continue - } - KeyCode::Esc => { - log::debug!("关闭日志面板"); - self.log_panel_visible = false; - // 清除选择 - self.selection_active = false; - self.selection_start = None; - self.selection_end = None; - KeyAction::Continue - } - KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => { - // 先让 tui-textarea 处理输入 - self.input_textarea.input(key); - // 尝试自动换行 - self.handle_auto_wrap(); - KeyAction::Continue - } - _ => { - self.input_textarea.input(key); - KeyAction::Continue - } - } - } - } - - /// 处理自动换行 - 检查所有行的显示宽度,必要时插入换行 - fn handle_auto_wrap(&mut self) { - // 使用实际的输入框宽度(减去边框的2个字符) - let max_display_width = if self.input_area_width > 2 { - (self.input_area_width as usize).saturating_sub(2) - } else { - 74 // 默认宽度 - }; - - loop { - let lines = self.input_textarea.lines().to_vec(); - let (cursor_row, cursor_col) = self.input_textarea.cursor(); - - // 检查所有行,找到第一行超过宽度的行 - let mut wrap_line_idx = None; - let mut wrap_pos = 0; - - for (line_idx, line) in lines.iter().enumerate() { - let line_width: usize = line.chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum(); - - if line_width > max_display_width { - // 找到这一行中需要换行的位置 - let chars: Vec = line.chars().collect(); - let mut current_width = 0usize; - - for (char_idx, c) in chars.iter().enumerate() { - let char_width = unicode_width::UnicodeWidthChar::width(*c).unwrap_or(0); - current_width += char_width; - - if current_width > max_display_width { - // 找到前一个空格的位置 - wrap_pos = char_idx; - for j in (0..char_idx).rev() { - if chars[j].is_whitespace() { - wrap_pos = j; - break; - } - } - if wrap_pos == 0 { - wrap_pos = 1; - } - wrap_line_idx = Some(line_idx); - break; - } - } - break; - } - } - - // 如果没有需要换行的行,退出 - if wrap_line_idx.is_none() { - return; - } - - let line_idx = wrap_line_idx.unwrap(); - let line = &lines[line_idx]; - let chars: Vec = line.chars().collect(); - - log::debug!("[AUTO_WRAP] line {} needs wrap, pos {}", line_idx, wrap_pos); - - // 分割这一行 - let prefix: String = chars[..wrap_pos].iter().collect(); - let suffix: String = chars[wrap_pos..].iter().collect(); - - // 构建新的行列表 - let mut new_lines = lines[..line_idx].to_vec(); - new_lines.push(prefix.trim_end().to_string()); - new_lines.push(suffix.trim_start().to_string()); - if line_idx + 1 < lines.len() { - new_lines.extend_from_slice(&lines[line_idx + 1..]); - } - - // 重新创建 TextArea - let mut new_textarea = TextArea::from(new_lines.iter().cloned()); - let _ = new_textarea.set_block(Block::default() - .borders(Borders::ALL) - .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); - let _ = new_textarea.set_cursor_line_style(Style::default()); - - // 重新计算光标位置 - let new_cursor_row = if line_idx < cursor_row { - cursor_row + 1 - } else if line_idx == cursor_row { - if cursor_col > wrap_pos { - line_idx + 1 - } else { - line_idx - } - } else { - cursor_row - }; - - let new_cursor_col = if cursor_row == line_idx && cursor_col > wrap_pos { - let suffix_prefix: String = chars[wrap_pos..cursor_col].iter().collect(); - suffix_prefix.chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum() - } else if cursor_row == line_idx { - cursor_col - } else { - cursor_col - }; - - // 移动光标到正确位置 - for _ in 0..new_cursor_row { - new_textarea.move_cursor(tui_textarea::CursorMove::Down); - } - for _ in 0..new_cursor_col { - new_textarea.move_cursor(tui_textarea::CursorMove::Forward); - } - - self.input_textarea = new_textarea; - } - } - - /// 处理日志面板的键盘事件 - fn handle_log_panel_key(&mut self, key: KeyEvent) -> bool { - use ratatui::crossterm::event::KeyCode; - match key.code { - KeyCode::Esc => { - self.log_panel_visible = false; - true - } - KeyCode::Up | KeyCode::Char('k') => { - if self.log_scroll_offset > 0 { - self.log_scroll_offset -= 1; - } - true - } - KeyCode::Down | KeyCode::Char('j') => { - if self.log_scroll_offset < self.log_lines.len().saturating_sub(1) { - self.log_scroll_offset += 1; - } - true - } - KeyCode::PageUp => { - self.log_scroll_offset = self.log_scroll_offset.saturating_sub(10); - true - } - KeyCode::PageDown => { - self.log_scroll_offset = self - .log_scroll_offset - .saturating_add(10) - .min(self.log_lines.len().saturating_sub(1)); - true - } - KeyCode::Home => { - self.log_scroll_offset = 0; - true - } - KeyCode::End => { - self.log_scroll_offset = self.log_lines.len().saturating_sub(1); - true - } - KeyCode::Char('l') => { - self.log_panel_visible = false; - true - } - _ => true, - } - } - - /// 处理选择模式的键盘事件 - fn handle_selection_key(&mut self, key: KeyEvent) -> KeyAction { - use ratatui::crossterm::event::{KeyCode, KeyModifiers}; - - match key.code { - KeyCode::Esc => { - // 退出选择模式 - log::debug!("退出选择模式"); - self.chat_state = ChatState::Normal; - self.selection_active = false; - self.selection_start = None; - self.selection_end = None; - KeyAction::Continue - } - KeyCode::Up | KeyCode::Char('k') => { - // 向上移动光标 - if self.cursor_position.0 > 0 { - self.cursor_position.0 -= 1; - // 调整列位置到新行的长度范围内 - let all_lines = self.get_all_rendered_lines(); - if self.cursor_position.0 < all_lines.len() { - let line_len = all_lines[self.cursor_position.0].len(); - self.cursor_position.1 = self.cursor_position.1.min(line_len); - } - self.selection_end = Some(self.cursor_position); - } - KeyAction::Continue - } - KeyCode::Down | KeyCode::Char('j') => { - // 向下移动光标 - let total_lines = self.calculate_total_lines(); - if self.cursor_position.0 < total_lines.saturating_sub(1) { - self.cursor_position.0 += 1; - // 调整列位置到新行的长度范围内 - let all_lines = self.get_all_rendered_lines(); - if self.cursor_position.0 < all_lines.len() { - let line_len = all_lines[self.cursor_position.0].len(); - self.cursor_position.1 = self.cursor_position.1.min(line_len); - } - self.selection_end = Some(self.cursor_position); - } - KeyAction::Continue - } - KeyCode::Left | KeyCode::Char('h') => { - // 向左移动光标 - if self.cursor_position.1 > 0 { - self.cursor_position.1 -= 1; - self.selection_end = Some(self.cursor_position); - } - KeyAction::Continue - } - KeyCode::Right | KeyCode::Char('l') => { - // 向右移动光标 - let all_lines = self.get_all_rendered_lines(); - if self.cursor_position.0 < all_lines.len() { - let line_len = all_lines[self.cursor_position.0].len(); - if self.cursor_position.1 < line_len { - self.cursor_position.1 += 1; - self.selection_end = Some(self.cursor_position); - } - } - KeyAction::Continue - } - KeyCode::Char('y') => { - // 复制选中的内容 - log::debug!("复制选中的内容"); - self.copy_selection(); - KeyAction::Continue - } - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+C 复制选中的内容 - log::debug!("Ctrl+C 复制选中的内容"); - self.copy_selection(); - KeyAction::Continue - } - _ => KeyAction::Continue, - } - } - - /// 复制选中的内容到剪贴板 - fn copy_selection(&mut self) { - if let (Some(start), Some(end)) = (self.selection_start, self.selection_end) { - let selected_text = self.get_selected_text(start, end); - - if !selected_text.is_empty() { - match clipboard::ClipboardContext::new() { - Ok(mut ctx) => { - match ctx.set_contents(selected_text.clone()) { - Ok(_) => { - log::info!("已复制 {} 个字符到剪贴板", selected_text.len()); - } - Err(e) => { - log::error!("复制到剪贴板失败: {}", e); - } - } - } - Err(e) => { - log::error!("无法访问剪贴板: {}", e); - } - } - } - } - } - - /// 获取选中的文本 - fn get_selected_text(&self, start: (usize, usize), end: (usize, usize)) -> String { - let mut result = String::new(); - let all_lines = self.get_all_rendered_lines(); - - let start_line = start.0.min(end.0); - let end_line = end.0.max(start.0); - let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; - let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; - - for line_idx in start_line..=end_line { - if line_idx < all_lines.len() { - let line = &all_lines[line_idx]; - let chars: Vec = line.chars().collect(); - let char_len = chars.len(); - - if line_idx == start_line && line_idx == end_line { - // 单行选择 - if start_col < char_len && end_col <= char_len && start_col < end_col { - let selected: String = chars[start_col..end_col].iter().collect(); - result.push_str(&selected); - } - } else if line_idx == start_line { - // 起始行 - if start_col < char_len { - let selected: String = chars[start_col..].iter().collect(); - result.push_str(&selected); - } - result.push('\n'); - } else if line_idx == end_line { - // 结束行 - if end_col <= char_len { - let selected: String = chars[..end_col].iter().collect(); - result.push_str(&selected); - } - } else { - // 中间行 - result.push_str(line); - result.push('\n'); - } - } - } - - result - } - - /// 获取所有渲染的行文本 - fn get_all_rendered_lines(&self) -> Vec { - let mut all_lines: Vec = vec![]; - - for message in &self.messages { - // 角色标签行 - let role_label = match message.role { - crate::agent::MessageRole::System => "[System]", - crate::agent::MessageRole::User => "[You]", - crate::agent::MessageRole::Assistant => "[AI]", - }; - all_lines.push(role_label.to_string()); - - // 渲染 Markdown 内容(与 render_messages 保持一致) - let markdown_text = from_str(&message.content); - for line in markdown_text.lines { - let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); - all_lines.push(line_text); - } - - // 空行分隔 - all_lines.push(String::new()); - } - - all_lines - } - - /// 处理鼠标事件 - pub fn handle_mouse_event(&mut self, event: MouseEvent, _area: Rect) -> bool { - if self.state != AppState::Chat { - return true; - } - - // 使用保存的消息区域 - let messages_area = match self.messages_area { - Some(area) => area, - None => return true, - }; - - match event.kind { - MouseEventKind::ScrollUp => { - if self.log_panel_visible { - if self.log_scroll_offset > 0 { - self.log_scroll_offset = self.log_scroll_offset.saturating_sub(3); - } - } else if self.scroll_offset > 0 { - self.scroll_offset = self.scroll_offset.saturating_sub(3); - // 用户手动滚动,禁用自动滚动 - self.auto_scroll = false; - } - true - } - MouseEventKind::ScrollDown => { - if self.log_panel_visible { - self.log_scroll_offset = self.log_scroll_offset.saturating_add(3); - } else { - self.scroll_offset = self.scroll_offset.saturating_add(3); - // 用户手动滚动,禁用自动滚动 - self.auto_scroll = false; - } - true - } - MouseEventKind::Down(but) if but == ratatui::crossterm::event::MouseButton::Left => { - // 鼠标左键按下,开始选择 - let (line_idx, col_idx) = self.mouse_to_text_position(event, messages_area); - self.selection_active = true; - self.selection_start = Some((line_idx, col_idx)); - self.selection_end = Some((line_idx, col_idx)); - true - } - MouseEventKind::Drag(but) if but == ratatui::crossterm::event::MouseButton::Left => { - // 鼠标拖拽,更新选择 - let (line_idx, col_idx) = self.mouse_to_text_position(event, messages_area); - self.selection_end = Some((line_idx, col_idx)); - true - } - MouseEventKind::Up(but) if but == ratatui::crossterm::event::MouseButton::Left => { - - // 保持选择状态,用户可以继续操作 - true - } - MouseEventKind::Up(but) if but == ratatui::crossterm::event::MouseButton::Right => { - // 鼠标右键释放,复制选中的文本 - log::debug!("鼠标右键,复制选中的文本"); - if self.selection_active { - self.copy_selection(); - } - true - } - _ => true, - } - } - - /// 将鼠标坐标转换为文本位置 (line_index, char_index) - fn mouse_to_text_position(&self, event: MouseEvent, area: Rect) -> (usize, usize) { - // 计算相对于消息区域的坐标 - let content_area = area.inner(Margin { - vertical: 1, - horizontal: 1, - }); - - // 检查鼠标是否在消息区域内 - if event.row < content_area.top() || event.row >= content_area.bottom() || - event.column < content_area.left() || event.column >= content_area.right() { - log::debug!("鼠标不在消息区域内"); - return (self.scroll_offset, 0); - } - - // 计算行索引(考虑滚动偏移) - let relative_row = event.row.saturating_sub(content_area.top()); - let line_idx = self.scroll_offset + relative_row as usize; - - // 计算列索引 - let relative_col = event.column.saturating_sub(content_area.left()); - let col_idx = relative_col as usize; - - // 获取实际行的文本,确保列索引不超出范围 - let all_lines = self.get_all_rendered_lines(); - if line_idx < all_lines.len() { - let line_len = all_lines[line_idx].len(); - (line_idx, col_idx.min(line_len)) - } else { - log::debug!("行索引超出范围: {} >= {}", line_idx, all_lines.len()); - (line_idx, 0) - } - } - - /// 渲染 UI - pub fn render(&mut self, frame: &mut Frame) { - match self.state { - AppState::BotSelection => self.render_bot_selection(frame), - AppState::Chat => self.render_chat(frame), - } - } - - /// 渲染机器人选择界面 - fn render_bot_selection(&mut self, frame: &mut Frame) { - let area = frame.area(); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(3)]) - .split(area); - - // 标题 - let title = Paragraph::new("选择机器人") - .block(Block::default().borders(Borders::ALL)) - .alignment(Alignment::Center) - .style(Style::default().add_modifier(Modifier::BOLD)); - - frame.render_widget(title, chunks[0]); - - // 机器人列表 - let items: Vec = self - .bot_list - .iter() - .map(|bot| { - ListItem::new(Line::from(vec![ - Span::styled( - bot.name.clone(), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), - ), - Span::raw(" - "), - Span::styled( - format!("{}...", &bot.system_prompt.chars().take(40).collect::()), - Style::default().fg(Color::Gray), - ), - ])) - }) - .collect(); - - let list = List::new(items) - .block(Block::default().borders(Borders::ALL).title("可用机器人")) - .highlight_style( - Style::default() - .bg(Color::DarkGray) - .add_modifier(Modifier::REVERSED), - ); - - frame.render_stateful_widget(list, chunks[1], &mut self.bot_list_state); - - // 帮助提示 - let help = Paragraph::new("↑/↓ 或 j/k: 选择 | Enter: 进入 | q 或 Ctrl-C: 退出") - .alignment(Alignment::Center); - - frame.render_widget(help, chunks[2]); - } - - /// 渲染聊天界面 - fn render_chat(&mut self, frame: &mut Frame) { - let area = frame.area(); - - if self.log_panel_visible { - self.render_chat_with_log_panel(frame, area); - } else { - self.render_chat_normal(frame, area); - } - } - - /// 渲染普通聊天界面 - fn render_chat_normal(&mut self, frame: &mut Frame, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([ - Constraint::Length(3), - Constraint::Min(0), - Constraint::Length(8), - ]) - .split(area); - - // 创建简洁的标题文字 - let title_line = Line::from(vec![ - Span::styled( - "TARS AI Program", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ]); - - let title = Paragraph::new(title_line) - .block( - Block::default() - .borders(Borders::ALL) - .border_style( - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) - ) - .border_type(ratatui::widgets::BorderType::Double) - .title_style( - Style::default() - .fg(match self.service_status { - ServiceStatus::Initing => Color::Blue, - ServiceStatus::Active => Color::Green, - ServiceStatus::Inactive => Color::Red, - }) - .add_modifier(Modifier::BOLD) - ) - .title(match self.service_status { - ServiceStatus::Initing => " [ SYSTEM INITING ] ", - ServiceStatus::Active => " [ SYSTEM ACTIVE ] ", - ServiceStatus::Inactive => " [ SYSTEM INACTIVE ] ", - }) - ) - .alignment(Alignment::Center) - .style( - Style::default() - .fg(Color::White) - .bg(Color::Rgb(20, 30, 40)) - ); - - frame.render_widget(title, chunks[0]); - - // 消息显示区域 - let messages_area = chunks[1]; - self.messages_area = Some(messages_area); - self.render_messages(frame, messages_area); - - // 输入区域 - self.render_input(frame, chunks[2]); - } - - /// 渲染带日志面板的聊天界面 - fn render_chat_with_log_panel(&mut self, frame: &mut Frame, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([ - Constraint::Length(3), - Constraint::Percentage(50), - Constraint::Percentage(50), - ]) - .split(area); - - // 创建简洁的标题文字 - let title_line = Line::from(vec![ - Span::styled( - "TARS AI Program", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ]); - - let title = Paragraph::new(title_line) - .block( - Block::default() - .borders(Borders::ALL) - .border_style( - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) - ) - .border_type(ratatui::widgets::BorderType::Double) - .title_style( - Style::default() - .fg(match self.service_status { - ServiceStatus::Initing => Color::Blue, - ServiceStatus::Active => Color::Green, - ServiceStatus::Inactive => Color::Red, - }) - .add_modifier(Modifier::BOLD) - ) - .title(match self.service_status { - ServiceStatus::Initing => " [ SYSTEM INITING ] ", - ServiceStatus::Active => " [ SYSTEM ACTIVE ] ", - ServiceStatus::Inactive => " [ SYSTEM INACTIVE ] ", - }) - ) - .alignment(Alignment::Center) - .style( - Style::default() - .fg(Color::White) - .bg(Color::Rgb(20, 30, 40)) - ); - - frame.render_widget(title, chunks[0]); - - // 消息显示区域 - let messages_area = chunks[1]; - self.messages_area = Some(messages_area); - self.render_messages(frame, messages_area); - - // 日志面板 - self.render_log_panel(frame, chunks[2]); - } - - /// 计算消息的总行数 - fn calculate_total_lines(&self) -> usize { - let mut total = 0; - for message in &self.messages { - // 角色标签占 1 行 - total += 1; - // 消息内容行数 - total += message.content.lines().count().max(1); - // 空行分隔 - total += 1; - } - total - } - - /// 滚动到底部 - pub fn scroll_to_bottom(&mut self, area_height: u16) { - let total_lines = self.calculate_total_lines(); - let visible_lines = area_height.saturating_sub(2) as usize; - let max_scroll = total_lines.saturating_sub(visible_lines); - self.scroll_offset = max_scroll; - } - - /// 渲染消息 - fn render_messages(&mut self, frame: &mut Frame, area: Rect) { - // 使用 tui-markdown 渲染每个消息 - let content_area = area.inner(Margin { - vertical: 1, - horizontal: 1, - }); - - // 收集所有消息的渲染行 - let mut all_lines: Vec = vec![]; - - for message in &self.messages { - let role_label = match message.role { - crate::agent::MessageRole::System => "System", - crate::agent::MessageRole::User => "You", - crate::agent::MessageRole::Assistant => "TARS AI", - }; - - let role_color = match message.role { - crate::agent::MessageRole::System => Color::Yellow, - crate::agent::MessageRole::User => Color::Green, - crate::agent::MessageRole::Assistant => Color::Cyan, - }; - - // 格式化时间戳 - let time_str = message.timestamp.format("%H:%M:%S").to_string(); - - // 添加角色标签和时间戳 - all_lines.push(Line::from(vec![ - Span::styled( - format!("[{}]", role_label), - Style::default() - .fg(role_color) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::styled( - format!("[{}]", time_str), - Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::DIM), - ), - ])); - - // 渲染 Markdown 内容 - let markdown_text = from_str(&message.content); - // 将 tui_markdown 的 Text 转换为 ratatui 的 Text - for line in markdown_text.lines { - all_lines.push(Line::from(line.spans.iter().map(|s| { - Span::raw(s.content.clone()) - }).collect::>())); - } - - // 添加空行分隔 - all_lines.push(Line::from("")); - } - - // 计算滚动 - let total_lines = all_lines.len(); - let visible_lines = area.height.saturating_sub(2) as usize; // 减去边框 - let max_scroll = total_lines.saturating_sub(visible_lines); - - // 如果启用了自动滚动,始终滚动到底部 - if self.auto_scroll { - self.scroll_offset = max_scroll; - } else { - // 限制 scroll_offset 在有效范围内 - if self.scroll_offset > max_scroll { - self.scroll_offset = max_scroll; - } - } - - // 应用选择高亮 - let display_lines: Vec = if self.selection_active { - self.apply_selection_highlight(all_lines, self.scroll_offset, visible_lines) - } else { - all_lines - .into_iter() - .skip(self.scroll_offset) - .take(visible_lines) - .collect() - }; - - // 渲染边框 - let title = "交互信息 (鼠标拖拽选择, Esc 清除选择)"; - let block = Block::default() - .borders(Borders::ALL) - .title(title); - frame.render_widget(block, area); - - // 渲染消息内容(在边框内部) - let paragraph = Paragraph::new(display_lines) - .wrap(Wrap { trim: false }); - frame.render_widget(paragraph, content_area); - - // 渲染滚动条 - if total_lines > visible_lines { - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")); - - let mut scrollbar_state = ScrollbarState::new(total_lines) - .position(self.scroll_offset); - - let scrollbar_area = area.inner(Margin { - vertical: 1, - horizontal: 0, - }); - - frame.render_stateful_widget( - scrollbar, - scrollbar_area, - &mut scrollbar_state, - ); - } - } - - /// 应用选择高亮 - fn apply_selection_highlight<'a>(&self, lines: Vec>, scroll_offset: usize, visible_lines: usize) -> Vec> { - let (start, end) = match (self.selection_start, self.selection_end) { - (Some(s), Some(e)) => (s, e), - _ => return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(), - }; - - // 如果选择范围完全在可见区域之外,直接返回 - let start_line = start.0.min(end.0); - let end_line = end.0.max(start.0); - - // 可见区域的行范围是 [scroll_offset, scroll_offset + visible_lines) - let visible_start = scroll_offset; - let visible_end = scroll_offset + visible_lines; - - // 如果选择区域和可见区域没有重叠,直接返回 - if end_line < visible_start || start_line >= visible_end { - // log::debug!("选择区域和可见区域没有重叠,直接返回"); - return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(); - } - - let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; - let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; - - // 使用反色样式使高亮更明显 - let highlight_style = Style::default() - .fg(Color::Black) - .bg(Color::White) - .add_modifier(Modifier::BOLD); - - let mut highlighted_count = 0; - let mut total_processed = 0; - - let result = lines - .into_iter() - .enumerate() - .skip(scroll_offset) - .take(visible_lines) - .map(|(original_idx, line)| { - total_processed += 1; - // original_idx 是原始的行索引(从 0 开始) - // skip(scroll_offset) 后,visible_idx 从 0 开始 - let visible_idx = original_idx - scroll_offset; - let in_range = original_idx >= start_line && original_idx <= end_line; - - if in_range { - // 这一行在选择范围内 - highlighted_count += 1; - let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); - let chars: Vec = line_text.chars().collect(); - let char_len = chars.len(); - - if original_idx == start_line && original_idx == end_line { - // 单行选择 - let safe_start_col = start_col.min(char_len); - let safe_end_col = end_col.min(char_len); - if safe_start_col < char_len && safe_end_col <= char_len && safe_start_col < safe_end_col { - let before: String = chars[..safe_start_col].iter().collect(); - let selected: String = chars[safe_start_col..safe_end_col].iter().collect(); - let after: String = chars[safe_end_col..].iter().collect(); - - log::debug!("单行高亮: original_idx={}, before_len={}, selected_len={}, after_len={}", - original_idx, before.len(), selected.len(), after.len()); - - Line::from(vec![ - Span::raw(before), - Span::styled(selected, highlight_style), - Span::raw(after), - ]) - } else { - line - } - } else if original_idx == start_line { - // 起始行 - let safe_start_col = start_col.min(char_len); - if safe_start_col < char_len { - let before: String = chars[..safe_start_col].iter().collect(); - let selected: String = chars[safe_start_col..].iter().collect(); - - Line::from(vec![ - Span::raw(before), - Span::styled(selected, highlight_style), - ]) - } else { - line - } - } else if original_idx == end_line { - // 结束行 - let safe_end_col = end_col.min(char_len); - if safe_end_col <= char_len { - let selected: String = chars[..safe_end_col].iter().collect(); - let after: String = chars[safe_end_col..].iter().collect(); - - Line::from(vec![ - Span::styled(selected, highlight_style), - Span::raw(after), - ]) - } else { - line - } - } else { - // 中间行,整行高亮 - Line::from(vec![Span::styled( - line_text, - highlight_style, - )]) - } - } else { - line - } - }) - .collect(); - - result - } - - /// 渲染输入框 - 使用 tui-textarea - fn render_input(&mut self, frame: &mut Frame, area: Rect) { - // 保存输入框可用宽度(减去边框的2个字符) - self.input_area_width = area.width.saturating_sub(2); - frame.render_widget(&self.input_textarea, area); - } - - /// 渲染日志面板 - fn render_log_panel(&mut self, frame: &mut Frame, area: Rect) { - let visible_lines = area.height as usize; - let max_scroll = self.log_lines.len().saturating_sub(visible_lines); - self.log_scroll_offset = self.log_scroll_offset.min(max_scroll); - - let display_lines: Vec = self - .log_lines - .iter() - .skip(self.log_scroll_offset) - .take(visible_lines) - .map(|line| { - let style = if line.contains("ERROR") { - Style::default().fg(Color::Red) - } else if line.contains("WARN") { - Style::default().fg(Color::Yellow) - } else if line.contains("INFO") { - Style::default().fg(Color::Green) - } else { - Style::default() - }; - Line::from(Span::styled(line.clone(), style)) - }) - .collect(); - - let paragraph = Paragraph::new(display_lines) - .block(Block::default().borders(Borders::ALL).title("日志 (Esc 关闭)")) - .wrap(Wrap { trim: false }); - - frame.render_widget(paragraph, area); - - // 渲染滚动条 - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")); - - let mut scrollbar_state = ScrollbarState::new(self.log_lines.len()) - .position(self.log_scroll_offset); - - let scrollbar_area = area.inner(Margin { - vertical: 1, - horizontal: 0, - }); - - frame.render_stateful_widget( - scrollbar, - scrollbar_area, - &mut scrollbar_state, - ); - } - - /// Get input text, filtering out auto-wrap newlines - /// Heuristic: next line starts with whitespace = user newline (Shift+Enter) - /// next line starts without whitespace = auto-wrap continuation - pub fn get_input_text(&self) -> String { - let lines = self.input_textarea.lines(); - if lines.len() <= 1 { - return lines.first().map(|s| s.as_str()).unwrap_or("").to_string(); - } - - let mut result = Vec::new(); - let mut current_line = lines[0].clone(); - - for i in 1..lines.len() { - let line = &lines[i]; - let starts_with_space = line.starts_with(' ') || line.starts_with('\t'); - - if starts_with_space { - result.push(current_line); - current_line = line.trim_start().to_string(); - } else { - if !current_line.is_empty() && !current_line.ends_with(' ') { - current_line.push(' '); - } - current_line.push_str(line); - } - } - - result.push(current_line); - - if result.len() <= 1 { - return result.first().map(|s| s.as_str()).unwrap_or("").to_string(); - } - - result.join("\n") - } - - /// Clear input box - pub fn clear_input(&mut self) { - self.input_textarea = TextArea::default(); - let _ = self.input_textarea.set_block(Block::default() - .borders(Borders::ALL) - .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); - let _ = self.input_textarea.set_cursor_line_style(Style::default()); - } - - /// 解析并执行命令 - pub fn parse_and_execute_command(&mut self, input: &str) -> Option { - let trimmed = input.trim(); - - // 检查是否是命令(以 / 开头) - if !trimmed.starts_with('/') { - return None; - } - - let parts: Vec<&str> = trimmed.split_whitespace().collect(); - let command = parts.get(0).map(|s| s.to_lowercase()).unwrap_or_default(); - - match command.as_str() { - "/quit" => { - log::info!("执行命令: /quit"); - Some(KeyAction::Quit) - } - "/cls" | "/clear" => { - log::info!("执行命令: {}", command); - Some(KeyAction::ClearChat) - } - "/help" => { - log::info!("执行命令: /help"); - Some(KeyAction::ShowHelp) - } - "/dump-chats" => { - log::info!("执行命令: /dump-chats"); - Some(KeyAction::DumpChats) - } - _ => { - log::warn!("未知命令: {}", command); - None - } - } - } - - /// 获取帮助信息 - pub fn get_help_message() -> String { - "# TARS AI Program - 帮助信息\n\n欢迎使用TARS演示程序,我是由Cortex Memory技术驱动的人工智能程序,作为你的第二大脑,我能够作为你的外脑与你的记忆深度链接。\n\n## 可用命令\n\n| 命令 | 说明 |\n|------|------|\n| `/quit` | 退出程序 |\n| `/cls` 或 `/clear` | 清空会话区域 |\n| `/help` | 显示此帮助信息 |\n| `/dump-chats` | 复制会话区域的所有内容到剪贴板 |\n\n## 快捷键\n\n- **Enter**: 发送消息\n- **Shift+Enter**: 换行\n- **Ctrl+L**: 打开/关闭日志面板\n- **Esc**: 关闭日志面板\n\n---\n\n*Powered by TARS AI*".to_string() - } - - /// 导出所有会话内容到剪贴板 - pub fn dump_chats_to_clipboard(&self) -> Result { - let mut content = String::new(); - - for message in &self.messages { - let role = match message.role { - crate::agent::MessageRole::System => "System", - crate::agent::MessageRole::User => "You", - crate::agent::MessageRole::Assistant => "TARS AI", - }; - - let time_str = message.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(); - - content.push_str(&format!("[{}] [{}]\n", role, time_str)); - content.push_str(&message.content); - content.push_str("\n\n"); - } - - if content.is_empty() { - return Err("没有会话内容可导出".to_string()); - } - - // 尝试复制到剪贴板 - match clipboard::ClipboardContext::new() { - Ok(mut ctx) => { - match ctx.set_contents(content.clone()) { - Ok(_) => { - log::info!("已导出 {} 个字符到剪贴板", content.len()); - Ok(format!("已导出 {} 条消息到剪贴板", self.messages.len())) - } - Err(e) => { - log::error!("复制到剪贴板失败: {}", e); - Err(format!("复制到剪贴板失败: {}", e)) - } - } - } - Err(e) => { - log::error!("无法访问剪贴板: {}", e); - Err(format!("无法访问剪贴板: {}", e)) - } - } - } -} - -impl Default for AppUi { - fn default() -> Self { - Self::new() - } -} diff --git a/examples/cortex-mem-tars-new/test.rs b/examples/cortex-mem-tars-new/test.rs deleted file mode 100644 index 7ab789e4528085038ed3991242796d1ee4682db8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58378 zcmeHQYm8mhbv~4&QbLpzAw?wVld43H4iiPJUyPdp7G2W zF!qcg526-9L(Yt#O`AXc5w$pK1te;kM^i$11uC`uL83^F9R&kTLQsU1D3zS9@2vT} z_c>?peb2e`Fd=AUpL@?ed++sLYp=cbKEFA0I2w=oqDs^gjYa#S3Hkgn`P&x_$ftwy z9p5%a`;*^pl6&Lwr@vkmy*qNZ?ukaEZGSWBi{!T>~VLi)vLm}Po5e` z+TnZ4Y>PB3=>3R{uKYQgj5#cQjwK@j*IwxXGzXQ?PwUwh>7GExJLNZ}^wwnDzS$O| z0^7k174DEHdJ}n=5I$(D>jaV!!D}dK2mX%9GZ=Y1xr1H-vEr+rx5)QCp__dVFsjty z?k(~e6dXwY9~G%nJX8jTWz6p63HRM?(hAsv`smY8yfu;T5oymJ(&O%$9WPC%>yx|S zGh|{ivSn6fwJX60v;^FJ@;|7vT|VL7hS_^)hrcV5ze=>O_ldsVzCWD)$R|%!)YLk7-|8}ee%ra=umPO*HzIT8E;hj=$0Np zk;6jwia<0fe+KtTO4C$JTjhC3nJu$M8)*rYA~sfsrmt(?(!6~7)19Lu=K?oE=ELsh zL`JI#t!y3ND)$Ex$pejtXL)U7ru?czJEAQ@L*hp2jti&9gub8`DZE2?lBTWwG_5S3 zHW1rzu|?0U?d?kR$+XSm>uTLC*iA%Tf|u%GwioJY7=~;w9~11hiOqG52z@a=0`|iH zEG#AP1cbCoMw2QzO}aW-d6o);Lh);z5R6;e-X-|=%O89kEM7%M8y8yKCEs1p!>SDk zG-?MS4_%^_pxvP8l3L%2d>WBIl@UBkGwI@V+6zet7 zpQJE`Z-eZ1i4BME?2`YDPX*6LM!1h77|}RTI5aS@v0o zN_34pnYR(}a`5*3B14B|tchfN_&JWkYu0ku_NPhY&-j`~fqO)t>XW{j1)Eigzs>iU zh2NI`tPN54v;Md^^c<^8@B$@5V<|SEBWQz#%hz7`66ANI{5B@Nu9opD(RbpPnzv7_lK#mzV!>;Dl;=EM)V5a}Zwo+pvF!T^mlUQwR?BmG<;Sv1jYK zU}ICz=lN^VBOC=l_H^!%w=wWnlmqo${KlEiJre^h_a1&GescSN9q6n+x&0q@HB1=q z$cRb(7<_?R&veAv7_B(#tJGigCtg^k)8#&6$1Uv`kR(%q2z_&s=AQQyE#69VVb%i7Agy_h{ z8Rav5l!-&2U+^e*ieFP{;i!3O^79Um&qXSlWJUtrXiVY{!eeM==wFokizG;!>9&(1DAL#0uW%x8(=Oge!*{q)7b*(6$G|@O)=Onx1_Vzn>Hq2Z${POAV z#@{*kdUssi(Ei31fSK;{qneoYdiD)sF9D@0o=!i8+hYSI$8XwGmv*N5u~ zm;Mk&Ue&iT;?-Q%QjyXYFa_3iy0fq8!YBmL-;5F>u=$j=-zY z$1Hwn$|8EF)VnM_`{NbaJmmPsM1m=Y_M8&9WoMG1er?;xG1B_QyJavkmdd7?(MR@) ztk;A{%6`Gc^lxI@u;i7AdLe~AD@$m2RV^~`r0oTxQvKbItZ7RcXEPVjvLaH+ar3sr z#mzhw*`C+p-j*$Rbwj`$Ly?jF&$uLwJR&*g2_ryR3(;w+gLo*F!R&?i=#GL*mm)bWHx`e^VJ46AQgkbZX^nv{O!x^U;&7 zxrl-C-jF(u8I*bR+cKr+Et=8 z-pq*)4&L|s+DB%fQ5xgK)y*dvZ6izLk~a33z1q5*{f*y?JfrI*eb%gzt$7Xgl9$>| zqB(aY{<*ePANU=R>Z?VL{LGTrtnVs>it8>6RxTjc$uaPWi6> z@%{6ouy(9oVC`7d@y1`XzSYLe&oB`XKz?3$5P<8^eFABvNTSk90gqvp)FHMD*Bq0-jQNBw2fE7tsA z*FVeZ)FGj~u5zs}=s!#rwZ3RIZ}Dv$duDM|A3r1!)RCx3q##`fGcNZqL)MxQNY=R6 zx4Fb?*-9I&h}kIXr8moZb&FiFBEx7p6r)iRYSW&pEtkujbFsP5bru&&>|Ek~yGo=? zl%Jn{)F0EUT-mdEx38O9bkCYJ#`jn?(~z@mzDMfh%mFkNT*k5hSTYhbXeP^$l z$F<>2*G^=gul3#7+^i;;F_0O!HGXl&WvA0)h2`EiGGi*%C}^Z%Vi3*ocO|hn^5|Hp z;W)xZ$bRZ&b52&Xs@i!8eMh8aOqlE^>KFnMC~?Z^w}*H<*j&q(y>|Q zl08Dx;@K5SqhDLL#oz3HWB=mv$IR1zduVU$VplTTc4vUEle`f2sEmtG!6&SeyPw=7 zziEV~E9phoKFCq^#M|QKyYD_#kq83S!k3?oyXe6xk*<1a6=reIY;?|eKocFZ_qqGW zSu94M8@b~4JEk|Ce6>yedOeV)=Xw2#~ zvt;sy2S&G_KK-5eBcI&ZwN0K}yT`5+bglckBr2a1@@1hF-k$0!_xJW)Ir=Z7|9GHr z`n8syO=9c|Ub=Gh+0Q)I(J-@Q_gy=8$M5U^;av~4eSgP;Lu;eBy6NQ06E99(edYs` z27g)s&hao;zf)u#JMv(AP@94s7G_U|dd6&y#MW+Vb-ESo(pvc!lFLRJQ+EK^7NJXE z&^>DD9#Iu!?}}L^d8Y8{&|0y$dEAT7W<( zm=5HIsn5;MxovuLu1zW-+lJqS^w`>qHtRP`mL*rRK zBDVrakclO4VIRO{a|bI1*fD`U5z2SuF0g+Rcd%NDofJO1!qIspE*#I+J;dW?H=5Fi z&tcU99u&JDFq)sM9En$~fvA5yCVe2(#Xa{YdVV9-H_$vIJ~OHd?kK)@AJC`2i`mM z#$62)$2xDl=V{sPwPWJQHh8JKJ0BeCseX3Py@!iqPN^_FhAd!5WmpPyEepGtwVH)G zPk3uxIWDzT?FYO9vR-L=LR*_H`7FjELSVFyINI6=8KU0t_xaU3!oe#s=V*Cc=T}QE zt)VBNCSfaj4RLFAVqIO>mE#*>QB4bth)86<^^K$RYU^KX{OLWY)VTiS`!9JR{`L1mIbD`W^ur}}*uWrYSrouh@dwnZnq|@AFrVPeqo-GGesAv4<_b_T# zPmO%xmMx?2X+MAIhJ)FNreOj*V3?~X)w0*D%w~B+TXRriPt2r>+ZTKPMMyToc+%@< z;nFC}FMH|uwpNst9YuRzadwU|F1u*65bOzR9DAgfFuNS3rUi35)oMSyD!DDP zD*xPWM&xFHKvq6U$I-+?V4u^S$tp;zM7vlAN7buR)KI!3@y}Vf)Nk6eLY=W=1nf>M zDkGTj;5?FIkYu4i^;Jx+B~#J)hg1l zS!t+mWVNVSs#WBi1$gO7gc@)DiWw8+Piwl1#Ro3T^t39aS(nHra#U5}?RYX0R!y)| z(~O$8W`s}WGfdmC#a8`(bmrbY&keN14_7Zbo<9Z1FWJ=htdv4;?Nho`=eN(^-5?nA z>@H&8nAo+ngu|PqEdZ=1D5|m+#+$I|+5*pQajoJY`=>kM(LdI&=5me*HFP_Uel@2blGl#q1(6rqGj`A z&6a8Gi(tZ**)fZZ<$tC;9BEprbcOmyJnsKJEW6~?H6kl6P8!+96Ises#1COFdm3W) ziHuZz+$YSoXGcT|zc80r%>`%C_>I!xyZT`@*`?o&wFAE_rgfnnc`KsFE-n{^X(9R# z>!YrvjauRx$3A%K%Tr%J^_j`@kNam+%w{5j(KR#8Wrn>}5e(TwHcr$1 zJXmqRK$uHF#y_HZq}Ei+kZR-SDX?qHt>tiVE_ zgOI0(|Ae>tP9i8b!sW@zV6hB;h?1o2eDi}=*;kplakdTuXH(R>i@zMjFn7wgU@V%> zy40$bD!-FHKXOQZblvyO%Uzu4j-5@ab;T>C%TWPyg)%K_a}6iZy8IKhfY-e*HZ+l5nH=EYzGK zhALW(UPcErHq$CPa@TjAZzZL3-C0TLQfv}d#z)o7Q4^R*L1+cSN z``Ie(>^B8Mf)P=hx8vk{W3Npj@7ebR;w^C5dx(~HCYk|R>5<;CV=UA!jKM{nwde8GRfHGfpYEKr-tRObCp0WdrA$ zXHO{%^QFuig38qGghz$E;EkN--Ia|})tS8qyf!9$)mo=;pOA^7)iTg9q=z8tCo`dJgXH;d>JdHz9Yn_I8LgLb@;;^2rJSeuLEG9;lBTRipK`AU{0BmDg5*4ey6QPezK6^()>7`^ugJiV6tChkz8;+~wEU z)^}|^E7^cBZ_k?q{{iV8)(&HhC75g!Z2(rNJgryS%RpNWH=E|LW_WMhGLZ_LK#g-? z%8YFHHCO)Wy1GVZdg?2x#!*e$H2Y476|*vo%>9u>ULcuTqXQYlyLYm6FQ~x9t{}+3 zy2R_?3C%QUb_SfiI(dT_YlE*%1>y)>=S6?Gu9e4Ayh`g^=4(d2Q7&U6V;_x?4{cqdX&3URa zcHA$rH7pjXSme0+^Xnu0dtA;~J|Gn?L!vD>B@Jiy>zX)LOEeb4FM|UQSD$LtUvMTf z+WF5|%R_0Tr`~wb9V_{{sHX=o79(}?JX?D<5+jVE`KEo6u))$b%QSP{yfgp`1Npg_ zQ_gp;kI`l}A|@47hcIJ9)qD1B$96sdwBc-7^@4oiHJvFA#g?_9!XCjf@#K)ovQQZQ zha~3T<8PljQTg!jKi&M*k&TxO^*=LXY8^vz(x`fVTmwvX0T`fN_dL}qypQhTkXh>!O9v8>n3deywo4trC3 zk5S&n_@Jmgr%uCBF6*K6G0?c?t$8{uwx8tr3&_tQ^P61}(=$|;%F43FO$HzO7w)u6 zN^ddTyER7r(Sx>l=UwHFe8UcQIGVQ;ehXTHok9go5z3 zX5==3?QpURvruDhNcg8~1z0;X)ho+neR@lcv3>8(A|3J^*EJwybl$uiM_grA72AX- zsE^gNgBOYqS>C5%z~M<`KfEHnSBP))>#_|-K}p$d_GoE^BIngB)_pI(VmL%PA!*h@4~A!)E{2wZ{3PL zas}PMW;8PtU9o-ky09zm#i(WT(q3V&wYsh=wO2W-P>82WEUPGQ5|4s)zTU(t-~{Vi z;_CL#1?3m#3PBd2#TvaKpQl#P7tg>S+T-XhiL_q?-*k-%|GU=;hy^`6wTV7G4^`Z}DjhI#UE65lWeQ$CY6>3yt_JdV#RdrR-(HfJTV_K|BuO%gNp z&909_&a!V72#o5Yu=d03mI>l&kKZL!#94jN4AwLlE%85z zm=rW`Ok&9_R=zn0*8$CLnC$qdz;|;m9FG#W?As_zUs#PY^=jsRUM46Tyy8qW=sezx zP^>P^v>X@fH7Ht(-I_kEi}-=2MAo&=C&c$6*r*2r#jrBxuXrIAq%CUM()FP#z2Ul1 zKSzq;y-@29pr~CXqnX=#CXNr>KdE-B2rYRQQ=^<83fk!VI&=pb-a!eeGrJBeQt{=} z4N}{z)Mr<>(=`TpZrE$DwMMc1tXkS7e?g~JgZ0&c8Jcn8&c-$iVz$|w_+8j*m>=EGr*1NFUZcbTVmeitz*S$!A zrUXi?2cjk~+*>%ZAq|-~$kWoU(ecq05vrMQlfrKQQH)}GCM`69doygv()PKQR1rtJ z2bBl2x}%sGe+pg!53HSPyr=vN+k5f{Sr(i|%Dn>6w>5=kZ~H|qAcc3y71kY*1X?!- zThB#*vHqyMb60xRmhU`WL$gEDDBsF(qey!@vu1o4`D(BV^`(+Sh*ovR)i@2G4vrYRB>2r_tT4V;bGU^l^Unx%)KV5)WYs|V-JRIWi zJa>5hJ8HF2p^a0ve0%fQr|(LRr)PRw1yfkaboRv$PkLW~ZwBUrf%n7xWA~9(ySQdN z>?3o^?u#MXlBobV-dOI?mB~wQ(rJCAR0aESK4?j3mt`#VU#X)I+hk zV4C+vq(k;fSu?XQjeB*6oq818;@fQ?uT~$ivyK8wT6DKli|7H)kzCl!YnDoQ z|K(huwpUXY8}<*+5q7f*v)GWEQvhnqO1WJmW}jy$O3q3_t@WMk^VBUyq_&PR`cn-9 ze%udc&(+;=dUhOspAWmGC;ylTtsWhB;c4f}vr%I?EQ#%6*+-4`io}5SN(^Q$YTdBE z)I|}U?J(zMTV2G6#b{E*tF4SKorf6awW(_d?{mC=nB2%-HBMI^nfHho&Zp-auDFBe z;A2+7_(L(NM<$K5tSxlPViupQE^8hOnW%-(WR9NIX#1TkVOnBOt9c?D$0;4lP<@qf zp&q&ulRUlV-y(1_7$E#ntkg*Yrg38Unf)vt7%TwJ!ycv;z~$G+CKtHsVeV13!fWR(*u zTA0n*Gw3qVmG#AC`CEjqD`mZlOuQMp(hYF2J*+f>(S zR^|+uvX*lGGA2DjmGUc-Wj!Aw%O1&PYc(5uA+vy3wLF*h-{l){FK@#aV&+>$Lwt1i zQO}Kq8PQ;vev&m1TIHboo*!ykpPXdYQ+?(B-ab?>;2mL?C)U`0`>W~GRTF^>BX>jF z)g4&z>zDeMBIm{6DOT~Ib^_}pp_4w;J3n20YUGs(;_~8-v94v)m#mpSwXXN-GZ#uomb8 zLey9rX(26)2HJD3S`y*MBF#5?r~J5fJyyEDPU%plMSXSHuP-*@+P9yRZviOvl57m;LB4c0Wh~p?byl!>Yx!Jip3ht4-nni)fM?)=X)mvWMY1HKr~4| z*g;wU&Ii(p)hM>V&R387=m+bQ-|@hR#6$zM2I)P2VcB%;eJB%Uz^dgXiFmv4WxTK0 z@|(_J%I|_Gzx$!wt_S~a2fr@D^47P@Ay2PRMkfEXnfry>*|VOkuG%|rTz#Y?eD}<3 zYWnrg8@f2d{r-*zhn|RE**SW!VP^U6jZ+_QzqdKM?T+b9Ctq#*TD*Mig)cSCOm+@* zJzaffA6x*b=J-g&z{_r)x?TkNmS@$|e|>`LT(-pnkqhnZS} zctqdg3~ge@#~q_kdW}A*tg^54Y@4+xMv_fC?kuE#=QSN2ru7xO1g;TiQMDP0QBa4k zJ7cjc+>D9!5WG_aQi$>GSbaP>yAk6RKd}+9oG)AEifPg^L5$V6z-Lzb*0w)7dfn{} z*14?dr*KZaEg#3Kmkl2{*dAZ9`$XlhZo%8@pE`B%H=6P1RU zW1a6j8dv4h#8l_627lbKq2;Q>zTNT)PjVuDB5{<&)w^)^cfZhXwRn2WKd`&1VdmX! z5AAK3xx2Hs@A3FQ9{yagmY z4<=A4JJ!37?ybJu{QgT`h;KfW9b?Il{$}U)c*Powad+o~L&}MepP+w)Opl9x4-4H@ z=P(1e4V23RM9wSk7>%>|}6d`83otqbce z;bmKeYlYR9R*$(MA=WAhkeKJ=A2-{w-yW|&Lur6+Kn592Wh<5;}f+K^s# zbMM1)>jrz{wU}u(uj$N3J=Dxw6VlW61BeHf4V`y<|EVRD?eUV`Km26Fgn!32 zp;U<|zk5w9HAC%mc3|s*E!jorKp1^IvAIs2f&8pa z4~2cwACqkKR{68}!`8fF9>Cob;HdIQPpj127$fG2ymMzo_lXL)*D9-w`Ix#AsUJjU zif=YqE#p@r&9yPJwh%N`+c%z2!0a+tdsp;5cWGM)NmgmbnJ1&dvx>Ya$9_8`W~`ui pMd&;#e>;RG=_pZWw0WsAGgr;bq+5|Utuh+cuG7)7kqCwG{{aB^&=UXv diff --git a/examples/cortex-mem-tars-new/.gitignore b/examples/cortex-mem-tars/.gitignore similarity index 100% rename from examples/cortex-mem-tars-new/.gitignore rename to examples/cortex-mem-tars/.gitignore diff --git a/examples/cortex-mem-tars-new/Cargo.lock b/examples/cortex-mem-tars/Cargo.lock similarity index 100% rename from examples/cortex-mem-tars-new/Cargo.lock rename to examples/cortex-mem-tars/Cargo.lock diff --git a/examples/cortex-mem-tars/Cargo.toml b/examples/cortex-mem-tars/Cargo.toml index e0073db..19d70e7 100644 --- a/examples/cortex-mem-tars/Cargo.toml +++ b/examples/cortex-mem-tars/Cargo.toml @@ -1,42 +1,55 @@ [package] name = "cortex-mem-tars" -version = "1.0.0" +version = "1.1.0" edition = "2024" -description = "A TUI demo application for demonstrating and testing the core features of Cortex Memory" -license = "MIT" [dependencies] +# Cortex Memory dependencies cortex-mem-config = { path = "../../cortex-mem-config" } -clap = { workspace = true, features = ["derive"] } -# Workspace dependencies from parent cortex-mem-core = { path = "../../cortex-mem-core" } cortex-mem-rig = { path = "../../cortex-mem-rig" } -# Runtime -tokio = { workspace = true, features = ["full"] } - # LLM framework rig-core = "0.23" # TUI ratatui = "0.29" -crossterm = "0.29" -unicode-width = "0.1" +tui-markdown = "0.3.7" +ratatui-core = "0.1" +crossterm = "0.28" +tui-textarea = "0.7" +unicode-width = "0.2" + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.9" + +# Error handling +anyhow = "1.0" + +# Time handling +chrono = { version = "0.4", features = ["serde"] } + +# File system +directories = "6.0" # Logging +log = "0.4" +env_logger = "0.11" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } once_cell = "1.21" -# Async traits +# Async async-trait = "0.1" - -# JSON处理 -serde_json = "1.0" - -# Stream processing +tokio = { version = "1.40", features = ["full"] } futures = "0.3" async-stream = "0.3" -# 时间处理 -chrono = { version = "0.4", features = ["serde"] } +# HTTP client +reqwest = { version = "0.12", features = ["json"] } + +# Utilities +uuid = { version = "1.10", features = ["v4"] } +clipboard = "0.5" diff --git a/examples/cortex-mem-tars-new/README.md b/examples/cortex-mem-tars/README.md similarity index 99% rename from examples/cortex-mem-tars-new/README.md rename to examples/cortex-mem-tars/README.md index 0572345..408c55e 100644 --- a/examples/cortex-mem-tars-new/README.md +++ b/examples/cortex-mem-tars/README.md @@ -1,4 +1,4 @@ -# Cortex Memory TARS New +# Cortex Memory TARS 这是一个基于 Cortex Memory 的 TUI(终端用户界面)聊天应用,具有记忆功能。它能够记住用户的对话历史和个人信息,提供更智能的对话体验。 @@ -181,4 +181,4 @@ MIT - [Cortex Memory](https://github.com/sopaco/cortex-mem) - 记忆管理系统 - [RatATUI](https://github.com/ratatui-org/ratatui) - TUI 框架 -- [Rig](https://github.com/0xPlaygrounds/rig) - LLM Agent 框架 \ No newline at end of file +- [Rig](https://github.com/0xPlaygrounds/rig) - LLM Agent 框架 diff --git a/examples/cortex-mem-tars-new/config.example.toml b/examples/cortex-mem-tars/config.example.toml similarity index 100% rename from examples/cortex-mem-tars-new/config.example.toml rename to examples/cortex-mem-tars/config.example.toml diff --git a/examples/cortex-mem-tars/src/agent.rs b/examples/cortex-mem-tars/src/agent.rs index 384d5cb..f94e6c0 100644 --- a/examples/cortex-mem-tars/src/agent.rs +++ b/examples/cortex-mem-tars/src/agent.rs @@ -1,25 +1,66 @@ +use anyhow::Result; +use chrono::{DateTime, Local}; use cortex_mem_config::Config; use cortex_mem_core::memory::MemoryManager; use cortex_mem_rig::{ListMemoriesArgs, create_memory_tools, tool::MemoryToolConfig}; +use futures::StreamExt; use rig::{ - agent::Agent, + agent::Agent as RigAgent, client::CompletionClient, providers::openai::{Client, CompletionModel}, tool::Tool, }; - -use chrono::Local; +use rig::agent::MultiTurnStreamItem; +use rig::completion::Message; +use rig::streaming::{StreamedAssistantContent, StreamingChat}; use std::sync::Arc; +use tokio::sync::mpsc; + +/// 消息角色 +#[derive(Debug, Clone, PartialEq)] +pub enum MessageRole { + System, + User, + Assistant, +} + +/// 聊天消息 +#[derive(Debug, Clone)] +pub struct ChatMessage { + pub role: MessageRole, + pub content: String, + pub timestamp: DateTime, +} -// 导入日志重定向函数 -use crate::app::redirect_log_to_ui; +impl ChatMessage { + pub fn new(role: MessageRole, content: String) -> Self { + Self { + role, + content, + timestamp: Local::now(), + } + } + + #[allow(dead_code)] + pub fn system(content: impl Into) -> Self { + Self::new(MessageRole::System, content.into()) + } + + pub fn user(content: impl Into) -> Self { + Self::new(MessageRole::User, content.into()) + } + + pub fn assistant(content: impl Into) -> Self { + Self::new(MessageRole::Assistant, content.into()) + } +} /// 创建带记忆功能的Agent pub async fn create_memory_agent( memory_manager: Arc, memory_tool_config: MemoryToolConfig, config: &Config, -) -> Result, Box> { +) -> Result, Box> { // 创建记忆工具 let memory_tools = create_memory_tools(memory_manager.clone(), &config, Some(memory_tool_config)); @@ -42,13 +83,6 @@ pub async fn create_memory_agent( 此会话发生的初始时间:{current_time} -你的工具: -- CortexMemoryTool: 可以存储、搜索和检索记忆。支持以下操作: - * store_memory: 存储新记忆 - * query_memory: 搜索相关记忆 - * list_memories: 获取一系列的记忆集合 - * get_memory: 获取特定记忆 - 重要指令: - 对话历史将作为上下文提供,请使用这些信息来理解当前的对话流程 - 用户基本信息将在上下文中提供一次,请不要再使用memory工具来创建或更新用户基本信息 @@ -138,15 +172,9 @@ pub async fn extract_user_basic_info( } } -use futures::StreamExt; -use rig::agent::MultiTurnStreamItem; -use rig::completion::Message; -use rig::streaming::{StreamedAssistantContent, StreamingChat}; -use tokio::sync::mpsc; - /// Agent回复函数 - 基于tool call的记忆引擎使用(真实流式版本) pub async fn agent_reply_with_memory_retrieval_streaming( - agent: &Agent, + agent: &RigAgent, _memory_manager: Arc, user_input: &str, _user_id: &str, @@ -154,9 +182,6 @@ pub async fn agent_reply_with_memory_retrieval_streaming( conversations: &[(String, String)], stream_sender: mpsc::UnboundedSender, ) -> Result> { - // 记录开始处理 - redirect_log_to_ui("DEBUG", &format!("开始处理用户请求: {}", user_input)); - // 构建对话历史 - 转换为rig的Message格式 let mut chat_history = Vec::new(); for (user_msg, assistant_msg) in conversations { @@ -180,17 +205,15 @@ pub async fn agent_reply_with_memory_retrieval_streaming( // 构建完整的prompt let prompt_content = if let Some(info) = user_info { - redirect_log_to_ui("DEBUG", "已添加用户基本信息和对话历史到上下文"); format!( "{}\n\n用户基本信息:\n{}\n\n当前用户输入: {}", system_prompt, info, user_input ) } else { - redirect_log_to_ui("DEBUG", "已添加对话历史到上下文"); format!("{}\n\n当前用户输入: {}", system_prompt, user_input) }; - redirect_log_to_ui("DEBUG", "正在生成AI回复(真实流式模式)..."); + log::debug!("正在生成AI回复(真实流式模式)..."); // 使用rig的真实流式API let prompt_message = Message::user(&prompt_content); @@ -223,45 +246,42 @@ pub async fn agent_reply_with_memory_retrieval_streaming( } StreamedAssistantContent::ToolCall(_) => { // 处理工具调用(如果需要) - redirect_log_to_ui("DEBUG", "收到工具调用"); + log::debug!("收到工具调用"); } StreamedAssistantContent::Reasoning(_) => { // 处理推理过程(如果需要) - redirect_log_to_ui("DEBUG", "收到推理过程"); + log::debug!("收到推理过程"); } StreamedAssistantContent::Final(_) => { // 处理最终响应 - redirect_log_to_ui("DEBUG", "收到最终响应"); + log::debug!("收到最终响应"); } StreamedAssistantContent::ToolCallDelta { .. } => { // 处理工具调用增量 - redirect_log_to_ui("DEBUG", "收到工具调用增量"); + log::debug!("收到工具调用增量"); } } } MultiTurnStreamItem::FinalResponse(final_response) => { // 处理最终响应 - redirect_log_to_ui( - "DEBUG", - &format!("收到最终响应: {}", final_response.response()), - ); + log::debug!("收到最终响应: {}", final_response.response()); full_response = final_response.response().to_string(); break; } _ => { // 处理其他未知的流式项目类型 - redirect_log_to_ui("DEBUG", "收到未知的流式项目类型"); + log::debug!("收到未知的流式项目类型"); } } } Err(e) => { - redirect_log_to_ui("ERROR", &format!("流式处理错误: {}", e)); + log::error!("流式处理错误: {}", e); return Err(format!("Streaming error: {}", e).into()); } } } - redirect_log_to_ui("DEBUG", "AI回复生成完成"); + log::debug!("AI回复生成完成"); Ok(full_response.trim().to_string()) } diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index 557afb2..1046ca8 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -1,366 +1,567 @@ -use ratatui::widgets::ScrollbarState; -use std::collections::VecDeque; +use crate::agent::{ChatMessage, create_memory_agent, extract_user_basic_info, store_conversations_batch, agent_reply_with_memory_retrieval_streaming}; +use crate::config::{BotConfig, ConfigManager}; +use crate::infrastructure::Infrastructure; +use crate::logger::LogManager; +use crate::ui::{AppState, AppUi}; +use anyhow::{Context, Result}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ratatui::backend::CrosstermBackend; +use ratatui::layout::Rect; +use rig::agent::Agent as RigAgent; +use rig::providers::openai::CompletionModel; +use std::io; +use std::sync::Arc; +use std::time::{Duration, Instant}; use tokio::sync::mpsc; -use chrono::{DateTime, Local}; -// 全局消息发送器,用于日志重定向 -use once_cell::sync::OnceCell; -use std::sync::Mutex; - -static LOG_SENDER: OnceCell>>> = OnceCell::new(); - -// 设置全局日志发送器 (crate可见性) -pub(crate) fn set_global_log_sender(sender: mpsc::UnboundedSender) { - LOG_SENDER - .get_or_init(|| Mutex::new(None)) - .lock() - .unwrap() - .replace(sender); -} - -// 获取全局日志发送器 (crate可见性) -pub(crate) fn get_global_log_sender() -> Option> { - LOG_SENDER - .get() - .and_then(|mutex| mutex.lock().unwrap().clone()) -} - -// 简单的日志重定向函数 -pub fn redirect_log_to_ui(level: &str, message: &str) { - if let Some(sender) = get_global_log_sender() { - let full_message = format!("[{}] {}", level, message); - let _ = sender.send(AppMessage::Log(full_message)); - } +/// 应用程序 +pub struct App { + #[allow(dead_code)] + config_manager: ConfigManager, + log_manager: Arc, + ui: AppUi, + current_bot: Option, + rig_agent: Option>, + infrastructure: Option>, + user_id: String, + user_info: Option, + should_quit: bool, + message_sender: mpsc::UnboundedSender, + message_receiver: mpsc::UnboundedReceiver, } -#[derive(Debug)] +/// 应用消息类型 +#[derive(Debug, Clone)] pub enum AppMessage { + #[allow(dead_code)] Log(String), - Conversation { - user: String, - assistant: String, - }, StreamingChunk { + #[allow(dead_code)] user: String, chunk: String, }, StreamingComplete { + #[allow(dead_code)] user: String, full_response: String, }, - #[allow(dead_code)] - MemoryIterationCompleted, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum FocusArea { - Input, // 输入框 - Conversation, // 对话区域 - Logs, // 日志区域 } -/// 应用状态 -pub struct App { - // 对话历史 - 包含时间戳 - pub conversations: VecDeque<(String, String, DateTime)>, - // 当前输入 - pub current_input: String, - // 光标位置(以字符为单位) - pub cursor_position: usize, - // 日志信息 - pub logs: VecDeque, - // Agent 是否正在处理 - pub is_processing: bool, - // 用户信息 - pub user_info: Option, - // 是否需要退出 - pub should_quit: bool, - // 是否在shut down过程中 - pub is_shutting_down: bool, - // 记忆迭代是否完成 - pub memory_iteration_completed: bool, - // 消息发送器 - pub message_sender: Option>, - // 日志滚动偏移 - pub log_scroll_offset: usize, - // 对话滚动偏移 - pub conversation_scroll_offset: usize, - // 当前焦点区域 - pub focus_area: FocusArea, - // 用户是否手动滚动过日志(用于决定是否自动滚动到底部) - pub user_scrolled_logs: bool, - // 用户是否手动滚动过对话(用于决定是否自动滚动到底部) - pub user_scrolled_conversations: bool, - // 滚动条状态 - pub conversation_scrollbar_state: ScrollbarState, - pub log_scrollbar_state: ScrollbarState, - // 当前正在流式生成的回复 - pub current_streaming_response: Option<(String, String)>, // (user_input, partial_response) -} - -impl Default for App { - fn default() -> Self { - Self { - conversations: VecDeque::with_capacity(100), - current_input: String::new(), - cursor_position: 0, - logs: VecDeque::with_capacity(50), - is_processing: false, +impl App { + /// 创建新的应用 + pub fn new(config_manager: ConfigManager, log_manager: Arc, infrastructure: Option>) -> Result { + let mut ui = AppUi::new(); + + // 加载机器人列表 + let bots = config_manager.get_bots()?; + ui.set_bot_list(bots); + + // 创建消息通道 + let (msg_tx, msg_rx) = mpsc::unbounded_channel::(); + + log::info!("应用程序初始化完成"); + + Ok(Self { + config_manager, + log_manager, + ui, + current_bot: None, + rig_agent: None, + infrastructure, + user_id: "tars_user".to_string(), user_info: None, should_quit: false, - is_shutting_down: false, - memory_iteration_completed: false, - message_sender: None, - log_scroll_offset: 0, - conversation_scroll_offset: 0, - focus_area: FocusArea::Input, - user_scrolled_logs: false, - user_scrolled_conversations: false, - conversation_scrollbar_state: ScrollbarState::default(), - log_scrollbar_state: ScrollbarState::default(), - current_streaming_response: None, - } + message_sender: msg_tx, + message_receiver: msg_rx, + }) } -} -impl App { - pub fn new(message_sender: mpsc::UnboundedSender) -> Self { - Self { - message_sender: Some(message_sender), - current_streaming_response: None, - ..Default::default() + /// 设置用户信息 + pub async fn load_user_info(&mut self) -> Result<()> { + if let Some(infrastructure) = &self.infrastructure { + let user_info = extract_user_basic_info( + infrastructure.config(), + infrastructure.memory_manager().clone(), + &self.user_id, + ).await.map_err(|e| anyhow::anyhow!("加载用户信息失败: {}", e))?; + + if let Some(info) = user_info { + log::info!("已加载用户基本信息"); + self.user_info = Some(info); + } else { + log::info!("未找到用户基本信息"); + } } + Ok(()) } - pub fn add_log(&mut self, log: String) { - self.logs.push_back(log); - if self.logs.len() > 50 { - self.logs.pop_front(); + /// 检查服务可用性 + pub async fn check_service_status(&mut self) -> Result<()> { + use reqwest::Method; + + if let Some(infrastructure) = &self.infrastructure { + let api_base_url = &infrastructure.config().llm.api_base_url; + // 拼接完整的 API 地址 + let check_url = format!("{}/chat/completions", api_base_url.trim_end_matches('/')); + + log::info!("检查服务可用性: {}", check_url); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .context("无法创建 HTTP 客户端")?; + + match client + .request(Method::OPTIONS, &check_url) + .send() + .await + { + Ok(response) => { + if response.status().is_success() || response.status().as_u16() == 405 { + // 200 OK 或 405 Method Not Allowed 都表示服务可用 + log::info!("服务可用,状态码: {}", response.status()); + self.ui.service_status = crate::ui::ServiceStatus::Active; + } else { + log::warn!("服务不可用,状态码: {}", response.status()); + self.ui.service_status = crate::ui::ServiceStatus::Inactive; + } + } + Err(e) => { + log::error!("服务检查失败: {}", e); + self.ui.service_status = crate::ui::ServiceStatus::Inactive; + } + } + } else { + log::warn!("基础设施未初始化,无法检查服务状态"); + self.ui.service_status = crate::ui::ServiceStatus::Inactive; } - // 如果用户没有手动滚动过,自动滚动到最新日志 - if !self.user_scrolled_logs { - self.scroll_logs_to_bottom(); - } + Ok(()) } - pub fn add_conversation(&mut self, user: String, assistant: String) { - let timestamp = Local::now(); - self.conversations.push_back((user, assistant, timestamp)); - if self.conversations.len() > 100 { - self.conversations.pop_front(); - } + /// 运行应用 + pub async fn run(&mut self) -> Result<()> { + enable_raw_mode().context("无法启用原始模式")?; + + let mut stdout = io::stdout(); + execute!( + stdout, + EnterAlternateScreen, + EnableMouseCapture, + crossterm::terminal::DisableLineWrap + ) + .context("无法设置终端")?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = ratatui::Terminal::new(backend).context("无法创建终端")?; + + let mut last_log_update = Instant::now(); + let mut last_service_check = Instant::now(); + let tick_rate = Duration::from_millis(100); + + loop { + // 更新日志 + if last_log_update.elapsed() > Duration::from_secs(1) { + self.update_logs(); + last_log_update = Instant::now(); + } - // 如果用户没有手动滚动过,自动滚动到最新对话 - if !self.user_scrolled_conversations { - self.scroll_conversations_to_bottom(); - } - } + // 定期检查服务状态(每5秒) + if last_service_check.elapsed() > Duration::from_secs(5) { + // 在后台检查服务状态,不阻塞主循环 + let _ = self.check_service_status().await; + last_service_check = Instant::now(); + } - /// 开始流式回复 - pub fn start_streaming_response(&mut self, user_input: String) { - self.current_streaming_response = Some((user_input, String::new())); - self.is_processing = true; - } + // 处理流式消息 + if let Ok(msg) = self.message_receiver.try_recv() { + match msg { + AppMessage::StreamingChunk { user: _, chunk } => { + // 添加流式内容到当前正在生成的消息 + if let Some(last_msg) = self.ui.messages.last_mut() { + if last_msg.role == crate::agent::MessageRole::Assistant { + last_msg.content.push_str(&chunk); + } else { + // 如果最后一条不是助手消息,创建新的助手消息 + self.ui.messages.push(ChatMessage::assistant(chunk)); + } + } else { + // 如果没有消息,创建新的助手消息 + self.ui.messages.push(ChatMessage::assistant(chunk)); + } + // 确保自动滚动启用 + self.ui.auto_scroll = true; + } + AppMessage::StreamingComplete { user: _, full_response } => { + // 流式完成,确保完整响应已保存 + if let Some(last_msg) = self.ui.messages.last_mut() { + if last_msg.role == crate::agent::MessageRole::Assistant { + last_msg.content = full_response; + } else { + self.ui.messages.push(ChatMessage::assistant(full_response)); + } + } else { + self.ui.messages.push(ChatMessage::assistant(full_response)); + } + // 确保自动滚动启用 + self.ui.auto_scroll = true; + } + AppMessage::Log(_) => { + // 日志消息暂时忽略 + } + } + } - /// 添加流式内容块 - pub fn add_streaming_chunk(&mut self, chunk: String) { - if let Some((_, ref mut response)) = self.current_streaming_response { - response.push_str(&chunk); - - // 如果用户没有手动滚动过,自动滚动到最新对话 - if !self.user_scrolled_conversations { - self.scroll_conversations_to_bottom(); + // 渲染 UI + terminal.draw(|f| self.ui.render(f)).context("渲染失败")?; + + // 处理事件 + if event::poll(tick_rate).context("事件轮询失败")? { + let event = event::read().context("读取事件失败")?; + log::trace!("收到事件: {:?}", event); + + match event { + Event::Key(key) => { + let action = self.ui.handle_key_event(key); + + match action { + crate::ui::KeyAction::Quit => { + self.should_quit = true; + break; + } + crate::ui::KeyAction::SendMessage => { + if self.ui.state == AppState::Chat { + self.send_message().await?; + } + } + crate::ui::KeyAction::ClearChat => { + if self.ui.state == AppState::Chat { + self.clear_chat(); + } + } + crate::ui::KeyAction::ShowHelp => { + if self.ui.state == AppState::Chat { + self.show_help(); + } + } + crate::ui::KeyAction::DumpChats => { + if self.ui.state == AppState::Chat { + self.dump_chats(); + } + } + crate::ui::KeyAction::Continue => {} + } + } + Event::Mouse(mouse) => { + let size = terminal.size()?; + self.ui + .handle_mouse_event(mouse, Rect::new(0, 0, size.width, size.height)); + } + _ => {} + } } - } - } - /// 完成流式回复 - pub fn complete_streaming_response(&mut self) { - if let Some((user_input, full_response)) = self.current_streaming_response.take() { - self.add_conversation(user_input, full_response); + if self.should_quit { + break; + } } - self.is_processing = false; - } - /// 获取当前显示的对话(包括正在流式生成的) - pub fn get_display_conversations(&self) -> Vec<(String, String, Option>)> { - let mut conversations: Vec<(String, String, Option>)> = self.conversations - .iter() - .map(|(user, assistant, timestamp)| (user.clone(), assistant.clone(), Some(*timestamp))) - .collect(); - - // 如果有正在流式生成的回复,添加到显示列表(没有时间戳) - if let Some((ref user_input, ref partial_response)) = self.current_streaming_response { - conversations.push((user_input.clone(), partial_response.clone(), None)); - } - - conversations - } + disable_raw_mode().context("无法禁用原始模式")?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ) + .context("无法恢复终端")?; - /// 在光标位置插入字符 - pub fn insert_char_at_cursor(&mut self, c: char) { - // 将光标位置转换为字节索引 - let byte_pos = self - .current_input - .chars() - .take(self.cursor_position) - .map(|ch| ch.len_utf8()) - .sum(); - - self.current_input.insert(byte_pos, c); - self.cursor_position += 1; + terminal.show_cursor().context("无法显示光标")?; + + log::info!("应用程序退出"); + Ok(()) } - /// 在光标位置删除字符(退格键) - pub fn delete_char_at_cursor(&mut self) { - if self.cursor_position > 0 { - // 将光标位置转换为字节索引 - let chars: Vec = self.current_input.chars().collect(); - if self.cursor_position <= chars.len() { - // 找到要删除字符的字节范围 - let byte_start: usize = chars - .iter() - .take(self.cursor_position - 1) - .map(|ch| ch.len_utf8()) - .sum(); - - let byte_end: usize = chars - .iter() - .take(self.cursor_position) - .map(|ch| ch.len_utf8()) - .sum(); - - // 安全地删除字符 - self.current_input.drain(byte_start..byte_end); - self.cursor_position -= 1; + /// 更新日志 + fn update_logs(&mut self) { + match self.log_manager.read_logs(1000) { + Ok(logs) => { + self.ui.log_lines = logs; + } + Err(e) => { + log::error!("读取日志失败: {}", e); } } } - /// 将光标向左移动一个字符 - pub fn move_cursor_left(&mut self) { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } + /// 发送消息 + async fn send_message(&mut self) -> Result<()> { + let input_text = self.ui.get_input_text(); + let input_text = input_text.trim(); + + log::debug!("准备发送消息,长度: {}", input_text.len()); - /// 将光标向右移动一个字符 - pub fn move_cursor_right(&mut self) { - let input_len = self.current_input.chars().count(); - if self.cursor_position < input_len { - self.cursor_position += 1; + if input_text.is_empty() { + log::debug!("消息为空,忽略"); + return Ok(()); } - } - /// 重置光标位置到末尾 - pub fn reset_cursor_to_end(&mut self) { - self.cursor_position = self.current_input.chars().count(); - } + // 检查是否是命令 + if let Some(command_action) = self.ui.parse_and_execute_command(input_text) { + self.ui.clear_input(); - /// 滚动到日志底部(最新日志) - pub fn scroll_logs_to_bottom(&mut self) { - self.log_scroll_offset = 0; - } + match command_action { + crate::ui::KeyAction::Quit => { + self.should_quit = true; + } + crate::ui::KeyAction::ClearChat => { + self.clear_chat(); + } + crate::ui::KeyAction::ShowHelp => { + self.show_help(); + } + crate::ui::KeyAction::DumpChats => { + self.dump_chats(); + } + _ => {} + } + return Ok(()); + } - /// 滚动到对话底部(最新对话) - pub fn scroll_conversations_to_bottom(&mut self) { - self.conversation_scroll_offset = 0; - } + // 检查是否刚进入聊天模式 + if self.current_bot.is_none() { + if let Some(bot) = self.ui.selected_bot() { + self.current_bot = Some(bot.clone()); + + // 如果有基础设施,创建真实的带记忆的 Agent + if let Some(infrastructure) = &self.infrastructure { + let memory_tool_config = cortex_mem_rig::tool::MemoryToolConfig { + default_user_id: Some(self.user_id.clone()), + ..Default::default() + }; + + match create_memory_agent( + infrastructure.memory_manager().clone(), + memory_tool_config, + infrastructure.config(), + ).await { + Ok(rig_agent) => { + self.rig_agent = Some(rig_agent); + log::info!("已创建带记忆功能的真实 Agent"); + } + Err(e) => { + log::error!("创建真实 Agent 失败,使用 Mock Agent: {}", e); + } + } + } - /// 向前滚动日志(查看更早日志) - pub fn scroll_logs_forward(&mut self) { - if self.logs.is_empty() { - return; + log::info!("选择机器人: {}", bot.name); + } else { + log::warn!("没有选中的机器人"); + return Ok(()); + } } - let page_size = 10; // 每次翻页的行数 + // 添加用户消息 + let user_message = ChatMessage::user(input_text); + self.ui.messages.push(user_message.clone()); + self.ui.clear_input(); + + // 用户发送新消息,重新启用自动滚动 + self.ui.auto_scroll = true; + + log::info!("用户发送消息: {}", input_text); + log::debug!("当前消息总数: {}", self.ui.messages.len()); + + // 使用真实的带记忆的 Agent 或 Mock Agent + if let Some(rig_agent) = &self.rig_agent { + // 使用真实 Agent 进行流式响应 + let current_conversations: Vec<(String, String)> = self.ui.messages + .iter() + .filter_map(|msg| match msg.role { + crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), + crate::agent::MessageRole::Assistant => { + if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { + Some((last.content.clone(), msg.content.clone())) + } else { + None + } + } + _ => None + }) + .collect(); + + let user_info_clone = self.user_info.clone(); + let infrastructure_clone = self.infrastructure.clone(); + let rig_agent_clone = rig_agent.clone(); + let msg_tx = self.message_sender.clone(); + let user_input = input_text.to_string(); + let user_id = self.user_id.clone(); + let user_input_for_stream = user_input.clone(); + + tokio::spawn(async move { + let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::(); + + let generation_task = tokio::spawn(async move { + agent_reply_with_memory_retrieval_streaming( + &rig_agent_clone, + infrastructure_clone.unwrap().memory_manager().clone(), + &user_input, + &user_id, + user_info_clone.as_deref(), + ¤t_conversations, + stream_tx, + ).await + }); + + while let Some(chunk) = stream_rx.recv().await { + if let Err(_) = msg_tx.send(AppMessage::StreamingChunk { + user: user_input_for_stream.clone(), + chunk, + }) { + break; + } + } - // 简单增加偏移量,让UI层处理边界 - self.log_scroll_offset += page_size; - self.user_scrolled_logs = true; - } + match generation_task.await { + Ok(Ok(full_response)) => { + let _ = msg_tx.send(AppMessage::StreamingComplete { + user: user_input_for_stream.clone(), + full_response, + }); + } + Ok(Err(e)) => { + log::error!("生成回复失败: {}", e); + } + Err(e) => { + log::error!("任务执行失败: {}", e); + } + } + }); - /// 向后滚动日志(查看更新日志) - pub fn scroll_logs_backward(&mut self) { - if self.logs.is_empty() { - return; + } else { + log::warn!("Agent 未初始化"); } - let page_size = 10; // 每次翻页的行数 + // 滚动到底部 - 将在渲染时自动计算 + self.ui.auto_scroll = true; - // 向后翻页(减少偏移量,查看更新的日志) - if self.log_scroll_offset >= page_size { - self.log_scroll_offset -= page_size; - } else { - self.log_scroll_offset = 0; - self.user_scrolled_logs = false; - } + Ok(()) } - /// 向前滚动对话(查看更早内容) - pub fn scroll_conversations_forward(&mut self) { - if self.conversations.is_empty() { - return; - } - - let page_size = 5; // 每次翻页的行数 + /// 清空会话 + fn clear_chat(&mut self) { + log::info!("清空会话"); + self.ui.messages.clear(); + self.ui.scroll_offset = 0; + self.ui.auto_scroll = true; + } - // 简单增加偏移量,让UI层处理边界 - self.conversation_scroll_offset += page_size; - self.user_scrolled_conversations = true; + /// 显示帮助信息 + fn show_help(&mut self) { + log::info!("显示帮助信息"); + let help_message = ChatMessage::assistant(AppUi::get_help_message()); + self.ui.messages.push(help_message); + self.ui.auto_scroll = true; } - /// 向后滚动对话(查看更新内容) - pub fn scroll_conversations_backward(&mut self) { - if self.conversations.is_empty() { - return; + /// 导出会话到剪贴板 + fn dump_chats(&mut self) { + match self.ui.dump_chats_to_clipboard() { + Ok(msg) => { + log::info!("{}", msg); + let success_message = ChatMessage::assistant(msg); + self.ui.messages.push(success_message); + } + Err(e) => { + log::error!("{}", e); + let error_message = ChatMessage::assistant(format!("❌ {}", e)); + self.ui.messages.push(error_message); + } } + self.ui.auto_scroll = true; + } - let page_size = 5; // 每次翻页的行数 - - // 向后翻页(减少偏移量,查看更新的内容) - if self.conversation_scroll_offset >= page_size { - self.conversation_scroll_offset -= page_size; - } else { - self.conversation_scroll_offset = 0; - self.user_scrolled_conversations = false; + /// 退出时保存对话到记忆系统 + pub async fn save_conversations_to_memory(&self) -> Result<()> { + if let Some(infrastructure) = &self.infrastructure { + let conversations: Vec<(String, String)> = self.ui.messages + .iter() + .filter_map(|msg| match msg.role { + crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), + crate::agent::MessageRole::Assistant => { + if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { + Some((last.content.clone(), msg.content.clone())) + } else { + None + } + }, + _ => None + }) + .filter(|(user, assistant)| !user.is_empty() && !assistant.is_empty()) + .collect(); + + if !conversations.is_empty() { + log::info!("正在保存 {} 条对话到记忆系统...", conversations.len()); + store_conversations_batch( + infrastructure.memory_manager().clone(), + &conversations, + &self.user_id, + ).await.map_err(|e| anyhow::anyhow!("保存对话到记忆系统失败: {}", e))?; + log::info!("对话保存完成"); + } } + Ok(()) } - /// 切换焦点到下一个区域 - pub fn next_focus(&mut self) { - self.focus_area = match self.focus_area { - FocusArea::Input => { - if self.is_shutting_down { - // 在退出过程中,跳过输入框,直接到对话区域 - FocusArea::Conversation - } else { - FocusArea::Conversation - } - } - FocusArea::Conversation => { - if self.is_shutting_down { - // 在退出过程中,从对话区域切换到日志区域 - FocusArea::Logs - } else { - FocusArea::Logs - } - } - FocusArea::Logs => { - if self.is_shutting_down { - // 在退出过程中,从日志区域切换回对话区域 - FocusArea::Conversation - } else { - FocusArea::Input - } - } - }; + /// 获取所有对话 + pub fn get_conversations(&self) -> Vec<(String, String)> { + self.ui.messages + .iter() + .filter_map(|msg| match msg.role { + crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), + crate::agent::MessageRole::Assistant => { + if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { + Some((last.content.clone(), msg.content.clone())) + } else { + None + } + }, + _ => None, + }) + .collect() } - pub fn log_info(&self, message: &str) { - if let Some(sender) = &self.message_sender { - let _ = sender.send(AppMessage::Log(format!("[INFO] {}", message))); - } + /// 获取用户ID + pub fn get_user_id(&self) -> String { + self.user_id.clone() } } + +/// 创建默认机器人 +pub fn create_default_bots(config_manager: &ConfigManager) -> Result<()> { + let bots = config_manager.get_bots()?; + + if bots.is_empty() { + // 创建默认机器人 + let default_bot = BotConfig::new( + "助手", + "你是一个有用的 AI 助手,能够回答各种问题并提供帮助。", + "password", + ); + config_manager.add_bot(default_bot)?; + + let coder_bot = BotConfig::new( + "程序员", + "你是一个经验丰富的程序员,精通多种编程语言,能够帮助解决编程问题。", + "password", + ); + config_manager.add_bot(coder_bot)?; + + log::info!("已创建默认机器人"); + } + + Ok(()) +} diff --git a/examples/cortex-mem-tars-new/src/config.rs b/examples/cortex-mem-tars/src/config.rs similarity index 94% rename from examples/cortex-mem-tars-new/src/config.rs rename to examples/cortex-mem-tars/src/config.rs index 1c2bc30..79307af 100644 --- a/examples/cortex-mem-tars-new/src/config.rs +++ b/examples/cortex-mem-tars/src/config.rs @@ -141,6 +141,7 @@ impl ConfigManager { } /// 删除机器人 + #[allow(dead_code)] pub fn remove_bot(&self, bot_id: &str) -> Result { let mut bots = self.get_bots()?; let original_len = bots.len(); @@ -156,6 +157,7 @@ impl ConfigManager { } /// 更新机器人 + #[allow(dead_code)] pub fn update_bot(&self, bot_id: &str, updated_bot: BotConfig) -> Result { let bot_name = updated_bot.name.clone(); let mut bots = self.get_bots()?; @@ -170,12 +172,14 @@ impl ConfigManager { } /// 根据 ID 获取机器人 + #[allow(dead_code)] pub fn get_bot(&self, bot_id: &str) -> Result> { let bots = self.get_bots()?; Ok(bots.into_iter().find(|bot| bot.id == bot_id)) } /// 获取配置目录路径 + #[allow(dead_code)] pub fn config_dir(&self) -> &Path { &self.config_dir } @@ -191,17 +195,3 @@ impl Default for ConfigManager { Self::new().expect("无法初始化配置管理器") } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_bot_config_creation() { - let bot = BotConfig::new("TestBot", "You are a helpful assistant", "password123"); - assert_eq!(bot.name, "TestBot"); - assert_eq!(bot.system_prompt, "You are a helpful assistant"); - assert_eq!(bot.access_password, "password123"); - assert!(!bot.id.is_empty()); - } -} \ No newline at end of file diff --git a/examples/cortex-mem-tars/src/events.rs b/examples/cortex-mem-tars/src/events.rs deleted file mode 100644 index 443a3a8..0000000 --- a/examples/cortex-mem-tars/src/events.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::app::{App, FocusArea}; -use crossterm::event::{Event, KeyCode, KeyEventKind, MouseButton, MouseEvent, MouseEventKind}; - -pub fn handle_key_event(event: Event, app: &mut App) -> Option { - // 处理鼠标事件 - if let Event::Mouse(mouse) = event { - return handle_mouse_event(mouse, app); - } - - // Some(input)表示需要处理的输入,None表示不需要处理 - if let Event::Key(key) = event { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Enter => { - if app.focus_area == FocusArea::Input && !app.current_input.trim().is_empty() { - let input = app.current_input.clone(); - app.current_input.clear(); - app.reset_cursor_to_end(); - app.is_processing = true; - Some(input) // 返回输入内容给上层处理 - } else { - None - } - } - KeyCode::Char(c) => { - if !app.is_processing - && !app.is_shutting_down - && app.focus_area == FocusArea::Input - { - app.insert_char_at_cursor(c); - } - None - } - KeyCode::Backspace => { - if !app.is_processing - && !app.is_shutting_down - && app.focus_area == FocusArea::Input - { - app.delete_char_at_cursor(); - } - None - } - KeyCode::Left => { - if !app.is_processing - && !app.is_shutting_down - && app.focus_area == FocusArea::Input - { - app.move_cursor_left(); - } - None - } - KeyCode::Right => { - if !app.is_processing - && !app.is_shutting_down - && app.focus_area == FocusArea::Input - { - app.move_cursor_right(); - } - None - } - KeyCode::Up => { - // 上键:向后滚动(查看更新内容) - match app.focus_area { - FocusArea::Logs => { - app.scroll_logs_backward(); - } - FocusArea::Conversation => { - app.scroll_conversations_backward(); - } - FocusArea::Input => {} - } - None - } - KeyCode::Down => { - // 下键:向前滚动(查看更早内容) - match app.focus_area { - FocusArea::Logs => { - app.scroll_logs_forward(); - } - FocusArea::Conversation => { - app.scroll_conversations_forward(); - } - FocusArea::Input => {} - } - None - } - KeyCode::Tab => { - // 切换焦点 - let _old_focus = app.focus_area; - app.next_focus(); - None - } - KeyCode::Home => { - match app.focus_area { - FocusArea::Logs => { - // 滚动到最旧的日志(设置一个较大的偏移量) - app.log_scroll_offset = app.logs.len().saturating_sub(1); - app.user_scrolled_logs = true; - } - FocusArea::Conversation => { - // 滚动到最旧的对话(设置一个较大的偏移量) - let total_lines = app.conversations.len() * 3; - app.conversation_scroll_offset = total_lines.saturating_sub(1); - app.user_scrolled_conversations = true; - } - FocusArea::Input => { - // 将光标移动到输入框开头 - app.cursor_position = 0; - } - } - None - } - KeyCode::End => { - match app.focus_area { - FocusArea::Logs => { - // 滚动到最新的日志 - app.scroll_logs_to_bottom(); - } - FocusArea::Conversation => { - // 滚动到最新的对话 - app.scroll_conversations_to_bottom(); - } - FocusArea::Input => { - // 将光标移动到输入框末尾 - app.reset_cursor_to_end(); - } - } - None - } - KeyCode::Esc => { - app.should_quit = true; - app.is_shutting_down = true; - Some("/quit".to_string()) // 模拟quit命令 - } - _ => None, - } - } else { - None - } - } else { - None - } -} - -/// 处理鼠标事件 -fn handle_mouse_event(mouse: MouseEvent, app: &mut App) -> Option { - match mouse.kind { - MouseEventKind::Down(MouseButton::Left) => { - // 左键点击时更新焦点区域 - // 这里可以根据鼠标位置判断点击了哪个区域 - // 简化处理:如果鼠标在左边区域,设置为输入或对话焦点;如果在右边区域,设置为日志焦点 - // 由于我们没有详细的坐标信息,这里只是简化处理 - None - } - MouseEventKind::ScrollUp => { - // 鼠标向上滚动 - match app.focus_area { - FocusArea::Logs => { - app.scroll_logs_backward(); - } - FocusArea::Conversation => { - app.scroll_conversations_backward(); - } - FocusArea::Input => {} - } - None - } - MouseEventKind::ScrollDown => { - // 鼠标向下滚动 - match app.focus_area { - FocusArea::Logs => { - app.scroll_logs_forward(); - } - FocusArea::Conversation => { - app.scroll_conversations_forward(); - } - FocusArea::Input => {} - } - None - } - MouseEventKind::Drag(MouseButton::Left) => { - // 鼠标左键拖拽 - 这里我们不需要特别处理,终端默认支持文本选择 - None - } - _ => None, - } -} - -pub fn process_user_input(input: String, app: &mut App) -> bool { - // true表示是quit命令,false表示普通输入 - // 检查是否为退出命令 - let is_quit = input.trim() == "/quit"; - if is_quit { - app.should_quit = true; - } - is_quit -} diff --git a/examples/cortex-mem-tars-new/src/infrastructure.rs b/examples/cortex-mem-tars/src/infrastructure.rs similarity index 100% rename from examples/cortex-mem-tars-new/src/infrastructure.rs rename to examples/cortex-mem-tars/src/infrastructure.rs diff --git a/examples/cortex-mem-tars-new/src/lib.rs b/examples/cortex-mem-tars/src/lib.rs similarity index 100% rename from examples/cortex-mem-tars-new/src/lib.rs rename to examples/cortex-mem-tars/src/lib.rs diff --git a/examples/cortex-mem-tars/src/log_monitor.rs b/examples/cortex-mem-tars/src/log_monitor.rs deleted file mode 100644 index 4cda7e0..0000000 --- a/examples/cortex-mem-tars/src/log_monitor.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::fs::File; -use std::io::{BufRead, BufReader, Seek, SeekFrom}; -use std::path::{Path, PathBuf}; -use std::time::Duration; -use tokio::time::sleep; - -/// 日志文件监听器 -pub struct LogFileMonitor { - log_file_path: Option, - last_position: u64, -} - -impl LogFileMonitor { - /// 创建新的日志文件监听器 - pub fn new() -> Self { - Self { - log_file_path: None, - last_position: 0, - } - } - - /// 查找最新的日志文件 - pub async fn find_latest_log_file(&mut self, log_dir: &str) -> Result<(), Box> { - let log_path = Path::new(log_dir); - - if !log_path.exists() { - return Err("日志目录不存在".into()); - } - - let mut latest_file = None; - let mut latest_time = std::time::UNIX_EPOCH; - - if let Ok(entries) = std::fs::read_dir(log_path) { - for entry in entries.flatten() { - if let Ok(metadata) = entry.metadata() { - if let Ok(modified) = metadata.modified() { - if modified > latest_time && entry.file_name().to_string_lossy().ends_with(".log") { - latest_time = modified; - latest_file = Some(entry.path()); - } - } - } - } - } - - if let Some(log_file) = latest_file { - self.log_file_path = Some(log_file); - // 设置初始位置为文件末尾,只读取新增内容 - if let Ok(file) = File::open(self.log_file_path.as_ref().unwrap()) { - if let Ok(metadata) = file.metadata() { - self.last_position = metadata.len(); - } - } - Ok(()) - } else { - Err("未找到日志文件".into()) - } - } - - /// 读取新增的日志内容 - pub fn read_new_logs(&mut self) -> Result, Box> { - let mut new_logs = Vec::new(); - - if let Some(ref log_file_path) = self.log_file_path { - let mut file = File::open(log_file_path)?; - - // 检查文件大小 - let metadata = file.metadata()?; - let current_size = metadata.len(); - - // 如果文件没有新内容,直接返回 - if current_size <= self.last_position { - return Ok(new_logs); - } - - // 移动到上次读取的位置 - file.seek(SeekFrom::Start(self.last_position))?; - - // 读取新内容 - let reader = BufReader::new(file); - for line in reader.lines() { - if let Ok(line) = line { - if !line.trim().is_empty() { - new_logs.push(line); - } - } - } - - // 更新位置 - self.last_position = current_size; - } - - Ok(new_logs) - } - - /// 启动日志监听,持续输出新日志到控制台 - pub async fn start_monitoring(&mut self, log_dir: &str) -> Result<(), Box> { - // 查找最新日志文件 - self.find_latest_log_file(log_dir).await?; - - println!("🔍 开始监听日志文件: {:?}", self.log_file_path); - - loop { - match self.read_new_logs() { - Ok(new_logs) => { - for log_line in new_logs { - // 直接输出到控制台,保持原始格式 - let formatted_log = self.format_log_for_console(&log_line); - println!("{}", formatted_log); - } - } - Err(e) => { - eprintln!("读取日志文件时出错: {}", e); - // 尝试重新查找日志文件(可能有新的日志文件生成) - if let Err(_find_err) = self.find_latest_log_file(log_dir).await { - eprintln!("重新查找日志文件失败"); - } - } - } - - // 短暂休眠,避免过度占用CPU - sleep(Duration::from_millis(100)).await; - } - } - - /// 格式化日志内容用于控制台显示 - fn format_log_for_console(&self, log_line: &str) -> String { - // 解析日志级别并添加颜色 - let colored_line = if log_line.contains(" ERROR ") { - format!("\x1b[91m{}\x1b[0m", log_line) // 亮红色 - } else if log_line.contains(" WARN ") { - format!("\x1b[93m{}\x1b[0m", log_line) // 亮黄色 - } else if log_line.contains(" INFO ") { - format!("\x1b[36m{}\x1b[0m", log_line) // 亮青色 - } else if log_line.contains(" DEBUG ") { - format!("\x1b[94m{}\x1b[0m", log_line) // 亮蓝色 - } else if log_line.contains(" TRACE ") { - format!("\x1b[95m{}\x1b[0m", log_line) // 亮紫色 - } else { - log_line.to_string() // 默认颜色 - }; - - // 添加前缀标识这是来自日志文件的内容 - format!("📋 {}", colored_line) - } -} - -/// 启动日志监听任务(异步) -pub async fn start_log_monitoring_task(log_dir: String) -> Result<(), Box> { - let mut monitor = LogFileMonitor::new(); - monitor.start_monitoring(&log_dir).await -} \ No newline at end of file diff --git a/examples/cortex-mem-tars-new/src/logger.rs b/examples/cortex-mem-tars/src/logger.rs similarity index 89% rename from examples/cortex-mem-tars-new/src/logger.rs rename to examples/cortex-mem-tars/src/logger.rs index 551032b..9d29f7e 100644 --- a/examples/cortex-mem-tars-new/src/logger.rs +++ b/examples/cortex-mem-tars/src/logger.rs @@ -7,6 +7,7 @@ use std::sync::{Arc, Mutex}; /// 日志管理器 pub struct LogManager { + #[allow(dead_code)] log_file: PathBuf, file: Arc>, lines: Arc>>, @@ -71,19 +72,6 @@ impl LogManager { Ok(lines.clone()) } } - - /// 获取日志文件路径 - pub fn log_file_path(&self) -> &Path { - &self.log_file - } - - /// 清空日志 - pub fn clear(&self) -> Result<()> { - File::create(&self.log_file).context("无法清空日志文件")?; - let mut lines = self.lines.lock().map_err(|e| anyhow::anyhow!("无法获取日志行锁: {}", e))?; - lines.clear(); - Ok(()) - } } /// 自定义 Logger @@ -126,4 +114,4 @@ pub fn init_logger(log_dir: &Path) -> Result> { log::info!("日志文件路径: {}", log_dir.display()); Ok(manager) -} \ No newline at end of file +} diff --git a/examples/cortex-mem-tars/src/main.rs b/examples/cortex-mem-tars/src/main.rs index dc2953f..b42b4c8 100644 --- a/examples/cortex-mem-tars/src/main.rs +++ b/examples/cortex-mem-tars/src/main.rs @@ -1,559 +1,123 @@ -use clap::Parser; -use crossterm::{ - event, execute, - terminal::{EnterAlternateScreen, enable_raw_mode}, -}; -use cortex_mem_config::Config; -use cortex_mem_core::init_logging; -use cortex_mem_rig::{ - llm::OpenAILLMClient, memory::manager::MemoryManager, vector_store::qdrant::QdrantVectorStore, -}; -use ratatui::{Terminal, backend::CrosstermBackend}; -use std::{io, path::PathBuf, sync::Arc}; -use tokio::sync::mpsc; -use tokio::time::Duration; - mod agent; mod app; -mod events; -mod log_monitor; -mod terminal; +mod config; +mod infrastructure; +mod logger; mod ui; -use agent::{ - agent_reply_with_memory_retrieval_streaming, create_memory_agent, extract_user_basic_info, - store_conversations_batch, -}; -use app::{App, AppMessage, redirect_log_to_ui, set_global_log_sender}; -use events::{handle_key_event, process_user_input}; -use log_monitor::start_log_monitoring_task; -use terminal::cleanup_terminal_final; -use ui::draw_ui; - -#[derive(Parser)] -#[command(name = "Cortex Memory Tars")] -#[command(about = "A Multi-round interactive conversation with a memory-enabled agent")] -#[command(author = "Sopaco")] -#[command(version)] -struct Cli { - /// Path to the configuration file - #[arg(short, long, default_value = "config.toml")] - config: PathBuf, -} +use anyhow::{Context, Result}; +use app::{create_default_bots, App}; +use config::ConfigManager; +use infrastructure::Infrastructure; +use logger::init_logger; +use std::sync::Arc; #[tokio::main] -async fn main() -> Result<(), Box> { - // 加载基本配置以获取日志设置 - let cli = Cli::parse(); - let config = Config::load(&cli.config)?; - - // 初始化日志系统 - init_logging(&config.logging)?; - - // 设置终端 - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!( - stdout, - EnterAlternateScreen, - crossterm::event::EnableMouseCapture - )?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let result = run_application(&mut terminal).await; - - // 最终清理 - 使用最彻底的方法 - cleanup_terminal_final(&mut terminal); - - result -} - -/// 主应用逻辑 -async fn run_application( - terminal: &mut Terminal>, -) -> Result<(), Box> { - // 创建消息通道 - let (msg_tx, mut msg_rx) = mpsc::unbounded_channel::(); - - // 使用我们的自定义日志系统,禁用tracing - // tracing_subscriber::fmt::init(); - - // 设置全局日志发送器以便我们的日志系统正常工作 - set_global_log_sender(msg_tx.clone()); - - // 初始化组件 - // 配置加载已经在main函数中完成,这里只获取文件路径 - let cli = Cli::parse(); - let config = Config::load(&cli.config)?; - - let llm_client = OpenAILLMClient::new(&config.llm, &config.embedding)?; - let vector_store = QdrantVectorStore::new(&config.qdrant) - .await - .expect("无法连接到Qdrant"); +async fn main() -> Result<()> { + // 解析命令行参数 + let args: Vec = std::env::args().collect(); + let enhance_memory_saver = args.contains(&"--enhance-memory-saver".to_string()); - let memory_config = config.memory.clone(); - let memory_manager = Arc::new(MemoryManager::new( - Box::new(vector_store), - Box::new(llm_client.clone()), - memory_config, - )); - - // 创建带记忆的Agent - let memory_tool_config = cortex_mem_rig::tool::MemoryToolConfig { - default_user_id: Some("demo_user".to_string()), - ..Default::default() - }; - - let agent = create_memory_agent(memory_manager.clone(), memory_tool_config, &config).await?; - - // 初始化用户信息 - let user_id = "demo_user"; - let user_info = extract_user_basic_info(&config, memory_manager.clone(), user_id).await?; - - // 创建应用状态 - let mut app = App::new(msg_tx); - - if let Some(info) = user_info { - app.user_info = Some(info.clone()); - app.log_info("已加载用户基本信息"); - } else { - app.log_info("未找到用户基本信息"); + if enhance_memory_saver { + log::info!("已启用增强记忆保存功能"); } - app.log_info("初始化完成,开始对话..."); + // 初始化配置管理器 + let config_manager = ConfigManager::new().context("无法初始化配置管理器")?; + log::info!("配置管理器初始化成功"); - // 主事件循环 - loop { - // 更新消息(包括在quit过程中收到的所有消息) - while let Ok(msg) = msg_rx.try_recv() { - match msg { - AppMessage::Log(log_msg) => { - app.add_log(log_msg); - } - AppMessage::Conversation { user, assistant } => { - app.add_conversation(user, assistant); - } - AppMessage::StreamingChunk { user, chunk } => { - // 如果是新的用户输入,开始新的流式回复 - if app.current_streaming_response.is_none() || - app.current_streaming_response.as_ref().map(|(u, _)| u != &user).unwrap_or(false) { - app.start_streaming_response(user); - } - app.add_streaming_chunk(chunk); - } - AppMessage::StreamingComplete { user: _, full_response: _ } => { - app.complete_streaming_response(); - } - AppMessage::MemoryIterationCompleted => { - app.memory_iteration_completed = true; - app.should_quit = true; - } - } - } - - // 绘制UI - terminal.draw(|f| draw_ui(f, &mut app))?; - - // 处理事件 - if event::poll(std::time::Duration::from_millis(100))? { - if let Some(input) = handle_key_event(event::read()?, &mut app) { - // 先检查是否是quit命令 - let is_quit = process_user_input(input.clone(), &mut app); - - // 如果是quit命令,先添加到对话历史 - if is_quit { - app.add_conversation(input.clone(), "正在执行退出命令...".to_string()); - } - - if is_quit { - // 立即退出到terminal,后台执行记忆化任务 - let conversations_vec: Vec<(String, String)> = - app.conversations.iter().map(|(user, assistant, _)| (user.clone(), assistant.clone())).collect(); - handle_quit_async( - terminal, - &mut app, - &conversations_vec, - &memory_manager, - user_id, - ) - .await?; - - // 退出主循环 - break; - } else { - // 记录用户输入 - redirect_log_to_ui("INFO", &format!("接收用户输入: {}", input)); - - // 处理用户输入 - let agent_clone = agent.clone(); - let memory_manager_clone = memory_manager.clone(); - let config_clone = config.clone(); - let user_info_clone = app.user_info.clone(); - let user_id_clone = user_id.to_string(); - let msg_tx_clone = app.message_sender.clone(); - - // 获取当前对话历史的引用(转换为slice) - let current_conversations: Vec<(String, String)> = - app.conversations.iter().map(|(user, assistant, _)| (user.clone(), assistant.clone())).collect(); - - // 记录开始处理 - redirect_log_to_ui("INFO", "开始处理用户请求..."); - - tokio::spawn(async move { - // 创建流式通道 - let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::(); - - // 启动流式处理任务 - let agent_clone2 = agent_clone.clone(); - let memory_manager_clone2 = memory_manager_clone.clone(); - let config_clone2 = config_clone.clone(); - let user_info_clone2 = user_info_clone.clone(); - let user_id_clone2 = user_id_clone.clone(); - let input_clone = input.clone(); - let current_conversations_clone = current_conversations.clone(); - - let generation_task = tokio::spawn(async move { - agent_reply_with_memory_retrieval_streaming( - &agent_clone2, - memory_manager_clone2, - &input_clone, - &user_id_clone2, - user_info_clone2.as_deref(), - ¤t_conversations_clone, - stream_tx, - ) - .await - }); - - // 处理流式内容 - while let Some(chunk) = stream_rx.recv().await { - if let Some(sender) = &msg_tx_clone { - let _ = sender.send(AppMessage::StreamingChunk { - user: input.clone(), - chunk, - }); - } - } - - // 等待生成任务完成 - match generation_task.await { - Ok(Ok(full_response)) => { - // 发送完成消息 - if let Some(sender) = &msg_tx_clone { - let _ = sender.send(AppMessage::StreamingComplete { - user: input.clone(), - full_response: full_response.clone(), - }); - redirect_log_to_ui("INFO", &format!("生成回复完成: {}", full_response)); - } - } - Ok(Err(e)) => { - let error_msg = format!("抱歉,我遇到了一些技术问题: {}", e); - redirect_log_to_ui("ERROR", &error_msg); - // 完成流式回复(即使出错也要清理状态) - if let Some(sender) = &msg_tx_clone { - let _ = sender.send(AppMessage::StreamingComplete { - user: input.clone(), - full_response: error_msg, - }); - } - } - Err(e) => { - let error_msg = format!("任务执行失败: {}", e); - redirect_log_to_ui("ERROR", &error_msg); - // 完成流式回复(即使出错也要清理状态) - if let Some(sender) = &msg_tx_clone { - let _ = sender.send(AppMessage::StreamingComplete { - user: input.clone(), - full_response: error_msg, - }); - } - } - } - }); - } - } - } + // 初始化日志系统 + let log_manager = init_logger(config_manager.config_dir()).context("无法初始化日志系统")?; + log::info!("日志系统初始化成功"); - // 检查是否有新的对话结果 - app.is_processing = false; + // 创建默认机器人 + create_default_bots(&config_manager).context("无法创建默认机器人")?; - // 只有在没有在shutting down状态或者记忆化已完成时才能退出 - if app.should_quit && app.memory_iteration_completed { - break; + // 初始化基础设施(LLM 客户端、向量存储、记忆管理器) + let infrastructure = match Infrastructure::new(config_manager.cortex_config().clone()).await { + Ok(inf) => { + log::info!("基础设施初始化成功"); + Some(Arc::new(inf)) } - - // **在quit过程中处理剩余的日志消息但不退出** - if app.is_shutting_down && !app.memory_iteration_completed { - // **立即处理所有待处理的日志消息** - while let Ok(msg) = msg_rx.try_recv() { - match msg { - AppMessage::Log(log_msg) => { - app.add_log(log_msg); - } - AppMessage::Conversation { user, assistant } => { - app.add_conversation(user, assistant); - } - AppMessage::StreamingChunk { user, chunk } => { - // 如果是新的用户输入,开始新的流式回复 - if app.current_streaming_response.is_none() || - app.current_streaming_response.as_ref().map(|(u, _)| u != &user).unwrap_or(false) { - app.start_streaming_response(user); - } - app.add_streaming_chunk(chunk); - } - AppMessage::StreamingComplete { user: _, full_response: _ } => { - app.complete_streaming_response(); - } - AppMessage::MemoryIterationCompleted => { - app.memory_iteration_completed = true; - app.should_quit = true; - break; - } - } - } - - // 在shutting down期间立即刷新UI显示最新日志 - if let Err(e) = terminal.draw(|f| draw_ui(f, &mut app)) { - eprintln!("UI绘制错误: {}", e); - } - - // 在shutting down期间添加短暂延迟,让用户能看到日志更新 - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + Err(e) => { + log::warn!("基础设施初始化失败,将使用 Mock Agent: {}", e); + None } - } - - println!("Cortex TARS powering down. Goodbye!"); - Ok(()) -} - -/// 异步处理退出逻辑,立即退出TUI到terminal -async fn handle_quit_async( - _terminal: &mut Terminal>, - app: &mut App, - conversations: &Vec<(String, String)>, - memory_manager: &Arc, - user_id: &str, -) -> Result<(), Box> { - use crossterm::cursor::{MoveTo, Show}; - use crossterm::style::{ - Attribute, Color, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor, }; - use crossterm::{ - event::DisableMouseCapture, - execute, - terminal::{Clear, ClearType, LeaveAlternateScreen}, - }; - use std::io::{Write, stdout}; - - // 记录退出命令到UI - redirect_log_to_ui("INFO", "🚀 用户输入退出命令 /quit,开始后台记忆化..."); - - // 先获取所有日志内容 - let all_logs: Vec = app.logs.iter().cloned().collect(); - - // 彻底清理terminal状态 - let mut stdout = stdout(); - - // 执行完整的terminal重置序列 - execute!(&mut stdout, ResetColor)?; - execute!(&mut stdout, Clear(ClearType::All))?; - execute!(&mut stdout, MoveTo(0, 0))?; - execute!(&mut stdout, Show)?; - execute!(&mut stdout, LeaveAlternateScreen)?; - execute!(&mut stdout, DisableMouseCapture)?; - execute!(&mut stdout, SetAttribute(Attribute::Reset))?; - execute!(&mut stdout, SetForegroundColor(Color::Reset))?; - execute!(&mut stdout, SetBackgroundColor(Color::Reset))?; - - // 禁用原始模式 - let _ = crossterm::terminal::disable_raw_mode(); - // 刷新输出确保清理完成 - stdout.flush()?; - - // 输出分隔线 - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ 🧠 Cortex Memory - 退出流程 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); - - // 显示会话摘要 - println!("📋 会话摘要:"); - println!(" • 对话轮次: {} 轮", conversations.len()); - println!(" • 用户ID: {}", user_id); - - // 显示最近的日志(如果有) - if !all_logs.is_empty() { - println!("\n📜 最近的操作日志:"); - let recent_logs = if all_logs.len() > 10 { - &all_logs[all_logs.len() - 10..] - } else { - &all_logs[..] - }; - - println!(" {}", "─".repeat(70)); - for (i, log) in recent_logs.iter().enumerate() { - let beautified_content = beautify_log_content(log); - - // 添加日志条目编号 - if i > 0 { - println!(" {}", "─".repeat(70)); + // 创建并运行应用 + let mut app = App::new( + config_manager, + log_manager, + infrastructure.clone(), + ).context("无法创建应用")?; + log::info!("应用创建成功"); + + // 检查服务可用性 + app.check_service_status().await.context("无法检查服务状态")?; + + // 加载用户基本信息 + app.load_user_info().await.context("无法加载用户信息")?; + + // 运行应用 + app.run().await.context("应用运行失败")?; + + // 退出时保存对话到记忆系统(仅在启用增强记忆保存功能时) + if enhance_memory_saver { + if let Some(_inf) = infrastructure { + println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ 🧠 Cortex Memory - 退出流程 ║"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + + log::info!("🚀 开始退出流程,准备保存对话到记忆系统..."); + + let conversations = app.get_conversations(); + let user_id = app.get_user_id(); + + println!("📋 会话摘要:"); + println!(" • 对话轮次: {} 轮", conversations.len()); + println!(" • 用户ID: {}", user_id); + + if conversations.is_empty() { + println!("⚠️ 没有需要存储的内容"); + println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ ✅ 退出流程完成 ║"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + println!("👋 Cortex TARS powering down. Goodbye!"); + return Ok(()); } - // 显示美化后的内容,支持多行显示 - let lines: Vec<&str> = beautified_content.split('\n').collect(); - for (line_i, line) in lines.iter().enumerate() { - if line_i == 0 { - // 第一行显示编号和完整内容 - let colored_line = get_log_level_color(log, line); - println!(" {}", colored_line); - } else { - // 后续行添加缩进 - println!(" │ {}", line); + println!("\n🧠 开始执行记忆化存储..."); + println!("📝 正在保存 {} 条对话记录到记忆库...", conversations.len()); + println!("🚀 开始存储对话到记忆系统..."); + + match app.save_conversations_to_memory().await { + Ok(_) => { + println!("✨ 记忆化完成!"); + println!("✅ 所有对话已成功存储到记忆系统"); + println!("🔍 存储详情:"); + println!(" • 对话轮次: {} 轮", conversations.len()); + println!(" • 用户消息: {} 条", conversations.len()); + println!(" • 助手消息: {} 条", conversations.len()); + } + Err(e) => { + println!("❌ 记忆存储失败: {}", e); + println!("⚠️ 虽然记忆化失败,但仍正常退出"); } } - } - if all_logs.len() > 10 { - println!(" {}", "─".repeat(70)); - println!(" ... (显示最近10条,共{}条)", all_logs.len()); - } - } - - println!("\n🧠 开始执行记忆化存储..."); - - // 准备对话数据(过滤quit命令) - let mut valid_conversations = Vec::new(); - for (user_msg, assistant_msg) in conversations { - let user_msg_trimmed = user_msg.trim().to_lowercase(); - if user_msg_trimmed == "quit" - || user_msg_trimmed == "exit" - || user_msg_trimmed == "/quit" - || user_msg_trimmed == "/exit" - { - continue; - } - valid_conversations.push((user_msg.clone(), assistant_msg.clone())); - } - - if valid_conversations.is_empty() { - println!("⚠️ 没有需要存储的内容"); - println!( - "\n╔══════════════════════════════════════════════════════════════════════════════╗" - ); - println!( - "║ ✅ 退出流程完成 ║" - ); - println!( - "╚══════════════════════════════════════════════════════════════════════════════╝" - ); - println!("👋 感谢使用Cortex Memory!"); - return Ok(()); - } - - // 只有在有内容需要存储时才启动日志监听任务 - let log_dir = "logs".to_string(); - let log_monitoring_handle = tokio::spawn(async move { - if let Err(e) = start_log_monitoring_task(log_dir).await { - eprintln!("日志监听任务失败: {}", e); - } - }); - - println!( - "📝 正在保存 {} 条对话记录到记忆库...", - valid_conversations.len() - ); - println!("🚀 开始存储对话到记忆系统..."); - - // 执行批量记忆化 - match store_conversations_batch(memory_manager.clone(), &valid_conversations, user_id).await { - Ok(_) => { - println!("✨ 记忆化完成!"); - println!("✅ 所有对话已成功存储到记忆系统"); - println!("🔍 存储详情:"); - println!(" • 对话轮次: {} 轮", valid_conversations.len()); - println!(" • 用户消息: {} 条", valid_conversations.len()); - println!(" • 助手消息: {} 条", valid_conversations.len()); - } - Err(e) => { - println!("❌ 记忆存储失败: {}", e); - println!("⚠️ 虽然记忆化失败,但仍正常退出"); - } - } - - // 停止日志监听任务 - log_monitoring_handle.abort(); - - tokio::time::sleep(Duration::from_secs(3)).await; - - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ 🎉 退出流程完成 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); - println!("👋 感谢使用Cortex Memory!"); - - Ok(()) -} - -/// 美化日志内容显示 -fn beautify_log_content(log_line: &str) -> String { - // 过滤掉时间戳前缀,保持简洁 - let content = if let Some(content_start) = log_line.find("] ") { - &log_line[content_start + 2..] - } else { - log_line - }; - - // 判断是否为JSON内容 - let trimmed_content = content.trim(); - let is_json = trimmed_content.starts_with('{') && trimmed_content.ends_with('}'); - - if is_json { - // 尝试美化JSON,保留完整内容 - match prettify_json(trimmed_content) { - Ok(formatted_json) => { - // 如果格式化成功,返回完整的带缩进的JSON - formatted_json - } - Err(_) => { - // 如果JSON格式化失败,返回原始内容 - content.to_string() - } - } - } else { - // 非JSON内容,保持原样 - content.to_string() - } -} - -/// 美化JSON内容 -fn prettify_json(json_str: &str) -> Result> { - use serde_json::Value; - let value: Value = serde_json::from_str(json_str)?; - Ok(serde_json::to_string_pretty(&value)?) -} - -/// 根据日志级别返回带颜色的文本 -fn get_log_level_color(log_line: &str, text: &str) -> String { - let log_level = if let Some(level_start) = log_line.find("[") { - if let Some(level_end) = log_line[level_start..].find("]") { - &log_line[level_start + 1..level_start + level_end] + println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ 🎉 退出流程完成 ║"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + println!("👋 Cortex TARS powering down. Goodbye!"); } else { - "UNKNOWN" + println!("\n⚠️ 基础设施未初始化,无法保存对话到记忆系统"); + println!("👋 Cortex TARS powering down. Goodbye!"); } } else { - "UNKNOWN" - }; - - // ANSI颜色代码 - let (color_code, reset_code) = match log_level.to_uppercase().as_str() { - "ERROR" => ("\x1b[91m", "\x1b[0m"), // 亮红色 - "WARN" | "WARNING" => ("\x1b[93m", "\x1b[0m"), // 亮黄色 - "INFO" => ("\x1b[36m", "\x1b[0m"), // 亮青色 - "DEBUG" => ("\x1b[94m", "\x1b[0m"), // 亮蓝色 - "TRACE" => ("\x1b[95m", "\x1b[0m"), // 亮紫色 - _ => ("\x1b[0m", "\x1b[0m"), // 白色 - }; + log::info!("未启用增强记忆保存功能,跳过对话保存"); + println!("\n👋 Cortex TARS powering down. Goodbye!"); + } - format!("{}{}{}", color_code, text, reset_code) + Ok(()) } diff --git a/examples/cortex-mem-tars/src/terminal.rs b/examples/cortex-mem-tars/src/terminal.rs deleted file mode 100644 index 75de17f..0000000 --- a/examples/cortex-mem-tars/src/terminal.rs +++ /dev/null @@ -1,28 +0,0 @@ -// use crossterm::execute; -// use std::io::Write; - -/// 终极终端清理函数 -pub fn cleanup_terminal_final(_terminal: &mut ratatui::Terminal>) { - // 直接使用标准输出流进行最彻底的清理 - // let mut stdout = std::io::stdout(); - - // // 执行必要的重置命令,但不清除屏幕内容 - // let _ = execute!(&mut stdout, crossterm::style::ResetColor); - // let _ = execute!(&mut stdout, crossterm::cursor::Show); - // let _ = execute!(&mut stdout, crossterm::terminal::LeaveAlternateScreen); - // let _ = execute!(&mut stdout, crossterm::event::DisableMouseCapture); - // let _ = execute!(&mut stdout, crossterm::style::SetAttribute(crossterm::style::Attribute::Reset)); - // let _ = execute!(&mut stdout, crossterm::style::SetForegroundColor(crossterm::style::Color::Reset)); - // let _ = execute!(&mut stdout, crossterm::style::SetBackgroundColor(crossterm::style::Color::Reset)); - - // // 禁用原始模式 - // let _ = crossterm::terminal::disable_raw_mode(); - - // // 立即刷新输出 - // let _ = stdout.flush(); - - // // 只重置样式,不清除屏幕内容 - // let style_reset = "\x1b[0m\x1b[?25h"; - // print!("{}", style_reset); - // let _ = stdout.flush(); -} \ No newline at end of file diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index d5bc440..ef2d02f 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -1,320 +1,1249 @@ +use crate::agent::ChatMessage; +use crate::config::BotConfig; +use clipboard::ClipboardProvider; use ratatui::{ - Frame, - layout::{Constraint, Direction, Layout}, + crossterm::event::{KeyEvent, MouseEvent, MouseEventKind}, + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, Wrap}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap}, + Frame, }; +use tui_markdown::from_str; +use tui_textarea::TextArea; + +/// 应用状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppState { + BotSelection, + Chat, +} + +/// 服务状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServiceStatus { + Initing, // 初始化中 + Active, // 服务可用 + Inactive, // 服务不可用 +} + +/// 聊天界面状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChatState { + Normal, + #[allow(dead_code)] + LogPanel, + #[allow(dead_code)] + Selection, +} + +/// 应用 UI 状态 +pub struct AppUi { + pub state: AppState, + pub service_status: ServiceStatus, + #[allow(dead_code)] + pub chat_state: ChatState, + pub bot_list_state: ListState, + pub bot_list: Vec, + pub messages: Vec, + pub input_textarea: TextArea<'static>, + pub scroll_offset: usize, + pub auto_scroll: bool, // 是否自动滚动到底部 + pub log_panel_visible: bool, + pub log_lines: Vec, + pub log_scroll_offset: usize, + pub input_area_width: u16, + last_key_event: Option, + // 选择模式相关字段 + pub selection_active: bool, + pub selection_start: Option<(usize, usize)>, // (line_index, char_index) + pub selection_end: Option<(usize, usize)>, // (line_index, char_index) + #[allow(dead_code)] + pub cursor_position: (usize, usize), // 当前光标位置 (line_index, char_index) + // 消息显示区域位置 + pub messages_area: Option, +} + +/// 键盘事件处理结果 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyAction { + Continue, // 继续运行 + Quit, // 退出程序 + SendMessage, // 发送消息 + ClearChat, // 清空会话 + ShowHelp, // 显示帮助 + DumpChats, // 导出会话到剪贴板 +} + +impl AppUi { + pub fn new() -> Self { + let mut bot_list_state = ListState::default(); + bot_list_state.select(Some(0)); + + let mut input_textarea = TextArea::default(); + let _ = input_textarea.set_block(Block::default() + .borders(Borders::ALL) + .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); + let _ = input_textarea.set_cursor_line_style(Style::default()); + + Self { + state: AppState::BotSelection, + service_status: ServiceStatus::Initing, + chat_state: ChatState::Normal, + bot_list_state, + bot_list: vec![], + messages: vec![], + input_textarea, + scroll_offset: 0, + auto_scroll: true, + log_panel_visible: false, + log_lines: vec![], + log_scroll_offset: 0, + input_area_width: 0, + last_key_event: None, + selection_active: false, + selection_start: None, + selection_end: None, + cursor_position: (0, 0), + messages_area: None, + } + } + + /// 设置机器人列表 + pub fn set_bot_list(&mut self, bots: Vec) { + self.bot_list = bots; + if !self.bot_list.is_empty() { + self.bot_list_state.select(Some(0)); + } else { + self.bot_list_state.select(None); + } + } + + /// 获取选中的机器人 + pub fn selected_bot(&self) -> Option<&BotConfig> { + if let Some(index) = self.bot_list_state.selected() { + self.bot_list.get(index) + } else { + None + } + } + + /// 处理键盘事件 + pub fn handle_key_event(&mut self, key: KeyEvent) -> KeyAction { + // 事件去重:如果和上一次事件完全相同,则忽略 + if let Some(last_key) = self.last_key_event { + if last_key.code == key.code && last_key.modifiers == key.modifiers { + // log::debug!("忽略重复事件: {:?}", key); + self.last_key_event = None; + return KeyAction::Continue; + } + } + + self.last_key_event = Some(key); + + match self.state { + AppState::BotSelection => { + if self.handle_bot_selection_key(key) { + KeyAction::Continue + } else { + KeyAction::Quit + } + } + AppState::Chat => self.handle_chat_key(key), + } + } + + /// 处理机器人选择界面的键盘事件 + fn handle_bot_selection_key(&mut self, key: KeyEvent) -> bool { + use ratatui::crossterm::event::KeyCode; + match key.code { + KeyCode::Up | KeyCode::Char('k') => { + if let Some(selected) = self.bot_list_state.selected() { + if selected > 0 { + self.bot_list_state.select(Some(selected - 1)); + log::debug!("选择上一个机器人"); + } + } + true + } + KeyCode::Down | KeyCode::Char('j') => { + if let Some(selected) = self.bot_list_state.selected() { + if selected < self.bot_list.len().saturating_sub(1) { + self.bot_list_state.select(Some(selected + 1)); + log::debug!("选择下一个机器人"); + } + } + true + } + KeyCode::Enter => { + if let Some(bot) = self.selected_bot() { + log::info!("选择机器人: {}", bot.name); + self.state = AppState::Chat; + } + true + } + KeyCode::Char('q') => { + log::info!("用户按 q 退出"); + false + } + KeyCode::Char('c') if key.modifiers.contains(ratatui::crossterm::event::KeyModifiers::CONTROL) => { + log::info!("用户按 Ctrl-C 退出"); + false + } + _ => true, + } + } + + /// 处理聊天界面的键盘事件 + fn handle_chat_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::{KeyCode, KeyModifiers}; -use crate::app::{App, FocusArea}; -use unicode_width::UnicodeWidthChar; - -/// UI 绘制函数 -pub fn draw_ui(f: &mut Frame, app: &mut App) { - // 创建主布局 - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) - .split(f.area()); - - // 左列:对话区域和输入框 - let left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(75), Constraint::Percentage(25)]) - .split(chunks[0]); - - // 对话历史 - 构建所有对话文本,包括正在流式生成的内容 - let display_conversations = app.get_display_conversations(); - let conversation_text = display_conversations - .iter() - .rev() // 反转顺序,使最新对话显示在前面 - .enumerate() - .flat_map(|(index, (user, assistant, timestamp))| { - // 由于反转了顺序,流式生成的对话现在是第一个(index == 0) - let is_streaming = app.current_streaming_response.is_some() && - index == 0; - - let assistant_style = if is_streaming { - Style::default().fg(Color::Yellow) // 流式生成中用黄色 + if self.log_panel_visible { + log::debug!("日志面板打开,处理日志面板键盘事件"); + if self.handle_log_panel_key(key) { + KeyAction::Continue } else { - Style::default().fg(Color::Green) // 完成的回复用绿色 - }; - - let assistant_prefix = if is_streaming { - "助手 (生成中): " + KeyAction::Quit + } + } else { + match key.code { + KeyCode::Enter => { + if key.modifiers.is_empty() { + // Enter 发送消息 + log::debug!("Enter: 准备发送消息"); + let text = self.get_input_text(); + if !text.trim().is_empty() { + KeyAction::SendMessage + } else { + KeyAction::Continue + } + } else { + // Shift+Enter 换行 + log::debug!("Shift+Enter: 换行"); + self.input_textarea.input(key); + KeyAction::Continue + } + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + log::debug!("Ctrl-C: 退出"); + KeyAction::Quit + } + KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => { + log::debug!("Ctrl-L: 切换日志面板"); + self.log_panel_visible = !self.log_panel_visible; + KeyAction::Continue + } + KeyCode::Esc => { + log::debug!("关闭日志面板"); + self.log_panel_visible = false; + // 清除选择 + self.selection_active = false; + self.selection_start = None; + self.selection_end = None; + KeyAction::Continue + } + KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => { + // 先让 tui-textarea 处理输入 + self.input_textarea.input(key); + // 尝试自动换行 + self.handle_auto_wrap(); + KeyAction::Continue + } + _ => { + self.input_textarea.input(key); + KeyAction::Continue + } + } + } + } + + /// 处理自动换行 - 检查所有行的显示宽度,必要时插入换行 + fn handle_auto_wrap(&mut self) { + // 使用实际的输入框宽度(减去边框的2个字符) + let max_display_width = if self.input_area_width > 2 { + (self.input_area_width as usize).saturating_sub(2) + } else { + 74 // 默认宽度 + }; + + loop { + let lines = self.input_textarea.lines().to_vec(); + let (cursor_row, cursor_col) = self.input_textarea.cursor(); + + // 检查所有行,找到第一行超过宽度的行 + let mut wrap_line_idx = None; + let mut wrap_pos = 0; + + for (line_idx, line) in lines.iter().enumerate() { + let line_width: usize = line.chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum(); + + if line_width > max_display_width { + // 找到这一行中需要换行的位置 + let chars: Vec = line.chars().collect(); + let mut current_width = 0usize; + + for (char_idx, c) in chars.iter().enumerate() { + let char_width = unicode_width::UnicodeWidthChar::width(*c).unwrap_or(0); + current_width += char_width; + + if current_width > max_display_width { + // 找到前一个空格的位置 + wrap_pos = char_idx; + for j in (0..char_idx).rev() { + if chars[j].is_whitespace() { + wrap_pos = j; + break; + } + } + if wrap_pos == 0 { + wrap_pos = 1; + } + wrap_line_idx = Some(line_idx); + break; + } + } + break; + } + } + + // 如果没有需要换行的行,退出 + if wrap_line_idx.is_none() { + return; + } + + let line_idx = wrap_line_idx.unwrap(); + let line = &lines[line_idx]; + let chars: Vec = line.chars().collect(); + + log::debug!("[AUTO_WRAP] line {} needs wrap, pos {}", line_idx, wrap_pos); + + // 分割这一行 + let prefix: String = chars[..wrap_pos].iter().collect(); + let suffix: String = chars[wrap_pos..].iter().collect(); + + // 构建新的行列表 + let mut new_lines = lines[..line_idx].to_vec(); + new_lines.push(prefix.trim_end().to_string()); + new_lines.push(suffix.trim_start().to_string()); + if line_idx + 1 < lines.len() { + new_lines.extend_from_slice(&lines[line_idx + 1..]); + } + + // 重新创建 TextArea + let mut new_textarea = TextArea::from(new_lines.iter().cloned()); + let _ = new_textarea.set_block(Block::default() + .borders(Borders::ALL) + .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); + let _ = new_textarea.set_cursor_line_style(Style::default()); + + // 重新计算光标位置 + let new_cursor_row = if line_idx < cursor_row { + cursor_row + 1 + } else if line_idx == cursor_row { + if cursor_col > wrap_pos { + line_idx + 1 + } else { + line_idx + } } else { - "助手: " + cursor_row }; - - // 格式化时间戳 - let time_str = if let Some(ts) = timestamp { - format!(" [{}]", ts.format("%H:%M:%S")) + + let new_cursor_col = if cursor_row == line_idx && cursor_col > wrap_pos { + let suffix_prefix: String = chars[wrap_pos..cursor_col].iter().collect(); + suffix_prefix.chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum() + } else if cursor_row == line_idx { + cursor_col } else { - String::new() + cursor_col }; - - vec![ - Line::from(vec![ - Span::styled("用户: ", Style::default().fg(Color::Cyan)), - Span::raw(user.clone()), - Span::styled(time_str.clone(), Style::default().fg(Color::DarkGray)), - ]), - Line::from(vec![ - Span::styled(assistant_prefix, assistant_style), - Span::styled(assistant.clone(), assistant_style), - if is_streaming { - Span::styled("▋", Style::default().fg(Color::Yellow)) // 光标效果 - } else { - Span::raw("") + + // 移动光标到正确位置 + for _ in 0..new_cursor_row { + new_textarea.move_cursor(tui_textarea::CursorMove::Down); + } + for _ in 0..new_cursor_col { + new_textarea.move_cursor(tui_textarea::CursorMove::Forward); + } + + self.input_textarea = new_textarea; + } + } + + /// 处理日志面板的键盘事件 + fn handle_log_panel_key(&mut self, key: KeyEvent) -> bool { + use ratatui::crossterm::event::KeyCode; + match key.code { + KeyCode::Esc => { + self.log_panel_visible = false; + true + } + KeyCode::Up | KeyCode::Char('k') => { + if self.log_scroll_offset > 0 { + self.log_scroll_offset -= 1; + } + true + } + KeyCode::Down | KeyCode::Char('j') => { + if self.log_scroll_offset < self.log_lines.len().saturating_sub(1) { + self.log_scroll_offset += 1; + } + true + } + KeyCode::PageUp => { + self.log_scroll_offset = self.log_scroll_offset.saturating_sub(10); + true + } + KeyCode::PageDown => { + self.log_scroll_offset = self + .log_scroll_offset + .saturating_add(10) + .min(self.log_lines.len().saturating_sub(1)); + true + } + KeyCode::Home => { + self.log_scroll_offset = 0; + true + } + KeyCode::End => { + self.log_scroll_offset = self.log_lines.len().saturating_sub(1); + true + } + KeyCode::Char('l') => { + self.log_panel_visible = false; + true + } + _ => true, + } + } + + /// 复制选中的内容到剪贴板 + fn copy_selection(&mut self) { + if let (Some(start), Some(end)) = (self.selection_start, self.selection_end) { + let selected_text = self.get_selected_text(start, end); + + if !selected_text.is_empty() { + match clipboard::ClipboardContext::new() { + Ok(mut ctx) => { + match ctx.set_contents(selected_text.clone()) { + Ok(_) => { + log::info!("已复制 {} 个字符到剪贴板", selected_text.len()); + } + Err(e) => { + log::error!("复制到剪贴板失败: {}", e); + } + } } - ]), - Line::from(""), // 空行分隔 - ] - }) - .collect::>(); - - let total_conversations = display_conversations.len(); - - // 构建对话区域标题,显示滚动状态和焦点状态 - let conversation_title = if app.focus_area == FocusArea::Conversation { - if total_conversations > 0 { - format!( - "💬 对话历史 ({} 对, 偏移:{}) [Tab切换焦点 ↑向后 ↓向前 Home/End快速跳转]", - total_conversations, app.conversation_scroll_offset - ) - } else { - format!("💬 对话历史 (0 对) [Tab切换焦点]") + Err(e) => { + log::error!("无法访问剪贴板: {}", e); + } + } + } } - } else { - if total_conversations > 0 { - format!( - "对话历史 ({} 对, 偏移:{}) [Tab切换焦点]", - total_conversations, app.conversation_scroll_offset - ) - } else { - format!("对话历史 (0 对) [Tab切换焦点]") + } + + /// 获取选中的文本 + fn get_selected_text(&self, start: (usize, usize), end: (usize, usize)) -> String { + let mut result = String::new(); + let all_lines = self.get_all_rendered_lines(); + + let start_line = start.0.min(end.0); + let end_line = end.0.max(start.0); + let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; + let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; + + for line_idx in start_line..=end_line { + if line_idx < all_lines.len() { + let line = &all_lines[line_idx]; + let chars: Vec = line.chars().collect(); + let char_len = chars.len(); + + if line_idx == start_line && line_idx == end_line { + // 单行选择 + if start_col < char_len && end_col <= char_len && start_col < end_col { + let selected: String = chars[start_col..end_col].iter().collect(); + result.push_str(&selected); + } + } else if line_idx == start_line { + // 起始行 + if start_col < char_len { + let selected: String = chars[start_col..].iter().collect(); + result.push_str(&selected); + } + result.push('\n'); + } else if line_idx == end_line { + // 结束行 + if end_col <= char_len { + let selected: String = chars[..end_col].iter().collect(); + result.push_str(&selected); + } + } else { + // 中间行 + result.push_str(line); + result.push('\n'); + } + } } - }; - let conversation_paragraph = Paragraph::new(conversation_text) - .block( - Block::default() - .borders(Borders::ALL) - .title(conversation_title) - .title_style(if app.focus_area == FocusArea::Conversation { - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) + result + } + + /// 获取所有渲染的行文本 + fn get_all_rendered_lines(&self) -> Vec { + let mut all_lines: Vec = vec![]; + + for message in &self.messages { + // 角色标签行 + let role_label = match message.role { + crate::agent::MessageRole::System => "[System]", + crate::agent::MessageRole::User => "[You]", + crate::agent::MessageRole::Assistant => "[AI]", + }; + all_lines.push(role_label.to_string()); + + // 渲染 Markdown 内容(与 render_messages 保持一致) + let markdown_text = from_str(&message.content); + for line in markdown_text.lines { + let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); + all_lines.push(line_text); + } + + // 空行分隔 + all_lines.push(String::new()); + } + + all_lines + } + + /// 处理鼠标事件 + pub fn handle_mouse_event(&mut self, event: MouseEvent, _area: Rect) -> bool { + if self.state != AppState::Chat { + return true; + } + + // 使用保存的消息区域 + let messages_area = match self.messages_area { + Some(area) => area, + None => return true, + }; + + match event.kind { + MouseEventKind::ScrollUp => { + if self.log_panel_visible { + if self.log_scroll_offset > 0 { + self.log_scroll_offset = self.log_scroll_offset.saturating_sub(3); + } + } else if self.scroll_offset > 0 { + self.scroll_offset = self.scroll_offset.saturating_sub(3); + // 用户手动滚动,禁用自动滚动 + self.auto_scroll = false; + } + true + } + MouseEventKind::ScrollDown => { + if self.log_panel_visible { + self.log_scroll_offset = self.log_scroll_offset.saturating_add(3); } else { - Style::default().fg(Color::White) - }), - ) - .style(Style::default().bg(Color::Black)) - .wrap(ratatui::widgets::Wrap { trim: true }) - .scroll((app.conversation_scroll_offset as u16, 0)); - - f.render_widget(Clear, left_chunks[0]); - f.render_widget(conversation_paragraph, left_chunks[0]); - - // 渲染会话区滚动条 - if total_conversations > 0 { - let total_lines = total_conversations * 3; // 每个对话3行 - let visible_height = left_chunks[0].height.saturating_sub(2) as usize; // 减去边框 - - // 更新滚动条状态,使用实际的可见高度 - app.conversation_scrollbar_state = app - .conversation_scrollbar_state - .content_length(total_lines) - .viewport_content_length(visible_height) - .position(app.conversation_scroll_offset); - - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")), - left_chunks[0], - &mut app.conversation_scrollbar_state, - ); + self.scroll_offset = self.scroll_offset.saturating_add(3); + // 用户手动滚动,禁用自动滚动 + self.auto_scroll = false; + } + true + } + MouseEventKind::Down(but) if but == ratatui::crossterm::event::MouseButton::Left => { + // 鼠标左键按下,开始选择 + let (line_idx, col_idx) = self.mouse_to_text_position(event, messages_area); + self.selection_active = true; + self.selection_start = Some((line_idx, col_idx)); + self.selection_end = Some((line_idx, col_idx)); + true + } + MouseEventKind::Drag(but) if but == ratatui::crossterm::event::MouseButton::Left => { + // 鼠标拖拽,更新选择 + let (line_idx, col_idx) = self.mouse_to_text_position(event, messages_area); + self.selection_end = Some((line_idx, col_idx)); + true + } + MouseEventKind::Up(but) if but == ratatui::crossterm::event::MouseButton::Left => { + + // 保持选择状态,用户可以继续操作 + true + } + MouseEventKind::Up(but) if but == ratatui::crossterm::event::MouseButton::Right => { + // 鼠标右键释放,复制选中的文本 + log::debug!("鼠标右键,复制选中的文本"); + if self.selection_active { + self.copy_selection(); + } + true + } + _ => true, + } } - // 输入区域 - 根据状态显示不同的内容 - if app.is_shutting_down { - // 在shutting down时显示说明文案,不显示输入框 - let shutdown_text = Paragraph::new(Text::from( - "正在执行记忆化存储,请稍候...\n\n系统将自动保存本次对话记录到记忆库中。", - )) - .style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("正在退出程序... (记忆迭代中)") - .title_style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - ) - .wrap(Wrap { trim: true }); - - f.render_widget(Clear, left_chunks[1]); - f.render_widget(shutdown_text, left_chunks[1]); - // 不设置光标,光标会自动隐藏 - } else { - // 正常状态显示输入框 - let input_title = if app.focus_area == FocusArea::Input { - "📝 输入消息 (Enter发送, Tab切换焦点, /quit退出)" + /// 将鼠标坐标转换为文本位置 (line_index, char_index) + fn mouse_to_text_position(&self, event: MouseEvent, area: Rect) -> (usize, usize) { + // 计算相对于消息区域的坐标 + let content_area = area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + // 检查鼠标是否在消息区域内 + if event.row < content_area.top() || event.row >= content_area.bottom() || + event.column < content_area.left() || event.column >= content_area.right() { + log::debug!("鼠标不在消息区域内"); + return (self.scroll_offset, 0); + } + + // 计算行索引(考虑滚动偏移) + let relative_row = event.row.saturating_sub(content_area.top()); + let line_idx = self.scroll_offset + relative_row as usize; + + // 计算列索引 + let relative_col = event.column.saturating_sub(content_area.left()); + let col_idx = relative_col as usize; + + // 获取实际行的文本,确保列索引不超出范围 + let all_lines = self.get_all_rendered_lines(); + if line_idx < all_lines.len() { + let line_len = all_lines[line_idx].len(); + (line_idx, col_idx.min(line_len)) } else { - "输入消息 (Enter发送, Tab切换焦点, /quit退出)" - }; + log::debug!("行索引超出范围: {} >= {}", line_idx, all_lines.len()); + (line_idx, 0) + } + } + + /// 渲染 UI + pub fn render(&mut self, frame: &mut Frame) { + match self.state { + AppState::BotSelection => self.render_bot_selection(frame), + AppState::Chat => self.render_chat(frame), + } + } + + /// 渲染机器人选择界面 + fn render_bot_selection(&mut self, frame: &mut Frame) { + let area = frame.area(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(3)]) + .split(area); + + // 标题 + let title = Paragraph::new("选择机器人") + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Center) + .style(Style::default().add_modifier(Modifier::BOLD)); + + frame.render_widget(title, chunks[0]); + + // 机器人列表 + let items: Vec = self + .bot_list + .iter() + .map(|bot| { + ListItem::new(Line::from(vec![ + Span::styled( + bot.name.clone(), + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + Span::raw(" - "), + Span::styled( + format!("{}...", &bot.system_prompt.chars().take(40).collect::()), + Style::default().fg(Color::Gray), + ), + ])) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("可用机器人")) + .highlight_style( + Style::default() + .bg(Color::DarkGray) + .add_modifier(Modifier::REVERSED), + ); + + frame.render_stateful_widget(list, chunks[1], &mut self.bot_list_state); + + // 帮助提示 + let help = Paragraph::new("↑/↓ 或 j/k: 选择 | Enter: 进入 | q 或 Ctrl-C: 退出") + .alignment(Alignment::Center); + + frame.render_widget(help, chunks[2]); + } - let input_paragraph = Paragraph::new(Text::from(app.current_input.as_str())) - .style(Style::default().fg(Color::White)) + /// 渲染聊天界面 + fn render_chat(&mut self, frame: &mut Frame) { + let area = frame.area(); + + if self.log_panel_visible { + self.render_chat_with_log_panel(frame, area); + } else { + self.render_chat_normal(frame, area); + } + } + + /// 渲染普通聊天界面 + fn render_chat_normal(&mut self, frame: &mut Frame, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(8), + ]) + .split(area); + + // 创建简洁的标题文字 + let title_line = Line::from(vec![ + Span::styled( + "Cortex TARS AI Program", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ]); + + let title = Paragraph::new(title_line) .block( Block::default() .borders(Borders::ALL) - .title(input_title) - .title_style(if app.focus_area == FocusArea::Input { + .border_style( Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::White) - }), + ) + .border_type(ratatui::widgets::BorderType::Double) + .title_style( + Style::default() + .fg(match self.service_status { + ServiceStatus::Initing => Color::Blue, + ServiceStatus::Active => Color::Green, + ServiceStatus::Inactive => Color::Red, + }) + .add_modifier(Modifier::BOLD) + ) + .title(match self.service_status { + ServiceStatus::Initing => " [ SYSTEM INITING ] ", + ServiceStatus::Active => " [ SYSTEM ACTIVE ] ", + ServiceStatus::Inactive => " [ SYSTEM INACTIVE ] ", + }) ) - .wrap(Wrap { trim: true }); - - f.render_widget(Clear, left_chunks[1]); - f.render_widget(input_paragraph, left_chunks[1]); - - // 只有当焦点在输入框时才设置光标 - if app.focus_area == FocusArea::Input { - // 计算输入框可用宽度(减去边框和边距) - let available_width = left_chunks[1].width.saturating_sub(2) as usize; - - // 使用ratatui的wrap逻辑来计算光标位置 - // 我们需要模拟ratatui::widgets::Wrap的行为 - - // 获取光标前的所有字符 - let chars_before_cursor: Vec = app - .current_input - .chars() - .take(app.cursor_position) - .collect(); - - // 模拟ratatui的换行逻辑 - let mut line_offset = 0; - let mut current_line_width = 0; - - // 遍历光标前的所有字符,计算换行 - for ch in chars_before_cursor { - let char_width = ch.width().unwrap_or(0); - - // 如果当前字符会超出行宽,则换行 - if current_line_width + char_width > available_width { - line_offset += 1; - current_line_width = 0; - } + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::White) + .bg(Color::Rgb(20, 30, 40)) + ); - current_line_width += char_width; - } + frame.render_widget(title, chunks[0]); - // 计算最终的光标位置 - let cursor_x = left_chunks[1].x + 1 + current_line_width as u16; - let cursor_y = left_chunks[1].y + 1 + line_offset as u16; + // 消息显示区域 + let messages_area = chunks[1]; + self.messages_area = Some(messages_area); + self.render_messages(frame, messages_area); - // 确保光标在输入框范围内 - if cursor_y < left_chunks[1].y + left_chunks[1].height { - f.set_cursor_position((cursor_x, cursor_y)); - } - } + // 输入区域 + self.render_input(frame, chunks[2]); } - // 右列:日志区域 - 构建所有日志文本,使用Paragraph的scroll功能 - let total_logs = app.logs.len(); - - // 构建要显示的日志文本,反转顺序使最新日志显示在前面 - let log_text = app - .logs - .iter() - .rev() // 反转顺序,使最新日志显示在前面 - .map(|log| { - let style = if log.starts_with("[WARN]") { - Style::default().fg(Color::Yellow) - } else if log.starts_with("[ERROR]") { - Style::default().fg(Color::Red) - } else { - Style::default().fg(Color::Gray) - }; + /// 渲染带日志面板的聊天界面 + fn render_chat_with_log_panel(&mut self, frame: &mut Frame, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Percentage(50), + Constraint::Percentage(50), + ]) + .split(area); - Line::from(Span::styled(log.clone(), style)) - }) - .collect::>(); + // 创建简洁的标题文字 + let title_line = Line::from(vec![ + Span::styled( + "Cortex TARS AI Program", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ]); - // 构建日志区域标题,显示滚动状态和焦点状态 - let log_title = if app.focus_area == FocusArea::Logs { - if total_logs > 0 { - format!( - "🔍 系统日志 ({} 行, 偏移:{}) [Tab切换焦点 ↑向后 ↓向前 Home/End快速跳转]", - total_logs, app.log_scroll_offset + let title = Paragraph::new(title_line) + .block( + Block::default() + .borders(Borders::ALL) + .border_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + ) + .border_type(ratatui::widgets::BorderType::Double) + .title_style( + Style::default() + .fg(match self.service_status { + ServiceStatus::Initing => Color::Blue, + ServiceStatus::Active => Color::Green, + ServiceStatus::Inactive => Color::Red, + }) + .add_modifier(Modifier::BOLD) + ) + .title(match self.service_status { + ServiceStatus::Initing => " [ SYSTEM INITING ] ", + ServiceStatus::Active => " [ SYSTEM ACTIVE ] ", + ServiceStatus::Inactive => " [ SYSTEM INACTIVE ] ", + }) ) + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::White) + .bg(Color::Rgb(20, 30, 40)) + ); + + frame.render_widget(title, chunks[0]); + + // 消息显示区域 + let messages_area = chunks[1]; + self.messages_area = Some(messages_area); + self.render_messages(frame, messages_area); + + // 日志面板 + self.render_log_panel(frame, chunks[2]); + } + + /// 渲染消息 + fn render_messages(&mut self, frame: &mut Frame, area: Rect) { + // 使用 tui-markdown 渲染每个消息 + let content_area = area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + // 收集所有消息的渲染行 + let mut all_lines: Vec = vec![]; + + for message in &self.messages { + let role_label = match message.role { + crate::agent::MessageRole::System => "System", + crate::agent::MessageRole::User => "You", + crate::agent::MessageRole::Assistant => "TARS AI", + }; + + let role_color = match message.role { + crate::agent::MessageRole::System => Color::Yellow, + crate::agent::MessageRole::User => Color::Green, + crate::agent::MessageRole::Assistant => Color::Cyan, + }; + + // 格式化时间戳 + let time_str = message.timestamp.format("%H:%M:%S").to_string(); + + // 添加角色标签和时间戳 + all_lines.push(Line::from(vec![ + Span::styled( + format!("[{}]", role_label), + Style::default() + .fg(role_color) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::styled( + format!("[{}]", time_str), + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::DIM), + ), + ])); + + // 渲染 Markdown 内容 + let markdown_text = from_str(&message.content); + // 将 tui_markdown 的 Text 转换为 ratatui 的 Text + for line in markdown_text.lines { + all_lines.push(Line::from(line.spans.iter().map(|s| { + Span::raw(s.content.clone()) + }).collect::>())); + } + + // 添加空行分隔 + all_lines.push(Line::from("")); + } + + // 计算滚动 + let total_lines = all_lines.len(); + let visible_lines = area.height.saturating_sub(2) as usize; // 减去边框 + let max_scroll = total_lines.saturating_sub(visible_lines); + + // 如果启用了自动滚动,始终滚动到底部 + if self.auto_scroll { + self.scroll_offset = max_scroll; } else { - format!("🔍 系统日志 (0 行) [Tab切换焦点]") + // 限制 scroll_offset 在有效范围内 + if self.scroll_offset > max_scroll { + self.scroll_offset = max_scroll; + } } - } else { - if total_logs > 0 { - format!( - "系统日志 ({} 行, 偏移:{}) [Tab切换焦点]", - total_logs, app.log_scroll_offset - ) + + // 应用选择高亮 + let display_lines: Vec = if self.selection_active { + self.apply_selection_highlight(all_lines, self.scroll_offset, visible_lines) } else { - format!("系统日志 (0 行) [Tab切换焦点]") + all_lines + .into_iter() + .skip(self.scroll_offset) + .take(visible_lines) + .collect() + }; + + // 渲染边框 + let title = "交互信息 (鼠标拖拽选择, Esc 清除选择)"; + let block = Block::default() + .borders(Borders::ALL) + .title(title); + frame.render_widget(block, area); + + // 渲染消息内容(在边框内部) + let paragraph = Paragraph::new(display_lines) + .wrap(Wrap { trim: false }); + frame.render_widget(paragraph, content_area); + + // 渲染滚动条 + if total_lines > visible_lines { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + let mut scrollbar_state = ScrollbarState::new(total_lines) + .position(self.scroll_offset); + + let scrollbar_area = area.inner(Margin { + vertical: 1, + horizontal: 0, + }); + + frame.render_stateful_widget( + scrollbar, + scrollbar_area, + &mut scrollbar_state, + ); } - }; + } - let log_paragraph = Paragraph::new(log_text) - .block( - Block::default() - .borders(Borders::ALL) - .title(log_title) - .title_style(if app.focus_area == FocusArea::Logs { - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) + /// 应用选择高亮 + fn apply_selection_highlight<'a>(&self, lines: Vec>, scroll_offset: usize, visible_lines: usize) -> Vec> { + let (start, end) = match (self.selection_start, self.selection_end) { + (Some(s), Some(e)) => (s, e), + _ => return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(), + }; + + // 如果选择范围完全在可见区域之外,直接返回 + let start_line = start.0.min(end.0); + let end_line = end.0.max(start.0); + + // 可见区域的行范围是 [scroll_offset, scroll_offset + visible_lines) + let visible_start = scroll_offset; + let visible_end = scroll_offset + visible_lines; + + // 如果选择区域和可见区域没有重叠,直接返回 + if end_line < visible_start || start_line >= visible_end { + // log::debug!("选择区域和可见区域没有重叠,直接返回"); + return lines.into_iter().skip(scroll_offset).take(visible_lines).collect(); + } + + let start_col = if start.0 <= end.0 { start.1 } else { end.1 }; + let end_col = if start.0 <= end.0 { end.1 } else { start.1 }; + + // 使用反色样式使高亮更明显 + let highlight_style = Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD); + + let mut highlighted_count = 0; + let mut total_processed = 0; + + let result = lines + .into_iter() + .enumerate() + .skip(scroll_offset) + .take(visible_lines) + .map(|(original_idx, line)| { + total_processed += 1; + // original_idx 是原始的行索引(从 0 开始) + // skip(scroll_offset) 后,visible_idx 从 0 开始 + let in_range = original_idx >= start_line && original_idx <= end_line; + + if in_range { + // 这一行在选择范围内 + highlighted_count += 1; + let line_text: String = line.spans.iter().map(|s| s.content.clone()).collect(); + let chars: Vec = line_text.chars().collect(); + let char_len = chars.len(); + + if original_idx == start_line && original_idx == end_line { + // 单行选择 + let safe_start_col = start_col.min(char_len); + let safe_end_col = end_col.min(char_len); + if safe_start_col < char_len && safe_end_col <= char_len && safe_start_col < safe_end_col { + let before: String = chars[..safe_start_col].iter().collect(); + let selected: String = chars[safe_start_col..safe_end_col].iter().collect(); + let after: String = chars[safe_end_col..].iter().collect(); + + log::debug!("单行高亮: original_idx={}, before_len={}, selected_len={}, after_len={}", + original_idx, before.len(), selected.len(), after.len()); + + Line::from(vec![ + Span::raw(before), + Span::styled(selected, highlight_style), + Span::raw(after), + ]) + } else { + line + } + } else if original_idx == start_line { + // 起始行 + let safe_start_col = start_col.min(char_len); + if safe_start_col < char_len { + let before: String = chars[..safe_start_col].iter().collect(); + let selected: String = chars[safe_start_col..].iter().collect(); + + Line::from(vec![ + Span::raw(before), + Span::styled(selected, highlight_style), + ]) + } else { + line + } + } else if original_idx == end_line { + // 结束行 + let safe_end_col = end_col.min(char_len); + if safe_end_col <= char_len { + let selected: String = chars[..safe_end_col].iter().collect(); + let after: String = chars[safe_end_col..].iter().collect(); + + Line::from(vec![ + Span::styled(selected, highlight_style), + Span::raw(after), + ]) + } else { + line + } + } else { + // 中间行,整行高亮 + Line::from(vec![Span::styled( + line_text, + highlight_style, + )]) + } } else { - Style::default().fg(Color::White) - }), - ) - .style(Style::default().bg(Color::Black)) - .wrap(ratatui::widgets::Wrap { trim: true }) - .scroll((app.log_scroll_offset as u16, 0)); - - f.render_widget(Clear, chunks[1]); - f.render_widget(log_paragraph, chunks[1]); - - // 渲染日志区滚动条 - if total_logs > 0 { - let visible_height = chunks[1].height.saturating_sub(2) as usize; // 减去边框 - - // 更新滚动条状态,使用实际的可见高度 - app.log_scrollbar_state = app - .log_scrollbar_state - .content_length(total_logs) - .viewport_content_length(visible_height) - .position(app.log_scroll_offset); - - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")), - chunks[1], - &mut app.log_scrollbar_state, + line + } + }) + .collect(); + + result + } + + /// 渲染输入框 - 使用 tui-textarea + fn render_input(&mut self, frame: &mut Frame, area: Rect) { + // 保存输入框可用宽度(减去边框的2个字符) + self.input_area_width = area.width.saturating_sub(2); + frame.render_widget(&self.input_textarea, area); + } + + /// 渲染日志面板 + fn render_log_panel(&mut self, frame: &mut Frame, area: Rect) { + let visible_lines = area.height as usize; + let max_scroll = self.log_lines.len().saturating_sub(visible_lines); + self.log_scroll_offset = self.log_scroll_offset.min(max_scroll); + + let display_lines: Vec = self + .log_lines + .iter() + .skip(self.log_scroll_offset) + .take(visible_lines) + .map(|line| { + let style = if line.contains("ERROR") { + Style::default().fg(Color::Red) + } else if line.contains("WARN") { + Style::default().fg(Color::Yellow) + } else if line.contains("INFO") { + Style::default().fg(Color::Green) + } else { + Style::default() + }; + Line::from(Span::styled(line.clone(), style)) + }) + .collect(); + + let paragraph = Paragraph::new(display_lines) + .block(Block::default().borders(Borders::ALL).title("日志 (Esc 关闭)")) + .wrap(Wrap { trim: false }); + + frame.render_widget(paragraph, area); + + // 渲染滚动条 + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + let mut scrollbar_state = ScrollbarState::new(self.log_lines.len()) + .position(self.log_scroll_offset); + + let scrollbar_area = area.inner(Margin { + vertical: 1, + horizontal: 0, + }); + + frame.render_stateful_widget( + scrollbar, + scrollbar_area, + &mut scrollbar_state, ); } - // 不再使用全屏覆盖层,保持所有UI区域可见 - // 这样用户可以在日志区域看到详细的quit执行过程 + /// Get input text, filtering out auto-wrap newlines + /// Heuristic: next line starts with whitespace = user newline (Shift+Enter) + /// next line starts without whitespace = auto-wrap continuation + pub fn get_input_text(&self) -> String { + let lines = self.input_textarea.lines(); + if lines.len() <= 1 { + return lines.first().map(|s| s.as_str()).unwrap_or("").to_string(); + } + + let mut result = Vec::new(); + let mut current_line = lines[0].clone(); + + for i in 1..lines.len() { + let line = &lines[i]; + let starts_with_space = line.starts_with(' ') || line.starts_with('\t'); + + if starts_with_space { + result.push(current_line); + current_line = line.trim_start().to_string(); + } else { + if !current_line.is_empty() && !current_line.ends_with(' ') { + current_line.push(' '); + } + current_line.push_str(line); + } + } + + result.push(current_line); + + if result.len() <= 1 { + return result.first().map(|s| s.as_str()).unwrap_or("").to_string(); + } + + result.join("\n") + } + + /// Clear input box + pub fn clear_input(&mut self) { + self.input_textarea = TextArea::default(); + let _ = self.input_textarea.set_block(Block::default() + .borders(Borders::ALL) + .title("输入消息或命令 (Enter 发送, 输入 /help 查看命令)")); + let _ = self.input_textarea.set_cursor_line_style(Style::default()); + } + + /// 解析并执行命令 + pub fn parse_and_execute_command(&mut self, input: &str) -> Option { + let trimmed = input.trim(); + + // 检查是否是命令(以 / 开头) + if !trimmed.starts_with('/') { + return None; + } + + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + let command = parts.get(0).map(|s| s.to_lowercase()).unwrap_or_default(); + + match command.as_str() { + "/quit" => { + log::info!("执行命令: /quit"); + Some(KeyAction::Quit) + } + "/cls" | "/clear" => { + log::info!("执行命令: {}", command); + Some(KeyAction::ClearChat) + } + "/help" => { + log::info!("执行命令: /help"); + Some(KeyAction::ShowHelp) + } + "/dump-chats" => { + log::info!("执行命令: /dump-chats"); + Some(KeyAction::DumpChats) + } + _ => { + log::warn!("未知命令: {}", command); + None + } + } + } + + /// 获取帮助信息 + pub fn get_help_message() -> String { + "# Cortex TARS AI Program - 帮助信息\n\n欢迎使用TARS演示程序,我是由Cortex Memory技术驱动的人工智能程序,作为你的第二大脑,我能够作为你的外脑与你的记忆深度链接。\n\n## 可用命令\n\n| 命令 | 说明 |\n|------|------|\n| `/quit` | 退出程序 |\n| `/cls` 或 `/clear` | 清空会话区域 |\n| `/help` | 显示此帮助信息 |\n| `/dump-chats` | 复制会话区域的所有内容到剪贴板 |\n\n## 快捷键\n\n- **Enter**: 发送消息\n- **Shift+Enter**: 换行\n- **Ctrl+L**: 打开/关闭日志面板\n- **Esc**: 关闭日志面板\n\n---\n\n*Powered by TARS AI*".to_string() + } + + /// 导出所有会话内容到剪贴板 + pub fn dump_chats_to_clipboard(&self) -> Result { + let mut content = String::new(); + + for message in &self.messages { + let role = match message.role { + crate::agent::MessageRole::System => "System", + crate::agent::MessageRole::User => "You", + crate::agent::MessageRole::Assistant => "TARS AI", + }; + + let time_str = message.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(); + + content.push_str(&format!("[{}] [{}]\n", role, time_str)); + content.push_str(&message.content); + content.push_str("\n\n"); + } + + if content.is_empty() { + return Err("没有会话内容可导出".to_string()); + } + + // 尝试复制到剪贴板 + match clipboard::ClipboardContext::new() { + Ok(mut ctx) => { + match ctx.set_contents(content.clone()) { + Ok(_) => { + log::info!("已导出 {} 个字符到剪贴板", content.len()); + Ok(format!("已导出 {} 条消息到剪贴板", self.messages.len())) + } + Err(e) => { + log::error!("复制到剪贴板失败: {}", e); + Err(format!("复制到剪贴板失败: {}", e)) + } + } + } + Err(e) => { + log::error!("无法访问剪贴板: {}", e); + Err(format!("无法访问剪贴板: {}", e)) + } + } + } +} + +impl Default for AppUi { + fn default() -> Self { + Self::new() + } }

Cortex Memory Evaluation: Excellent retrieval performance with 93.33% Recall@1 and 93.72% MRR

-

LangMem Evaluation: Modest performance with 26.32% Recall@1 and 38.83% MRR

+
Cortex Memory Evaluation: Excellent retrieval performance with 93.33% Recall@1 and 93.72% MRR + LangMem Evaluation: Modest performance with 26.32% Recall@1 and 38.83% MRR
Cortex Memory Evaluation