diff --git a/Cargo.lock b/Cargo.lock index 55f8459..21d624a 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" @@ -420,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", @@ -475,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" @@ -640,25 +695,36 @@ dependencies = [ [[package]] name = "cortex-mem-tars" -version = "1.0.0" +version = "1.1.0" dependencies = [ + "anyhow", "async-stream", "async-trait", "chrono", - "clap", + "clipboard", "cortex-mem-config", "cortex-mem-core", "cortex-mem-rig", - "crossterm 0.29.0", + "crossterm", + "directories", + "env_logger", "futures", + "log", "once_cell", "ratatui", + "ratatui-core", + "reqwest", "rig-core", + "serde", "serde_json", "tokio", + "toml", "tracing", "tracing-subscriber", - "unicode-width 0.1.14", + "tui-markdown", + "tui-textarea", + "unicode-width 0.2.0", + "uuid", ] [[package]] @@ -728,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" @@ -881,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" @@ -915,6 +942,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 +958,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 +984,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" @@ -966,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" @@ -1002,6 +1047,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 +1104,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 +1142,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 +1277,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 +1358,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1283,6 +1366,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 +1754,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 +1799,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 +1831,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" @@ -1727,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" @@ -1757,12 +1879,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 +2007,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 +2040,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 +2081,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 +2311,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 +2363,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 +2447,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 +2487,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 +2633,36 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags", "cassowary", - "compact_str", - "crossterm 0.28.1", + "compact_str 0.8.1", + "crossterm", "indoc", "instability", "itertools 0.13.0", - "lru", + "lru 0.12.5", "paste", - "strum", + "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.0", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru 0.16.2", + "strum 0.27.2", + "thiserror 2.0.17", "unicode-segmentation", - "unicode-truncate", + "unicode-truncate 2.0.0", "unicode-width 0.2.0", ] @@ -2391,6 +2686,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 +2746,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 +2890,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 +2935,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 +3039,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 +3297,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 +3359,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 +3384,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 +3444,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 +3726,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 +3863,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 +3887,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 +3898,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 +3954,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", + "ratatui", + "unicode-width 0.2.0", +] + [[package]] name = "typeid" version = "1.0.3" @@ -3582,6 +4028,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 +4111,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 +4261,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 +4622,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 +4661,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/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
- diff --git a/examples/cortex-mem-tars/.gitignore b/examples/cortex-mem-tars/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/examples/cortex-mem-tars/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/cortex-mem-tars/Cargo.lock b/examples/cortex-mem-tars/Cargo.lock new file mode 100644 index 0000000..2a84d57 --- /dev/null +++ b/examples/cortex-mem-tars/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/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/README.md b/examples/cortex-mem-tars/README.md new file mode 100644 index 0000000..408c55e --- /dev/null +++ b/examples/cortex-mem-tars/README.md @@ -0,0 +1,184 @@ +# Cortex Memory TARS + +这是一个基于 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 框架 diff --git a/examples/cortex-mem-tars/config.example.toml b/examples/cortex-mem-tars/config.example.toml new file mode 100644 index 0000000..ce1475d --- /dev/null +++ b/examples/cortex-mem-tars/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/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/src/config.rs b/examples/cortex-mem-tars/src/config.rs new file mode 100644 index 0000000..79307af --- /dev/null +++ b/examples/cortex-mem-tars/src/config.rs @@ -0,0 +1,197 @@ +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}; + +/// 机器人配置 +#[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, + 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() + .to_path_buf(); + + fs::create_dir_all(&config_dir).context("无法创建配置目录")?; + + 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, + }) + } + + /// 获取所有机器人配置 + 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(()) + } + + /// 删除机器人 + #[allow(dead_code)] + 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) + } + } + + /// 更新机器人 + #[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()?; + 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 获取机器人 + #[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 + } + + /// 获取 cortex-mem 配置 + pub fn cortex_config(&self) -> &CortexConfig { + &self.cortex_config + } +} + +impl Default for ConfigManager { + fn default() -> Self { + Self::new().expect("无法初始化配置管理器") + } +} 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/src/infrastructure.rs b/examples/cortex-mem-tars/src/infrastructure.rs new file mode 100644 index 0000000..504105b --- /dev/null +++ b/examples/cortex-mem-tars/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/src/lib.rs b/examples/cortex-mem-tars/src/lib.rs new file mode 100644 index 0000000..5c681a7 --- /dev/null +++ b/examples/cortex-mem-tars/src/lib.rs @@ -0,0 +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/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/src/logger.rs b/examples/cortex-mem-tars/src/logger.rs new file mode 100644 index 0000000..9d29f7e --- /dev/null +++ b/examples/cortex-mem-tars/src/logger.rs @@ -0,0 +1,117 @@ +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 { + #[allow(dead_code)] + 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()) + } + } +} + +/// 自定义 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) +} 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