feat(tui): preserve event order relative to final assistant message; stabilize interrupt processing order\n\n- Track per-event sequence in and \n- Move exec/tool cells ahead of final assistant cell to keep narrative flow\n- Render tool results with markdown extracted from JSON\n- Improve history insertion: use Reverse Index + Clear instead of raw escape strings\n\nchore(deps): remove ratatui git override; downgrade (tests only); refresh lockfile with minor bumps (image, clap, deadpool, deranged)

This commit is contained in:
James Peter
2025-09-01 22:21:39 +10:00
parent 6d029d5552
commit dfb703aad4
8 changed files with 423 additions and 216 deletions
+164 -155
View File
@@ -193,7 +193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image 0.25.6",
"image 0.25.7",
"log",
"objc2",
"objc2-app-kit",
@@ -423,9 +423,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit_field"
version = "0.10.2"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
@@ -461,7 +461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"serde",
]
@@ -650,9 +650,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]]
name = "clap"
version = "4.5.45"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [
"clap_builder",
"clap_derive",
@@ -660,9 +660,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.44"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [
"anstream",
"anstyle",
@@ -761,7 +761,7 @@ dependencies = [
"chromiumoxide_types",
"chrono",
"futures",
"image 0.25.6",
"image 0.25.7",
"once_cell",
"rand 0.8.5",
"regex",
@@ -849,7 +849,7 @@ dependencies = [
"fs2",
"futures",
"htmd",
"image 0.25.6",
"image 0.25.7",
"img_hash",
"landlock",
"lazy_static",
@@ -1097,7 +1097,7 @@ dependencies = [
"color-eyre",
"crossterm",
"diffy",
"image 0.25.6",
"image 0.25.7",
"insta",
"lazy_static",
"libc",
@@ -1398,11 +1398,10 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "deadpool"
version = "0.10.0"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490"
checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f"
dependencies = [
"async-trait",
"deadpool-runtime",
"num_cpus",
"tokio",
@@ -1427,9 +1426,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
dependencies = [
"powerfmt",
"serde",
@@ -1507,7 +1506,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
"nu-ansi-term 0.50.1",
"nu-ansi-term",
]
[[package]]
@@ -1833,6 +1832,26 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fax"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
dependencies = [
"fax_derive",
]
[[package]]
name = "fax_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "fd-lock"
version = "4.0.4"
@@ -2046,21 +2065,21 @@ dependencies = [
[[package]]
name = "gethostname"
version = "0.4.3"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55"
dependencies = [
"libc",
"windows-targets 0.48.5",
"rustix 1.0.8",
"windows-targets 0.52.6",
]
[[package]]
name = "getopts"
version = "0.2.23"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width 0.2.1",
"unicode-width 0.2.0",
]
[[package]]
@@ -2086,7 +2105,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasi 0.14.3+wasi-0.2.4",
"wasm-bindgen",
]
@@ -2121,8 +2140,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax 0.8.6",
]
[[package]]
@@ -2350,7 +2369,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.0",
"socket2",
"tokio",
"tower-service",
"tracing",
@@ -2509,7 +2528,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
@@ -2531,9 +2550,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.6"
version = "0.25.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
checksum = "1c6a3ce16143778e24df6f95365f12ed105425b22abefd289dd88a64bab59605"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -2541,6 +2560,7 @@ dependencies = [
"exr",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png",
"qoi",
@@ -2554,9 +2574,9 @@ dependencies = [
[[package]]
name = "image-webp"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b"
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [
"byteorder-lite",
"quick-error",
@@ -2799,20 +2819,14 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
[[package]]
name = "js-sys"
version = "0.3.77"
@@ -3052,11 +3066,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
@@ -3157,6 +3171,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "moxcms"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "multimap"
version = "0.10.1"
@@ -3221,16 +3245,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
@@ -3491,12 +3505,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "4.2.2"
@@ -3649,11 +3657,11 @@ dependencies = [
[[package]]
name = "png"
version = "0.17.16"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.9.3",
"crc32fast",
"fdeflate",
"flate2",
@@ -3698,9 +3706,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
@@ -3822,6 +3830,15 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "pxfm"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e790881194f6f6e86945f0a42a6981977323669aeb6c40e9c7ec253133b96f8"
dependencies = [
"num-traits",
]
[[package]]
name = "qoi"
version = "0.4.1"
@@ -3839,18 +3856,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.38.2"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8"
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.8"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases 0.2.1",
@@ -3859,7 +3876,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"socket2",
"thiserror 2.0.16",
"tokio",
"tracing",
@@ -3868,9 +3885,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.12"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"getrandom 0.3.3",
@@ -3889,16 +3906,16 @@ dependencies = [
[[package]]
name = "quinn-udp"
version = "0.5.13"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases 0.2.1",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -3988,7 +4005,8 @@ dependencies = [
[[package]]
name = "ratatui"
version = "0.29.0"
source = "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags 2.9.3",
"cassowary",
@@ -4002,7 +4020,7 @@ dependencies = [
"strum 0.26.3",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.1",
"unicode-width 0.2.0",
]
[[package]]
@@ -4013,7 +4031,7 @@ checksum = "e8fe71c551c67f34e4fa49797227f614cd064b82855d7b72d424e40d08ec0542"
dependencies = [
"base64 0.21.7",
"icy_sixel",
"image 0.25.6",
"image 0.25.7",
"rand 0.8.5",
"ratatui",
"rustix 0.38.44",
@@ -4144,41 +4162,32 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.1"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax 0.8.6",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax 0.8.6",
]
[[package]]
name = "regex-lite"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
[[package]]
name = "regex-syntax"
@@ -4188,9 +4197,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "relative-path"
@@ -4819,16 +4828,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
@@ -5100,7 +5099,7 @@ dependencies = [
"once_cell",
"onig",
"plist",
"regex-syntax 0.8.5",
"regex-syntax 0.8.6",
"serde",
"serde_derive",
"serde_json",
@@ -5205,7 +5204,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width 0.2.1",
"unicode-width 0.2.0",
]
[[package]]
@@ -5259,23 +5258,25 @@ dependencies = [
[[package]]
name = "tiff"
version = "0.9.1"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
dependencies = [
"fax",
"flate2",
"jpeg-decoder",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]]
name = "time"
version = "0.3.41"
version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
checksum = "8ca967379f9d8eb8058d86ed467d81d03e81acd45757e4ca341c24affbe8e8e3"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
@@ -5287,15 +5288,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "a9108bb380861b07264b950ded55a44a14a4adc68b9f5efd85aafc3aa4d40a68"
[[package]]
name = "time-macros"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
checksum = "7182799245a7264ce590b349d90338f1c1affad93d2639aed5f8f69c090b334c"
dependencies = [
"num-conv",
"time-core",
@@ -5362,7 +5363,7 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2 0.6.0",
"socket2",
"tokio-macros",
"windows-sys 0.59.0",
]
@@ -5624,14 +5625,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term 0.46.0",
"nu-ansi-term",
"once_cell",
"regex",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -5664,7 +5665,7 @@ checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2"
dependencies = [
"cc",
"regex",
"regex-syntax 0.8.5",
"regex-syntax 0.8.6",
"serde_json",
"streaming-iterator",
"tree-sitter-language",
@@ -5723,7 +5724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19"
dependencies = [
"ratatui",
"unicode-width 0.2.1",
"unicode-width 0.2.0",
]
[[package]]
@@ -5809,9 +5810,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unicode-xid"
@@ -5827,9 +5828,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.6"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@@ -5910,23 +5911,35 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vt100"
version = "0.16.2"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9"
checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
dependencies = [
"itoa",
"unicode-width 0.2.1",
"log",
"unicode-width 0.1.14",
"vte",
]
[[package]]
name = "vte"
version = "0.15.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
dependencies = [
"arrayvec",
"memchr",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
@@ -5965,11 +5978,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
version = "0.14.3+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
dependencies = [
"wit-bindgen-rt",
"wit-bindgen",
]
[[package]]
@@ -6632,12 +6645,11 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wiremock"
version = "0.6.4"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a"
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
dependencies = [
"assert-json-diff",
"async-trait",
"base64 0.22.1",
"deadpool",
"futures",
@@ -6655,13 +6667,10 @@ dependencies = [
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.3",
]
checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
[[package]]
name = "writeable"
@@ -6671,20 +6680,20 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "x11rb"
version = "0.13.1"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix 0.38.44",
"rustix 1.0.8",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.1"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xml5ever"
+1 -2
View File
@@ -65,5 +65,4 @@ lto = "fat" # Maximum link-time optimization
strip = "debuginfo" # Fully strip debug info in production artifacts
[patch.crates-io]
# ratatui = { path = "../../ratatui" }
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }
# No overrides; use crates.io ratatui release
+1 -1
View File
@@ -26,7 +26,7 @@ tokio = { version = "1", features = [
] }
toml = "0.9.5"
tracing = { version = "0.1.41", features = ["log"] }
wiremock = "0.6"
[dev-dependencies]
tempfile = "3"
wiremock = "0.6"
+4 -1
View File
@@ -94,4 +94,7 @@ chrono = { version = "0.4", features = ["serde"] }
insta = "1.43.1"
pretty_assertions = "1"
rand = "0.9"
vt100 = "0.16.2"
# Downgrade vt100 to avoid a hard requirement on unicode-width 0.2.1,
# which conflicts with ratatui 0.29.x pinning 0.2.0. This crate is
# only used in tests.
vt100 = "0.15.0"
+90 -13
View File
@@ -254,6 +254,11 @@ pub(crate) struct ChatWidget<'a> {
// Track active task ids so we don't drop the working status while any
// agent/sub‑agent is still running (long‑running sessions can interleave).
active_task_ids: HashSet<String>,
// Event sequencing to preserve original order across streaming/tool events
next_seq: u64,
// Sequence marker for the final assistant message in the current turn
seq_answer_final: Option<u64>,
}
struct PendingJumpBack {
@@ -875,11 +880,43 @@ impl ChatWidget<'_> {
F1: FnOnce(&mut interrupts::InterruptManager),
F2: FnOnce(&mut Self),
{
if self.is_write_cycle_active() {
defer_fn(&mut self.interrupts);
} else {
handle_fn(self);
}
if self.is_write_cycle_active() { defer_fn(&mut self.interrupts); } else { handle_fn(self); }
}
fn next_sequence(&mut self) -> u64 {
let s = self.next_seq; self.next_seq = self.next_seq.saturating_add(1); s
}
fn index_of_final_assistant(&self) -> Option<usize> {
self.history_cells.iter().rposition(|c| c.as_any().downcast_ref::<history_cell::AssistantMarkdownCell>().is_some())
}
fn maybe_move_last_before_final_assistant_exec(&mut self, call_id: &str) {
let assistant_idx = match self.index_of_final_assistant() { Some(i) => i, None => return };
if self.history_cells.is_empty() { return; }
let last_idx = self.history_cells.len() - 1; if last_idx <= assistant_idx { return; }
let cell = self.history_cells.remove(last_idx); self.history_cells.insert(assistant_idx, cell);
if let Some(rc) = self.running_commands.get_mut(call_id) { rc.history_index = Some(assistant_idx); }
self.invalidate_height_cache(); self.request_redraw();
}
fn maybe_move_last_before_final_assistant_tool(&mut self, call_id: &str) {
let assistant_idx = match self.index_of_final_assistant() { Some(i) => i, None => return };
if self.history_cells.is_empty() { return; }
let last_idx = self.history_cells.len() - 1; if last_idx <= assistant_idx { return; }
let cell = self.history_cells.remove(last_idx); self.history_cells.insert(assistant_idx, cell);
if let Some(idx) = self.running_custom_tools.get_mut(call_id) { *idx = assistant_idx; }
self.invalidate_height_cache(); self.request_redraw();
}
fn maybe_move_last_before_final_assistant(&mut self, seq: u64) {
let ans_seq = match self.seq_answer_final { Some(s) => s, None => return };
if seq >= ans_seq { return; }
let assistant_idx = match self.index_of_final_assistant() { Some(i) => i, None => return };
if self.history_cells.is_empty() { return; }
let last_idx = self.history_cells.len() - 1; if last_idx <= assistant_idx { return; }
let cell = self.history_cells.remove(last_idx); self.history_cells.insert(assistant_idx, cell);
self.invalidate_height_cache(); self.request_redraw();
}
@@ -1752,6 +1789,8 @@ impl ChatWidget<'_> {
pending_jump_back: None,
active_task_ids: HashSet::new(),
browser_is_external: false,
next_seq: 1,
seq_answer_final: None,
};
// Note: Initial redraw needs to be triggered after widget is added to app_state
@@ -1870,6 +1909,8 @@ impl ChatWidget<'_> {
pending_jump_back: None,
active_task_ids: HashSet::new(),
browser_is_external: false,
next_seq: 1,
seq_answer_final: None,
}
}
@@ -2836,6 +2877,8 @@ impl ChatWidget<'_> {
self.mark_needs_redraw();
}
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
// Record sequence for the final assistant message
self.seq_answer_final = Some(self.next_sequence());
tracing::debug!(
"AgentMessage final id={} bytes={} preview={:?}",
id,
@@ -3153,7 +3196,8 @@ impl ChatWidget<'_> {
// If a stream is active, defer until the stream finalizes so
// the plan block doesn't split a heading and its content.
if self.is_write_cycle_active() {
self.interrupts.push_plan_update(update);
let seq = self.next_sequence();
self.interrupts.push_plan_update(seq, update);
} else {
self.add_to_history(history_cell::new_plan_update(update));
}
@@ -3161,8 +3205,9 @@ impl ChatWidget<'_> {
EventMsg::ExecApprovalRequest(ev) => {
let id2 = id.clone();
let ev2 = ev.clone();
let seq = self.next_sequence();
self.defer_or_handle(
|interrupts| interrupts.push_exec_approval(id, ev),
move |interrupts| interrupts.push_exec_approval(seq, id, ev),
|this| {
this.finalize_active_stream();
this.flush_interrupt_queue();
@@ -3174,8 +3219,9 @@ impl ChatWidget<'_> {
EventMsg::ApplyPatchApprovalRequest(ev) => {
let id2 = id.clone();
let ev2 = ev.clone();
let seq = self.next_sequence();
self.defer_or_handle(
|interrupts| interrupts.push_apply_patch_approval(id, ev),
move |interrupts| interrupts.push_apply_patch_approval(seq, id, ev),
|this| {
this.finalize_active_stream();
this.flush_interrupt_queue();
@@ -3186,8 +3232,9 @@ impl ChatWidget<'_> {
}
EventMsg::ExecCommandBegin(ev) => {
let ev2 = ev.clone();
let seq = self.next_sequence();
self.defer_or_handle(
|interrupts| interrupts.push_exec_begin(ev),
move |interrupts| interrupts.push_exec_begin(seq, ev),
|this| {
// Finalize any active streaming sections, then establish
// the running Exec cell before flushing queued interrupts.
@@ -3241,22 +3288,25 @@ impl ChatWidget<'_> {
}
EventMsg::PatchApplyEnd(ev) => {
let ev2 = ev.clone();
let seq = self.next_sequence();
self.defer_or_handle(
|interrupts| interrupts.push_patch_end(ev),
move |interrupts| interrupts.push_patch_end(seq, ev),
|this| this.handle_patch_apply_end_now(ev2),
);
}
EventMsg::ExecCommandEnd(ev) => {
let ev2 = ev.clone();
let seq = self.next_sequence();
self.defer_or_handle(
|interrupts| interrupts.push_exec_end(ev),
move |interrupts| interrupts.push_exec_end(seq, ev),
|this| this.handle_exec_end_now(ev2),
);
}
EventMsg::McpToolCallBegin(ev) => {
let ev2 = ev.clone();
let seq = self.next_sequence();
self.defer_or_handle(
|interrupts| interrupts.push_mcp_begin(ev),
move |interrupts| interrupts.push_mcp_begin(seq, ev),
|this| {
this.finalize_active_stream();
this.flush_interrupt_queue();
@@ -3266,8 +3316,9 @@ impl ChatWidget<'_> {
}
EventMsg::McpToolCallEnd(ev) => {
let ev2 = ev.clone();
let seq = self.next_sequence();
self.defer_or_handle(
|interrupts| interrupts.push_mcp_end(ev),
move |interrupts| interrupts.push_mcp_end(seq, ev),
|this| this.handle_mcp_end_now(ev2),
);
}
@@ -3319,6 +3370,32 @@ impl ChatWidget<'_> {
Ok(content) => (true, content),
Err(error) => (false, error),
};
// Special-case web_fetch to render returned markdown nicely.
if tool_name == "web_fetch" {
let completed = history_cell::new_completed_web_fetch_tool_call(
&self.config,
params_string,
duration,
success,
content,
);
if let Some(idx) = self.running_custom_tools.remove(&call_id) {
if idx < self.history_cells.len() {
self.history_cells[idx] = Box::new(completed);
self.invalidate_height_cache();
self.request_redraw();
} else {
self.add_to_history(completed);
}
} else {
self.add_to_history(completed);
}
// After tool completes, likely transitioning to response
self.bottom_pane.update_status_text("responding".to_string());
self.maybe_hide_spinner();
return;
}
let completed = history_cell::new_completed_custom_tool_call(
tool_name,
params_string,
+67 -37
View File
@@ -1,4 +1,4 @@
use std::collections::VecDeque;
// (none)
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::ExecApprovalRequestEvent;
@@ -13,80 +13,110 @@ use super::ChatWidget;
#[derive(Debug)]
pub(crate) enum QueuedInterrupt {
ExecApproval(String, ExecApprovalRequestEvent),
ApplyPatchApproval(String, ApplyPatchApprovalRequestEvent),
ExecBegin(ExecCommandBeginEvent),
ExecEnd(ExecCommandEndEvent),
McpBegin(McpToolCallBeginEvent),
McpEnd(McpToolCallEndEvent),
PatchEnd(PatchApplyEndEvent),
PlanUpdate(UpdatePlanArgs),
ExecApproval { seq: u64, id: String, ev: ExecApprovalRequestEvent },
ApplyPatchApproval { seq: u64, id: String, ev: ApplyPatchApprovalRequestEvent },
ExecBegin { seq: u64, ev: ExecCommandBeginEvent },
ExecEnd { seq: u64, ev: ExecCommandEndEvent },
McpBegin { seq: u64, ev: McpToolCallBeginEvent },
McpEnd { seq: u64, ev: McpToolCallEndEvent },
PatchEnd { seq: u64, ev: PatchApplyEndEvent },
PlanUpdate { seq: u64, ev: UpdatePlanArgs },
}
#[derive(Default)]
pub(crate) struct InterruptManager {
queue: VecDeque<QueuedInterrupt>,
queue: Vec<QueuedInterrupt>,
}
impl InterruptManager {
pub(crate) fn new() -> Self {
Self {
queue: VecDeque::new(),
queue: Vec::new(),
}
}
pub(crate) fn push_exec_approval(&mut self, id: String, ev: ExecApprovalRequestEvent) {
self.queue.push_back(QueuedInterrupt::ExecApproval(id, ev));
pub(crate) fn push_exec_approval(&mut self, seq: u64, id: String, ev: ExecApprovalRequestEvent) {
self.queue.push(QueuedInterrupt::ExecApproval { seq, id, ev });
}
pub(crate) fn push_apply_patch_approval(
&mut self,
seq: u64,
id: String,
ev: ApplyPatchApprovalRequestEvent,
) {
self.queue
.push_back(QueuedInterrupt::ApplyPatchApproval(id, ev));
self.queue.push(QueuedInterrupt::ApplyPatchApproval { seq, id, ev });
}
pub(crate) fn push_exec_begin(&mut self, ev: ExecCommandBeginEvent) {
self.queue.push_back(QueuedInterrupt::ExecBegin(ev));
pub(crate) fn push_exec_begin(&mut self, seq: u64, ev: ExecCommandBeginEvent) {
self.queue.push(QueuedInterrupt::ExecBegin { seq, ev });
}
pub(crate) fn push_exec_end(&mut self, ev: ExecCommandEndEvent) {
self.queue.push_back(QueuedInterrupt::ExecEnd(ev));
pub(crate) fn push_exec_end(&mut self, seq: u64, ev: ExecCommandEndEvent) {
self.queue.push(QueuedInterrupt::ExecEnd { seq, ev });
}
pub(crate) fn push_mcp_begin(&mut self, ev: McpToolCallBeginEvent) {
self.queue.push_back(QueuedInterrupt::McpBegin(ev));
pub(crate) fn push_mcp_begin(&mut self, seq: u64, ev: McpToolCallBeginEvent) {
self.queue.push(QueuedInterrupt::McpBegin { seq, ev });
}
pub(crate) fn push_mcp_end(&mut self, ev: McpToolCallEndEvent) {
self.queue.push_back(QueuedInterrupt::McpEnd(ev));
pub(crate) fn push_mcp_end(&mut self, seq: u64, ev: McpToolCallEndEvent) {
self.queue.push(QueuedInterrupt::McpEnd { seq, ev });
}
pub(crate) fn push_patch_end(&mut self, ev: PatchApplyEndEvent) {
self.queue.push_back(QueuedInterrupt::PatchEnd(ev));
pub(crate) fn push_patch_end(&mut self, seq: u64, ev: PatchApplyEndEvent) {
self.queue.push(QueuedInterrupt::PatchEnd { seq, ev });
}
pub(crate) fn push_plan_update(&mut self, ev: UpdatePlanArgs) {
self.queue.push_back(QueuedInterrupt::PlanUpdate(ev));
pub(crate) fn push_plan_update(&mut self, seq: u64, ev: UpdatePlanArgs) {
self.queue.push(QueuedInterrupt::PlanUpdate { seq, ev });
}
pub(crate) fn flush_all(&mut self, chat: &mut ChatWidget<'_>) {
while let Some(q) = self.queue.pop_front() {
// Ensure stable order
self.queue.sort_by(|a, b| seq_of(a).cmp(&seq_of(b)));
for q in self.queue.drain(..) {
match q {
QueuedInterrupt::ExecApproval(id, ev) => chat.handle_exec_approval_now(id, ev),
QueuedInterrupt::ApplyPatchApproval(id, ev) => {
chat.handle_apply_patch_approval_now(id, ev)
QueuedInterrupt::ExecApproval { id, ev, .. } => chat.handle_exec_approval_now(id, ev),
QueuedInterrupt::ApplyPatchApproval { seq, id, ev } => {
chat.handle_apply_patch_approval_now(id, ev);
chat.maybe_move_last_before_final_assistant(seq);
}
QueuedInterrupt::ExecBegin { ev, .. } => {
let call_id = ev.call_id.clone();
chat.handle_exec_begin_now(ev);
chat.maybe_move_last_before_final_assistant_exec(&call_id);
}
QueuedInterrupt::ExecEnd { ev, .. } => chat.handle_exec_end_now(ev),
QueuedInterrupt::McpBegin { ev, .. } => {
let call_id = ev.call_id.clone();
chat.handle_mcp_begin_now(ev);
chat.maybe_move_last_before_final_assistant_tool(&call_id);
}
QueuedInterrupt::McpEnd { ev, .. } => chat.handle_mcp_end_now(ev),
QueuedInterrupt::PatchEnd { seq, ev, .. } => {
chat.handle_patch_apply_end_now(ev);
chat.maybe_move_last_before_final_assistant(seq);
}
QueuedInterrupt::PlanUpdate { seq, ev, .. } => {
chat.add_to_history(crate::history_cell::new_plan_update(ev));
chat.maybe_move_last_before_final_assistant(seq);
}
QueuedInterrupt::ExecBegin(ev) => chat.handle_exec_begin_now(ev),
QueuedInterrupt::ExecEnd(ev) => chat.handle_exec_end_now(ev),
QueuedInterrupt::McpBegin(ev) => chat.handle_mcp_begin_now(ev),
QueuedInterrupt::McpEnd(ev) => chat.handle_mcp_end_now(ev),
QueuedInterrupt::PatchEnd(ev) => chat.handle_patch_apply_end_now(ev),
QueuedInterrupt::PlanUpdate(ev) => chat.add_to_history(crate::history_cell::new_plan_update(ev)),
}
}
}
}
fn seq_of(q: &QueuedInterrupt) -> u64 {
match q {
QueuedInterrupt::ExecApproval { seq, .. }
| QueuedInterrupt::ApplyPatchApproval { seq, .. }
| QueuedInterrupt::ExecBegin { seq, .. }
| QueuedInterrupt::ExecEnd { seq, .. }
| QueuedInterrupt::McpBegin { seq, .. }
| QueuedInterrupt::McpEnd { seq, .. }
| QueuedInterrupt::PatchEnd { seq, .. }
| QueuedInterrupt::PlanUpdate { seq, .. } => *seq,
}
}
+66
View File
@@ -4908,6 +4908,72 @@ pub(crate) fn new_completed_custom_tool_call(
}
}
/// Completed web_fetch tool call with markdown rendering of the `markdown` field.
pub(crate) fn new_completed_web_fetch_tool_call(
cfg: &Config,
args: Option<String>,
duration: Duration,
success: bool,
result: String,
) -> ToolCallCell {
let duration = format_duration(duration);
let status_str = if success { "Complete" } else { "Error" };
let title_line = if success {
Line::from(vec![
Span::styled(status_str, Style::default().fg(crate::colors::success())),
format!(", duration: {duration}").dim(),
])
} else {
Line::from(vec![
Span::styled(status_str, Style::default().fg(crate::colors::error())),
format!(", duration: {duration}").dim(),
])
};
let invocation_str = if let Some(args) = args {
format!("{}({})", "web_fetch", args)
} else {
format!("{}()", "web_fetch")
};
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(title_line);
lines.push(Line::styled(
invocation_str,
Style::default()
.fg(crate::colors::text_dim())
.add_modifier(Modifier::ITALIC),
));
// Try to parse JSON and extract the markdown field
let mut appended_markdown = false;
if !result.is_empty() {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&result) {
if let Some(md) = value.get("markdown").and_then(|v| v.as_str()) {
lines.push(Line::from(""));
crate::markdown::append_markdown(md, &mut lines, cfg);
appended_markdown = true;
}
}
}
// Fallback: compact preview if JSON parse failed or no markdown present
if !appended_markdown && !result.is_empty() {
lines.push(Line::from(""));
let preview = build_preview_lines(&result, true)
.into_iter()
.map(|l| l.style(Style::default().fg(crate::colors::text_dim())))
.collect::<Vec<_>>();
lines.extend(preview);
}
lines.push(Line::from(""));
ToolCallCell {
lines,
state: if success { ToolState::Success } else { ToolState::Failed },
}
}
// Map `browser_*` tool names to friendly titles
fn browser_tool_title(tool_name: &str) -> &'static str {
match tool_name {
+30 -7
View File
@@ -4,7 +4,7 @@ use std::io::Write;
use crate::tui;
use crossterm::Command;
use crossterm::cursor::MoveTo;
use crossterm::cursor::{MoveTo, MoveToNextLine};
use crossterm::queue;
use crossterm::style::Color as CColor;
use crossterm::style::Colors;
@@ -13,6 +13,7 @@ use crossterm::style::SetAttribute;
use crossterm::style::SetBackgroundColor;
use crossterm::style::SetColors;
use crossterm::style::SetForegroundColor;
use crossterm::terminal::{Clear, ClearType};
use ratatui::layout::Size;
use ratatui::style::Color;
use ratatui::style::Modifier;
@@ -63,14 +64,15 @@ pub fn insert_history_lines_to_writer<B, W>(
queue!(writer, SetScrollRegion(top_1based..screen_size.height)).ok();
queue!(writer, MoveTo(0, area.top())).ok();
for _ in 0..scroll_amount {
// Reverse Index (RI): ESC M
queue!(writer, Print("\x1bM")).ok();
// Reverse Index (RI)
queue!(writer, ReverseIndex).ok();
}
queue!(writer, ResetScrollRegion).ok();
let cursor_top = area.top().saturating_sub(1);
// Adjust our local notion of area to account for the pre-scroll,
// but avoid touching ratatui::Terminal internals (set_viewport_area is private).
area.y += scroll_amount;
terminal.set_viewport_area(area);
cursor_top
} else {
area.top().saturating_sub(1)
@@ -108,10 +110,10 @@ pub fn insert_history_lines_to_writer<B, W>(
queue!(writer, MoveTo(0, cursor_top)).ok();
for line in wrapped {
queue!(writer, Print("\r\n")).ok();
queue!(writer, Print("\x1b[K")).ok(); // Clear to end of line with current bg
queue!(writer, MoveToNextLine(1)).ok();
queue!(writer, Clear(ClearType::UntilNewLine)).ok(); // Clear to end of line with current bg
write_spans(writer, line.iter()).ok();
queue!(writer, Print("\x1b[K")).ok(); // Clear remainder of line after content
queue!(writer, Clear(ClearType::UntilNewLine)).ok(); // Clear remainder of line after content
}
queue!(writer, ResetScrollRegion).ok();
@@ -124,6 +126,27 @@ pub fn insert_history_lines_to_writer<B, W>(
writer.flush().ok();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReverseIndex;
impl Command for ReverseIndex {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
// RI (Reverse Index): ESC M
write!(f, "\x1bM")
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
// Use ANSI path through ConPTY; WinAPI equivalent isn't exposed.
Ok(())
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
true
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetScrollRegion(pub std::ops::Range<u16>);