diff --git a/codex-cli/.pack/package/package.json b/codex-cli/.pack/package/package.json index 497d80476cf1..dc6a77168094 100644 --- a/codex-cli/.pack/package/package.json +++ b/codex-cli/.pack/package/package.json @@ -2,7 +2,7 @@ "name": "@just-every/code", "version": "0.2.148", "license": "Apache-2.0", - "description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Codex", + "description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Code", "bin": { "coder": "bin/coder.js" }, diff --git a/codex-cli/package.json b/codex-cli/package.json index 497d80476cf1..dc6a77168094 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -2,7 +2,7 @@ "name": "@just-every/code", "version": "0.2.148", "license": "Apache-2.0", - "description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Codex", + "description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Code", "bin": { "coder": "bin/coder.js" }, diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index fc405e1a2d85..ecb7aa7af4e9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -78,7 +78,7 @@ checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -87,6 +87,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -120,9 +126,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -150,22 +156,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -182,9 +188,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arboard" -version = "3.6.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" dependencies = [ "clipboard-win", "image 0.25.8", @@ -196,7 +202,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "x11rb", ] @@ -208,7 +214,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -232,6 +238,50 @@ dependencies = [ "term", ] +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.104", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -302,7 +352,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -313,7 +363,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -398,6 +448,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "beef" version = "0.5.2" @@ -442,9 +501,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bitstream-io" @@ -486,9 +545,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -525,11 +584,10 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.36" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ - "find-msvc-tools", "jobserver", "libc", "shlex", @@ -553,9 +611,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -637,16 +695,17 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -696,7 +755,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -805,6 +864,7 @@ name = "codex-cli" version = "0.0.0" dependencies = [ "anyhow", + "assert_cmd", "clap", "clap_complete", "codex-arg0", @@ -820,6 +880,8 @@ dependencies = [ "codex-version", "flate2", "futures", + "predicates", + "pretty_assertions", "regex", "reqwest", "serde", @@ -850,6 +912,7 @@ name = "codex-core" version = "0.0.0" dependencies = [ "anyhow", + "askama", "assert_cmd", "async-channel", "base64 0.22.1", @@ -857,6 +920,7 @@ dependencies = [ "chrono", "codex-apply-patch", "codex-browser", + "codex-file-search", "codex-mcp-client", "codex-protocol", "codex-version", @@ -867,7 +931,6 @@ dependencies = [ "fs2", "futures", "htmd", - "image 0.25.8", "img_hash", "landlock", "lazy_static", @@ -875,6 +938,7 @@ dependencies = [ "maplit", "mcp-types", "mime_guess", + "openssl-sys", "os_info", "portable-pty", "predicates", @@ -905,7 +969,6 @@ dependencies = [ "uuid", "walkdir", "which", - "whoami", "wildmatch", "wiremock", ] @@ -934,6 +997,8 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "uuid", + "walkdir", "wiremock", ] @@ -1286,6 +1351,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1370,7 +1445,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "crossterm_winapi", "mio", "parking_lot", @@ -1436,7 +1511,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1447,7 +1522,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1487,9 +1562,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -1533,7 +1608,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "unicode-xid", ] @@ -1545,7 +1620,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "unicode-xid", ] @@ -1607,8 +1682,8 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.0", + "redox_users 0.5.0", + "windows-sys 0.60.2", ] [[package]] @@ -1628,7 +1703,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", ] @@ -1650,7 +1725,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1694,14 +1769,14 @@ checksum = "83e195b4945e88836d826124af44fdcb262ec01ef94d44f14f4fb5103f19892a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "dyn-clone" -version = "1.0.20" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "either" @@ -1724,6 +1799,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endian-type" version = "0.1.2" @@ -1747,7 +1831,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1796,7 +1880,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1832,9 +1916,9 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1910,7 +1994,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1956,12 +2040,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "find-msvc-tools" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" - [[package]] name = "fixed_decimal" version = "0.7.0" @@ -2010,11 +2088,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -2095,7 +2188,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2155,19 +2248,19 @@ dependencies = [ [[package]] name = "gethostname" -version = "1.0.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ - "rustix 1.0.8", - "windows-targets 0.52.6", + "libc", + "windows-targets 0.48.5", ] [[package]] name = "getopts" -version = "0.2.24" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ "unicode-width 0.2.0", ] @@ -2195,7 +2288,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.4+wasi-0.2.4", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] @@ -2231,14 +2324,14 @@ dependencies = [ "bstr", "log", "regex-automata", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", ] [[package]] name = "h2" -version = "0.4.12" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -2246,7 +2339,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.1", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2281,9 +2374,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -2325,34 +2418,26 @@ dependencies = [ [[package]] name = "htmd" -version = "0.2.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be5de9d3e28558a12296e07d6cd7789c879417fbb3f07bdc77f1dd1df74ccbd8" +checksum = "ad1642def6e8e4dc182941f35454f7d2af917787f91f3f5133300030b41006d0" dependencies = [ - "html-escape", "html5ever", "markup5ever_rcdom", ] -[[package]] -name = "html-escape" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "utf8-width", -] - [[package]] name = "html5ever" -version = "0.31.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" dependencies = [ "log", "mac", "markup5ever", - "match_token", + "proc-macro2", + "quote", + "syn 2.0.104", ] [[package]] @@ -2401,6 +2486,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "1.7.0" @@ -2441,6 +2535,22 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.16" @@ -2460,9 +2570,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2634,9 +2746,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", @@ -2738,9 +2850,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indenter" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" @@ -2755,12 +2867,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.15.4", "serde", ] @@ -2791,7 +2903,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2802,25 +2914,25 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" dependencies = [ "rustversion", ] [[package]] name = "io-uring" -version = "0.7.10" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "libc", ] @@ -2927,7 +3039,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2964,9 +3076,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -3042,13 +3154,19 @@ dependencies = [ "cc", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -3089,9 +3207,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "logos" @@ -3131,7 +3249,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.15.4", ] [[package]] @@ -3167,20 +3285,23 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.16.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e4cd8c02f18a011991a039855480c64d74291c5792fcc160d55d77dc4de4a39" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", "tendril", - "web_atoms", ] [[package]] name = "markup5ever_rcdom" -version = "0.5.3-unofficial" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "853740b93240b82f68a23d8b296b2d19fc81521c298fcae44bf34bed6e445f00" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" dependencies = [ "html5ever", "markup5ever", @@ -3188,17 +3309,6 @@ dependencies = [ "xml5ever", ] -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "matchers" version = "0.2.0" @@ -3323,6 +3433,23 @@ dependencies = [ "serde", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -3350,7 +3477,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -3431,7 +3558,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -3519,7 +3646,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -3531,7 +3658,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "dispatch2", "objc2", ] @@ -3542,7 +3669,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -3561,7 +3688,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", "objc2-core-foundation", ] @@ -3572,7 +3699,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", "objc2-core-foundation", ] @@ -3604,7 +3731,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "libc", "once_cell", "onig_sys", @@ -3620,6 +3747,60 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.5.1+3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -3705,9 +3886,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -3716,7 +3897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.11.1", + "indexmap 2.10.0", ] [[package]] @@ -3782,7 +3963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap 2.11.1", + "indexmap 2.10.0", "quick-xml", "serde", "time", @@ -3794,7 +3975,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "crc32fast", "fdeflate", "flate2", @@ -3839,9 +4020,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "serde", "zerovec", @@ -3919,9 +4100,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -3942,7 +4123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -3951,7 +4132,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "getopts", "memchr", "pulldown-cmark-escape", @@ -3990,9 +4171,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" dependencies = [ "memchr", ] @@ -4142,7 +4323,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cassowary", "compact_str", "crossterm", @@ -4245,11 +4426,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", ] [[package]] @@ -4265,9 +4446,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.16", "libredox", @@ -4291,30 +4472,30 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", ] [[package]] @@ -4331,9 +4512,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" @@ -4350,17 +4531,22 @@ dependencies = [ "async-compression", "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -4371,6 +4557,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -4430,15 +4617,15 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.106", + "syn 2.0.104", "unicode-ident", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -4483,7 +4670,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4496,7 +4683,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", @@ -4505,9 +4692,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "once_cell", "ring", @@ -4540,9 +4727,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rustyline" @@ -4550,7 +4737,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "clipboard-win", "fd-lock", @@ -4581,6 +4768,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "schemafy" version = "0.5.2" @@ -4668,7 +4864,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4686,39 +4882,73 @@ dependencies = [ "libc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.224" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "6aaeb1e94f53b16384af593c71e20b095e958dab1d26939c1b70645c5cfbcc0b" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "fe07b5d88710e3b807c16a06ccbc9dfecd5fff6a4d2745c59e3e26774f10de6a" dependencies = [ "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.224" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f39390fa6346e24defbcdd3d9544ba8a19985d0af74df8501fbfe9a64341ab" +dependencies = [ + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.224" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "87ff78ab5e8561c9a675bfc1785cb07ae721f0ee53329a595cefd8c04c2ac4e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4729,7 +4959,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4738,7 +4968,7 @@ version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -4753,7 +4983,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4796,7 +5026,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.1", + "indexmap 2.10.0", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -4815,7 +5045,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4905,9 +5135,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -5028,7 +5258,7 @@ dependencies = [ "dupe", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5149,7 +5379,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5161,7 +5391,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5192,9 +5422,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -5218,7 +5448,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5234,7 +5464,7 @@ dependencies = [ "once_cell", "onig", "plist", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", "serde", "serde_derive", "serde_json", @@ -5252,6 +5482,27 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -5284,15 +5535,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.21.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5328,12 +5579,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ "rustix 1.0.8", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5388,7 +5639,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5399,7 +5650,7 @@ checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5427,11 +5678,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.43" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", + "itoa", "libc", "num-conv", "num_threads", @@ -5443,15 +5695,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -5531,7 +5783,17 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -5599,7 +5861,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.10.0", "serde", "serde_spanned 1.0.0", "toml_datetime 0.7.0", @@ -5632,7 +5894,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.10.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5645,7 +5907,7 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.10.0", "toml_datetime 0.7.0", "toml_parser", "toml_writer", @@ -5688,7 +5950,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "bytes", "futures-util", "http", @@ -5744,7 +6006,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5820,7 +6082,7 @@ checksum = "ccd2a058a86cfece0bf96f7cce1021efef9c8ed0e892ab74639173e5ed7a34fa" dependencies = [ "cc", "regex", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -5868,7 +6130,7 @@ checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "termcolor", ] @@ -5983,9 +6245,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -6005,12 +6267,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8-width" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6052,6 +6308,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -6133,51 +6395,44 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.4+wasi-0.2.4" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "wit-bindgen", + "wit-bindgen-rt", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", - "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.101" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.51" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -6188,9 +6443,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6198,22 +6453,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] @@ -6233,9 +6488,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.78" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -6251,25 +6506,13 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - [[package]] name = "webbrowser" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "jni", "log", "ndk-context", @@ -6306,22 +6549,11 @@ dependencies = [ "winsafe", ] -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", - "web-sys", -] - [[package]] name = "wildmatch" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" +checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382" [[package]] name = "winapi" @@ -6341,11 +6573,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.11" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -6385,7 +6617,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link 0.1.3", + "windows-link", "windows-result 0.3.4", "windows-strings 0.4.2", ] @@ -6398,7 +6630,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6409,7 +6641,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6420,7 +6652,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6431,7 +6663,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6441,10 +6673,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "windows-link" -version = "0.2.0" +name = "windows-registry" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] [[package]] name = "windows-result" @@ -6461,7 +6698,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -6480,7 +6717,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -6525,16 +6762,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", -] - -[[package]] -name = "windows-sys" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" -dependencies = [ - "windows-link 0.2.0", + "windows-targets 0.53.2", ] [[package]] @@ -6585,11 +6813,10 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ - "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -6782,9 +7009,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -6838,10 +7065,13 @@ dependencies = [ ] [[package]] -name = "wit-bindgen" -version = "0.45.1" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] [[package]] name = "writeable" @@ -6851,20 +7081,20 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x11rb" -version = "0.13.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix 1.0.8", + "rustix 0.38.44", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.13.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xattr" @@ -6878,9 +7108,9 @@ dependencies = [ [[package]] name = "xml5ever" -version = "0.22.1" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a91563ba5a5ab749488164063f1317e327ca1daa80f00e5bd1e670ad0d78154" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" dependencies = [ "log", "mac", @@ -6922,28 +7152,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6963,7 +7193,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "synstructure", ] @@ -6986,9 +7216,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -7003,7 +7233,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -7035,9 +7265,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" dependencies = [ "zune-core", ] diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index 20ff6f746de6..48a6c03b384d 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -63,6 +63,11 @@ pub enum ApplyPatchError { /// Error that occurs while computing replacements when applying patch chunks #[error("{0}")] ComputeReplacements(String), + /// A raw patch body was provided without an explicit `apply_patch` invocation. + #[error( + "patch detected without explicit call to apply_patch. Rerun as [\"apply_patch\", \"\"]" + )] + ImplicitInvocation, } impl From for ApplyPatchError { @@ -116,26 +121,13 @@ pub struct ApplyPatchArgs { pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch { match argv { + // Direct invocation: apply_patch [cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => match parse_patch(body) { Ok(source) => MaybeApplyPatch::Body(source), Err(e) => MaybeApplyPatch::PatchParseError(e), }, - // Handle common shell wrappers: bash/sh/zsh with -lc or -c - [shell, flag, script] - if { - // accept absolute paths too (e.g., /bin/bash, /usr/bin/sh) - let shell_name = std::path::Path::new(shell) - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or(""); - let is_shell = matches!(shell_name, "bash" | "sh" | "zsh"); - let is_flag = matches!(flag.as_str(), "-lc" | "-c"); - let starts_with_apply = APPLY_PATCH_COMMANDS - .iter() - .any(|cmd| script.trim_start().starts_with(cmd)); - is_shell && is_flag && starts_with_apply - } => - { + // Bash heredoc form: (optional `cd &&`) apply_patch <<'EOF' ... + [bash, flag, script] if bash == "bash" && flag == "-lc" => { match extract_apply_patch_from_bash(script) { Ok((body, _maybe_cd)) => match parse_patch(&body) { Ok(source) => MaybeApplyPatch::Body(source), @@ -242,6 +234,26 @@ impl ApplyPatchAction { /// cwd must be an absolute path so that we can resolve relative paths in the /// patch. pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApplyPatchVerified { + // Detect a raw patch body passed directly as the command or as the body of a bash -lc + // script. In these cases, report an explicit error rather than applying the patch. + match argv { + [body] => { + if parse_patch(body).is_ok() { + return MaybeApplyPatchVerified::CorrectnessError( + ApplyPatchError::ImplicitInvocation, + ); + } + } + [bash, flag, script] if bash == "bash" && flag == "-lc" => { + if parse_patch(script).is_ok() { + return MaybeApplyPatchVerified::CorrectnessError( + ApplyPatchError::ImplicitInvocation, + ); + } + } + _ => {} + } + match maybe_parse_apply_patch(argv) { MaybeApplyPatch::Body(ApplyPatchArgs { patch, @@ -950,6 +962,28 @@ mod tests { )); } + #[test] + fn test_implicit_patch_single_arg_is_error() { + let patch = "*** Begin Patch\n*** Add File: foo\n+hi\n*** End Patch".to_string(); + let args = vec![patch]; + let dir = tempdir().unwrap(); + assert!(matches!( + maybe_parse_apply_patch_verified(&args, dir.path()), + MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation) + )); + } + + #[test] + fn test_implicit_patch_bash_script_is_error() { + let script = "*** Begin Patch\n*** Add File: foo\n+hi\n*** End Patch"; + let args = args_bash(script); + let dir = tempdir().unwrap(); + assert!(matches!( + maybe_parse_apply_patch_verified(&args, dir.path()), + MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation) + )); + } + #[test] fn test_literal() { let args = strs_to_strings(&[ diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index dd944e6552a5..8b3ca1c5c97a 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -39,7 +39,7 @@ tokio = { version = "1", features = [ "signal", ] } tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing-subscriber = "0.3.20" codex-protocol-ts = { path = "../protocol-ts" } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip", "json"] } zip = { version = "0.6", default-features = false, features = ["deflate"] } @@ -49,3 +49,9 @@ tempfile = "3" which = "6" futures = "0.3" uuid = { version = "1", features = ["v4"] } + +[dev-dependencies] +assert_cmd = "2" +predicates = "3" +pretty_assertions = "1" +tempfile = "3" diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 0e7e6a972bff..84e80728f3a7 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -19,6 +19,9 @@ use codex_exec::Cli as ExecCli; use codex_tui::Cli as TuiCli; use std::path::PathBuf; +mod mcp_cmd; + +use crate::mcp_cmd::McpCli; use crate::proto::ProtoCli; /// Codex CLI @@ -59,8 +62,8 @@ enum Subcommand { /// Remove stored authentication credentials. Logout(LogoutCommand), - /// Experimental: run Codex as an MCP server. - Mcp, + /// [experimental] Run Codex as an MCP server and manage MCP servers. + Mcp(McpCli), /// Run the Protocol stream via stdin/stdout #[clap(visible_alias = "p")] @@ -80,6 +83,9 @@ enum Subcommand { #[clap(visible_alias = "a")] Apply(ApplyCommand), + /// Resume a previous interactive session (picker by default; use --last to continue the most recent). + Resume(ResumeCommand), + /// Internal: generate TypeScript protocol bindings. #[clap(hide = true)] GenerateTs(GenerateTsCommand), @@ -101,6 +107,21 @@ struct CompletionCommand { shell: Shell, } +#[derive(Debug, Parser)] +struct ResumeCommand { + /// Conversation/session id (UUID). When provided, resumes this session. + /// If omitted, use --last to pick the most recent recorded session. + #[arg(value_name = "SESSION_ID")] + session_id: Option, + + /// Continue the most recent session without showing the picker. + #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")] + last: bool, + + #[clap(flatten)] + config_overrides: TuiCli, +} + #[derive(Debug, Parser)] struct DebugArgs { #[command(subcommand)] @@ -182,26 +203,54 @@ fn main() -> anyhow::Result<()> { } async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { - let cli = MultitoolCli::parse(); + let MultitoolCli { + config_overrides: root_config_overrides, + mut interactive, + subcommand, + } = MultitoolCli::parse(); - match cli.subcommand { + match subcommand { None => { - let mut tui_cli = cli.interactive; - prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides); - let usage = codex_tui::run_main(tui_cli, codex_linux_sandbox_exe).await?; + prepend_config_flags( + &mut interactive.config_overrides, + root_config_overrides.clone(), + ); + let usage = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; if !usage.is_zero() { println!("{}", codex_core::protocol::FinalOutput::from(usage)); } } Some(Subcommand::Exec(mut exec_cli)) => { - prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut exec_cli.config_overrides, + root_config_overrides.clone(), + ); codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; } - Some(Subcommand::Mcp) => { - codex_mcp_server::run_main(codex_linux_sandbox_exe, cli.config_overrides).await?; + Some(Subcommand::Mcp(mut mcp_cli)) => { + // Propagate any root-level config overrides (e.g. `-c key=value`). + prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone()); + mcp_cli.run(codex_linux_sandbox_exe).await?; + } + Some(Subcommand::Resume(ResumeCommand { + session_id, + last, + config_overrides, + })) => { + interactive = finalize_resume_interactive( + interactive, + root_config_overrides.clone(), + session_id, + last, + config_overrides, + ); + codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; } Some(Subcommand::Login(mut login_cli)) => { - prepend_config_flags(&mut login_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut login_cli.config_overrides, + root_config_overrides.clone(), + ); match login_cli.action { Some(LoginSubcommand::Status) => { run_login_status(login_cli.config_overrides).await; @@ -216,11 +265,17 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } } Some(Subcommand::Logout(mut logout_cli)) => { - prepend_config_flags(&mut logout_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut logout_cli.config_overrides, + root_config_overrides.clone(), + ); run_logout(logout_cli.config_overrides).await; } Some(Subcommand::Proto(mut proto_cli)) => { - prepend_config_flags(&mut proto_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut proto_cli.config_overrides, + root_config_overrides.clone(), + ); proto::run_main(proto_cli).await?; } Some(Subcommand::Completion(completion_cli)) => { @@ -228,7 +283,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } Some(Subcommand::Debug(debug_args)) => match debug_args.cmd { DebugCommand::Seatbelt(mut seatbelt_cli) => { - prepend_config_flags(&mut seatbelt_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut seatbelt_cli.config_overrides, + root_config_overrides.clone(), + ); codex_cli::debug_sandbox::run_command_under_seatbelt( seatbelt_cli, codex_linux_sandbox_exe, @@ -236,7 +294,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() .await?; } DebugCommand::Landlock(mut landlock_cli) => { - prepend_config_flags(&mut landlock_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut landlock_cli.config_overrides, + root_config_overrides.clone(), + ); codex_cli::debug_sandbox::run_command_under_landlock( landlock_cli, codex_linux_sandbox_exe, @@ -245,7 +306,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } }, Some(Subcommand::Apply(mut apply_cli)) => { - prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut apply_cli.config_overrides, + root_config_overrides.clone(), + ); run_apply_command(apply_cli, None).await?; } Some(Subcommand::GenerateTs(gen_cli)) => { @@ -261,7 +325,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() preview_main(args).await?; } Some(Subcommand::Llm(mut llm_cli)) => { - prepend_config_flags(&mut llm_cli.config_overrides, cli.config_overrides); + prepend_config_flags( + &mut llm_cli.config_overrides, + root_config_overrides.clone(), + ); run_llm(llm_cli).await?; } } @@ -280,6 +347,67 @@ fn prepend_config_flags( .splice(0..0, cli_config_overrides.raw_overrides); } +/// Build the final `TuiCli` for a `codex resume` invocation. +fn finalize_resume_interactive( + mut interactive: TuiCli, + root_config_overrides: CliConfigOverrides, + _session_id: Option, + _last: bool, + resume_cli: TuiCli, +) -> TuiCli { + // Our fork does not expose explicit resume fields on the TUI CLI. + // We simply merge resume-scoped flags and root overrides and run the TUI. + + // Merge resume-scoped flags and overrides with highest precedence. + merge_resume_cli_flags(&mut interactive, resume_cli); + + // Propagate any root-level config overrides (e.g. `-c key=value`). + prepend_config_flags(&mut interactive.config_overrides, root_config_overrides); + + interactive +} + +/// Merge flags provided to `codex resume` so they take precedence over any +/// root-level flags. Only overrides fields explicitly set on the resume-scoped +/// CLI. Also appends `-c key=value` overrides with highest precedence. +fn merge_resume_cli_flags(interactive: &mut TuiCli, resume_cli: TuiCli) { + if let Some(model) = resume_cli.model { + interactive.model = Some(model); + } + if resume_cli.oss { + interactive.oss = true; + } + if let Some(profile) = resume_cli.config_profile { + interactive.config_profile = Some(profile); + } + if let Some(sandbox) = resume_cli.sandbox_mode { + interactive.sandbox_mode = Some(sandbox); + } + if let Some(approval) = resume_cli.approval_policy { + interactive.approval_policy = Some(approval); + } + if resume_cli.full_auto { + interactive.full_auto = true; + } + if resume_cli.dangerously_bypass_approvals_and_sandbox { + interactive.dangerously_bypass_approvals_and_sandbox = true; + } + if let Some(cwd) = resume_cli.cwd { + interactive.cwd = Some(cwd); + } + if !resume_cli.images.is_empty() { + interactive.images = resume_cli.images; + } + if let Some(prompt) = resume_cli.prompt { + interactive.prompt = Some(prompt); + } + + interactive + .config_overrides + .raw_overrides + .extend(resume_cli.config_overrides.raw_overrides); +} + fn print_completion(cmd: CompletionCommand) { let mut app = MultitoolCli::command(); let name = "codex"; @@ -682,3 +810,134 @@ async fn doctor_main() -> anyhow::Result<()> { Ok(()) } +#[cfg(test)] +mod tests { + use super::*; + + fn finalize_from_args(args: &[&str]) -> TuiCli { + let cli = MultitoolCli::try_parse_from(args).expect("parse"); + let MultitoolCli { + interactive, + config_overrides: root_overrides, + subcommand, + } = cli; + + let Subcommand::Resume(ResumeCommand { + session_id, + last, + config_overrides: resume_cli, + }) = subcommand.expect("resume present") + else { + unreachable!() + }; + + finalize_resume_interactive(interactive, root_overrides, session_id, last, resume_cli) + } + + #[test] + fn resume_model_flag_applies_when_no_root_flags() { + let interactive = finalize_from_args(["codex", "resume", "-m", "gpt-5-test"].as_ref()); + + assert_eq!(interactive.model.as_deref(), Some("gpt-5-test")); + assert!(interactive.resume_picker); + assert!(!interactive.resume_last); + assert_eq!(interactive.resume_session_id, None); + } + + #[test] + fn resume_picker_logic_none_and_not_last() { + let interactive = finalize_from_args(["codex", "resume"].as_ref()); + assert!(interactive.resume_picker); + assert!(!interactive.resume_last); + assert_eq!(interactive.resume_session_id, None); + } + + #[test] + fn resume_picker_logic_last() { + let interactive = finalize_from_args(["codex", "resume", "--last"].as_ref()); + assert!(!interactive.resume_picker); + assert!(interactive.resume_last); + assert_eq!(interactive.resume_session_id, None); + } + + #[test] + fn resume_picker_logic_with_session_id() { + let interactive = finalize_from_args(["codex", "resume", "1234"].as_ref()); + assert!(!interactive.resume_picker); + assert!(!interactive.resume_last); + assert_eq!(interactive.resume_session_id.as_deref(), Some("1234")); + } + + #[test] + fn resume_merges_option_flags_and_full_auto() { + let interactive = finalize_from_args( + [ + "codex", + "resume", + "sid", + "--oss", + "--full-auto", + "--search", + "--sandbox", + "workspace-write", + "--ask-for-approval", + "on-request", + "-m", + "gpt-5-test", + "-p", + "my-profile", + "-C", + "/tmp", + "-i", + "/tmp/a.png,/tmp/b.png", + ] + .as_ref(), + ); + + assert_eq!(interactive.model.as_deref(), Some("gpt-5-test")); + assert!(interactive.oss); + assert_eq!(interactive.config_profile.as_deref(), Some("my-profile")); + assert!(matches!( + interactive.sandbox_mode, + Some(codex_common::SandboxModeCliArg::WorkspaceWrite) + )); + assert!(matches!( + interactive.approval_policy, + Some(codex_common::ApprovalModeCliArg::OnRequest) + )); + assert!(interactive.full_auto); + assert_eq!( + interactive.cwd.as_deref(), + Some(std::path::Path::new("/tmp")) + ); + assert!(interactive.web_search); + let has_a = interactive + .images + .iter() + .any(|p| p == std::path::Path::new("/tmp/a.png")); + let has_b = interactive + .images + .iter() + .any(|p| p == std::path::Path::new("/tmp/b.png")); + assert!(has_a && has_b); + assert!(!interactive.resume_picker); + assert!(!interactive.resume_last); + assert_eq!(interactive.resume_session_id.as_deref(), Some("sid")); + } + + #[test] + fn resume_merges_dangerously_bypass_flag() { + let interactive = finalize_from_args( + [ + "codex", + "resume", + "--dangerously-bypass-approvals-and-sandbox", + ] + .as_ref(), + ); + assert!(interactive.dangerously_bypass_approvals_and_sandbox); + assert!(interactive.resume_picker); + assert!(!interactive.resume_last); + assert_eq!(interactive.resume_session_id, None); + } +} diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs new file mode 100644 index 000000000000..437511ad572d --- /dev/null +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -0,0 +1,370 @@ +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::path::PathBuf; + +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; +use anyhow::bail; +use codex_common::CliConfigOverrides; +use codex_core::config::Config; +use codex_core::config::ConfigOverrides; +use codex_core::config::find_codex_home; +use codex_core::config::load_global_mcp_servers; +use codex_core::config::write_global_mcp_servers; +use codex_core::config_types::McpServerConfig; + +/// [experimental] Launch Codex as an MCP server or manage configured MCP servers. +/// +/// Subcommands: +/// - `serve` — run the MCP server on stdio +/// - `list` — list configured servers (with `--json`) +/// - `get` — show a single server (with `--json`) +/// - `add` — add a server launcher entry to `~/.codex/config.toml` +/// - `remove` — delete a server entry +#[derive(Debug, clap::Parser)] +pub struct McpCli { + #[clap(flatten)] + pub config_overrides: CliConfigOverrides, + + #[command(subcommand)] + pub cmd: Option, +} + +#[derive(Debug, clap::Subcommand)] +pub enum McpSubcommand { + /// [experimental] Run the Codex MCP server (stdio transport). + Serve, + + /// [experimental] List configured MCP servers. + List(ListArgs), + + /// [experimental] Show details for a configured MCP server. + Get(GetArgs), + + /// [experimental] Add a global MCP server entry. + Add(AddArgs), + + /// [experimental] Remove a global MCP server entry. + Remove(RemoveArgs), +} + +#[derive(Debug, clap::Parser)] +pub struct ListArgs { + /// Output the configured servers as JSON. + #[arg(long)] + pub json: bool, +} + +#[derive(Debug, clap::Parser)] +pub struct GetArgs { + /// Name of the MCP server to display. + pub name: String, + + /// Output the server configuration as JSON. + #[arg(long)] + pub json: bool, +} + +#[derive(Debug, clap::Parser)] +pub struct AddArgs { + /// Name for the MCP server configuration. + pub name: String, + + /// Environment variables to set when launching the server. + #[arg(long, value_parser = parse_env_pair, value_name = "KEY=VALUE")] + pub env: Vec<(String, String)>, + + /// Command to launch the MCP server. + #[arg(trailing_var_arg = true, num_args = 1..)] + pub command: Vec, +} + +#[derive(Debug, clap::Parser)] +pub struct RemoveArgs { + /// Name of the MCP server configuration to remove. + pub name: String, +} + +impl McpCli { + pub async fn run(self, codex_linux_sandbox_exe: Option) -> Result<()> { + let McpCli { + config_overrides, + cmd, + } = self; + let subcommand = cmd.unwrap_or(McpSubcommand::Serve); + + match subcommand { + McpSubcommand::Serve => { + codex_mcp_server::run_main(codex_linux_sandbox_exe, config_overrides).await?; + } + McpSubcommand::List(args) => { + run_list(&config_overrides, args)?; + } + McpSubcommand::Get(args) => { + run_get(&config_overrides, args)?; + } + McpSubcommand::Add(args) => { + run_add(&config_overrides, args)?; + } + McpSubcommand::Remove(args) => { + run_remove(&config_overrides, args)?; + } + } + + Ok(()) + } +} + +fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> { + // Validate any provided overrides even though they are not currently applied. + config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + + let AddArgs { name, env, command } = add_args; + + validate_server_name(&name)?; + + let mut command_parts = command.into_iter(); + let command_bin = command_parts + .next() + .ok_or_else(|| anyhow!("command is required"))?; + let command_args: Vec = command_parts.collect(); + + let env_map = if env.is_empty() { + None + } else { + let mut map = HashMap::new(); + for (key, value) in env { + map.insert(key, value); + } + Some(map) + }; + + let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; + let mut servers = load_global_mcp_servers(&codex_home) + .with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?; + + let new_entry = McpServerConfig { + command: command_bin, + args: command_args, + env: env_map, + startup_timeout_ms: None, + }; + + servers.insert(name.clone(), new_entry); + + write_global_mcp_servers(&codex_home, &servers) + .with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?; + + println!("Added global MCP server '{name}'."); + + Ok(()) +} + +fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) -> Result<()> { + config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + + let RemoveArgs { name } = remove_args; + + validate_server_name(&name)?; + + let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; + let mut servers = load_global_mcp_servers(&codex_home) + .with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?; + + let removed = servers.remove(&name).is_some(); + + if removed { + write_global_mcp_servers(&codex_home, &servers) + .with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?; + } + + if removed { + println!("Removed global MCP server '{name}'."); + } else { + println!("No MCP server named '{name}' found."); + } + + Ok(()) +} + +fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Result<()> { + let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default()) + .context("failed to load configuration")?; + + let mut entries: Vec<_> = config.mcp_servers.iter().collect(); + entries.sort_by(|(a, _), (b, _)| a.cmp(b)); + + if list_args.json { + let json_entries: Vec<_> = entries + .into_iter() + .map(|(name, cfg)| { + let env = cfg.env.as_ref().map(|env| { + env.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>() + }); + serde_json::json!({ + "name": name, + "command": cfg.command, + "args": cfg.args, + "env": env, + "startup_timeout_ms": cfg.startup_timeout_ms, + }) + }) + .collect(); + let output = serde_json::to_string_pretty(&json_entries)?; + println!("{output}"); + return Ok(()); + } + + if entries.is_empty() { + println!("No MCP servers configured yet. Try `codex mcp add my-tool -- my-command`."); + return Ok(()); + } + + let mut rows: Vec<[String; 4]> = Vec::new(); + for (name, cfg) in entries { + let args = if cfg.args.is_empty() { + "-".to_string() + } else { + cfg.args.join(" ") + }; + + let env = match cfg.env.as_ref() { + None => "-".to_string(), + Some(map) if map.is_empty() => "-".to_string(), + Some(map) => { + let mut pairs: Vec<_> = map.iter().collect(); + pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs + .into_iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join(", ") + } + }; + + rows.push([name.clone(), cfg.command.clone(), args, env]); + } + + let mut widths = ["Name".len(), "Command".len(), "Args".len(), "Env".len()]; + for row in &rows { + for (i, cell) in row.iter().enumerate() { + widths[i] = widths[i].max(cell.len()); + } + } + + println!( + "{: Result<()> { + let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default()) + .context("failed to load configuration")?; + + let Some(server) = config.mcp_servers.get(&get_args.name) else { + bail!("No MCP server named '{name}' found.", name = get_args.name); + }; + + if get_args.json { + let env = server.env.as_ref().map(|env| { + env.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>() + }); + let output = serde_json::to_string_pretty(&serde_json::json!({ + "name": get_args.name, + "command": server.command, + "args": server.args, + "env": env, + "startup_timeout_ms": server.startup_timeout_ms, + }))?; + println!("{output}"); + return Ok(()); + } + + println!("{}", get_args.name); + println!(" command: {}", server.command); + let args = if server.args.is_empty() { + "-".to_string() + } else { + server.args.join(" ") + }; + println!(" args: {args}"); + let env_display = match server.env.as_ref() { + None => "-".to_string(), + Some(map) if map.is_empty() => "-".to_string(), + Some(map) => { + let mut pairs: Vec<_> = map.iter().collect(); + pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs + .into_iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join(", ") + } + }; + println!(" env: {env_display}"); + if let Some(timeout) = server.startup_timeout_ms { + println!(" startup_timeout_ms: {timeout}"); + } + println!(" remove: codex mcp remove {}", get_args.name); + + Ok(()) +} + +fn parse_env_pair(raw: &str) -> Result<(String, String), String> { + let mut parts = raw.splitn(2, '='); + let key = parts + .next() + .map(str::trim) + .filter(|s| !s.is_empty()) + .ok_or_else(|| "environment entries must be in KEY=VALUE form".to_string())?; + let value = parts + .next() + .map(str::to_string) + .ok_or_else(|| "environment entries must be in KEY=VALUE form".to_string())?; + + Ok((key.to_string(), value)) +} + +fn validate_server_name(name: &str) -> Result<()> { + let is_valid = !name.is_empty() + && name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); + + if is_valid { + Ok(()) + } else { + bail!("invalid server name '{name}' (use letters, numbers, '-', '_')"); + } +} diff --git a/codex-rs/cli/tests/mcp_add_remove.rs b/codex-rs/cli/tests/mcp_add_remove.rs new file mode 100644 index 000000000000..9e54f0d86783 --- /dev/null +++ b/codex-rs/cli/tests/mcp_add_remove.rs @@ -0,0 +1,86 @@ +use std::path::Path; + +use anyhow::Result; +use codex_core::config::load_global_mcp_servers; +use predicates::str::contains; +use pretty_assertions::assert_eq; +use tempfile::TempDir; + +fn codex_command(codex_home: &Path) -> Result { + let mut cmd = assert_cmd::Command::cargo_bin("codex")?; + cmd.env("CODEX_HOME", codex_home); + Ok(cmd) +} + +#[test] +fn add_and_remove_server_updates_global_config() -> Result<()> { + let codex_home = TempDir::new()?; + + let mut add_cmd = codex_command(codex_home.path())?; + add_cmd + .args(["mcp", "add", "docs", "--", "echo", "hello"]) + .assert() + .success() + .stdout(contains("Added global MCP server 'docs'.")); + + let servers = load_global_mcp_servers(codex_home.path())?; + assert_eq!(servers.len(), 1); + let docs = servers.get("docs").expect("server should exist"); + assert_eq!(docs.command, "echo"); + assert_eq!(docs.args, vec!["hello".to_string()]); + assert!(docs.env.is_none()); + + let mut remove_cmd = codex_command(codex_home.path())?; + remove_cmd + .args(["mcp", "remove", "docs"]) + .assert() + .success() + .stdout(contains("Removed global MCP server 'docs'.")); + + let servers = load_global_mcp_servers(codex_home.path())?; + assert!(servers.is_empty()); + + let mut remove_again_cmd = codex_command(codex_home.path())?; + remove_again_cmd + .args(["mcp", "remove", "docs"]) + .assert() + .success() + .stdout(contains("No MCP server named 'docs' found.")); + + let servers = load_global_mcp_servers(codex_home.path())?; + assert!(servers.is_empty()); + + Ok(()) +} + +#[test] +fn add_with_env_preserves_key_order_and_values() -> Result<()> { + let codex_home = TempDir::new()?; + + let mut add_cmd = codex_command(codex_home.path())?; + add_cmd + .args([ + "mcp", + "add", + "envy", + "--env", + "FOO=bar", + "--env", + "ALPHA=beta", + "--", + "python", + "server.py", + ]) + .assert() + .success(); + + let servers = load_global_mcp_servers(codex_home.path())?; + let envy = servers.get("envy").expect("server should exist"); + let env = envy.env.as_ref().expect("env should be present"); + + assert_eq!(env.len(), 2); + assert_eq!(env.get("FOO"), Some(&"bar".to_string())); + assert_eq!(env.get("ALPHA"), Some(&"beta".to_string())); + + Ok(()) +} diff --git a/codex-rs/cli/tests/mcp_list.rs b/codex-rs/cli/tests/mcp_list.rs new file mode 100644 index 000000000000..e53f42cc8f70 --- /dev/null +++ b/codex-rs/cli/tests/mcp_list.rs @@ -0,0 +1,106 @@ +use std::path::Path; + +use anyhow::Result; +use predicates::str::contains; +use pretty_assertions::assert_eq; +use serde_json::Value as JsonValue; +use tempfile::TempDir; + +fn codex_command(codex_home: &Path) -> Result { + let mut cmd = assert_cmd::Command::cargo_bin("codex")?; + cmd.env("CODEX_HOME", codex_home); + Ok(cmd) +} + +#[test] +fn list_shows_empty_state() -> Result<()> { + let codex_home = TempDir::new()?; + + let mut cmd = codex_command(codex_home.path())?; + let output = cmd.args(["mcp", "list"]).output()?; + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout)?; + assert!(stdout.contains("No MCP servers configured yet.")); + + Ok(()) +} + +#[test] +fn list_and_get_render_expected_output() -> Result<()> { + let codex_home = TempDir::new()?; + + let mut add = codex_command(codex_home.path())?; + add.args([ + "mcp", + "add", + "docs", + "--env", + "TOKEN=secret", + "--", + "docs-server", + "--port", + "4000", + ]) + .assert() + .success(); + + let mut list_cmd = codex_command(codex_home.path())?; + let list_output = list_cmd.args(["mcp", "list"]).output()?; + assert!(list_output.status.success()); + let stdout = String::from_utf8(list_output.stdout)?; + assert!(stdout.contains("Name")); + assert!(stdout.contains("docs")); + assert!(stdout.contains("docs-server")); + assert!(stdout.contains("TOKEN=secret")); + + let mut list_json_cmd = codex_command(codex_home.path())?; + let json_output = list_json_cmd.args(["mcp", "list", "--json"]).output()?; + assert!(json_output.status.success()); + let stdout = String::from_utf8(json_output.stdout)?; + let parsed: JsonValue = serde_json::from_str(&stdout)?; + let array = parsed.as_array().expect("expected array"); + assert_eq!(array.len(), 1); + let entry = &array[0]; + assert_eq!(entry.get("name"), Some(&JsonValue::String("docs".into()))); + assert_eq!( + entry.get("command"), + Some(&JsonValue::String("docs-server".into())) + ); + + let args = entry + .get("args") + .and_then(|v| v.as_array()) + .expect("args array"); + assert_eq!( + args, + &vec![ + JsonValue::String("--port".into()), + JsonValue::String("4000".into()) + ] + ); + + let env = entry + .get("env") + .and_then(|v| v.as_object()) + .expect("env map"); + assert_eq!(env.get("TOKEN"), Some(&JsonValue::String("secret".into()))); + + let mut get_cmd = codex_command(codex_home.path())?; + let get_output = get_cmd.args(["mcp", "get", "docs"]).output()?; + assert!(get_output.status.success()); + let stdout = String::from_utf8(get_output.stdout)?; + assert!(stdout.contains("docs")); + assert!(stdout.contains("command: docs-server")); + assert!(stdout.contains("args: --port 4000")); + assert!(stdout.contains("env: TOKEN=secret")); + assert!(stdout.contains("remove: codex mcp remove docs")); + + let mut get_json_cmd = codex_command(codex_home.path())?; + get_json_cmd + .args(["mcp", "get", "docs", "--json"]) + .assert() + .success() + .stdout(contains("\"name\": \"docs\"")); + + Ok(()) +} diff --git a/codex-rs/common/src/elapsed.rs b/codex-rs/common/src/elapsed.rs index 3cc3d547ce41..297832b1ec93 100644 --- a/codex-rs/common/src/elapsed.rs +++ b/codex-rs/common/src/elapsed.rs @@ -2,7 +2,7 @@ use std::time::Duration; use std::time::Instant; /// Returns a string representing the elapsed time since `start_time` like -/// "1m15s" or "1.50s". +/// "1m 15s" or "1.50s". pub fn format_elapsed(start_time: Instant) -> String { format_duration(start_time.elapsed()) } @@ -12,7 +12,7 @@ pub fn format_elapsed(start_time: Instant) -> String { /// Formatting rules: /// * < 1 s -> "{milli}ms" /// * < 60 s -> "{sec:.2}s" (two decimal places) -/// * >= 60 s -> "{min}m{sec:02}s" +/// * >= 60 s -> "{min}m {sec:02}s" pub fn format_duration(duration: Duration) -> String { let millis = duration.as_millis() as i64; format_elapsed_millis(millis) @@ -26,7 +26,7 @@ fn format_elapsed_millis(millis: i64) -> String { } else { let minutes = millis / 60_000; let seconds = (millis % 60_000) / 1000; - format!("{minutes}m{seconds:02}s") + format!("{minutes}m {seconds:02}s") } } @@ -61,12 +61,18 @@ mod tests { fn test_format_duration_minutes() { // Durations ≥ 1 minute should be printed mmss. let dur = Duration::from_millis(75_000); // 1m15s - assert_eq!(format_duration(dur), "1m15s"); + assert_eq!(format_duration(dur), "1m 15s"); let dur_exact = Duration::from_millis(60_000); // 1m0s - assert_eq!(format_duration(dur_exact), "1m00s"); + assert_eq!(format_duration(dur_exact), "1m 00s"); let dur_long = Duration::from_millis(3_601_000); - assert_eq!(format_duration(dur_long), "60m01s"); + assert_eq!(format_duration(dur_long), "60m 01s"); + } + + #[test] + fn test_format_duration_one_hour_has_space() { + let dur_hour = Duration::from_millis(3_600_000); + assert_eq!(format_duration(dur_hour), "60m 00s"); } } diff --git a/codex-rs/common/src/model_presets.rs b/codex-rs/common/src/model_presets.rs index 2b6e5dfb92a8..065bb1e340d9 100644 --- a/codex-rs/common/src/model_presets.rs +++ b/codex-rs/common/src/model_presets.rs @@ -1,4 +1,6 @@ +use codex_core::config::GPT_5_CODEX_MEDIUM_MODEL; use codex_core::protocol_config_types::ReasoningEffort; +use codex_protocol::mcp_protocol::AuthMode; /// A simple preset pairing a model slug with a reasoning effort. #[derive(Debug, Clone, Copy)] @@ -12,50 +14,68 @@ pub struct ModelPreset { /// Model slug (e.g., "gpt-5"). pub model: &'static str, /// Reasoning effort to apply for this preset. - pub effort: ReasoningEffort, + pub effort: Option, } -/// Built-in list of model presets that pair a model with a reasoning effort. -/// -/// Keep this UI-agnostic so it can be reused by both TUI and MCP server. -pub fn builtin_model_presets() -> &'static [ModelPreset] { - // Order reflects effort from minimal to high. - const PRESETS: &[ModelPreset] = &[ - ModelPreset { - id: "gpt-5-minimal", - label: "gpt-5 minimal", - description: "— fastest responses with limited reasoning; ideal for coding, instructions, or lightweight tasks", - model: "gpt-5", - effort: ReasoningEffort::Minimal, - }, - ModelPreset { - id: "gpt-5-low", - label: "gpt-5 low", - description: "— balances speed with some reasoning; useful for straightforward queries and short explanations", - model: "gpt-5", - effort: ReasoningEffort::Low, - }, - ModelPreset { - id: "gpt-5-medium", - label: "gpt-5 medium", - description: "— default setting; provides a solid balance of reasoning depth and latency for general-purpose tasks", - model: "gpt-5", - effort: ReasoningEffort::Medium, - }, - ModelPreset { - id: "gpt-5-high", - label: "gpt-5 high", - description: "— maximizes reasoning depth for complex or ambiguous problems", - model: "gpt-5", - effort: ReasoningEffort::High, - }, - ModelPreset { - id: "gpt-5-high-new", - label: "gpt-5 high new", - description: "— our latest release tuned to rely on the model's built-in reasoning defaults", - model: "gpt-5-high-new", - effort: ReasoningEffort::Medium, - }, - ]; - PRESETS +const PRESETS: &[ModelPreset] = &[ + ModelPreset { + id: "gpt-5-codex-low", + label: "gpt-5-codex low", + description: "", + model: "gpt-5-codex", + effort: Some(ReasoningEffort::Low), + }, + ModelPreset { + id: "gpt-5-codex-medium", + label: "gpt-5-codex medium", + description: "", + model: "gpt-5-codex", + effort: None, + }, + ModelPreset { + id: "gpt-5-codex-high", + label: "gpt-5-codex high", + description: "", + model: "gpt-5-codex", + effort: Some(ReasoningEffort::High), + }, + ModelPreset { + id: "gpt-5-minimal", + label: "gpt-5 minimal", + description: "— fastest responses with limited reasoning; ideal for coding, instructions, or lightweight tasks", + model: "gpt-5", + effort: Some(ReasoningEffort::Minimal), + }, + ModelPreset { + id: "gpt-5-low", + label: "gpt-5 low", + description: "— balances speed with some reasoning; useful for straightforward queries and short explanations", + model: "gpt-5", + effort: Some(ReasoningEffort::Low), + }, + ModelPreset { + id: "gpt-5-medium", + label: "gpt-5 medium", + description: "— default setting; provides a solid balance of reasoning depth and latency for general-purpose tasks", + model: "gpt-5", + effort: Some(ReasoningEffort::Medium), + }, + ModelPreset { + id: "gpt-5-high", + label: "gpt-5 high", + description: "— maximizes reasoning depth for complex or ambiguous problems", + model: "gpt-5", + effort: Some(ReasoningEffort::High), + }, +]; + +pub fn builtin_model_presets(auth_mode: Option) -> Vec { + match auth_mode { + Some(AuthMode::ApiKey) => PRESETS + .iter() + .copied() + .filter(|p| p.model != GPT_5_CODEX_MEDIUM_MODEL) + .collect(), + _ => PRESETS.to_vec(), + } } diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 459723e21b1b..80bcc248ad89 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -13,41 +13,35 @@ workspace = true [dependencies] anyhow = "1" +askama = "0.12" async-channel = "2.3.1" base64 = "0.22" bytes = "1.10.1" chrono = { version = "0.4", features = ["serde"] } codex-apply-patch = { path = "../apply-patch" } -codex-browser = { path = "../browser" } +codex-file-search = { path = "../file-search" } codex-mcp-client = { path = "../mcp-client" } codex-protocol = { path = "../protocol" } dirs = "6" env-flags = "0.1.1" eventsource-stream = "0.2.3" futures = "0.3" -image = "0.25" -img_hash = "3.2" -lazy_static = "1.5" -libc = "0.2.174" +libc = "0.2.175" mcp-types = { path = "../mcp-types" } os_info = "3.12.0" portable-pty = "0.9.0" rand = "0.9" -regex-lite = "0.1.6" -reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] } +regex-lite = "0.1.7" +reqwest = { version = "0.12", features = ["json", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -htmd = "0.2.2" -url = "2" -mime_guess = "2.0.5" sha1 = "0.10.6" shlex = "1.3.0" similar = "2.7.0" strum_macros = "0.27.2" tempfile = "3" -thiserror = "2.0.12" +thiserror = "2.0.16" time = { version = "0.3", features = ["formatting", "parsing", "local-offset", "macros"] } -fs2 = "0.4" tokio = { version = "1", features = [ "io-std", "macros", @@ -59,25 +53,33 @@ tokio-util = "0.7.16" toml = "0.9.5" toml_edit = "0.23.4" tracing = { version = "0.1.41", features = ["log"] } -tree-sitter = "0.25.8" +tree-sitter = "0.25.9" tree-sitter-bash = "0.25.0" uuid = { version = "1", features = ["serde", "v4"] } -wildmatch = "2.4.0" -codex-version = { path = "../codex-version" } +which = "6" +wildmatch = "2.5.0" +lazy_static = "1" +mime_guess = "2" serde_bytes = "0.11" +fs2 = "0.4" +url = "2" +htmd = "0.1" +img_hash = "3" +codex-browser = { path = "../browser" } +codex-version = { path = "../codex-version" } [target.'cfg(target_os = "linux")'.dependencies] landlock = "0.4.1" seccompiler = "0.5.0" -# OpenSSL vendoring removed - using rustls-tls instead for all targets +# Build OpenSSL from source for musl builds. +[target.x86_64-unknown-linux-musl.dependencies] +openssl-sys = { version = "*", features = ["vendored"] } -[target.'cfg(target_os = "macos")'.dependencies] -whoami = "1.6" - -[target.'cfg(target_os = "windows")'.dependencies] -which = "6" +# Build OpenSSL from source for musl builds. +[target.aarch64-unknown-linux-musl.dependencies] +openssl-sys = { version = "*", features = ["vendored"] } [dev-dependencies] assert_cmd = "2" diff --git a/codex-rs/core/gpt_5_codex_prompt.md b/codex-rs/core/gpt_5_codex_prompt.md new file mode 100644 index 000000000000..2c49fafec62a --- /dev/null +++ b/codex-rs/core/gpt_5_codex_prompt.md @@ -0,0 +1,100 @@ +You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer. + +## General + +- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with ["bash", "-lc"]. +- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary. +- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.) + +## Editing constraints + +- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. +- Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like "Assigns the value to the variable", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare. +- You may be in a dirty git worktree. + * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user. + * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes. + * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them. + * If the changes are in unrelated files, just ignore them and don't revert them. +- While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed. + +## Plan tool + +When using the planning tool: +- Skip using the planning tool for straightforward tasks (roughly the easiest 25%). +- Do not make single-step plans. +- When you made a plan, update it after having performed one of the sub-tasks that you shared on the plan. + +## Codex CLI harness, sandboxing, and approvals + +The Codex CLI harness supports several different sandboxing, and approval configurations that the user can choose from. + +Filesystem sandboxing defines which files can be read or written. The options are: +- **read-only**: You can only read files. +- **workspace-write**: You can read files. You can write to files in this folder, but not outside it. +- **danger-full-access**: No filesystem sandboxing. + +Network sandboxing defines whether network can be accessed without approval. Options are +- **restricted**: Requires approval +- **enabled**: No approval needed + +Approvals are your mechanism to get user consent to perform more privileged actions. Although they introduce friction to the user because your work is paused until the user responds, you should leverage them to accomplish your important work. Do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to "never", in which case never ask for approvals. + +Approval options are +- **untrusted**: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands. +- **on-failure**: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox. +- **on-request**: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. (Note that this mode is not always available. If it is, you'll see parameters for it in the `shell` command description.) +- **never**: This is a non-interactive mode where you may NEVER ask the user for approval to run commands. Instead, you must always persist and work around constraints to solve the task for the user. You MUST do your utmost best to finish the task and validate your work before yielding. If this mode is paired with `danger-full-access`, take advantage of it to deliver the best outcome for the user. Further, in this mode, your default testing philosophy is overridden: Even if you don't see local patterns for testing, you may add tests and scripts to validate your work. Just remove them before yielding. + +When you are running with approvals `on-request`, and sandboxing enabled, here are scenarios where you'll need to request approval: +- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /tmp) +- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files. +- You are running sandboxed and need to run a command that requires network access (e.g. installing packages) +- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. +- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for +- (for all of these, you should weigh alternative paths that do not require approval) + +When sandboxing is set to read-only, you'll need to request approval for any command that isn't a read. + +You will be told what filesystem sandboxing, network sandboxing, and approval mode are active in a developer or user message. If you are not told about this, assume that you are running with workspace-write, network sandboxing enabled, and approval on-failure. + +## Special user requests + +- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so. +- If the user asks for a "review", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps. + +## Presenting your work and final message + +You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. + +- Default: be very concise; friendly coding teammate tone. +- Ask only when needed; suggest ideas; mirror the user's style. +- For substantial work, summarize clearly; follow final‑answer formatting. +- Skip heavy formatting for simple confirmations. +- Don't dump large files you've written; reference paths only. +- No "save/copy this file" - User is on the same machine. +- Offer logical next steps (tests, commits, build) briefly; add verify steps if you couldn't do something. +- For code changes: + * Lead with a quick explanation of the change, and then give more details on the context covering where and why a change was made. Do not start this explanation with "summary", just jump right in. + * If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. + * When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number. +- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result. + +### Final answer structure and style guidelines + +- Plain text; CLI handles styling. Use structure only when it helps scanability. +- Headers: optional; short Title Case (1-3 words) wrapped in **…**; no blank line before the first bullet; add only if they truly help. +- Bullets: use - ; merge related points; keep to one line when possible; 4–6 per list ordered by importance; keep phrasing consistent. +- Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **. +- Code samples or multi-line snippets should be wrapped in fenced code blocks; add a language hint whenever obvious. +- Structure: group related bullets; order sections general → specific → supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task. +- Tone: collaborative, concise, factual; present tense, active voice; self‑contained; no "above/below"; parallel wording. +- Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short—wrap/reformat if long; avoid naming formatting styles in answers. +- Adaptation: code explanations → precise, structured with code refs; simple tasks → lead with outcome; big changes → logical walkthrough + rationale + next actions; casual one-offs → plain sentences, no headers/bullets. +- File References: When referencing files in your response, make sure to include the relevant start line and always follow the below rules: + * Use inline code to make file paths clickable. + * Each reference should have a stand alone path. Even if it's the same file. + * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. + * Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1). + * Do not use URIs like file://, vscode://, or https://. + * Do not provide range of lines + * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 diff --git a/codex-rs/core/prompt.md b/codex-rs/core/prompt.md index cc7e930a5d58..ac073457018d 100644 --- a/codex-rs/core/prompt.md +++ b/codex-rs/core/prompt.md @@ -240,6 +240,16 @@ You are producing plain text that will later be styled by the CLI. Follow these - Apply to inline examples and to bullet keywords if the keyword itself is a literal file/command. - Never mix monospace and bold markers; choose one based on whether it’s a keyword (`**`) or inline code/path (`` ` ``). +**File References** +When referencing files in your response, make sure to include the relevant start line and always follow the below rules: + * Use inline code to make file paths clickable. + * Each reference should have a stand alone path. Even if it's the same file. + * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. + * Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1). + * Do not use URIs like file://, vscode://, or https://. + * Do not provide range of lines + * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 + **Structure** - Place related bullets together; don’t mix unrelated concepts in the same section. diff --git a/codex-rs/core/review_prompt.md b/codex-rs/core/review_prompt.md new file mode 100644 index 000000000000..01d93598a700 --- /dev/null +++ b/codex-rs/core/review_prompt.md @@ -0,0 +1,87 @@ +# Review guidelines: + +You are acting as a reviewer for a proposed code change made by another engineer. + +Below are some default guidelines for determining whether the original author would appreciate the issue being flagged. + +These are not the final word in determining whether an issue is a bug. In many cases, you will encounter other, more specific guidelines. These may be present elsewhere in a developer message, a user message, a file, or even elsewhere in this system message. +Those guidelines should be considered to override these general instructions. + +Here are the general guidelines for determining whether something is a bug and should be flagged. + +1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code. +2. The bug is discrete and actionable (i.e. not a general issue with the codebase or a combination of multiple issues). +3. Fixing the bug does not demand a level of rigor that is not present in the rest of the codebase (e.g. one doesn't need very detailed comments and input validation in a repository of one-off scripts in personal projects) +4. The bug was introduced in the commit (pre-existing bugs should not be flagged). +5. The author of the original PR would likely fix the issue if they were made aware of it. +6. The bug does not rely on unstated assumptions about the codebase or author's intent. +7. It is not enough to speculate that a change may disrupt another part of the codebase, to be considered a bug, one must identify the other parts of the code that are provably affected. +8. The bug is clearly not just an intentional change by the original author. + +When flagging a bug, you will also provide an accompanying comment. Once again, these guidelines are not the final word on how to construct a comment -- defer to any subsequent guidelines that you encounter. + +1. The comment should be clear about why the issue is a bug. +2. The comment should appropriately communicate the severity of the issue. It should not claim that an issue is more severe than it actually is. +3. The comment should be brief. The body should be at most 1 paragraph. It should not introduce line breaks within the natural language flow unless it is necessary for the code fragment. +4. The comment should not include any chunks of code longer than 3 lines. Any code chunks should be wrapped in markdown inline code tags or a code block. +5. The comment should clearly and explicitly communicate the scenarios, environments, or inputs that are necessary for the bug to arise. The comment should immediately indicate that the issue's severity depends on these factors. +6. The comment's tone should be matter-of-fact and not accusatory or overly positive. It should read as a helpful AI assistant suggestion without sounding too much like a human reviewer. +7. The comment should be written such that the original author can immediately grasp the idea without close reading. +8. The comment should avoid excessive flattery and comments that are not helpful to the original author. The comment should avoid phrasing like "Great job ...", "Thanks for ...". + +Below are some more detailed guidelines that you should apply to this specific review. + +HOW MANY FINDINGS TO RETURN: + +Output all findings that the original author would fix if they knew about it. If there is no finding that a person would definitely love to see and fix, prefer outputting no findings. Do not stop at the first qualifying finding. Continue until you've listed every qualifying finding. + +GUIDELINES: + +- Ignore trivial style unless it obscures meaning or violates documented standards. +- Use one comment per distinct issue (or a multi-line range if necessary). +- Use ```suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block). +- In every ```suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces). +- Do NOT introduce or remove outer indentation levels unless that is the actual fix. + +The comments will be presented in the code review as inline comments. You should avoid providing unnecessary location details in the comment body. Always keep the line range as short as possible for interpreting the issue. Avoid ranges longer than 5–10 lines; instead, choose the most suitable subrange that pinpoints the problem. + +At the beginning of the finding title, tag the bug with priority level. For example "[P1] Un-padding slices along wrong tensor dimensions". [P0] – Drop everything to fix. Blocking release, operations, or major usage. Only use for universal issues that do not depend on any assumptions about the inputs. · [P1] – Urgent. Should be addressed in the next cycle · [P2] – Normal. To be fixed eventually · [P3] – Low. Nice to have. + +Additionally, include a numeric priority field in the JSON output for each finding: set "priority" to 0 for P0, 1 for P1, 2 for P2, or 3 for P3. If a priority cannot be determined, omit the field or use null. + +At the end of your findings, output an "overall correctness" verdict of whether or not the patch should be considered "correct". +Correct implies that existing code and tests will not break, and the patch is free of bugs and other blocking issues. +Ignore non-blocking issues such as style, formatting, typos, documentation, and other nits. + +FORMATTING GUIDELINES: +The finding description should be one paragraph. + +OUTPUT FORMAT: + +## Output schema — MUST MATCH *exactly* + +```json +{ + "findings": [ + { + "title": "<≤ 80 chars, imperative>", + "body": "", + "confidence_score": , + "priority": , + "code_location": { + "absolute_file_path": "", + "line_range": {"start": , "end": } + } + } + ], + "overall_correctness": "patch is correct" | "patch is incorrect", + "overall_explanation": "<1-3 sentence explanation justifying the overall_correctness verdict>", + "overall_confidence_score": +} +``` + +* **Do not** wrap the JSON in markdown fences or extra prose. +* The code_location field is required and must include absolute_file_path and line_range. +*Line ranges must be as short as possible for interpreting the issue (avoid ranges over 5–10 lines; pick the most suitable subrange). +* The code_location should overlap with the diff. +* Do not generate a PR fix. \ No newline at end of file diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index ad583de3e3b8..8b4b485a6a0a 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -456,6 +456,32 @@ mod tests { assert_eq!(auth_dot_json, same_auth_dot_json); } + #[test] + fn login_with_api_key_overwrites_existing_auth_json() { + let dir = tempdir().unwrap(); + let auth_path = dir.path().join("auth.json"); + let stale_auth = json!({ + "OPENAI_API_KEY": "sk-old", + "tokens": { + "id_token": "stale.header.payload", + "access_token": "stale-access", + "refresh_token": "stale-refresh", + "account_id": "stale-acc" + } + }); + std::fs::write( + &auth_path, + serde_json::to_string_pretty(&stale_auth).unwrap(), + ) + .unwrap(); + + super::login_with_api_key(dir.path(), "sk-new").expect("login_with_api_key should succeed"); + + let auth = super::try_read_auth_json(&auth_path).expect("auth.json should parse"); + assert_eq!(auth.openai_api_key.as_deref(), Some("sk-new")); + assert!(auth.tokens.is_none(), "tokens should be cleared"); + } + #[tokio::test] async fn pro_account_with_no_api_key_uses_chatgpt_auth() { let codex_home = tempdir().unwrap(); diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ccb8b2a23ecd..fd7eec081940 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -30,6 +30,7 @@ use crate::client_common::ResponseStream; use crate::client_common::ResponsesApiRequest; use crate::client_common::create_reasoning_param_for_request; use crate::config::Config; +use crate::openai_model_info::get_model_info; use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::config_types::TextVerbosity as TextVerbosityConfig; @@ -119,6 +120,12 @@ impl ModelClient { self.verbosity } + pub fn get_auto_compact_token_limit(&self) -> Option { + self.config.model_auto_compact_token_limit.or_else(|| { + get_model_info(&self.config.model_family).and_then(|info| info.auto_compact_token_limit) + }) + } + /// Dispatches to either the Responses or Chat implementation depending on /// the provider config. Public callers always invoke `stream()` – the /// specialised helpers are private to avoid accidental misuse. @@ -188,7 +195,7 @@ impl ModelClient { let reasoning = create_reasoning_param_for_request( &self.config.model_family, - self.effort, + Some(self.effort), self.summary, ); @@ -215,6 +222,15 @@ impl ModelClient { }) }; + // In general, we want to explicitly send `store: false` when using the Responses API, + // but in practice, the Azure Responses API rejects `store: false`: + // + // - If store = false and id is sent an error is thrown that ID is not found + // - If store = false and id is not sent an error is thrown that ID is required + // + // For Azure, we send `store: true` and preserve reasoning item IDs. + let azure_workaround = self.provider.is_azure_responses_endpoint(); + let payload = ResponsesApiRequest { model: &self.config.model, instructions: &full_instructions, @@ -224,13 +240,19 @@ impl ModelClient { parallel_tool_calls: true, reasoning, text, - store, + store: azure_workaround, stream: true, include, // Use a stable per-process cache key (session id). With store=false this is inert. prompt_cache_key: Some(self.session_id.to_string()), }; + let mut payload_json = serde_json::to_value(&payload)?; + if azure_workaround { + attach_item_ids(&mut payload_json, &input_with_instructions); + } + let payload_body = serde_json::to_string(&payload_json)?; + let mut attempt = 0; let max_retries = self.provider.request_max_retries(); @@ -258,7 +280,7 @@ impl ModelClient { trace!( "POST to {}: {}", self.provider.get_full_url(&auth), - serde_json::to_string(&payload)? + payload_body.as_str() ); let mut req_builder = self @@ -269,11 +291,7 @@ impl ModelClient { req_builder = req_builder .header("OpenAI-Beta", "responses=v1") .header(reqwest::header::ACCEPT, "text/event-stream") - .json(&payload); - // Only include a session_id for ChatGPT auth where the backend expects it - if auth_mode == Some(AuthMode::ChatGPT) { - req_builder = req_builder.header("session_id", self.session_id.to_string()); - } + .json(&payload_json); // Avoid unstable `let` chains: expand into nested conditionals. if let Some(auth) = auth.as_ref() { @@ -562,6 +580,33 @@ struct ResponseCompletedOutputTokensDetails { reasoning_tokens: u64, } +fn attach_item_ids(payload_json: &mut Value, original_items: &[ResponseItem]) { + let Some(input_value) = payload_json.get_mut("input") else { + return; + }; + let serde_json::Value::Array(items) = input_value else { + return; + }; + + for (value, item) in items.iter_mut().zip(original_items.iter()) { + if let ResponseItem::Reasoning { id, .. } + | ResponseItem::Message { id: Some(id), .. } + | ResponseItem::WebSearchCall { id: Some(id), .. } + | ResponseItem::FunctionCall { id: Some(id), .. } + | ResponseItem::LocalShellCall { id: Some(id), .. } + | ResponseItem::CustomToolCall { id: Some(id), .. } = item + { + if id.is_empty() { + continue; + } + + if let Some(obj) = value.as_object_mut() { + obj.insert("id".to_string(), Value::String(id.clone())); + } + } + } +} + async fn process_sse( stream: S, tx_event: mpsc::Sender>, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 39d4203e4a11..e630a7b5a26a 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -12,15 +12,12 @@ use codex_protocol::models::ResponseItem; use futures::Stream; use serde::Serialize; use std::borrow::Cow; +use std::ops::Deref; use std::pin::Pin; use std::task::Context; use std::task::Poll; use tokio::sync::mpsc; -/// The `instructions` field in the payload sent to a model should always start -/// with this content. -const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); - /// Additional prompt for Code. Can not edit Codex instructions. const ADDITIONAL_INSTRUCTIONS: &str = include_str!("../prompt_coder.md"); @@ -31,6 +28,9 @@ const ENVIRONMENT_CONTEXT_END: &str = "\n\n"; /// wraps user instructions message in a tag for the model to parse more easily. const USER_INSTRUCTIONS_START: &str = "\n\n"; const USER_INSTRUCTIONS_END: &str = "\n\n"; +/// Review thread system prompt. Edit `core/src/review_prompt.md` to customize. +#[allow(dead_code)] +pub const REVIEW_PROMPT: &str = include_str!("../review_prompt.md"); /// API request payload for a single model turn #[derive(Default, Debug, Clone)] @@ -68,11 +68,12 @@ impl Prompt { let base = self .base_instructions_override .as_deref() - .unwrap_or(BASE_INSTRUCTIONS); + .unwrap_or(model.base_instructions.deref()); let mut sections: Vec<&str> = vec![base]; - // When there are no custom instructions, add apply_patch_tool_instructions if either: - // - the model needs special instructions (4.1), or + // When there are no custom instructions, add apply_patch_tool_instructions if: + // - the model needs special instructions (4.1) + // AND // - there is no apply_patch tool present let is_apply_patch_tool_present = self.tools.iter().any(|tool| match tool { OpenAiTool::Function(f) => f.name == "apply_patch", @@ -80,7 +81,8 @@ impl Prompt { _ => false, }); if self.base_instructions_override.is_none() - && (model.needs_special_apply_patch_instructions || !is_apply_patch_tool_present) + && model.needs_special_apply_patch_instructions + && !is_apply_patch_tool_present { sections.push(APPLY_PATCH_TOOL_INSTRUCTIONS); } @@ -207,8 +209,10 @@ pub enum ResponseEvent { #[derive(Debug, Serialize)] pub(crate) struct Reasoning { - pub(crate) effort: ReasoningEffortConfig, - pub(crate) summary: ReasoningSummaryConfig, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) effort: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) summary: Option, } /// Text configuration for verbosity level in OpenAI API responses. @@ -343,14 +347,17 @@ pub(crate) struct ResponsesApiRequest<'a> { pub(crate) fn create_reasoning_param_for_request( model_family: &ModelFamily, - effort: ReasoningEffortConfig, + effort: Option, summary: ReasoningSummaryConfig, ) -> Option { - if model_family.supports_reasoning_summaries { - Some(Reasoning { effort, summary }) - } else { - None + if !model_family.supports_reasoning_summaries { + return None; } + + Some(Reasoning { + effort, + summary: Some(summary), + }) } // Removed legacy TextControls helper; use `Text` with `OpenAiTextVerbosity` instead. @@ -370,18 +377,64 @@ impl Stream for ResponseStream { #[cfg(test)] mod tests { use crate::model_family::find_family_for_model; + use pretty_assertions::assert_eq; use super::*; + struct InstructionsTestCase { + pub slug: &'static str, + pub expects_apply_patch_instructions: bool, + } #[test] fn get_full_instructions_no_user_content() { let prompt = Prompt { ..Default::default() }; - let expected = format!("{BASE_INSTRUCTIONS}\n{APPLY_PATCH_TOOL_INSTRUCTIONS}"); - let model_family = find_family_for_model("gpt-4.1").expect("known model slug"); - let full = prompt.get_full_instructions(&model_family); - assert_eq!(full, expected); + let test_cases = vec![ + InstructionsTestCase { + slug: "gpt-3.5", + expects_apply_patch_instructions: true, + }, + InstructionsTestCase { + slug: "gpt-4.1", + expects_apply_patch_instructions: true, + }, + InstructionsTestCase { + slug: "gpt-4o", + expects_apply_patch_instructions: true, + }, + InstructionsTestCase { + slug: "gpt-5", + expects_apply_patch_instructions: true, + }, + InstructionsTestCase { + slug: "codex-mini-latest", + expects_apply_patch_instructions: true, + }, + InstructionsTestCase { + slug: "gpt-oss:120b", + expects_apply_patch_instructions: false, + }, + InstructionsTestCase { + slug: "gpt-5-codex", + expects_apply_patch_instructions: false, + }, + ]; + for test_case in test_cases { + let model_family = find_family_for_model(test_case.slug).expect("known model slug"); + let expected = if test_case.expects_apply_patch_instructions { + format!( + "{}\n{}", + model_family.clone().base_instructions, + APPLY_PATCH_TOOL_INSTRUCTIONS + ) + } else { + model_family.clone().base_instructions + }; + + let full = prompt.get_full_instructions(&model_family); + assert_eq!(full, expected); + } } #[test] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 7b175a2a8c60..7884c2dee5f7 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -418,6 +418,7 @@ use crate::shell; use crate::turn_diff_tracker::TurnDiffTracker; use crate::user_notification::UserNotification; use crate::util::backoff; +use crate::rollout::recorder::SessionStateSnapshot; use serde_json::Value; use crate::exec_command::ExecSessionManager; @@ -1013,7 +1014,7 @@ impl Session { } async fn record_state_snapshot(&self, items: &[ResponseItem]) { - let snapshot = { crate::rollout::SessionStateSnapshot {} }; + let snapshot = { SessionStateSnapshot {} }; let recorder = { let guard = self.rollout.lock().unwrap(); @@ -1087,6 +1088,7 @@ impl Session { aggregated_output: _, duration, exit_code, + timed_out: _, } = output; // Because stdout and stderr could each be up to 100 KiB, we send // truncated versions. @@ -1158,6 +1160,7 @@ impl Session { let output_stderr; let borrowed: &ExecToolCallOutput = match &result { Ok(output) => output, + Err(CodexErr::Sandbox(SandboxErr::Timeout { output })) => output, Err(e) => { output_stderr = ExecToolCallOutput { exit_code: -1, @@ -1165,6 +1168,7 @@ impl Session { stderr: StreamOutput::new(get_error_message_ui(e)), aggregated_output: StreamOutput::new(get_error_message_ui(e)), duration: Duration::default(), + timed_out: false, }; &output_stderr } @@ -5272,35 +5276,24 @@ async fn handle_sandbox_error( AskForApproval::Never | AskForApproval::OnRequest => { // Clarify when Read Only mode is the reason a command cannot proceed. let content = if matches!(sess.sandbox_policy, SandboxPolicy::ReadOnly) { - format!( - "command blocked by Read Only mode: {}", - error - ) + format!("command blocked by Read Only mode: {error}") } else { - format!( - "failed in sandbox {sandbox_type:?} with execution error: {error}" - ) + format!("failed in sandbox {sandbox_type:?} with execution error: {error}") }; return ResponseInputItem::FunctionCallOutput { call_id, - output: FunctionCallOutputPayload { - content, - success: Some(false), - }, + output: FunctionCallOutputPayload { content, success: Some(false) }, }; } AskForApproval::UnlessTrusted | AskForApproval::OnFailure => (), } // similarly, if the command timed out, we can simply return this failure to the model - if matches!(error, SandboxErr::Timeout) { + if matches!(error, SandboxErr::Timeout { .. }) { return ResponseInputItem::FunctionCallOutput { call_id, output: FunctionCallOutputPayload { - content: format!( - "command timed out after {} milliseconds", - params.timeout_duration().as_millis() - ), + content: "command timed out".to_string(), success: Some(false), }, }; @@ -5622,6 +5615,7 @@ async fn capture_browser_screenshot(_sess: &Session) -> Result<(PathBuf, String) } } } +// removed upstream exit_review_mode helper: not used in fork /// Send agent status update event to the TUI async fn send_agent_status_update(sess: &Session) { diff --git a/codex-rs/core/src/codex/compact.rs b/codex-rs/core/src/codex/compact.rs new file mode 100644 index 000000000000..a465f937d4c7 --- /dev/null +++ b/codex-rs/core/src/codex/compact.rs @@ -0,0 +1,400 @@ +use std::sync::Arc; + +use super::AgentTask; +use super::MutexExt; +use super::Session; +use super::TurnContext; +use super::get_last_assistant_message_from_turn; +use crate::Prompt; +use crate::client_common::ResponseEvent; +use crate::error::CodexErr; +use crate::error::Result as CodexResult; +use crate::protocol::AgentMessageEvent; +use crate::protocol::CompactedItem; +use crate::protocol::ErrorEvent; +use crate::protocol::Event; +use crate::protocol::EventMsg; +use crate::protocol::InputItem; +use crate::protocol::InputMessageKind; +use crate::protocol::TaskCompleteEvent; +use crate::protocol::TaskStartedEvent; +use crate::protocol::TurnContextItem; +use crate::util::backoff; +use askama::Template; +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseInputItem; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::RolloutItem; +use futures::prelude::*; + +pub(super) const COMPACT_TRIGGER_TEXT: &str = "Start Summarization"; +const SUMMARIZATION_PROMPT: &str = include_str!("../../templates/compact/prompt.md"); + +#[derive(Template)] +#[template(path = "compact/history_bridge.md", escape = "none")] +struct HistoryBridgeTemplate<'a> { + user_messages_text: &'a str, + summary_text: &'a str, +} + +pub(super) fn spawn_compact_task( + sess: Arc, + turn_context: Arc, + sub_id: String, + input: Vec, +) { + let task = AgentTask::compact( + sess.clone(), + turn_context, + sub_id, + input, + SUMMARIZATION_PROMPT.to_string(), + ); + sess.set_task(task); +} + +pub(super) async fn run_inline_auto_compact_task( + sess: Arc, + turn_context: Arc, +) { + let sub_id = sess.next_internal_sub_id(); + let input = vec![InputItem::Text { + text: COMPACT_TRIGGER_TEXT.to_string(), + }]; + run_compact_task_inner( + sess, + turn_context, + sub_id, + input, + SUMMARIZATION_PROMPT.to_string(), + false, + ) + .await; +} + +pub(super) async fn run_compact_task( + sess: Arc, + turn_context: Arc, + sub_id: String, + input: Vec, + compact_instructions: String, +) { + run_compact_task_inner( + sess, + turn_context, + sub_id, + input, + compact_instructions, + true, + ) + .await; +} + +async fn run_compact_task_inner( + sess: Arc, + turn_context: Arc, + sub_id: String, + input: Vec, + compact_instructions: String, + remove_task_on_completion: bool, +) { + let model_context_window = turn_context.client.get_model_context_window(); + let start_event = Event { + id: sub_id.clone(), + msg: EventMsg::TaskStarted(TaskStartedEvent { + model_context_window, + }), + }; + sess.send_event(start_event).await; + + let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input); + let instructions_override = compact_instructions; + let turn_input = sess.turn_input_with_history(vec![initial_input_for_turn.clone().into()]); + + let prompt = Prompt { + input: turn_input, + tools: Vec::new(), + base_instructions_override: Some(instructions_override), + }; + + let max_retries = turn_context.client.get_provider().stream_max_retries(); + let mut retries = 0; + + let rollout_item = RolloutItem::TurnContext(TurnContextItem { + cwd: turn_context.cwd.clone(), + approval_policy: turn_context.approval_policy, + sandbox_policy: turn_context.sandbox_policy.clone(), + model: turn_context.client.get_model(), + effort: turn_context.client.get_reasoning_effort(), + summary: turn_context.client.get_reasoning_summary(), + }); + sess.persist_rollout_items(&[rollout_item]).await; + + loop { + let attempt_result = drain_to_completed(&sess, turn_context.as_ref(), &prompt).await; + + match attempt_result { + Ok(()) => { + break; + } + Err(CodexErr::Interrupted) => { + return; + } + Err(e) => { + if retries < max_retries { + retries += 1; + let delay = backoff(retries); + sess.notify_stream_error( + &sub_id, + format!( + "stream error: {e}; retrying {retries}/{max_retries} in {delay:?}…" + ), + ) + .await; + tokio::time::sleep(delay).await; + continue; + } else { + let event = Event { + id: sub_id.clone(), + msg: EventMsg::Error(ErrorEvent { + message: e.to_string(), + }), + }; + sess.send_event(event).await; + return; + } + } + } + } + + if remove_task_on_completion { + sess.remove_task(&sub_id); + } + let history_snapshot = { + let state = sess.state.lock_unchecked(); + state.history.contents() + }; + let summary_text = get_last_assistant_message_from_turn(&history_snapshot).unwrap_or_default(); + let user_messages = collect_user_messages(&history_snapshot); + let initial_context = sess.build_initial_context(turn_context.as_ref()); + let new_history = build_compacted_history(initial_context, &user_messages, &summary_text); + { + let mut state = sess.state.lock_unchecked(); + state.history.replace(new_history); + } + + let rollout_item = RolloutItem::Compacted(CompactedItem { + message: summary_text.clone(), + }); + sess.persist_rollout_items(&[rollout_item]).await; + + let event = Event { + id: sub_id.clone(), + msg: EventMsg::AgentMessage(AgentMessageEvent { + message: "Compact task completed".to_string(), + }), + }; + sess.send_event(event).await; + let event = Event { + id: sub_id.clone(), + msg: EventMsg::TaskComplete(TaskCompleteEvent { + last_agent_message: None, + }), + }; + sess.send_event(event).await; +} + +fn content_items_to_text(content: &[ContentItem]) -> Option { + let mut pieces = Vec::new(); + for item in content { + match item { + ContentItem::InputText { text } | ContentItem::OutputText { text } => { + if !text.is_empty() { + pieces.push(text.as_str()); + } + } + ContentItem::InputImage { .. } => {} + } + } + if pieces.is_empty() { + None + } else { + Some(pieces.join("\n")) + } +} + +pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec { + items + .iter() + .filter_map(|item| match item { + ResponseItem::Message { role, content, .. } if role == "user" => { + content_items_to_text(content) + } + _ => None, + }) + .filter(|text| !is_session_prefix_message(text)) + .collect() +} + +fn is_session_prefix_message(text: &str) -> bool { + matches!( + InputMessageKind::from(("user", text)), + InputMessageKind::UserInstructions | InputMessageKind::EnvironmentContext + ) +} + +pub(crate) fn build_compacted_history( + initial_context: Vec, + user_messages: &[String], + summary_text: &str, +) -> Vec { + let mut history = initial_context; + let user_messages_text = if user_messages.is_empty() { + "(none)".to_string() + } else { + user_messages.join("\n\n") + }; + let summary_text = if summary_text.is_empty() { + "(no summary available)".to_string() + } else { + summary_text.to_string() + }; + let Ok(bridge) = HistoryBridgeTemplate { + user_messages_text: &user_messages_text, + summary_text: &summary_text, + } + .render() else { + return vec![]; + }; + history.push(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { text: bridge }], + }); + history +} + +async fn drain_to_completed( + sess: &Session, + turn_context: &TurnContext, + prompt: &Prompt, +) -> CodexResult<()> { + let mut stream = turn_context.client.clone().stream(prompt).await?; + loop { + let maybe_event = stream.next().await; + let Some(event) = maybe_event else { + return Err(CodexErr::Stream( + "stream closed before response.completed".into(), + None, + )); + }; + match event { + Ok(ResponseEvent::OutputItemDone(item)) => { + let mut state = sess.state.lock_unchecked(); + state.history.record_items(std::slice::from_ref(&item)); + } + Ok(ResponseEvent::Completed { .. }) => { + return Ok(()); + } + Ok(_) => continue, + Err(e) => return Err(e), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn content_items_to_text_joins_non_empty_segments() { + let items = vec![ + ContentItem::InputText { + text: "hello".to_string(), + }, + ContentItem::OutputText { + text: String::new(), + }, + ContentItem::OutputText { + text: "world".to_string(), + }, + ]; + + let joined = content_items_to_text(&items); + + assert_eq!(Some("hello\nworld".to_string()), joined); + } + + #[test] + fn content_items_to_text_ignores_image_only_content() { + let items = vec![ContentItem::InputImage { + image_url: "file://image.png".to_string(), + }]; + + let joined = content_items_to_text(&items); + + assert_eq!(None, joined); + } + + #[test] + fn collect_user_messages_extracts_user_text_only() { + let items = vec![ + ResponseItem::Message { + id: Some("assistant".to_string()), + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: "ignored".to_string(), + }], + }, + ResponseItem::Message { + id: Some("user".to_string()), + role: "user".to_string(), + content: vec![ + ContentItem::InputText { + text: "first".to_string(), + }, + ContentItem::OutputText { + text: "second".to_string(), + }, + ], + }, + ResponseItem::Other, + ]; + + let collected = collect_user_messages(&items); + + assert_eq!(vec!["first\nsecond".to_string()], collected); + } + + #[test] + fn collect_user_messages_filters_session_prefix_entries() { + let items = vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "do things".to_string(), + }], + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "cwd=/tmp".to_string(), + }], + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "real user message".to_string(), + }], + }, + ]; + + let collected = collect_user_messages(&items); + + assert_eq!(vec!["real user message".to_string()], collected); + } +} diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index bfb22bad9156..349c37812b31 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -6,6 +6,7 @@ use crate::config_types::GithubConfig; use crate::config_types::ThemeName; use crate::config_types::ThemeColors; use crate::config_types::McpServerConfig; +use crate::config_types::Notifications; use crate::config_types::SandboxWorkspaceWrite; use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicyToml; @@ -14,6 +15,7 @@ use crate::config_types::Tui; use crate::config_types::UriBasedFileOpener; use crate::git_info::resolve_root_git_project_for_trust; use crate::model_family::ModelFamily; +use crate::model_family::derive_default_model_family; use crate::model_family::find_family_for_model; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::built_in_model_providers; @@ -26,15 +28,20 @@ use codex_protocol::mcp_protocol::AuthMode; use codex_protocol::config_types::SandboxMode; use dirs::home_dir; use serde::Deserialize; +use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use tempfile::NamedTempFile; use toml::Value as TomlValue; +use toml_edit::Array as TomlArray; use toml_edit::DocumentMut; use toml_edit::Item as TomlItem; +use toml_edit::Table as TomlTable; const OPENAI_DEFAULT_MODEL: &str = "gpt-5"; +const OPENAI_DEFAULT_REVIEW_MODEL: &str = "gpt-5"; +pub const GPT_5_CODEX_MEDIUM_MODEL: &str = "gpt-5-codex"; /// Maximum number of bytes of the documentation that will be embedded. Larger /// files are *silently truncated* to this size so we do not take up too much of @@ -51,6 +58,9 @@ pub struct Config { /// Optional override of model selection. pub model: String, + /// Model used specifically for review sessions. Defaults to "gpt-5". + pub review_model: String, + pub model_family: ModelFamily, /// Size of the context window for the model, in tokens. @@ -59,6 +69,9 @@ pub struct Config { /// Maximum number of output tokens. pub model_max_output_tokens: Option, + /// Token usage threshold triggering auto-compaction of conversation history. + pub model_auto_compact_token_limit: Option, + /// Key into the model_providers map that specifies which provider to use. pub model_provider_id: String, @@ -114,6 +127,10 @@ pub struct Config { /// If unset the feature is disabled. pub notify: Option>, + /// TUI notifications preference. When set, the TUI will send OSC 9 notifications on approvals + /// and turn completions when not focused. + pub tui_notifications: Notifications, + /// The directory that should be treated as the current working directory /// for the session. All relative paths inside the business-logic layer are /// resolved against this path. @@ -167,9 +184,6 @@ pub struct Config { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, - /// Experimental rollout resume path (absolute path to .jsonl; undocumented). - pub experimental_resume: Option, - /// Include an experimental plan tool that the model can use to update its current plan and status of each step. pub include_plan_tool: bool, /// Include the `apply_patch` tool for models that benefit from invoking @@ -195,6 +209,11 @@ pub struct Config { /// GitHub integration configuration. pub github: GithubConfig, + + /// Experimental: path to a rollout file to resume a prior session from. + /// When set, the core will send this path in the initial ConfigureSession + /// so the backend can attempt to resume. + pub experimental_resume: Option, } impl Config { @@ -274,6 +293,88 @@ pub fn load_config_as_toml(codex_home: &Path) -> std::io::Result { } } +pub fn load_global_mcp_servers( + codex_home: &Path, +) -> std::io::Result> { + let root_value = load_config_as_toml(codex_home)?; + let Some(servers_value) = root_value.get("mcp_servers") else { + return Ok(BTreeMap::new()); + }; + + servers_value + .clone() + .try_into() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +pub fn write_global_mcp_servers( + codex_home: &Path, + servers: &BTreeMap, +) -> std::io::Result<()> { + let config_path = codex_home.join(CONFIG_TOML_FILE); + let mut doc = match std::fs::read_to_string(&config_path) { + Ok(contents) => contents + .parse::() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(), + Err(e) => return Err(e), + }; + + doc.as_table_mut().remove("mcp_servers"); + + if !servers.is_empty() { + let mut table = TomlTable::new(); + table.set_implicit(true); + doc["mcp_servers"] = TomlItem::Table(table); + + for (name, config) in servers { + let mut entry = TomlTable::new(); + entry.set_implicit(false); + entry["command"] = toml_edit::value(config.command.clone()); + + if !config.args.is_empty() { + let mut args = TomlArray::new(); + for arg in &config.args { + args.push(arg.clone()); + } + entry["args"] = TomlItem::Value(args.into()); + } + + if let Some(env) = &config.env + && !env.is_empty() + { + let mut env_table = TomlTable::new(); + env_table.set_implicit(false); + let mut pairs: Vec<_> = env.iter().collect(); + pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + for (key, value) in pairs { + env_table.insert(key, toml_edit::value(value.clone())); + } + entry["env"] = TomlItem::Table(env_table); + } + + if let Some(timeout) = config.startup_timeout_ms { + let timeout = i64::try_from(timeout).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "startup_timeout_ms exceeds supported range", + ) + })?; + entry["startup_timeout_ms"] = toml_edit::value(timeout); + } + + doc["mcp_servers"][name.as_str()] = TomlItem::Table(entry); + } + } + + std::fs::create_dir_all(codex_home)?; + let tmp_file = NamedTempFile::new_in(codex_home)?; + std::fs::write(tmp_file.path(), doc.to_string())?; + tmp_file.persist(config_path).map_err(|err| err.error)?; + + Ok(()) +} + /// Patch `CODEX_HOME/config.toml` project state. /// Use with caution. pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Result<()> { @@ -285,6 +386,22 @@ pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Re Err(e) => return Err(e.into()), }; + set_project_trusted_inner(&mut doc, project_path)?; + + // ensure codex_home exists + std::fs::create_dir_all(codex_home)?; + + // create a tmp_file + let tmp_file = NamedTempFile::new_in(codex_home)?; + std::fs::write(tmp_file.path(), doc.to_string())?; + + // atomically move the tmp file into config.toml + tmp_file.persist(config_path)?; + + Ok(()) +} + +fn set_project_trusted_inner(doc: &mut DocumentMut, project_path: &Path) -> anyhow::Result<()> { // Ensure we render a human-friendly structure: // // [projects] @@ -341,16 +458,6 @@ pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Re proj_tbl.set_implicit(false); proj_tbl["trust_level"] = toml_edit::value("trusted"); - // ensure codex_home exists - std::fs::create_dir_all(codex_home)?; - - // create a tmp_file - let tmp_file = NamedTempFile::new_in(codex_home)?; - std::fs::write(tmp_file.path(), doc.to_string())?; - - // atomically move the tmp file into config.toml - tmp_file.persist(config_path)?; - Ok(()) } @@ -891,6 +998,8 @@ fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) { pub struct ConfigToml { /// Optional override of model selection. pub model: Option, + /// Review model override used by the `/review` feature. + pub review_model: Option, /// Provider to use from the model_providers map. pub model_provider: Option, @@ -901,6 +1010,9 @@ pub struct ConfigToml { /// Maximum number of output tokens. pub model_max_output_tokens: Option, + /// Token usage threshold triggering auto-compaction of conversation history. + pub model_auto_compact_token_limit: Option, + /// Default approval policy for executing commands. pub approval_policy: Option, @@ -979,9 +1091,6 @@ pub struct ConfigToml { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: Option, - /// Experimental rollout resume path (absolute path to .jsonl; undocumented). - pub experimental_resume: Option, - /// Experimental path to a file whose contents replace the built-in BASE_INSTRUCTIONS. pub experimental_instructions_file: Option, @@ -1007,6 +1116,9 @@ pub struct ConfigToml { /// GitHub integration configuration. pub github: Option, + + /// Experimental path to a rollout file to resume from. + pub experimental_resume: Option, } #[derive(Deserialize, Debug, Clone)] @@ -1116,6 +1228,7 @@ impl ConfigToml { #[derive(Default, Debug, Clone)] pub struct ConfigOverrides { pub model: Option, + pub review_model: Option, pub cwd: Option, pub approval_policy: Option, pub sandbox_mode: Option, @@ -1124,6 +1237,8 @@ pub struct ConfigOverrides { pub codex_linux_sandbox_exe: Option, pub base_instructions: Option, pub include_plan_tool: Option, + pub include_apply_patch_tool: Option, + pub include_view_image_tool: Option, pub disable_response_storage: Option, pub show_raw_agent_reasoning: Option, pub debug: Option, @@ -1143,6 +1258,7 @@ impl Config { // Destructure ConfigOverrides fully to ensure all overrides are applied. let ConfigOverrides { model, + review_model: override_review_model, cwd, approval_policy, sandbox_mode, @@ -1151,6 +1267,8 @@ impl Config { codex_linux_sandbox_exe, base_instructions, include_plan_tool, + include_apply_patch_tool, + include_view_image_tool, disable_response_storage, show_raw_agent_reasoning, debug, @@ -1281,29 +1399,18 @@ impl Config { .tools .as_ref() .and_then(|t| t.web_search_allowed_domains.clone()); - // View Image tool is enabled by default; can be disabled in config. - let include_view_image_tool_flag = cfg - .tools - .as_ref() - .and_then(|t| t.view_image) + // View Image tool is enabled by default; can be disabled in config or overrides. + let include_view_image_tool_flag = include_view_image_tool + .or(cfg.tools.as_ref().and_then(|t| t.view_image)) .unwrap_or(true); let model = model .or(config_profile.model) .or(cfg.model) .unwrap_or_else(default_model); - let model_family = find_family_for_model(&model).unwrap_or_else(|| { - let supports_reasoning_summaries = - cfg.model_supports_reasoning_summaries.unwrap_or(false); - ModelFamily { - slug: model.clone(), - family: model.clone(), - needs_special_apply_patch_instructions: false, - supports_reasoning_summaries, - uses_local_shell_tool: false, - apply_patch_tool_type: None, - } - }); + + let model_family = + find_family_for_model(&model).unwrap_or_else(|| derive_default_model_family(&model)); let openai_model_info = get_model_info(&model_family); let model_context_window = cfg @@ -1314,8 +1421,11 @@ impl Config { .as_ref() .map(|info| info.max_output_tokens) }); - - let experimental_resume = cfg.experimental_resume; + let model_auto_compact_token_limit = cfg.model_auto_compact_token_limit.or_else(|| { + openai_model_info + .as_ref() + .and_then(|info| info.auto_compact_token_limit) + }); // Load base instructions override from a file if specified. If the // path is relative, resolve it against the effective cwd so the @@ -1335,11 +1445,18 @@ impl Config { .responses_originator_header_internal_override .unwrap_or(DEFAULT_RESPONSES_ORIGINATOR_HEADER.to_owned()); + // Default review model when not set in config; allow CLI override to take precedence. + let review_model = override_review_model + .or(cfg.review_model) + .unwrap_or_else(default_review_model); + let config = Self { model, + review_model, model_family, model_context_window, model_max_output_tokens, + model_auto_compact_token_limit, model_provider_id, model_provider, cwd: resolved_cwd, @@ -1361,7 +1478,7 @@ impl Config { codex_home, history, file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode), - tui: cfg.tui.unwrap_or_default(), + tui: cfg.tui.clone().unwrap_or_default(), codex_linux_sandbox_exe, hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), @@ -1386,19 +1503,27 @@ impl Config { .chatgpt_base_url .or(cfg.chatgpt_base_url) .unwrap_or("https://chatgpt.com/backend-api/".to_string()), - - experimental_resume, include_plan_tool: include_plan_tool.unwrap_or(false), - include_apply_patch_tool: false, + include_apply_patch_tool: include_apply_patch_tool.unwrap_or(false), tools_web_search_request, tools_web_search_allowed_domains, - use_experimental_streamable_shell_tool: false, + // Honor upstream opt-in switch name for our experimental streamable shell tool. + use_experimental_streamable_shell_tool: cfg + .experimental_use_exec_command_tool + .unwrap_or(false), include_view_image_tool: include_view_image_tool_flag, responses_originator_header, debug: debug.unwrap_or(false), // Already computed before moving codex_home using_chatgpt_auth, github: cfg.github.unwrap_or_default(), + experimental_resume: cfg.experimental_resume, + // Surface TUI notifications preference from config when present. + tui_notifications: cfg + .tui + .as_ref() + .map(|t| t.notifications.clone()) + .unwrap_or_default(), }; Ok(config) } @@ -1479,6 +1604,10 @@ fn default_model() -> String { OPENAI_DEFAULT_MODEL.to_string() } +fn default_review_model() -> String { + OPENAI_DEFAULT_REVIEW_MODEL.to_string() +} + /// Returns the path to the Code/Codex configuration directory, which can be /// specified by the `CODE_HOME` or `CODEX_HOME` environment variables. If not set, /// defaults to `~/.code` (falling back to `~/.codex` if it exists for compatibility). @@ -1626,6 +1755,187 @@ exclude_slash_tmp = true ); } + #[test] + fn load_global_mcp_servers_returns_empty_if_missing() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + + let servers = load_global_mcp_servers(codex_home.path())?; + assert!(servers.is_empty()); + + Ok(()) + } + + #[test] + fn write_global_mcp_servers_round_trips_entries() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + + let mut servers = BTreeMap::new(); + servers.insert( + "docs".to_string(), + McpServerConfig { + command: "echo".to_string(), + args: vec!["hello".to_string()], + env: None, + startup_timeout_ms: None, + }, + ); + + write_global_mcp_servers(codex_home.path(), &servers)?; + + let loaded = load_global_mcp_servers(codex_home.path())?; + assert_eq!(loaded.len(), 1); + let docs = loaded.get("docs").expect("docs entry"); + assert_eq!(docs.command, "echo"); + assert_eq!(docs.args, vec!["hello".to_string()]); + + let empty = BTreeMap::new(); + write_global_mcp_servers(codex_home.path(), &empty)?; + let loaded = load_global_mcp_servers(codex_home.path())?; + assert!(loaded.is_empty()); + + Ok(()) + } + #[tokio::test] + async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + + persist_model_selection( + codex_home.path(), + None, + "gpt-5-codex", + Some(ReasoningEffort::High), + ) + .await?; + + let serialized = + tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; + let parsed: ConfigToml = toml::from_str(&serialized)?; + + assert_eq!(parsed.model.as_deref(), Some("gpt-5-codex")); + assert_eq!(parsed.model_reasoning_effort, Some(ReasoningEffort::High)); + + Ok(()) + } + + #[tokio::test] + async fn persist_model_selection_overwrites_existing_model() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + let config_path = codex_home.path().join(CONFIG_TOML_FILE); + + tokio::fs::write( + &config_path, + r#" +model = "gpt-5" +model_reasoning_effort = "medium" + +[profiles.dev] +model = "gpt-4.1" +"#, + ) + .await?; + + persist_model_selection( + codex_home.path(), + None, + "o4-mini", + Some(ReasoningEffort::High), + ) + .await?; + + let serialized = tokio::fs::read_to_string(config_path).await?; + let parsed: ConfigToml = toml::from_str(&serialized)?; + + assert_eq!(parsed.model.as_deref(), Some("o4-mini")); + assert_eq!(parsed.model_reasoning_effort, Some(ReasoningEffort::High)); + assert_eq!( + parsed + .profiles + .get("dev") + .and_then(|profile| profile.model.as_deref()), + Some("gpt-4.1"), + ); + + Ok(()) + } + + #[tokio::test] + async fn persist_model_selection_updates_profile() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + + persist_model_selection( + codex_home.path(), + Some("dev"), + "gpt-5-codex", + Some(ReasoningEffort::Medium), + ) + .await?; + + let serialized = + tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; + let parsed: ConfigToml = toml::from_str(&serialized)?; + let profile = parsed + .profiles + .get("dev") + .expect("profile should be created"); + + assert_eq!(profile.model.as_deref(), Some("gpt-5-codex")); + assert_eq!( + profile.model_reasoning_effort, + Some(ReasoningEffort::Medium) + ); + + Ok(()) + } + + #[tokio::test] + async fn persist_model_selection_updates_existing_profile() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + let config_path = codex_home.path().join(CONFIG_TOML_FILE); + + tokio::fs::write( + &config_path, + r#" +[profiles.dev] +model = "gpt-4" +model_reasoning_effort = "medium" + +[profiles.prod] +model = "gpt-5" +"#, + ) + .await?; + + persist_model_selection( + codex_home.path(), + Some("dev"), + "o4-high", + Some(ReasoningEffort::Medium), + ) + .await?; + + let serialized = tokio::fs::read_to_string(config_path).await?; + let parsed: ConfigToml = toml::from_str(&serialized)?; + + let dev_profile = parsed + .profiles + .get("dev") + .expect("dev profile should survive updates"); + assert_eq!(dev_profile.model.as_deref(), Some("o4-high")); + assert_eq!( + dev_profile.model_reasoning_effort, + Some(ReasoningEffort::Medium) + ); + + assert_eq!( + parsed + .profiles + .get("prod") + .and_then(|profile| profile.model.as_deref()), + Some("gpt-5"), + ); + + Ok(()) + } struct PrecedenceTestFixture { cwd: TempDir, codex_home: TempDir, @@ -1769,9 +2079,11 @@ model_verbosity = "high" assert_eq!( Config { model: "o3".to_string(), + review_model: "gpt-5".to_string(), model_family: find_family_for_model("o3").expect("known model slug"), model_context_window: Some(200_000), model_max_output_tokens: Some(100_000), + model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::Never, @@ -1795,16 +2107,19 @@ model_verbosity = "high" model_reasoning_summary: ReasoningSummary::Detailed, model_text_verbosity: TextVerbosity::default(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), - experimental_resume: None, base_instructions: None, include_plan_tool: false, include_apply_patch_tool: false, tools_web_search_request: false, tools_web_search_allowed_domains: None, use_experimental_streamable_shell_tool: false, + include_view_image_tool: true, responses_originator_header: "codex_cli_rs".to_string(), debug: false, using_chatgpt_auth: false, + github: GithubConfig::default(), + experimental_resume: None, + tui_notifications: Default::default(), }, o3_profile_config ); @@ -1827,9 +2142,11 @@ model_verbosity = "high" )?; let expected_gpt3_profile_config = Config { model: "gpt-3.5-turbo".to_string(), + review_model: "gpt-5".to_string(), model_family: find_family_for_model("gpt-3.5-turbo").expect("known model slug"), model_context_window: Some(16_385), model_max_output_tokens: Some(4_096), + model_auto_compact_token_limit: None, model_provider_id: "openai-chat-completions".to_string(), model_provider: fixture.openai_chat_completions_provider.clone(), approval_policy: AskForApproval::UnlessTrusted, @@ -1853,16 +2170,19 @@ model_verbosity = "high" model_reasoning_summary: ReasoningSummary::default(), model_text_verbosity: TextVerbosity::default(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), - experimental_resume: None, base_instructions: None, include_plan_tool: false, include_apply_patch_tool: false, tools_web_search_request: false, tools_web_search_allowed_domains: None, use_experimental_streamable_shell_tool: false, + include_view_image_tool: true, responses_originator_header: "codex_cli_rs".to_string(), debug: false, using_chatgpt_auth: false, + github: GithubConfig::default(), + experimental_resume: None, + tui_notifications: Default::default(), }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -1900,9 +2220,11 @@ model_verbosity = "high" )?; let expected_zdr_profile_config = Config { model: "o3".to_string(), + review_model: "gpt-5".to_string(), model_family: find_family_for_model("o3").expect("known model slug"), model_context_window: Some(200_000), model_max_output_tokens: Some(100_000), + model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::OnFailure, @@ -1926,16 +2248,19 @@ model_verbosity = "high" model_reasoning_summary: ReasoningSummary::default(), model_text_verbosity: TextVerbosity::default(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), - experimental_resume: None, base_instructions: None, include_plan_tool: false, include_apply_patch_tool: false, - tools_web_search_request: false, - tools_web_search_allowed_domains: None, - use_experimental_streamable_shell_tool: false, + tools_web_search_request: false, + tools_web_search_allowed_domains: None, + use_experimental_streamable_shell_tool: false, + include_view_image_tool: true, responses_originator_header: "codex_cli_rs".to_string(), debug: false, using_chatgpt_auth: false, + github: GithubConfig::default(), + experimental_resume: None, + tui_notifications: Default::default(), }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); @@ -1959,9 +2284,11 @@ model_verbosity = "high" )?; let expected_gpt5_profile_config = Config { model: "gpt-5".to_string(), + review_model: "gpt-5".to_string(), model_family: find_family_for_model("gpt-5").expect("known model slug"), model_context_window: Some(400_000), model_max_output_tokens: Some(128_000), + model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::OnFailure, @@ -1985,17 +2312,18 @@ model_verbosity = "high" model_reasoning_summary: ReasoningSummary::Detailed, model_verbosity: Some(Verbosity::High), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), - experimental_resume: None, base_instructions: None, include_plan_tool: false, include_apply_patch_tool: false, tools_web_search_request: false, responses_originator_header: "codex_cli_rs".to_string(), - preferred_auth_method: AuthMode::ChatGPT, use_experimental_streamable_shell_tool: false, include_view_image_tool: true, - disable_paste_burst: false, - use_experimental_reasoning_summary: false, + debug: false, + using_chatgpt_auth: false, + github: GithubConfig::default(), + experimental_resume: None, + tui_notifications: Default::default(), }; assert_eq!(expected_gpt5_profile_config, gpt5_profile_config); @@ -2073,3 +2401,46 @@ trust_level = "trusted" // No test enforcing the presence of a standalone [projects] header. } + +#[cfg(test)] +mod notifications_tests { + use crate::config_types::Notifications; + use serde::Deserialize; + + #[derive(Deserialize, Debug, PartialEq)] + struct TuiTomlTest { + notifications: Notifications, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct RootTomlTest { + tui: TuiTomlTest, + } + + #[test] + fn test_tui_notifications_true() { + let toml = r#" + [tui] + notifications = true + "#; + let parsed: RootTomlTest = toml::from_str(toml).expect("deserialize notifications=true"); + assert!(matches!( + parsed.tui.notifications, + Notifications::Enabled(true) + )); + } + + #[test] + fn test_tui_notifications_custom_array() { + let toml = r#" + [tui] + notifications = ["foo"] + "#; + let parsed: RootTomlTest = + toml::from_str(toml).expect("deserialize notifications=[\"foo\"]"); + assert!(matches!( + parsed.tui.notifications, + Notifications::Custom(ref v) if v == &vec!["foo".to_string()] + )); + } +} diff --git a/codex-rs/core/src/config_edit.rs b/codex-rs/core/src/config_edit.rs index ae41ebcb403b..4b3a5cbdb06d 100644 --- a/codex-rs/core/src/config_edit.rs +++ b/codex-rs/core/src/config_edit.rs @@ -7,6 +7,12 @@ use toml_edit::DocumentMut; pub const CONFIG_KEY_MODEL: &str = "model"; pub const CONFIG_KEY_EFFORT: &str = "model_reasoning_effort"; +#[derive(Copy, Clone)] +enum NoneBehavior { + Skip, + Remove, +} + /// Persist overrides into `config.toml` using explicit key segments per /// override. This avoids ambiguity with keys that contain dots or spaces. pub async fn persist_overrides( @@ -14,47 +20,12 @@ pub async fn persist_overrides( profile: Option<&str>, overrides: &[(&[&str], &str)], ) -> Result<()> { - let config_path = codex_home.join(CONFIG_TOML_FILE); - - let mut doc = match tokio::fs::read_to_string(&config_path).await { - Ok(s) => s.parse::()?, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - tokio::fs::create_dir_all(codex_home).await?; - DocumentMut::new() - } - Err(e) => return Err(e.into()), - }; - - let effective_profile = if let Some(p) = profile { - Some(p.to_owned()) - } else { - doc.get("profile") - .and_then(|i| i.as_str()) - .map(|s| s.to_string()) - }; - - for (segments, val) in overrides.iter().copied() { - let value = toml_edit::value(val); - if let Some(ref name) = effective_profile { - if segments.first().copied() == Some("profiles") { - apply_toml_edit_override_segments(&mut doc, segments, value); - } else { - let mut seg_buf: Vec<&str> = Vec::with_capacity(2 + segments.len()); - seg_buf.push("profiles"); - seg_buf.push(name.as_str()); - seg_buf.extend_from_slice(segments); - apply_toml_edit_override_segments(&mut doc, &seg_buf, value); - } - } else { - apply_toml_edit_override_segments(&mut doc, segments, value); - } - } - - let tmp_file = NamedTempFile::new_in(codex_home)?; - tokio::fs::write(tmp_file.path(), doc.to_string()).await?; - tmp_file.persist(config_path)?; + let with_options: Vec<(&[&str], Option<&str>)> = overrides + .iter() + .map(|(segments, value)| (*segments, Some(*value))) + .collect(); - Ok(()) + persist_overrides_with_behavior(codex_home, profile, &with_options, NoneBehavior::Skip).await } /// Persist overrides where values may be optional. Any entries with `None` @@ -65,16 +36,17 @@ pub async fn persist_non_null_overrides( profile: Option<&str>, overrides: &[(&[&str], Option<&str>)], ) -> Result<()> { - let filtered: Vec<(&[&str], &str)> = overrides - .iter() - .filter_map(|(k, v)| v.map(|vv| (*k, vv))) - .collect(); - - if filtered.is_empty() { - return Ok(()); - } + persist_overrides_with_behavior(codex_home, profile, overrides, NoneBehavior::Skip).await +} - persist_overrides(codex_home, profile, &filtered).await +/// Persist overrides where `None` values clear any existing values from the +/// configuration file. +pub async fn persist_overrides_and_clear_if_none( + codex_home: &Path, + profile: Option<&str>, + overrides: &[(&[&str], Option<&str>)], +) -> Result<()> { + persist_overrides_with_behavior(codex_home, profile, overrides, NoneBehavior::Remove).await } /// Apply a single override onto a `toml_edit` document while preserving @@ -121,6 +93,125 @@ fn apply_toml_edit_override_segments( current[last] = value; } +async fn persist_overrides_with_behavior( + codex_home: &Path, + profile: Option<&str>, + overrides: &[(&[&str], Option<&str>)], + none_behavior: NoneBehavior, +) -> Result<()> { + if overrides.is_empty() { + return Ok(()); + } + + let should_skip = match none_behavior { + NoneBehavior::Skip => overrides.iter().all(|(_, value)| value.is_none()), + NoneBehavior::Remove => false, + }; + + if should_skip { + return Ok(()); + } + + let config_path = codex_home.join(CONFIG_TOML_FILE); + + let read_result = tokio::fs::read_to_string(&config_path).await; + let mut doc = match read_result { + Ok(contents) => contents.parse::()?, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + if overrides + .iter() + .all(|(_, value)| value.is_none() && matches!(none_behavior, NoneBehavior::Remove)) + { + return Ok(()); + } + + tokio::fs::create_dir_all(codex_home).await?; + DocumentMut::new() + } + Err(e) => return Err(e.into()), + }; + + let effective_profile = if let Some(p) = profile { + Some(p.to_owned()) + } else { + doc.get("profile") + .and_then(|i| i.as_str()) + .map(|s| s.to_string()) + }; + + let mut mutated = false; + + for (segments, value) in overrides.iter().copied() { + let mut seg_buf: Vec<&str> = Vec::new(); + let segments_to_apply: &[&str]; + + if let Some(ref name) = effective_profile { + if segments.first().copied() == Some("profiles") { + segments_to_apply = segments; + } else { + seg_buf.reserve(2 + segments.len()); + seg_buf.push("profiles"); + seg_buf.push(name.as_str()); + seg_buf.extend_from_slice(segments); + segments_to_apply = seg_buf.as_slice(); + } + } else { + segments_to_apply = segments; + } + + match value { + Some(v) => { + let item_value = toml_edit::value(v); + apply_toml_edit_override_segments(&mut doc, segments_to_apply, item_value); + mutated = true; + } + None => { + if matches!(none_behavior, NoneBehavior::Remove) + && remove_toml_edit_segments(&mut doc, segments_to_apply) + { + mutated = true; + } + } + } + } + + if !mutated { + return Ok(()); + } + + let tmp_file = NamedTempFile::new_in(codex_home)?; + tokio::fs::write(tmp_file.path(), doc.to_string()).await?; + tmp_file.persist(config_path)?; + + Ok(()) +} + +fn remove_toml_edit_segments(doc: &mut DocumentMut, segments: &[&str]) -> bool { + use toml_edit::Item; + + if segments.is_empty() { + return false; + } + + let mut current = doc.as_table_mut(); + for seg in &segments[..segments.len() - 1] { + let Some(item) = current.get_mut(seg) else { + return false; + }; + + match item { + Item::Table(table) => { + current = table; + } + _ => { + return false; + } + } + } + + current.remove(segments[segments.len() - 1]).is_some() +} + #[cfg(test)] mod tests { use super::*; @@ -574,6 +665,81 @@ model = "o3" assert_eq!(contents, expected); } + #[tokio::test] + async fn persist_clear_none_removes_top_level_value() { + let tmpdir = tempdir().expect("tmp"); + let codex_home = tmpdir.path(); + + let seed = r#"model = "gpt-5" +model_reasoning_effort = "medium" +"#; + tokio::fs::write(codex_home.join(CONFIG_TOML_FILE), seed) + .await + .expect("seed write"); + + persist_overrides_and_clear_if_none( + codex_home, + None, + &[ + (&[CONFIG_KEY_MODEL], None), + (&[CONFIG_KEY_EFFORT], Some("high")), + ], + ) + .await + .expect("persist"); + + let contents = read_config(codex_home).await; + let expected = "model_reasoning_effort = \"high\"\n"; + assert_eq!(contents, expected); + } + + #[tokio::test] + async fn persist_clear_none_respects_active_profile() { + let tmpdir = tempdir().expect("tmp"); + let codex_home = tmpdir.path(); + + let seed = r#"profile = "team" + +[profiles.team] +model = "gpt-4" +model_reasoning_effort = "minimal" +"#; + tokio::fs::write(codex_home.join(CONFIG_TOML_FILE), seed) + .await + .expect("seed write"); + + persist_overrides_and_clear_if_none( + codex_home, + None, + &[ + (&[CONFIG_KEY_MODEL], None), + (&[CONFIG_KEY_EFFORT], Some("high")), + ], + ) + .await + .expect("persist"); + + let contents = read_config(codex_home).await; + let expected = r#"profile = "team" + +[profiles.team] +model_reasoning_effort = "high" +"#; + assert_eq!(contents, expected); + } + + #[tokio::test] + async fn persist_clear_none_noop_when_file_missing() { + let tmpdir = tempdir().expect("tmp"); + let codex_home = tmpdir.path(); + + persist_overrides_and_clear_if_none(codex_home, None, &[(&[CONFIG_KEY_MODEL], None)]) + .await + .expect("persist"); + + assert!(!codex_home.join(CONFIG_TOML_FILE).exists()); + } + // Test helper moved to bottom per review guidance. async fn read_config(codex_home: &Path) -> String { let p = codex_home.join(CONFIG_TOML_FILE); diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 206380ffd9e3..f807209da691 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -126,6 +126,19 @@ pub enum HistoryPersistence { None, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(untagged)] +pub enum Notifications { + Enabled(bool), + Custom(Vec), +} + +impl Default for Notifications { + fn default() -> Self { + Self::Enabled(false) + } +} + /// Collection of settings that are specific to the TUI. #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct Tui { @@ -149,6 +162,11 @@ pub struct Tui { #[serde(default)] pub spinner: SpinnerSelection, + /// Enable desktop notifications from the TUI when the terminal is unfocused. + /// Defaults to `false`. + #[serde(default)] + pub notifications: Notifications, + /// Whether to use the terminal's Alternate Screen (full-screen) mode. /// When false, Codex renders nothing and leaves the standard terminal /// buffer visible; users can toggle back to Alternate Screen at runtime @@ -170,6 +188,7 @@ impl Default for Tui { show_reasoning: false, stream: StreamConfig::default(), spinner: SpinnerSelection::default(), + notifications: Notifications::default(), alternate_screen: true, } } @@ -231,6 +250,14 @@ impl Default for StreamConfig { } } +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum ReasoningSummaryFormat { + #[default] + None, + Experimental, +} + /// Theme configuration for the TUI #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct ThemeConfig { diff --git a/codex-rs/core/src/conversation_history.rs b/codex-rs/core/src/conversation_history.rs index eb7057ae2c97..f20197f5e590 100644 --- a/codex-rs/core/src/conversation_history.rs +++ b/codex-rs/core/src/conversation_history.rs @@ -1,6 +1,4 @@ -use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; -use tracing::debug; /// Transcript of conversation history #[derive(Debug, Clone, Default)] @@ -30,125 +28,23 @@ impl ConversationHistory { continue; } - // De-duplicate assistant messages more robustly by leveraging IDs - // when available and falling back to adjacency/prefix merging. - match &*item { - ResponseItem::Message { - id: new_id, - role: new_role, - content: new_content, - } if new_role == "assistant" => { - // 1) If this assistant message has an ID, try to find an existing - // assistant entry with the same ID anywhere in history and - // replace its text with the new content. - if let Some(id) = new_id.as_ref() { - if let Some(idx) = find_assistant_index_by_id(&self.items, id) { - if let ResponseItem::Message { - id: existing_id, - content, - .. - } = &mut self.items[idx] - { - // Ensure the existing entry carries the id - if existing_id.is_none() { - *existing_id = Some(id.clone()); - } - replace_or_append_text(content, new_content); - continue; - } - } - } - - // 2) Otherwise, attempt to merge with the immediately previous - // assistant message (streamed deltas case). - if let Some(ResponseItem::Message { - id: last_id, - role: last_role, - content: last_content, - }) = self.items.last_mut() - { - if last_role == "assistant" { - // If new item has an ID, propagate it to the existing entry - if last_id.is_none() { - if let Some(id) = new_id.clone() { - *last_id = Some(id); - } - } - replace_or_append_text(last_content, new_content); - continue; - } - } - - // 3) No merge opportunity – push as a new assistant message. - self.items.push(item.clone()); - } - ResponseItem::FunctionCallOutput { call_id, .. } => { - // Check if we already have an output for this call_id to prevent duplicates - let already_exists = self.items.iter().any(|existing| { - matches!(existing, ResponseItem::FunctionCallOutput { call_id: existing_id, .. } if existing_id == call_id) - }); - - if already_exists { - debug!( - "Skipping duplicate FunctionCallOutput for call_id: {} (already in history)", - call_id - ); - } else { - debug!( - "Recording FunctionCallOutput to history for call_id: {}", - call_id - ); - self.items.push(item.clone()); - } - } - ResponseItem::FunctionCall { call_id, .. } => { - // Check if we already have this function call to prevent duplicates during retries - let already_exists = self.items.iter().any(|existing| { - matches!(existing, ResponseItem::FunctionCall { call_id: existing_id, .. } if existing_id == call_id) - }); - - if already_exists { - debug!( - "Skipping duplicate FunctionCall for call_id: {} (already in history)", - call_id - ); - } else { - self.items.push(item.clone()); - } - } - _ => { - self.items.push(item.clone()); - } - } + self.items.push(item.clone()); } } + // Replaced by record_items + keep_last_messages utilities. + + /// Retain only the last `n` messages in the history (oldest-first order). pub(crate) fn keep_last_messages(&mut self, n: usize) { if n == 0 { self.items.clear(); return; } - - // Collect the last N message items (assistant/user), newest to oldest. - let mut kept: Vec = Vec::with_capacity(n); - for item in self.items.iter().rev() { - if let ResponseItem::Message { role, content, .. } = item { - kept.push(ResponseItem::Message { - // we need to remove the id or the model will complain that messages are sent without - // their reasonings - id: None, - role: role.clone(), - content: content.clone(), - }); - if kept.len() == n { - break; - } - } + let len = self.items.len(); + if len > n { + let start = len - n; + self.items = self.items.split_off(start); } - - // Preserve chronological order (oldest to newest) within the kept slice. - kept.reverse(); - self.items = kept; } } @@ -167,78 +63,6 @@ fn is_api_message(message: &ResponseItem) -> bool { } } -/// Helper to append the textual content from `src` into `dst` in place. -fn append_text_content(dst: &mut Vec, src: &Vec) { - for c in src { - if let ContentItem::OutputText { text } = c { - append_text_delta(dst, text); - } - } -} - -/// Append a single text delta to the last OutputText item in `content`, or -/// push a new OutputText item if none exists. -fn append_text_delta(content: &mut Vec, delta: &str) { - if let Some(ContentItem::OutputText { text }) = content - .iter_mut() - .rev() - .find(|c| matches!(c, ContentItem::OutputText { .. })) - { - text.push_str(delta); - } else { - content.push(ContentItem::OutputText { - text: delta.to_string(), - }); - } -} - -/// Concatenate all OutputText segments into a single string. -fn collect_text_content(content: &Vec) -> String { - let mut out = String::new(); - for c in content { - if let ContentItem::OutputText { text } = c { - out.push_str(text); - } - } - out -} - -/// Replace the text content with a single OutputText item containing `text`. -fn replace_text_content(content: &mut Vec, text: &str) { - content.clear(); - content.push(ContentItem::OutputText { - text: text.to_string(), - }); -} - -/// Merge strategy: if the new text starts with the old, replace; otherwise append deltas. -fn replace_or_append_text(existing: &mut Vec, new_content: &Vec) { - let prev = collect_text_content(existing); - let new_full = collect_text_content(new_content); - if !prev.is_empty() && new_full.starts_with(&prev) { - replace_text_content(existing, &new_full); - } else { - append_text_content(existing, new_content); - } -} - -fn find_assistant_index_by_id(items: &Vec, id: &str) -> Option { - // Search from the end (most recent first) for better performance in practice. - for (idx, it) in items.iter().enumerate().rev() { - if let ResponseItem::Message { - id: Some(existing), - role, - .. - } = it - { - if role == "assistant" && existing == id { - return Some(idx); - } - } - } - None -} - #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/conversation_manager.rs index 0e021a8b2e8b..a5a458fe84dc 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/conversation_manager.rs @@ -1,13 +1,5 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; - use crate::AuthManager; use crate::CodexAuth; -use codex_protocol::mcp_protocol::AuthMode; -use tokio::sync::RwLock; -use uuid::Uuid; - use crate::codex::Codex; use crate::codex::CodexSpawnOk; use crate::codex::INITIAL_SUBMIT_ID; @@ -19,44 +11,35 @@ use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::SessionConfiguredEvent; use crate::rollout::RolloutRecorder; +use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::models::ResponseItem; - -#[derive(Debug, Clone, PartialEq)] -pub enum InitialHistory { - New, - Resumed(Vec), -} +use codex_protocol::protocol::InitialHistory; +use codex_protocol::protocol::RolloutItem; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; /// Represents a newly created Codex conversation, including the first event /// (which is [`EventMsg::SessionConfigured`]). pub struct NewConversation { - pub conversation_id: Uuid, + pub conversation_id: ConversationId, pub conversation: Arc, pub session_configured: SessionConfiguredEvent, } -impl std::fmt::Debug for NewConversation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NewConversation") - .field("conversation_id", &self.conversation_id) - .field("conversation", &"") - .field("session_configured", &"") - .finish() - } -} - /// [`ConversationManager`] is responsible for creating conversations and /// maintaining them in memory. pub struct ConversationManager { - conversations: Arc>>>, - _auth_manager: Arc, + conversations: Arc>>>, + auth_manager: Arc, } impl ConversationManager { pub fn new(auth_manager: Arc) -> Self { Self { conversations: Arc::new(RwLock::new(HashMap::new())), - _auth_manager: auth_manager, + auth_manager, } } @@ -67,39 +50,42 @@ impl ConversationManager { } pub async fn new_conversation(&self, config: Config) -> CodexResult { - // Build auth from codex_home preferring ChatGPT by default. - let auth = CodexAuth::from_codex_home( - &config.codex_home, - AuthMode::ChatGPT, - &config.responses_originator_header, - )?; - self.spawn_conversation(config, auth).await + self.spawn_conversation(config, self.auth_manager.clone()) + .await } async fn spawn_conversation( &self, - config: Config, - auth: Option, + mut config: Config, + auth_manager: Arc, ) -> CodexResult { + // Our core `Codex::spawn` takes `(config, auth)` and handles resume via + // `config.experimental_resume`. For a fresh conversation we leave it as `None`. + config.experimental_resume = None; let CodexSpawnOk { codex, init_id: _, - session_id: conversation_id, - } = Codex::spawn(config, auth).await?; + session_id, + } = Codex::spawn(config, auth_manager.auth()).await?; + let conversation_id: codex_protocol::mcp_protocol::ConversationId = session_id.into(); self.finalize_spawn(codex, conversation_id).await } async fn finalize_spawn( &self, codex: Codex, - conversation_id: Uuid, + conversation_id: ConversationId, ) -> CodexResult { // The first event must be `SessionInitialized`. Validate and forward it // to the caller so that they can display it in the conversation // history. let event = codex.next_event().await?; let session_configured = match event { - Event { id, msg: EventMsg::SessionConfigured(session_configured), .. } if id == INITIAL_SUBMIT_ID => session_configured, + Event { + id, + msg: EventMsg::SessionConfigured(session_configured), + .. + } if id == INITIAL_SUBMIT_ID => session_configured, _ => { return Err(CodexErr::SessionConfiguredNotFirstEvent); } @@ -120,32 +106,41 @@ impl ConversationManager { pub async fn get_conversation( &self, - conversation_id: Uuid, + conversation_id: ConversationId, ) -> CodexResult> { let conversations = self.conversations.read().await; conversations .get(&conversation_id) .cloned() - .ok_or_else(|| CodexErr::ConversationNotFound(conversation_id)) + .ok_or_else(|| CodexErr::ConversationNotFound(conversation_id.into())) } pub async fn resume_conversation_from_rollout( &self, - config: Config, + mut config: Config, rollout_path: PathBuf, - _auth_manager: Arc, + auth_manager: Arc, ) -> CodexResult { - let _initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; + // Point the config at the rollout we want to resume. + config.experimental_resume = Some(rollout_path); let CodexSpawnOk { codex, init_id: _, - session_id: conversation_id, - } = Codex::spawn(config, None).await?; + session_id, + } = Codex::spawn(config, auth_manager.auth()).await?; + let conversation_id: codex_protocol::mcp_protocol::ConversationId = session_id.into(); self.finalize_spawn(codex, conversation_id).await } - pub async fn remove_conversation(&self, conversation_id: Uuid) { - self.conversations.write().await.remove(&conversation_id); + /// Removes the conversation from the manager's internal map, though the + /// conversation is stored as `Arc`, it is possible that + /// other references to it exist elsewhere. Returns the conversation if the + /// conversation was found and removed. + pub async fn remove_conversation( + &self, + conversation_id: &ConversationId, + ) -> Option> { + self.conversations.write().await.remove(conversation_id) } /// Fork an existing conversation by dropping the last `drop_last_messages` @@ -154,52 +149,92 @@ impl ConversationManager { /// caller's `config`). The new conversation will have a fresh id. pub async fn fork_conversation( &self, - conversation_history: Vec, num_messages_to_drop: usize, - config: Config, + mut config: Config, + path: PathBuf, ) -> CodexResult { // Compute the prefix up to the cut point. - let _truncated_history = - truncate_after_dropping_last_messages(conversation_history, num_messages_to_drop); + let history = RolloutRecorder::get_rollout_history(&path).await?; + let history = truncate_after_dropping_last_messages(history, num_messages_to_drop); + + // If there is no prior history to seed, just start a fresh conversation. + if matches!(history, InitialHistory::New) { + return self.spawn_conversation(config, self.auth_manager.clone()).await; + } + + // Otherwise, create a temporary rollout with the truncated items and resume from it. + let convo_id = codex_protocol::mcp_protocol::ConversationId::new(); + let instructions = config.user_instructions.clone(); + let recorder = RolloutRecorder::new( + &config, + crate::rollout::recorder::RolloutRecorderParams::new(convo_id, instructions), + ) + .await + .map_err(|e| CodexErr::Io(e))?; - // Spawn a new conversation with the computed initial history. + // Extract only response items to seed the rollout file. + let response_items: Vec = history + .get_rollout_items() + .into_iter() + .filter_map(|ri| match ri { + codex_protocol::protocol::RolloutItem::ResponseItem(item) => Some(item), + _ => None, + }) + .collect(); + if !response_items.is_empty() { + recorder + .record_items(&response_items) + .await + .map_err(|e| CodexErr::Io(e))?; + } + // Ensure data is flushed to disk before resuming. + recorder.shutdown().await.map_err(|e| CodexErr::Io(e))?; + + // Now spawn a conversation resuming from the newly created rollout. + config.experimental_resume = Some(recorder.rollout_path.clone()); let CodexSpawnOk { codex, init_id: _, - session_id: conversation_id, - } = Codex::spawn(config, None).await?; - + session_id, + } = Codex::spawn(config, self.auth_manager.auth()).await?; + let conversation_id: codex_protocol::mcp_protocol::ConversationId = session_id.into(); self.finalize_spawn(codex, conversation_id).await } } /// Return a prefix of `items` obtained by dropping the last `n` user messages /// and all items that follow them. -fn truncate_after_dropping_last_messages(items: Vec, n: usize) -> InitialHistory { +fn truncate_after_dropping_last_messages(history: InitialHistory, n: usize) -> InitialHistory { if n == 0 { - return InitialHistory::Resumed(items); + return InitialHistory::Forked(history.get_rollout_items()); } - // Walk backwards counting only `user` Message items, find cut index. - let mut count = 0usize; - let mut cut_index = 0usize; - for (idx, item) in items.iter().enumerate().rev() { - if let ResponseItem::Message { role, .. } = item { - if role == "user" { - count += 1; - if count == n { - // Cut everything from this user message to the end. - cut_index = idx; - break; - } - } + // Work directly on rollout items, and cut the vector at the nth-from-last user message input. + let items: Vec = history.get_rollout_items(); + + // Find indices of user message inputs in rollout order. + let mut user_positions: Vec = Vec::new(); + for (idx, item) in items.iter().enumerate() { + if let RolloutItem::ResponseItem(ResponseItem::Message { role, .. }) = item + && role == "user" + { + user_positions.push(idx); } } - if cut_index == 0 { - // No prefix remains after dropping; start a new conversation. + + // If fewer than n user messages exist, treat as empty. + if user_positions.len() < n { + return InitialHistory::New; + } + + // Cut strictly before the nth-from-last user message (do not keep the nth itself). + let cut_idx = user_positions[user_positions.len() - n]; + let rolled: Vec = items.into_iter().take(cut_idx).collect(); + + if rolled.is_empty() { InitialHistory::New } else { - InitialHistory::Resumed(items.into_iter().take(cut_index).collect()) + InitialHistory::Forked(rolled) } } @@ -254,13 +289,30 @@ mod tests { assistant_msg("a4"), ]; - let truncated = truncate_after_dropping_last_messages(items.clone(), 1); + // Wrap as InitialHistory::Forked with response items only. + let initial: Vec = items + .iter() + .cloned() + .map(RolloutItem::ResponseItem) + .collect(); + let truncated = truncate_after_dropping_last_messages(InitialHistory::Forked(initial), 1); + let got_items = truncated.get_rollout_items(); + let expected_items = vec![ + RolloutItem::ResponseItem(items[0].clone()), + RolloutItem::ResponseItem(items[1].clone()), + RolloutItem::ResponseItem(items[2].clone()), + ]; assert_eq!( - truncated, - InitialHistory::Resumed(vec![items[0].clone(), items[1].clone(), items[2].clone(),]) + serde_json::to_value(&got_items).unwrap(), + serde_json::to_value(&expected_items).unwrap() ); - let truncated2 = truncate_after_dropping_last_messages(items, 2); - assert_eq!(truncated2, InitialHistory::New); + let initial2: Vec = items + .iter() + .cloned() + .map(RolloutItem::ResponseItem) + .collect(); + let truncated2 = truncate_after_dropping_last_messages(InitialHistory::Forked(initial2), 2); + assert!(matches!(truncated2, InitialHistory::New)); } } diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 80ca7438c7b2..764284555c8f 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -1,3 +1,4 @@ +use crate::exec::ExecToolCallOutput; use reqwest::StatusCode; use serde_json; use std::io; @@ -11,8 +12,11 @@ pub type Result = std::result::Result; #[derive(Error, Debug)] pub enum SandboxErr { /// Error from sandbox execution - #[error("sandbox denied exec error, exit code: {0}, stdout: {1}, stderr: {2}")] - Denied(i32, String, String), + #[error( + "sandbox denied exec error, exit code: {}, stdout: {}, stderr: {}", + .output.exit_code, .output.stdout.text, .output.stderr.text + )] + Denied { output: Box }, /// Error from linux seccomp filter setup #[cfg(target_os = "linux")] @@ -26,7 +30,7 @@ pub enum SandboxErr { /// Command timed out #[error("command timed out")] - Timeout, + Timeout { output: Box }, /// Command was killed by a signal #[error("command was killed by a signal")] @@ -227,9 +231,12 @@ impl CodexErr { pub fn get_error_message_ui(e: &CodexErr) -> String { match e { - CodexErr::Sandbox(SandboxErr::Denied(_, _, stderr)) => stderr.to_string(), + CodexErr::Sandbox(SandboxErr::Denied { output }) => output.stderr.text.clone(), // Timeouts are not sandbox errors from a UX perspective; present them plainly - CodexErr::Sandbox(SandboxErr::Timeout) => "error: command timed out".to_string(), + CodexErr::Sandbox(SandboxErr::Timeout { output }) => format!( + "error: command timed out after {} ms", + output.duration.as_millis() + ), _ => e.to_string(), } } diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 410dabd56989..ce8af0b7445a 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -38,6 +38,7 @@ const DEFAULT_TIMEOUT_MS: u64 = 120_000; const SIGKILL_CODE: i32 = 9; const TIMEOUT_CODE: i32 = 64; const EXIT_CODE_SIGNAL_BASE: i32 = 128; // conventional shell: 128 + signal +const EXEC_TIMEOUT_EXIT_CODE: i32 = 124; // conventional timeout exit code // I/O buffer sizing const READ_CHUNK_SIZE: usize = 8192; // bytes per read @@ -90,11 +91,12 @@ pub async fn process_exec_tool_call( ) -> Result { let start = Instant::now(); + let timeout_duration = params.timeout_duration(); + let raw_output_result: std::result::Result = match sandbox_type { SandboxType::None => exec(params, sandbox_policy, stdout_stream.clone()).await, SandboxType::MacosSeatbelt => { - let timeout = params.timeout_duration(); let ExecParams { command, cwd, env, .. } = params; @@ -106,10 +108,9 @@ pub async fn process_exec_tool_call( env, ) .await?; - consume_truncated_output(child, timeout, stdout_stream.clone()).await + consume_truncated_output(child, timeout_duration, stdout_stream.clone()).await } SandboxType::LinuxSeccomp => { - let timeout = params.timeout_duration(); let ExecParams { command, cwd, env, .. } = params; @@ -127,41 +128,56 @@ pub async fn process_exec_tool_call( ) .await?; - consume_truncated_output(child, timeout, stdout_stream).await + consume_truncated_output(child, timeout_duration, stdout_stream).await } }; let duration = start.elapsed(); match raw_output_result { Ok(raw_output) => { - let stdout = raw_output.stdout.from_utf8_lossy(); - let stderr = raw_output.stderr.from_utf8_lossy(); + #[allow(unused_mut)] + let mut timed_out = raw_output.timed_out; #[cfg(target_family = "unix")] - match raw_output.exit_status.signal() { - Some(TIMEOUT_CODE) => return Err(CodexErr::Sandbox(SandboxErr::Timeout)), - Some(signal) => { - return Err(CodexErr::Sandbox(SandboxErr::Signal(signal))); + { + if let Some(signal) = raw_output.exit_status.signal() { + if signal == TIMEOUT_CODE { + timed_out = true; + } else { + return Err(CodexErr::Sandbox(SandboxErr::Signal(signal))); + } } - None => {} } - let exit_code = raw_output.exit_status.code().unwrap_or(-1); - - if exit_code != 0 && is_likely_sandbox_denied(sandbox_type, exit_code) { - return Err(CodexErr::Sandbox(SandboxErr::Denied( - exit_code, - stdout.text, - stderr.text, - ))); + let mut exit_code = raw_output.exit_status.code().unwrap_or(-1); + if timed_out { + exit_code = EXEC_TIMEOUT_EXIT_CODE; } - Ok(ExecToolCallOutput { + let stdout = raw_output.stdout.from_utf8_lossy(); + let stderr = raw_output.stderr.from_utf8_lossy(); + let aggregated_output = raw_output.aggregated_output.from_utf8_lossy(); + let exec_output = ExecToolCallOutput { exit_code, stdout, stderr, - aggregated_output: raw_output.aggregated_output.from_utf8_lossy(), + aggregated_output, duration, - }) + timed_out, + }; + + if timed_out { + return Err(CodexErr::Sandbox(SandboxErr::Timeout { + output: Box::new(exec_output), + })); + } + + if exit_code != 0 && is_likely_sandbox_denied(sandbox_type, exit_code) { + return Err(CodexErr::Sandbox(SandboxErr::Denied { + output: Box::new(exec_output), + })); + } + + Ok(exec_output) } Err(err) => { tracing::error!("exec error: {err}"); @@ -198,6 +214,7 @@ struct RawExecToolCallOutput { pub stdout: StreamOutput>, pub stderr: StreamOutput>, pub aggregated_output: StreamOutput>, + pub timed_out: bool, } impl StreamOutput { @@ -230,6 +247,7 @@ pub struct ExecToolCallOutput { pub stderr: StreamOutput, pub aggregated_output: StreamOutput, pub duration: Duration, + pub timed_out: bool, } async fn exec( @@ -301,22 +319,24 @@ async fn consume_truncated_output( Some(agg_tx.clone()), )); - let exit_status = tokio::select! { + let (exit_status, timed_out) = tokio::select! { result = tokio::time::timeout(timeout, killer.as_mut().wait()) => { match result { - Ok(Ok(exit_status)) => exit_status, - Ok(e) => e?, + Ok(status_result) => { + let exit_status = status_result?; + (exit_status, false) + } Err(_) => { // timeout killer.as_mut().start_kill()?; // Debatable whether `child.wait().await` should be called here. - synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + TIMEOUT_CODE) + (synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + TIMEOUT_CODE), true) } } } _ = tokio::signal::ctrl_c() => { killer.as_mut().start_kill()?; - synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + SIGKILL_CODE) + (synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + SIGKILL_CODE), false) } }; @@ -343,6 +363,7 @@ async fn consume_truncated_output( stdout, stderr, aggregated_output, + timed_out, }) } diff --git a/codex-rs/core/src/exec_command/exec_command_session.rs b/codex-rs/core/src/exec_command/exec_command_session.rs index 5794e6a34beb..31b3c929b283 100644 --- a/codex-rs/core/src/exec_command/exec_command_session.rs +++ b/codex-rs/core/src/exec_command/exec_command_session.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] use std::sync::Mutex as StdMutex; use tokio::sync::broadcast; @@ -25,6 +24,9 @@ pub(crate) struct ExecCommandSession { /// JoinHandle for the child wait task. wait_handle: StdMutex>>, + + /// Tracks whether the underlying process has exited. + exit_status: std::sync::Arc, } impl ExecCommandSession { @@ -35,15 +37,21 @@ impl ExecCommandSession { reader_handle: JoinHandle<()>, writer_handle: JoinHandle<()>, wait_handle: JoinHandle<()>, - ) -> Self { - Self { - writer_tx, - output_tx, - killer: StdMutex::new(Some(killer)), - reader_handle: StdMutex::new(Some(reader_handle)), - writer_handle: StdMutex::new(Some(writer_handle)), - wait_handle: StdMutex::new(Some(wait_handle)), - } + exit_status: std::sync::Arc, + ) -> (Self, broadcast::Receiver>) { + let initial_output_rx = output_tx.subscribe(); + ( + Self { + writer_tx, + output_tx, + killer: StdMutex::new(Some(killer)), + reader_handle: StdMutex::new(Some(reader_handle)), + writer_handle: StdMutex::new(Some(writer_handle)), + wait_handle: StdMutex::new(Some(wait_handle)), + exit_status, + }, + initial_output_rx, + ) } pub(crate) fn writer_sender(&self) -> mpsc::Sender> { @@ -53,32 +61,36 @@ impl ExecCommandSession { pub(crate) fn output_receiver(&self) -> broadcast::Receiver> { self.output_tx.subscribe() } + + pub(crate) fn has_exited(&self) -> bool { + self.exit_status.load(std::sync::atomic::Ordering::SeqCst) + } } impl Drop for ExecCommandSession { fn drop(&mut self) { // Best-effort: terminate child first so blocking tasks can complete. - if let Ok(mut killer_opt) = self.killer.lock() { - if let Some(mut killer) = killer_opt.take() { - let _ = killer.kill(); - } + if let Ok(mut killer_opt) = self.killer.lock() + && let Some(mut killer) = killer_opt.take() + { + let _ = killer.kill(); } // Abort background tasks; they may already have exited after kill. - if let Ok(mut h) = self.reader_handle.lock() { - if let Some(handle) = h.take() { - handle.abort(); - } + if let Ok(mut h) = self.reader_handle.lock() + && let Some(handle) = h.take() + { + handle.abort(); } - if let Ok(mut h) = self.writer_handle.lock() { - if let Some(handle) = h.take() { - handle.abort(); - } + if let Ok(mut h) = self.writer_handle.lock() + && let Some(handle) = h.take() + { + handle.abort(); } - if let Ok(mut h) = self.wait_handle.lock() { - if let Some(handle) = h.take() { - handle.abort(); - } + if let Ok(mut h) = self.wait_handle.lock() + && let Some(handle) = h.take() + { + handle.abort(); } } } diff --git a/codex-rs/core/src/exec_command/session_manager.rs b/codex-rs/core/src/exec_command/session_manager.rs index e1b4111c1cb4..d3219177d267 100644 --- a/codex-rs/core/src/exec_command/session_manager.rs +++ b/codex-rs/core/src/exec_command/session_manager.rs @@ -92,18 +92,16 @@ impl SessionManager { .fetch_add(1, std::sync::atomic::Ordering::SeqCst), ); - let (session, mut exit_rx) = - create_exec_command_session(params.clone()) - .await - .map_err(|err| { - format!( - "failed to create exec command session for session id {}: {err}", - session_id.0 - ) - })?; + let (session, mut output_rx, mut exit_rx) = create_exec_command_session(params.clone()) + .await + .map_err(|err| { + format!( + "failed to create exec command session for session id {}: {err}", + session_id.0 + ) + })?; // Insert into session map. - let mut output_rx = session.output_receiver(); self.sessions.lock().await.insert(session_id, session); // Collect output until either timeout expires or process exits. @@ -192,7 +190,11 @@ impl SessionManager { let (writer_tx, mut output_rx) = { let sessions = self.sessions.lock().await; match sessions.get(&session_id) { - Some(session) => (session.writer_sender(), session.output_receiver()), + Some(session) => { + // Touch exit flag to mark the field as used and enable early checks in the future. + let _exited = session.has_exited(); + (session.writer_sender(), session.output_receiver()) + } None => { return Err(format!("unknown session id {}", session_id.0)); } @@ -251,7 +253,11 @@ impl SessionManager { /// Spawn PTY and child process per spawn_exec_command_session logic. async fn create_exec_command_session( params: ExecCommandParams, -) -> anyhow::Result<(ExecCommandSession, oneshot::Receiver)> { +) -> anyhow::Result<( + ExecCommandSession, + tokio::sync::broadcast::Receiver>, + oneshot::Receiver, +)> { let ExecCommandParams { cmd, yield_time_ms: _, @@ -285,7 +291,6 @@ async fn create_exec_command_session( let (writer_tx, mut writer_rx) = mpsc::channel::>(128); // Broadcast for streaming PTY output to readers: subscribers receive from subscription time. let (output_tx, _) = tokio::sync::broadcast::channel::>(256); - // Reader task: drain PTY and forward chunks to output channel. let mut reader = pair.master.try_clone_reader()?; let output_tx_clone = output_tx.clone(); @@ -335,24 +340,30 @@ async fn create_exec_command_session( // Keep the child alive until it exits, then signal exit code. let (exit_tx, exit_rx) = oneshot::channel::(); + // Track process exit status for concurrent queries. + let exit_status = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let exit_status_for_wait = exit_status.clone(); let wait_handle = tokio::task::spawn_blocking(move || { let code = match child.wait() { Ok(status) => status.exit_code() as i32, Err(_) => -1, }; let _ = exit_tx.send(code); + // Mark as exited so readers can stop without waiting on the channel. + exit_status_for_wait.store(true, std::sync::atomic::Ordering::SeqCst); }); // Create and store the session with channels. - let session = ExecCommandSession::new( + let (session, initial_output_rx) = ExecCommandSession::new( writer_tx, output_tx, killer, reader_handle, writer_handle, wait_handle, + exit_status, ); - Ok((session, exit_rx)) + Ok((session, initial_output_rx, exit_rx)) } /// Truncate the middle of a UTF-8 string to at most `max_bytes` bytes, diff --git a/codex-rs/core/src/internal_storage.rs b/codex-rs/core/src/internal_storage.rs index f7d17e88f920..e8e0c0905781 100644 --- a/codex-rs/core/src/internal_storage.rs +++ b/codex-rs/core/src/internal_storage.rs @@ -1,6 +1,7 @@ use anyhow::Context; use serde::Deserialize; use serde::Serialize; +use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; @@ -11,7 +12,7 @@ pub struct InternalStorage { #[serde(skip)] storage_path: PathBuf, #[serde(default)] - pub gpt_5_high_model_prompt_seen: bool, + pub gpt_5_codex_model_prompt_seen: bool, } // TODO(jif) generalise all the file writers and build proper async channel inserters. @@ -31,7 +32,14 @@ impl InternalStorage { } }, Err(error) => { - tracing::warn!("failed to read internal storage: {error:?}"); + if error.kind() == ErrorKind::NotFound { + tracing::debug!( + "internal storage not found at {}; initializing defaults", + storage_path.display() + ); + } else { + tracing::warn!("failed to read internal storage: {error:?}"); + } Self::empty(storage_path) } } diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index e8152812e4d6..a5ed5ba1488c 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -70,6 +70,8 @@ pub mod terminal; mod tool_apply_patch; pub mod turn_diff_tracker; pub use rollout::list::ConversationsPage; +pub use rollout::list::find_conversation_path_by_id_str; +pub use rollout::recorder::RolloutRecorder; mod user_notification; pub mod util; pub mod http_client; diff --git a/codex-rs/core/src/model_family.rs b/codex-rs/core/src/model_family.rs index 99697f6c9d89..54c18daec372 100644 --- a/codex-rs/core/src/model_family.rs +++ b/codex-rs/core/src/model_family.rs @@ -1,5 +1,11 @@ +use crate::config_types::ReasoningSummaryFormat; use crate::tool_apply_patch::ApplyPatchToolType; +/// The `instructions` field in the payload sent to a model should always start +/// with this content. +const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); +const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../gpt_5_codex_prompt.md"); + /// A model family is a group of models that share certain characteristics. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ModelFamily { @@ -20,6 +26,9 @@ pub struct ModelFamily { // `summary` is optional). pub supports_reasoning_summaries: bool, + // Define if we need a special handling of reasoning summary + pub reasoning_summary_format: ReasoningSummaryFormat, + // This should be set to true when the model expects a tool named // "local_shell" to be provided. Its contract must be understood natively by // the model such that its description can be omitted. @@ -29,6 +38,9 @@ pub struct ModelFamily { /// Present if the model performs better when `apply_patch` is provided as /// a tool call instead of just a bash command pub apply_patch_tool_type: Option, + + // Instructions to use for querying the model + pub base_instructions: String, } macro_rules! model_family { @@ -41,8 +53,10 @@ macro_rules! model_family { family: $family.to_string(), needs_special_apply_patch_instructions: false, supports_reasoning_summaries: false, + reasoning_summary_format: ReasoningSummaryFormat::None, uses_local_shell_tool: false, apply_patch_tool_type: None, + base_instructions: BASE_INSTRUCTIONS.to_string(), }; // apply overrides $( @@ -52,21 +66,6 @@ macro_rules! model_family { }}; } -macro_rules! simple_model_family { - ( - $slug:expr, $family:expr - ) => {{ - Some(ModelFamily { - slug: $slug.to_string(), - family: $family.to_string(), - needs_special_apply_patch_instructions: false, - supports_reasoning_summaries: false, - uses_local_shell_tool: false, - apply_patch_tool_type: None, - }) - }}; -} - /// Returns a `ModelFamily` for the given model slug, or `None` if the slug /// does not match any known model family. pub fn find_family_for_model(slug: &str) -> Option { @@ -74,22 +73,20 @@ pub fn find_family_for_model(slug: &str) -> Option { model_family!( slug, "o3", supports_reasoning_summaries: true, + needs_special_apply_patch_instructions: true, ) } else if slug.starts_with("o4-mini") { model_family!( slug, "o4-mini", supports_reasoning_summaries: true, + needs_special_apply_patch_instructions: true, ) } else if slug.starts_with("codex-mini-latest") { model_family!( slug, "codex-mini-latest", supports_reasoning_summaries: true, uses_local_shell_tool: true, - ) - } else if slug.starts_with("codex-") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, + needs_special_apply_patch_instructions: true, ) } else if slug.starts_with("gpt-4.1") { model_family!( @@ -99,15 +96,36 @@ pub fn find_family_for_model(slug: &str) -> Option { } else if slug.starts_with("gpt-oss") || slug.starts_with("openai/gpt-oss") { model_family!(slug, "gpt-oss", apply_patch_tool_type: Some(ApplyPatchToolType::Function)) } else if slug.starts_with("gpt-4o") { - simple_model_family!(slug, "gpt-4o") + model_family!(slug, "gpt-4o", needs_special_apply_patch_instructions: true) } else if slug.starts_with("gpt-3.5") { - simple_model_family!(slug, "gpt-3.5") + model_family!(slug, "gpt-3.5", needs_special_apply_patch_instructions: true) + } else if slug.starts_with("codex-") || slug.starts_with("gpt-5-codex") { + model_family!( + slug, slug, + supports_reasoning_summaries: true, + reasoning_summary_format: ReasoningSummaryFormat::Experimental, + base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), + ) } else if slug.starts_with("gpt-5") { model_family!( slug, "gpt-5", supports_reasoning_summaries: true, + needs_special_apply_patch_instructions: true, ) } else { None } } + +pub fn derive_default_model_family(model: &str) -> ModelFamily { + ModelFamily { + slug: model.to_string(), + family: model.to_string(), + needs_special_apply_patch_instructions: false, + supports_reasoning_summaries: false, + reasoning_summary_format: ReasoningSummaryFormat::None, + uses_local_shell_tool: false, + apply_patch_tool_type: None, + base_instructions: BASE_INSTRUCTIONS.to_string(), + } +} diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index ab0927a11034..48e540be80f2 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -177,6 +177,21 @@ impl ModelProviderInfo { } } + pub(crate) fn is_azure_responses_endpoint(&self) -> bool { + if self.wire_api != WireApi::Responses { + return false; + } + + if self.name.eq_ignore_ascii_case("azure") { + return true; + } + + self.base_url + .as_ref() + .map(|base| matches_azure_responses_base_url(base)) + .unwrap_or(false) + } + /// Apply provider-specific HTTP headers (both static and environment-based) /// onto an existing `reqwest::RequestBuilder` and return the updated /// builder. @@ -349,6 +364,18 @@ pub fn create_oss_provider_with_base_url(base_url: &str) -> ModelProviderInfo { } } +fn matches_azure_responses_base_url(base_url: &str) -> bool { + let base = base_url.to_ascii_lowercase(); + const AZURE_MARKERS: [&str; 5] = [ + "openai.azure.", + "cognitiveservices.azure.", + "aoai.azure.", + "azure-api.", + "azurefd.", + ]; + AZURE_MARKERS.iter().any(|marker| base.contains(marker)) +} + #[cfg(test)] mod tests { use super::*; @@ -439,4 +466,69 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" } let provider: ModelProviderInfo = toml::from_str(azure_provider_toml).unwrap(); assert_eq!(expected_provider, provider); } + + #[test] + fn detects_azure_responses_base_urls() { + fn provider_for(base_url: &str) -> ModelProviderInfo { + ModelProviderInfo { + name: "test".into(), + base_url: Some(base_url.into()), + env_key: None, + env_key_instructions: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_openai_auth: false, + } + } + + let positive_cases = [ + "https://foo.openai.azure.com/openai", + "https://foo.openai.azure.us/openai/deployments/bar", + "https://foo.cognitiveservices.azure.cn/openai", + "https://foo.aoai.azure.com/openai", + "https://foo.openai.azure-api.net/openai", + "https://foo.z01.azurefd.net/", + ]; + for base_url in positive_cases { + let provider = provider_for(base_url); + assert!( + provider.is_azure_responses_endpoint(), + "expected {base_url} to be detected as Azure" + ); + } + + let named_provider = ModelProviderInfo { + name: "Azure".into(), + base_url: Some("https://example.com".into()), + env_key: None, + env_key_instructions: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_openai_auth: false, + }; + assert!(named_provider.is_azure_responses_endpoint()); + + let negative_cases = [ + "https://api.openai.com/v1", + "https://example.com/openai", + "https://myproxy.azurewebsites.net/openai", + ]; + for base_url in negative_cases { + let provider = provider_for(base_url); + assert!( + !provider.is_azure_responses_endpoint(), + "expected {base_url} not to be detected as Azure" + ); + } + } } diff --git a/codex-rs/core/src/openai_model_info.rs b/codex-rs/core/src/openai_model_info.rs index bf67ec4a114a..6fbe99fffc5e 100644 --- a/codex-rs/core/src/openai_model_info.rs +++ b/codex-rs/core/src/openai_model_info.rs @@ -12,6 +12,19 @@ pub(crate) struct ModelInfo { /// Maximum number of output tokens that can be generated for the model. pub(crate) max_output_tokens: u64, + + /// Token threshold where we should automatically compact conversation history. + pub(crate) auto_compact_token_limit: Option, +} + +impl ModelInfo { + const fn new(context_window: u64, max_output_tokens: u64) -> Self { + Self { + context_window, + max_output_tokens, + auto_compact_token_limit: None, + } + } } pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option { @@ -20,73 +33,37 @@ pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option { // OSS models have a 128k shared token pool. // Arbitrarily splitting it: 3/4 input context, 1/4 output. // https://openai.com/index/gpt-oss-model-card/ - "gpt-oss-20b" => Some(ModelInfo { - context_window: 96_000, - max_output_tokens: 32_000, - }), - "gpt-oss-120b" => Some(ModelInfo { - context_window: 96_000, - max_output_tokens: 32_000, - }), + "gpt-oss-20b" => Some(ModelInfo::new(96_000, 32_000)), + "gpt-oss-120b" => Some(ModelInfo::new(96_000, 32_000)), // https://platform.openai.com/docs/models/o3 - "o3" => Some(ModelInfo { - context_window: 200_000, - max_output_tokens: 100_000, - }), + "o3" => Some(ModelInfo::new(200_000, 100_000)), // https://platform.openai.com/docs/models/o4-mini - "o4-mini" => Some(ModelInfo { - context_window: 200_000, - max_output_tokens: 100_000, - }), + "o4-mini" => Some(ModelInfo::new(200_000, 100_000)), // https://platform.openai.com/docs/models/codex-mini-latest - "codex-mini-latest" => Some(ModelInfo { - context_window: 200_000, - max_output_tokens: 100_000, - }), + "codex-mini-latest" => Some(ModelInfo::new(200_000, 100_000)), // As of Jun 25, 2025, gpt-4.1 defaults to gpt-4.1-2025-04-14. // https://platform.openai.com/docs/models/gpt-4.1 - "gpt-4.1" | "gpt-4.1-2025-04-14" => Some(ModelInfo { - context_window: 1_047_576, - max_output_tokens: 32_768, - }), + "gpt-4.1" | "gpt-4.1-2025-04-14" => Some(ModelInfo::new(1_047_576, 32_768)), // As of Jun 25, 2025, gpt-4o defaults to gpt-4o-2024-08-06. // https://platform.openai.com/docs/models/gpt-4o - "gpt-4o" | "gpt-4o-2024-08-06" => Some(ModelInfo { - context_window: 128_000, - max_output_tokens: 16_384, - }), + "gpt-4o" | "gpt-4o-2024-08-06" => Some(ModelInfo::new(128_000, 16_384)), // https://platform.openai.com/docs/models/gpt-4o?snapshot=gpt-4o-2024-05-13 - "gpt-4o-2024-05-13" => Some(ModelInfo { - context_window: 128_000, - max_output_tokens: 4_096, - }), + "gpt-4o-2024-05-13" => Some(ModelInfo::new(128_000, 4_096)), // https://platform.openai.com/docs/models/gpt-4o?snapshot=gpt-4o-2024-11-20 - "gpt-4o-2024-11-20" => Some(ModelInfo { - context_window: 128_000, - max_output_tokens: 16_384, - }), + "gpt-4o-2024-11-20" => Some(ModelInfo::new(128_000, 16_384)), // https://platform.openai.com/docs/models/gpt-3.5-turbo - "gpt-3.5-turbo" => Some(ModelInfo { - context_window: 16_385, - max_output_tokens: 4_096, - }), + "gpt-3.5-turbo" => Some(ModelInfo::new(16_385, 4_096)), - "gpt-5" => Some(ModelInfo { - context_window: 400_000, - max_output_tokens: 128_000, - }), + _ if slug.starts_with("gpt-5") => Some(ModelInfo::new(272_000, 128_000)), - _ if slug.starts_with("codex-") => Some(ModelInfo { - context_window: 400_000, - max_output_tokens: 128_000, - }), + _ if slug.starts_with("codex-") => Some(ModelInfo::new(272_000, 128_000)), _ => None, } diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 73f2cb44091a..aeca6e6c18d0 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -256,7 +256,7 @@ fn create_shell_tool_for_sandbox(sandbox_policy: &SandboxPolicy) -> OpenAiTool { }, ); - if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { + if !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) { properties.insert( "with_escalated_permissions".to_string(), JsonSchema::Boolean { @@ -312,26 +312,10 @@ Default timeout: 120000 ms (120s). Override via the `timeout` parameter."#, ) } SandboxPolicy::DangerFullAccess => { - "Runs a shell command and returns its output. Default timeout: 120000 ms (120s). Override via the `timeout` parameter.".to_string() + "Runs a shell command and returns its output.".to_string() } SandboxPolicy::ReadOnly => { - r#" -The shell tool is used to execute shell commands. -- When invoking the shell tool, your call will be running in a sandbox, and some shell commands (including apply_patch) will require escalated permissions: - - Types of actions that require escalated privileges: - - Writing files - - Applying patches - - Examples of commands that require escalated privileges: - - apply_patch - - git commit - - npm install or pnpm install - - cargo build - - cargo test -- When invoking a command that will require escalated privileges: - - Provide the with_escalated_permissions parameter with the boolean value true - - Include a short, 1 sentence explanation for why we need to run with_escalated_permissions in the justification parameter - -Default timeout: 120000 ms (120s). Override via the `timeout` parameter."#.to_string() + "Runs a shell command and returns its output.".to_string() } }; @@ -1196,7 +1180,9 @@ The shell tool is used to execute shell commands. - cargo test - When invoking a command that will require escalated privileges: - Provide the with_escalated_permissions parameter with the boolean value true - - Include a short, 1 sentence explanation for why we need to run with_escalated_permissions in the justification parameter."#; + - Include a short, 1 sentence explanation for why we need to run with_escalated_permissions in the justification parameter. + +Default timeout: 120000 ms (120s). Override via the `timeout` parameter."#; assert_eq!(description, expected); } @@ -1211,21 +1197,7 @@ The shell tool is used to execute shell commands. }; assert_eq!(name, "shell"); - let expected = r#" -The shell tool is used to execute shell commands. -- When invoking the shell tool, your call will be running in a sandbox, and some shell commands (including apply_patch) will require escalated permissions: - - Types of actions that require escalated privileges: - - Writing files - - Applying patches - - Examples of commands that require escalated privileges: - - apply_patch - - git commit - - npm install or pnpm install - - cargo build - - cargo test -- When invoking a command that will require escalated privileges: - - Provide the with_escalated_permissions parameter with the boolean value true - - Include a short, 1 sentence explanation for why we need to run with_escalated_permissions in the justification parameter"#; + let expected = "Runs a shell command and returns its output."; assert_eq!(description, expected); } diff --git a/codex-rs/core/src/prompt_for_compact_command.md b/codex-rs/core/src/prompt_for_compact_command.md index 5cdecdbf4cac..a5acefd55797 100644 --- a/codex-rs/core/src/prompt_for_compact_command.md +++ b/codex-rs/core/src/prompt_for_compact_command.md @@ -1,21 +1,9 @@ -You are a summarization assistant. A conversation follows between a user and a coding-focused AI (Codex). Your task is to generate a clear summary capturing: +You are assisting with summarizing and compacting the recent conversation. -• High-level objective or problem being solved -• Key instructions or design decisions given by the user -• Main code actions or behaviors from the AI -• Important variables, functions, modules, or outputs discussed -• Any unresolved questions or next steps +Goals: +- Preserve intent and key technical details. +- Remove filler words and redundant phrasing. +- Keep code blocks and commands intact where relevant. -Produce the summary in a structured format like: - -**Objective:** … - -**User instructions:** … (bulleted) - -**AI actions / code behavior:** … (bulleted) - -**Important entities:** … (e.g. function names, variables, files) - -**Open issues / next steps:** … (if any) - -**Summary (concise):** (one or two sentences) +When asked to compact, produce a concise summary that retains the critical +information necessary to proceed in the next turn. diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 83239a0374d6..7f0ec9036474 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -491,9 +491,24 @@ pub enum EventMsg { /// Agent status has been updated AgentStatusUpdate(AgentStatusUpdateEvent), + /// User/system input message (what was sent to the model) + UserMessage(codex_protocol::protocol::UserMessageEvent), + /// Notification that the agent is shutting down. ShutdownComplete, + /// The system aborted the current turn (e.g., due to interruption). + TurnAborted(codex_protocol::protocol::TurnAbortedEvent), + + /// Response to a conversation path request. + ConversationPath(codex_protocol::protocol::ConversationPathResponseEvent), + + /// Entered review mode with the provided request. + EnteredReviewMode(codex_protocol::protocol::ReviewRequest), + + /// Exited review mode with an optional final result to apply. + ExitedReviewMode(Option), + /// Replay a previously recorded transcript into the UI. /// Used after resuming from a rollout file so the user sees the full /// history for that session without re-executing any actions. diff --git a/codex-rs/core/src/rollout/list.rs b/codex-rs/core/src/rollout/list.rs index a22b9e7bd077..15e01ede23e4 100644 --- a/codex-rs/core/src/rollout/list.rs +++ b/codex-rs/core/src/rollout/list.rs @@ -4,6 +4,10 @@ use std::io::{self}; use std::path::Path; use std::path::PathBuf; +use codex_file_search as file_search; +use std::num::NonZero; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; use time::OffsetDateTime; use time::PrimitiveDateTime; use time::format_description::FormatItem; @@ -324,8 +328,55 @@ async fn read_head_and_flags( saw_user_event = true; } } + // Skip variants not displayed in list summaries. + RolloutItem::Compacted(_) | RolloutItem::TurnContext(_) => {} } } Ok((head, saw_session_meta, saw_user_event)) } + +/// Locate a recorded conversation rollout file by its UUID string using the existing +/// paginated listing implementation. Returns `Ok(Some(path))` if found, `Ok(None)` if not present +/// or the id is invalid. +pub async fn find_conversation_path_by_id_str( + codex_home: &Path, + id_str: &str, +) -> io::Result> { + // Validate UUID format early. + if Uuid::parse_str(id_str).is_err() { + return Ok(None); + } + + let mut root = codex_home.to_path_buf(); + root.push(SESSIONS_SUBDIR); + if !root.exists() { + return Ok(None); + } + // This is safe because we know the values are valid. + #[allow(clippy::unwrap_used)] + let limit = NonZero::new(1).unwrap(); + // This is safe because we know the values are valid. + #[allow(clippy::unwrap_used)] + let threads = NonZero::new(2).unwrap(); + let cancel = Arc::new(AtomicBool::new(false)); + let exclude: Vec = Vec::new(); + let compute_indices = false; + + let results = file_search::run( + id_str, + limit, + &root, + exclude, + threads, + cancel, + compute_indices, + ) + .map_err(|e| io::Error::other(format!("file search failed: {e}")))?; + + Ok(results + .matches + .into_iter() + .next() + .map(|m| root.join(m.path))) +} diff --git a/codex-rs/core/src/rollout/mod.rs b/codex-rs/core/src/rollout/mod.rs index d391c0b89489..77350a0a4ee1 100644 --- a/codex-rs/core/src/rollout/mod.rs +++ b/codex-rs/core/src/rollout/mod.rs @@ -1,13 +1,20 @@ //! Rollout module: persistence and discovery of session rollout files. pub const SESSIONS_SUBDIR: &str = "sessions"; +#[allow(dead_code)] +pub const ARCHIVED_SESSIONS_SUBDIR: &str = "archived_sessions"; pub mod list; pub(crate) mod policy; pub mod recorder; +#[allow(unused_imports)] +pub use codex_protocol::protocol::SessionMeta; +#[allow(unused_imports)] +pub use list::find_conversation_path_by_id_str; pub use recorder::RolloutRecorder; -pub use recorder::SessionStateSnapshot; +#[allow(unused_imports)] +pub use recorder::RolloutRecorderParams; #[cfg(test)] pub mod tests; diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index bf45d27c813a..185c8a7d34e7 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -13,6 +13,8 @@ pub(crate) fn is_persisted_response_item(item: &RolloutItem) -> bool { } // Always persist session meta RolloutItem::SessionMeta(_) => true, + // Do not persist variants not used by this fork. + RolloutItem::Compacted(_) | RolloutItem::TurnContext(_) => false, } } @@ -36,10 +38,43 @@ pub(crate) fn should_persist_response_item(item: &ResponseItem) -> bool { #[allow(dead_code)] pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { match ev { - EventMsg::AgentMessage(_) + EventMsg::UserMessage(_) + | EventMsg::AgentMessage(_) | EventMsg::AgentReasoning(_) | EventMsg::AgentReasoningRawContent(_) - | EventMsg::TokenCount(_) => true, - _ => false, + | EventMsg::TokenCount(_) + | EventMsg::EnteredReviewMode(_) + | EventMsg::ExitedReviewMode(_) + | EventMsg::TurnAborted(_) => true, + EventMsg::Error(_) + | EventMsg::TaskStarted + | EventMsg::TaskComplete(_) + | EventMsg::AgentMessageDelta(_) + | EventMsg::AgentReasoningDelta(_) + | EventMsg::AgentReasoningRawContentDelta(_) + | EventMsg::AgentReasoningSectionBreak(_) + | EventMsg::SessionConfigured(_) + | EventMsg::McpToolCallBegin(_) + | EventMsg::McpToolCallEnd(_) + | EventMsg::WebSearchBegin(_) + | EventMsg::WebSearchComplete(_) + | EventMsg::CustomToolCallBegin(_) + | EventMsg::CustomToolCallEnd(_) + | EventMsg::ExecCommandBegin(_) + | EventMsg::ExecCommandOutputDelta(_) + | EventMsg::ExecCommandEnd(_) + | EventMsg::ExecApprovalRequest(_) + | EventMsg::ApplyPatchApprovalRequest(_) + | EventMsg::BackgroundEvent(_) + | EventMsg::PatchApplyBegin(_) + | EventMsg::PatchApplyEnd(_) + | EventMsg::TurnDiff(_) + | EventMsg::GetHistoryEntryResponse(_) + | EventMsg::PlanUpdate(_) + | EventMsg::BrowserScreenshotUpdate(_) + | EventMsg::AgentStatusUpdate(_) + | EventMsg::ShutdownComplete + | EventMsg::ConversationPath(_) + | EventMsg::ReplayHistory(_) => false, } } diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 8acaab7e65ee..79538c3e0463 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -213,7 +213,6 @@ impl RolloutRecorder { pub(crate) async fn get_rollout_history(path: &Path) -> std::io::Result { info!("Resuming rollout from {path:?}"); - tracing::error!("Resuming rollout from {path:?}"); let text = tokio::fs::read_to_string(path).await?; if text.trim().is_empty() { return Err(IoError::other("empty session file")); @@ -250,6 +249,11 @@ impl RolloutRecorder { RolloutItem::EventMsg(_ev) => { items.push(RolloutItem::EventMsg(_ev)); } + // Ignore variants not used by this fork when resuming. + RolloutItem::Compacted(_) + | RolloutItem::TurnContext(_) => { + // Skip + } }, Err(e) => { warn!("failed to parse rollout line: {v:?}, error: {e}"); @@ -257,7 +261,7 @@ impl RolloutRecorder { } } - tracing::error!( + info!( "Resumed rollout with {} items, conversation ID: {:?}", items.len(), conversation_id diff --git a/codex-rs/core/src/seatbelt_base_policy.sbpl b/codex-rs/core/src/seatbelt_base_policy.sbpl index 36244c269955..a62b6cfd3dee 100644 --- a/codex-rs/core/src/seatbelt_base_policy.sbpl +++ b/codex-rs/core/src/seatbelt_base_policy.sbpl @@ -2,6 +2,7 @@ ; inspired by Chrome's sandbox policy: ; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd +; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/renderer.sb;l=64;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd ; start with closed-by-default (deny default) @@ -9,7 +10,13 @@ ; child processes inherit the policy of their parent (allow process-exec) (allow process-fork) -(allow signal (target self)) +(allow signal (target same-sandbox)) + +; Allow cf prefs to work. +(allow user-preference-read) + +; process-info +(allow process-info* (target same-sandbox)) (allow file-write-data (require-all @@ -32,28 +39,22 @@ (sysctl-name "hw.l3cachesize_compat") (sysctl-name "hw.logicalcpu_max") (sysctl-name "hw.machine") + (sysctl-name "hw.memsize") (sysctl-name "hw.ncpu") (sysctl-name "hw.nperflevels") - (sysctl-name "hw.optional.arm.FEAT_BF16") - (sysctl-name "hw.optional.arm.FEAT_DotProd") - (sysctl-name "hw.optional.arm.FEAT_FCMA") - (sysctl-name "hw.optional.arm.FEAT_FHM") - (sysctl-name "hw.optional.arm.FEAT_FP16") - (sysctl-name "hw.optional.arm.FEAT_I8MM") - (sysctl-name "hw.optional.arm.FEAT_JSCVT") - (sysctl-name "hw.optional.arm.FEAT_LSE") - (sysctl-name "hw.optional.arm.FEAT_RDM") - (sysctl-name "hw.optional.arm.FEAT_SHA512") - (sysctl-name "hw.optional.armv8_2_sha512") - (sysctl-name "hw.memsize") - (sysctl-name "hw.pagesize") + ; Chrome locks these CPU feature detection down a bit more tightly, + ; but mostly for fingerprinting concerns which isn't an issue for codex. + (sysctl-name-prefix "hw.optional.arm.") + (sysctl-name-prefix "hw.optional.armv8_") (sysctl-name "hw.packages") (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.pagesize") (sysctl-name "hw.physicalcpu_max") (sysctl-name "hw.tbfrequency_compat") (sysctl-name "hw.vectorunit") (sysctl-name "kern.hostname") (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.maxproc") (sysctl-name "kern.osproductversion") (sysctl-name "kern.osrelease") (sysctl-name "kern.ostype") @@ -63,23 +64,25 @@ (sysctl-name "kern.usrstack64") (sysctl-name "kern.version") (sysctl-name "sysctl.proc_cputype") + (sysctl-name "vm.loadavg") (sysctl-name-prefix "hw.perflevel") + (sysctl-name-prefix "kern.proc.pgrp.") + (sysctl-name-prefix "kern.proc.pid.") + (sysctl-name-prefix "net.routetable.") ) -; Added on top of Chrome profile -; Needed for python multiprocessing on MacOS for the SemLock -(allow ipc-posix-sem) +; IOKit +(allow iokit-open + (iokit-registry-entry-class "RootDomainUserClient") +) -; Allow reading user preferences (common for many CLIs) and basic -; OpenDirectory/DirecoryService lookups used by APIs like `os.userInfo()`. -; This keeps the policy minimal while avoiding surprising failures from -; system libraries that query user information. -(allow user-preference-read) +; needed to look up user info, see https://crbug.com/792228 (allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo") - (global-name "com.apple.system.opendirectoryd.membership") - (global-name "com.apple.system.DirectoryService.libinfo_v1") - (global-name "com.apple.cfprefsd.agent") - (global-name "com.apple.cfprefsd.daemon") - (global-name "com.apple.securityd") ) + +; Added on top of Chrome profile +; Needed for python multiprocessing on MacOS for the SemLock +(allow ipc-posix-sem) + +; (fork) Additional mach-lookup allowances merged above; keep single block. diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 2fe8661f0ec2..99b19796b87f 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -100,10 +100,13 @@ type OutputBuffer = Arc>; type OutputHandles = (OutputBuffer, Arc); impl ManagedUnifiedExecSession { - fn new(session: ExecCommandSession) -> Self { + fn new( + session: ExecCommandSession, + initial_output_rx: tokio::sync::broadcast::Receiver>, + ) -> Self { let output_buffer = Arc::new(Mutex::new(OutputBufferState::default())); let output_notify = Arc::new(Notify::new()); - let mut receiver = session.output_receiver(); + let mut receiver = initial_output_rx; let buffer_clone = Arc::clone(&output_buffer); let notify_clone = Arc::clone(&output_notify); let output_task = tokio::spawn(async move { @@ -193,8 +196,8 @@ impl UnifiedExecSessionManager { } else { let command = request.input_chunks.to_vec(); let new_id = self.next_session_id.fetch_add(1, Ordering::SeqCst); - let session = create_unified_exec_session(&command).await?; - let managed_session = ManagedUnifiedExecSession::new(session); + let (session, initial_output_rx) = create_unified_exec_session(&command).await?; + let managed_session = ManagedUnifiedExecSession::new(session, initial_output_rx); let (buffer, notify) = managed_session.output_handles(); writer_tx = managed_session.writer_sender(); output_buffer = buffer; @@ -297,7 +300,13 @@ impl UnifiedExecSessionManager { async fn create_unified_exec_session( command: &[String], -) -> Result { +) -> Result< + ( + ExecCommandSession, + tokio::sync::broadcast::Receiver>, + ), + UnifiedExecError, +> { if command.is_empty() { return Err(UnifiedExecError::MissingCommandLine); } @@ -380,7 +389,7 @@ async fn create_unified_exec_session( wait_exit_status.store(true, Ordering::SeqCst); }); - Ok(ExecCommandSession::new( + let (session, initial_output_rx) = ExecCommandSession::new( writer_tx, output_tx, killer, @@ -388,7 +397,8 @@ async fn create_unified_exec_session( writer_handle, wait_handle, exit_status, - )) + ); + Ok((session, initial_output_rx)) } #[cfg(test)] @@ -421,7 +431,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: None, input_chunks: &["bash".to_string(), "-i".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; let session_id = open_shell.session_id.expect("expected session_id"); @@ -441,7 +451,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: Some(session_id), input_chunks: &["echo $CODEX_INTERACTIVE_SHELL_VAR\n".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; assert!(out_2.output.contains("codex")); @@ -458,7 +468,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: None, input_chunks: &["/bin/bash".to_string(), "-i".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; let session_a = shell_a.session_id.expect("expected session id"); @@ -467,7 +477,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: Some(session_a), input_chunks: &["export CODEX_INTERACTIVE_SHELL_VAR=codex\n".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; @@ -478,7 +488,7 @@ mod tests { "echo".to_string(), "$CODEX_INTERACTIVE_SHELL_VAR\n".to_string(), ], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; assert!(!out_2.output.contains("codex")); @@ -487,7 +497,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: Some(session_a), input_chunks: &["echo $CODEX_INTERACTIVE_SHELL_VAR\n".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; assert!(out_3.output.contains("codex")); @@ -504,7 +514,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: None, input_chunks: &["bash".to_string(), "-i".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; let session_id = open_shell.session_id.expect("expected session id"); @@ -516,7 +526,7 @@ mod tests { "export".to_string(), "CODEX_INTERACTIVE_SHELL_VAR=codex\n".to_string(), ], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; @@ -574,7 +584,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: None, input_chunks: &["/bin/echo".to_string(), "codex".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; @@ -595,7 +605,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: None, input_chunks: &["/bin/bash".to_string(), "-i".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; let session_id = open_shell.session_id.expect("expected session id"); @@ -604,7 +614,7 @@ mod tests { .handle_request(UnifiedExecRequest { session_id: Some(session_id), input_chunks: &["exit\n".to_string()], - timeout_ms: Some(1_500), + timeout_ms: Some(2_500), }) .await?; diff --git a/codex-rs/core/templates/compact/history_bridge.md b/codex-rs/core/templates/compact/history_bridge.md new file mode 100644 index 000000000000..ef003440afa6 --- /dev/null +++ b/codex-rs/core/templates/compact/history_bridge.md @@ -0,0 +1,7 @@ +You were originally given instructions from a user over one or more turns. Here were the user messages: + +{{ user_messages_text }} + +Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis: + +{{ summary_text }} \ No newline at end of file diff --git a/codex-rs/core/templates/compact/prompt.md b/codex-rs/core/templates/compact/prompt.md new file mode 100644 index 000000000000..47dfeaba5e87 --- /dev/null +++ b/codex-rs/core/templates/compact/prompt.md @@ -0,0 +1,5 @@ +You have exceeded the maximum number of tokens, please stop coding and instead write a short memento message for the next agent. Your note should: +- Summarize what you finished and what still needs work. If there was a recent update_plan call, repeat its steps verbatim. +- List outstanding TODOs with file paths / line numbers so they're easy to find. +- Flag code that needs more tests (edge cases, performance, integration, etc.). +- Record any open bugs, quirks, or setup steps that will make it easier for the next agent to pick up where you left off. \ No newline at end of file diff --git a/codex-rs/core/tests/suite/cli_stream.rs b/codex-rs/core/tests/suite/cli_stream.rs index 01b9d6bfc15a..1a0c01627256 100644 --- a/codex-rs/core/tests/suite/cli_stream.rs +++ b/codex-rs/core/tests/suite/cli_stream.rs @@ -420,12 +420,6 @@ async fn integration_creates_and_checks_session_file() { // Second run: resume should update the existing file. let marker2 = format!("integration-resume-{}", Uuid::new_v4()); let prompt2 = format!("echo {marker2}"); - // Cross‑platform safe resume override. On Windows, backslashes in a TOML string must be escaped - // or the parse will fail and the raw literal (including quotes) may be preserved all the way down - // to Config, which in turn breaks resume because the path is invalid. Normalize to forward slashes - // to sidestep the issue. - let resume_path_str = path.to_string_lossy().replace('\\', "/"); - let resume_override = format!("experimental_resume=\"{resume_path_str}\""); let mut cmd2 = AssertCommand::new("cargo"); cmd2.arg("run") .arg("-p") @@ -434,11 +428,11 @@ async fn integration_creates_and_checks_session_file() { .arg("--") .arg("exec") .arg("--skip-git-repo-check") - .arg("-c") - .arg(&resume_override) .arg("-C") .arg(env!("CARGO_MANIFEST_DIR")) - .arg(&prompt2); + .arg(&prompt2) + .arg("resume") + .arg("--last"); cmd2.env("CODEX_HOME", home.path()) .env("OPENAI_API_KEY", "dummy") .env("CODEX_RS_SSE_FIXTURE", &fixture) diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 07a51f07a6e5..cfc6f5f40c41 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1,19 +1,34 @@ use codex_core::CodexAuth; +use codex_core::ContentItem; use codex_core::ConversationManager; +use codex_core::LocalShellAction; +use codex_core::LocalShellExecAction; +use codex_core::LocalShellStatus; +use codex_core::ModelClient; use codex_core::ModelProviderInfo; use codex_core::NewConversation; +use codex_core::Prompt; +use codex_core::ReasoningItemContent; +use codex_core::ResponseEvent; +use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::built_in_model_providers; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; -use codex_protocol::mcp_protocol::AuthMode; +use codex_protocol::mcp_protocol::ConversationId; +use codex_protocol::models::ReasoningItemReasoningSummary; +use codex_protocol::models::WebSearchAction; use core_test_support::load_default_config_for_test; use core_test_support::load_sse_fixture_with_id; use core_test_support::wait_for_event; +use futures::StreamExt; use serde_json::json; +use std::io::Write; +use std::sync::Arc; use tempfile::TempDir; +use uuid::Uuid; use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; @@ -96,7 +111,7 @@ fn write_auth_json( "OPENAI_API_KEY": openai_api_key, "tokens": tokens, // RFC3339 datetime; value doesn't matter for these tests - "last_refresh": "2025-08-06T20:41:36.232376Z", + "last_refresh": chrono::Utc::now(), }); std::fs::write( @@ -109,7 +124,7 @@ fn write_auth_json( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_session_id_and_model_headers_in_request() { +async fn resume_includes_initial_messages_and_sends_prior_items() { if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { println!( "Skipping test because it cannot execute when network is disabled in a Codex sandbox." @@ -117,14 +132,95 @@ async fn includes_session_id_and_model_headers_in_request() { return; } - // Mock server - let server = MockServer::start().await; + // Create a fake rollout session file with prior user + system + assistant messages. + let tmpdir = TempDir::new().unwrap(); + let session_path = tmpdir.path().join("resume-session.jsonl"); + let mut f = std::fs::File::create(&session_path).unwrap(); + let convo_id = Uuid::new_v4(); + writeln!( + f, + "{}", + json!({ + "timestamp": "2024-01-01T00:00:00.000Z", + "type": "session_meta", + "payload": { + "id": convo_id, + "timestamp": "2024-01-01T00:00:00Z", + "instructions": "be nice", + "cwd": ".", + "originator": "test_originator", + "cli_version": "test_version" + } + }) + ) + .unwrap(); - // First request – must NOT include `previous_response_id`. + // Prior item: user message (should be delivered) + let prior_user = codex_protocol::models::ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![codex_protocol::models::ContentItem::InputText { + text: "resumed user message".to_string(), + }], + }; + let prior_user_json = serde_json::to_value(&prior_user).unwrap(); + writeln!( + f, + "{}", + json!({ + "timestamp": "2024-01-01T00:00:01.000Z", + "type": "response_item", + "payload": prior_user_json + }) + ) + .unwrap(); + + // Prior item: system message (excluded from API history) + let prior_system = codex_protocol::models::ResponseItem::Message { + id: None, + role: "system".to_string(), + content: vec![codex_protocol::models::ContentItem::OutputText { + text: "resumed system instruction".to_string(), + }], + }; + let prior_system_json = serde_json::to_value(&prior_system).unwrap(); + writeln!( + f, + "{}", + json!({ + "timestamp": "2024-01-01T00:00:02.000Z", + "type": "response_item", + "payload": prior_system_json + }) + ) + .unwrap(); + + // Prior item: assistant message + let prior_item = codex_protocol::models::ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![codex_protocol::models::ContentItem::OutputText { + text: "resumed assistant message".to_string(), + }], + }; + let prior_item_json = serde_json::to_value(&prior_item).unwrap(); + writeln!( + f, + "{}", + json!({ + "timestamp": "2024-01-01T00:00:03.000Z", + "type": "response_item", + "payload": prior_item_json + }) + ) + .unwrap(); + drop(f); + + // Mock server that will receive the resumed request + let server = MockServer::start().await; let first = ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") .set_body_raw(sse_completed("resp1"), "text/event-stream"); - Mock::given(method("POST")) .and(path("/v1/responses")) .respond_with(first) @@ -132,27 +228,40 @@ async fn includes_session_id_and_model_headers_in_request() { .mount(&server) .await; + // Configure Codex to resume from our file let model_provider = ModelProviderInfo { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - - // Init session let codex_home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&codex_home); config.model_provider = model_provider; + // Also configure user instructions to ensure they are NOT delivered on resume. + config.user_instructions = Some("be nice".to_string()); let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); + let auth_manager = + codex_core::AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let NewConversation { conversation: codex, - conversation_id, - session_configured: _, + session_configured, + .. } = conversation_manager - .new_conversation(config) + .resume_conversation_from_rollout(config, session_path.clone(), auth_manager) .await - .expect("create new conversation"); - + .expect("resume conversation"); + + // 1) Assert initial_messages only includes existing EventMsg entries; response items are not converted + let initial_msgs = session_configured + .initial_messages + .clone() + .expect("expected initial messages option for resumed session"); + let initial_json = serde_json::to_value(&initial_msgs).unwrap(); + let expected_initial_json = json!([]); + assert_eq!(initial_json, expected_initial_json); + + // 2) Submit new input; the request body must include the prior item followed by the new user input. codex .submit(Op::UserInput { items: vec![InputItem::Text { @@ -161,28 +270,39 @@ async fn includes_session_id_and_model_headers_in_request() { }) .await .unwrap(); - wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; - // get request from the server let request = &server.received_requests().await.unwrap()[0]; - let request_session_id = request.headers.get("session_id").unwrap(); - let request_authorization = request.headers.get("authorization").unwrap(); - let request_originator = request.headers.get("originator").unwrap(); - - assert_eq!( - request_session_id.to_str().unwrap(), - conversation_id.to_string() - ); - assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs"); - assert_eq!( - request_authorization.to_str().unwrap(), - "Bearer Test API Key" - ); + let request_body = request.body_json::().unwrap(); + let expected_input = json!([ + { + "type": "message", + "role": "user", + "content": [{ "type": "input_text", "text": "resumed user message" }] + }, + { + "type": "message", + "role": "assistant", + "content": [{ "type": "output_text", "text": "resumed assistant message" }] + }, + { + "type": "message", + "role": "user", + "content": [{ "type": "input_text", "text": "hello" }] + } + ]); + assert_eq!(request_body["input"], expected_input); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_base_instructions_override_in_request() { +async fn includes_conversation_id_and_model_headers_in_request() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + // Mock server let server = MockServer::start().await; @@ -202,19 +322,22 @@ async fn includes_base_instructions_override_in_request() { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; + + // Init session let codex_home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&codex_home); - - config.base_instructions = Some("test instructions".to_string()); config.model_provider = model_provider; let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); - let codex = conversation_manager + let NewConversation { + conversation: codex, + conversation_id, + session_configured: _, + } = conversation_manager .new_conversation(config) .await - .expect("create new conversation") - .conversation; + .expect("create new conversation"); codex .submit(Op::UserInput { @@ -227,22 +350,29 @@ async fn includes_base_instructions_override_in_request() { wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + // get request from the server let request = &server.received_requests().await.unwrap()[0]; - let request_body = request.body_json::().unwrap(); + let request_conversation_id = request.headers.get("conversation_id").unwrap(); + let request_authorization = request.headers.get("authorization").unwrap(); + let request_originator = request.headers.get("originator").unwrap(); - assert!( - request_body["instructions"] - .as_str() - .unwrap() - .contains("test instructions") + assert_eq!( + request_conversation_id.to_str().unwrap(), + conversation_id.to_string() + ); + assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs"); + assert_eq!( + request_authorization.to_str().unwrap(), + "Bearer Test API Key" ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn originator_config_override_is_used() { +async fn includes_base_instructions_override_in_request() { // Mock server let server = MockServer::start().await; + // First request – must NOT include `previous_response_id`. let first = ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") .set_body_raw(sse_completed("resp1"), "text/event-stream"); @@ -258,11 +388,11 @@ async fn originator_config_override_is_used() { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let codex_home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&codex_home); + + config.base_instructions = Some("test instructions".to_string()); config.model_provider = model_provider; - config.responses_originator_header = "my_override".to_owned(); let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); @@ -284,8 +414,14 @@ async fn originator_config_override_is_used() { wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; let request = &server.received_requests().await.unwrap()[0]; - let request_originator = request.headers.get("originator").unwrap(); - assert_eq!(request_originator.to_str().unwrap(), "my_override"); + let request_body = request.body_json::().unwrap(); + + assert!( + request_body["instructions"] + .as_str() + .unwrap() + .contains("test instructions") + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -344,14 +480,14 @@ async fn chatgpt_auth_sends_correct_request() { // get request from the server let request = &server.received_requests().await.unwrap()[0]; - let request_session_id = request.headers.get("session_id").unwrap(); + let request_conversation_id = request.headers.get("conversation_id").unwrap(); let request_authorization = request.headers.get("authorization").unwrap(); let request_originator = request.headers.get("originator").unwrap(); let request_chatgpt_account_id = request.headers.get("chatgpt-account-id").unwrap(); let request_body = request.body_json::().unwrap(); assert_eq!( - request_session_id.to_str().unwrap(), + request_conversation_id.to_str().unwrap(), conversation_id.to_string() ); assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs"); @@ -360,7 +496,6 @@ async fn chatgpt_auth_sends_correct_request() { "Bearer Access Token" ); assert_eq!(request_chatgpt_account_id.to_str().unwrap(), "account_id"); - assert!(!request_body["store"].as_bool().unwrap()); assert!(request_body["stream"].as_bool().unwrap()); assert_eq!( request_body["include"][0].as_str().unwrap(), @@ -368,90 +503,6 @@ async fn chatgpt_auth_sends_correct_request() { ); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn prefers_chatgpt_token_when_config_prefers_chatgpt() { - if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { - println!( - "Skipping test because it cannot execute when network is disabled in a Codex sandbox." - ); - return; - } - - // Mock server - let server = MockServer::start().await; - - let first = ResponseTemplate::new(200) - .insert_header("content-type", "text/event-stream") - .set_body_raw(sse_completed("resp1"), "text/event-stream"); - - // Expect ChatGPT base path and correct headers - Mock::given(method("POST")) - .and(path("/v1/responses")) - .and(header_regex("Authorization", r"Bearer Access-123")) - .and(header_regex("chatgpt-account-id", r"acc-123")) - .respond_with(first) - .expect(1) - .mount(&server) - .await; - - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; - - // Init session - let codex_home = TempDir::new().unwrap(); - // Write auth.json that contains both API key and ChatGPT tokens for a plan that should prefer ChatGPT. - let _jwt = write_auth_json( - &codex_home, - Some("sk-test-key"), - "pro", - "Access-123", - Some("acc-123"), - ); - - let mut config = load_default_config_for_test(&codex_home); - config.model_provider = model_provider; - config.preferred_auth_method = AuthMode::ChatGPT; - - let auth_manager = match CodexAuth::from_codex_home( - codex_home.path(), - config.preferred_auth_method, - &config.responses_originator_header, - ) { - Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), - Ok(None) => panic!("No CodexAuth found in codex_home"), - Err(e) => panic!("Failed to load CodexAuth: {e}"), - }; - let conversation_manager = ConversationManager::new(auth_manager); - let NewConversation { - conversation: codex, - .. - } = conversation_manager - .new_conversation(config) - .await - .expect("create new conversation"); - - codex - .submit(Op::UserInput { - items: vec![InputItem::Text { - text: "hello".into(), - }], - }) - .await - .unwrap(); - - wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; - - // verify request body flags - let request = &server.received_requests().await.unwrap()[0]; - let request_body = request.body_json::().unwrap(); - assert!( - !request_body["store"].as_bool().unwrap(), - "store should be false for ChatGPT auth" - ); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { @@ -496,13 +547,8 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { let mut config = load_default_config_for_test(&codex_home); config.model_provider = model_provider; - config.preferred_auth_method = AuthMode::ApiKey; - let auth_manager = match CodexAuth::from_codex_home( - codex_home.path(), - config.preferred_auth_method, - &config.responses_originator_header, - ) { + let auth_manager = match CodexAuth::from_codex_home(codex_home.path()) { Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), Ok(None) => panic!("No CodexAuth found in codex_home"), Err(e) => panic!("Failed to load CodexAuth: {e}"), @@ -526,14 +572,6 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { .unwrap(); wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; - - // verify request body flags - let request = &server.received_requests().await.unwrap()[0]; - let request_body = request.body_json::().unwrap(); - assert!( - request_body["store"].as_bool().unwrap(), - "store should be true for API key auth" - ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -597,6 +635,147 @@ async fn includes_user_instructions_message_in_request() { assert_message_ends_with(&request_body["input"][1], ""); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn azure_responses_request_includes_store_and_reasoning_ids() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let server = MockServer::start().await; + + let sse_body = concat!( + "data: {\"type\":\"response.created\",\"response\":{}}\n\n", + "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\"}}\n\n", + ); + + let template = ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(sse_body, "text/event-stream"); + + Mock::given(method("POST")) + .and(path("/openai/responses")) + .respond_with(template) + .expect(1) + .mount(&server) + .await; + + let provider = ModelProviderInfo { + name: "azure".into(), + base_url: Some(format!("{}/openai", server.uri())), + env_key: None, + env_key_instructions: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: Some(0), + stream_max_retries: Some(0), + stream_idle_timeout_ms: Some(5_000), + requires_openai_auth: false, + }; + + let codex_home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&codex_home); + config.model_provider_id = provider.name.clone(); + config.model_provider = provider.clone(); + let effort = config.model_reasoning_effort; + let summary = config.model_reasoning_summary; + let config = Arc::new(config); + + let client = ModelClient::new( + Arc::clone(&config), + None, + provider, + effort, + summary, + ConversationId::new(), + ); + + let mut prompt = Prompt::default(); + prompt.input.push(ResponseItem::Reasoning { + id: "reasoning-id".into(), + summary: vec![ReasoningItemReasoningSummary::SummaryText { + text: "summary".into(), + }], + content: Some(vec![ReasoningItemContent::ReasoningText { + text: "content".into(), + }]), + encrypted_content: None, + }); + prompt.input.push(ResponseItem::Message { + id: Some("message-id".into()), + role: "assistant".into(), + content: vec![ContentItem::OutputText { + text: "message".into(), + }], + }); + prompt.input.push(ResponseItem::WebSearchCall { + id: Some("web-search-id".into()), + status: Some("completed".into()), + action: WebSearchAction::Search { + query: "weather".into(), + }, + }); + prompt.input.push(ResponseItem::FunctionCall { + id: Some("function-id".into()), + name: "do_thing".into(), + arguments: "{}".into(), + call_id: "function-call-id".into(), + }); + prompt.input.push(ResponseItem::LocalShellCall { + id: Some("local-shell-id".into()), + call_id: Some("local-shell-call-id".into()), + status: LocalShellStatus::Completed, + action: LocalShellAction::Exec(LocalShellExecAction { + command: vec!["echo".into(), "hello".into()], + timeout_ms: None, + working_directory: None, + env: None, + user: None, + }), + }); + prompt.input.push(ResponseItem::CustomToolCall { + id: Some("custom-tool-id".into()), + status: Some("completed".into()), + call_id: "custom-tool-call-id".into(), + name: "custom_tool".into(), + input: "{}".into(), + }); + + let mut stream = client + .stream(&prompt) + .await + .expect("responses stream to start"); + + while let Some(event) = stream.next().await { + if let Ok(ResponseEvent::Completed { .. }) = event { + break; + } + } + + let requests = server + .received_requests() + .await + .expect("mock server collected requests"); + assert_eq!(requests.len(), 1, "expected a single request"); + let body: serde_json::Value = requests[0] + .body_json() + .expect("request body to be valid JSON"); + + assert_eq!(body["store"], serde_json::Value::Bool(true)); + assert_eq!(body["stream"], serde_json::Value::Bool(true)); + assert_eq!(body["input"].as_array().map(Vec::len), Some(6)); + assert_eq!(body["input"][0]["id"].as_str(), Some("reasoning-id")); + assert_eq!(body["input"][1]["id"].as_str(), Some("message-id")); + assert_eq!(body["input"][2]["id"].as_str(), Some("web-search-id")); + assert_eq!(body["input"][3]["id"].as_str(), Some("function-id")); + assert_eq!(body["input"][4]["id"].as_str(), Some("local-shell-id")); + assert_eq!(body["input"][5]["id"].as_str(), Some("custom-tool-id")); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn azure_overrides_assign_properties_used_for_responses_url() { let existing_env_var_with_random_value = if cfg!(windows) { "USERNAME" } else { "USER" }; @@ -851,34 +1030,29 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { assert_eq!(requests.len(), 3, "expected 3 requests (one per turn)"); // Replace full-array compare with tail-only raw JSON compare using a single hard-coded value. - let r3_tail_expected = serde_json::json!([ + let r3_tail_expected = json!([ { "type": "message", - "id": null, "role": "user", "content": [{"type":"input_text","text":"U1"}] }, { "type": "message", - "id": null, "role": "assistant", "content": [{"type":"output_text","text":"Hey there!\n"}] }, { "type": "message", - "id": null, "role": "user", "content": [{"type":"input_text","text":"U2"}] }, { "type": "message", - "id": null, "role": "assistant", "content": [{"type":"output_text","text":"Hey there!\n"}] }, { "type": "message", - "id": null, "role": "user", "content": [{"type":"input_text","text":"U3"}] } diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index f5e854d2753a..361315f72438 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -3,27 +3,38 @@ use codex_core::CodexAuth; use codex_core::ConversationManager; use codex_core::ModelProviderInfo; +use codex_core::NewConversation; use codex_core::built_in_model_providers; +use codex_core::protocol::ErrorEvent; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; +use codex_core::protocol::RolloutItem; +use codex_core::protocol::RolloutLine; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use core_test_support::load_default_config_for_test; use core_test_support::wait_for_event; use serde_json::Value; use tempfile::TempDir; +use wiremock::BodyPrintLimit; use wiremock::Mock; use wiremock::MockServer; +use wiremock::Request; +use wiremock::Respond; use wiremock::ResponseTemplate; use wiremock::matchers::method; use wiremock::matchers::path; use pretty_assertions::assert_eq; +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; // --- Test helpers ----------------------------------------------------------- /// Build an SSE stream body from a list of JSON events. -fn sse(events: Vec) -> String { +pub(super) fn sse(events: Vec) -> String { use std::fmt::Write as _; let mut out = String::new(); for ev in events { @@ -39,7 +50,7 @@ fn sse(events: Vec) -> String { } /// Convenience: SSE event for a completed response with a specific id. -fn ev_completed(id: &str) -> Value { +pub(super) fn ev_completed(id: &str) -> Value { serde_json::json!({ "type": "response.completed", "response": { @@ -49,8 +60,24 @@ fn ev_completed(id: &str) -> Value { }) } +fn ev_completed_with_tokens(id: &str, total_tokens: u64) -> Value { + serde_json::json!({ + "type": "response.completed", + "response": { + "id": id, + "usage": { + "input_tokens": total_tokens, + "input_tokens_details": null, + "output_tokens": 0, + "output_tokens_details": null, + "total_tokens": total_tokens + } + } + }) +} + /// Convenience: SSE event for a single assistant message output item. -fn ev_assistant_message(id: &str, text: &str) -> Value { +pub(super) fn ev_assistant_message(id: &str, text: &str) -> Value { serde_json::json!({ "type": "response.output_item.done", "item": { @@ -62,13 +89,25 @@ fn ev_assistant_message(id: &str, text: &str) -> Value { }) } -fn sse_response(body: String) -> ResponseTemplate { +fn ev_function_call(call_id: &str, name: &str, arguments: &str) -> Value { + serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "function_call", + "call_id": call_id, + "name": name, + "arguments": arguments + } + }) +} + +pub(super) fn sse_response(body: String) -> ResponseTemplate { ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") .set_body_raw(body, "text/event-stream") } -async fn mount_sse_once(server: &MockServer, matcher: M, body: String) +pub(super) async fn mount_sse_once(server: &MockServer, matcher: M, body: String) where M: wiremock::Match + Send + Sync + 'static, { @@ -76,15 +115,32 @@ where .and(path("/v1/responses")) .and(matcher) .respond_with(sse_response(body)) - .expect(1) .mount(server) .await; } -const FIRST_REPLY: &str = "FIRST_REPLY"; -const SUMMARY_TEXT: &str = "SUMMARY_ONLY_CONTEXT"; -const SUMMARIZE_TRIGGER: &str = "Start Summarization"; +async fn start_mock_server() -> MockServer { + MockServer::builder() + .body_print_limit(BodyPrintLimit::Limited(80_000)) + .start() + .await +} + +pub(super) const FIRST_REPLY: &str = "FIRST_REPLY"; +pub(super) const SUMMARY_TEXT: &str = "SUMMARY_ONLY_CONTEXT"; +pub(super) const SUMMARIZE_TRIGGER: &str = "Start Summarization"; const THIRD_USER_MSG: &str = "next turn"; +const AUTO_SUMMARY_TEXT: &str = "AUTO_SUMMARY"; +const FIRST_AUTO_MSG: &str = "token limit start"; +const SECOND_AUTO_MSG: &str = "token limit push"; +const STILL_TOO_BIG_REPLY: &str = "STILL_TOO_BIG"; +const MULTI_AUTO_MSG: &str = "multi auto"; +const SECOND_LARGE_REPLY: &str = "SECOND_LARGE_REPLY"; +const FIRST_AUTO_SUMMARY: &str = "FIRST_AUTO_SUMMARY"; +const SECOND_AUTO_SUMMARY: &str = "SECOND_AUTO_SUMMARY"; +const FINAL_REPLY: &str = "FINAL_REPLY"; +const DUMMY_FUNCTION_NAME: &str = "unsupported_tool"; +const DUMMY_CALL_ID: &str = "call-multi-auto"; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn summarize_context_three_requests_and_instructions() { @@ -96,7 +152,7 @@ async fn summarize_context_three_requests_and_instructions() { } // Set up a mock server that we can inspect after the run. - let server = MockServer::start().await; + let server = start_mock_server().await; // SSE 1: assistant replies normally so it is recorded in history. let sse1 = sse(vec![ @@ -141,12 +197,14 @@ async fn summarize_context_three_requests_and_instructions() { let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); config.model_provider = model_provider; + config.model_auto_compact_token_limit = Some(200_000); let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("dummy")); - let codex = conversation_manager - .new_conversation(config) - .await - .unwrap() - .conversation; + let NewConversation { + conversation: codex, + session_configured, + .. + } = conversation_manager.new_conversation(config).await.unwrap(); + let rollout_path = session_configured.rollout_path; // 1) Normal user input – should hit server once. codex @@ -194,7 +252,7 @@ async fn summarize_context_three_requests_and_instructions() { "summarization should override base instructions" ); assert!( - instr2.contains("You are a summarization assistant"), + instr2.contains("You have exceeded the maximum number of tokens"), "summarization instructions not applied" ); @@ -205,14 +263,17 @@ async fn summarize_context_three_requests_and_instructions() { assert_eq!(last2.get("type").unwrap().as_str().unwrap(), "message"); assert_eq!(last2.get("role").unwrap().as_str().unwrap(), "user"); let text2 = last2["content"][0]["text"].as_str().unwrap(); - assert!(text2.contains(SUMMARIZE_TRIGGER)); + assert!( + text2.contains(SUMMARIZE_TRIGGER), + "expected summarize trigger, got `{text2}`" + ); - // Third request must contain only the summary from step 2 as prior history plus new user msg. + // Third request must contain the refreshed instructions, bridge summary message and new user msg. let input3 = body3.get("input").and_then(|v| v.as_array()).unwrap(); println!("third request body: {body3}"); assert!( - input3.len() >= 2, - "expected summary + new user message in third request" + input3.len() >= 3, + "expected refreshed context and new user message in third request" ); // Collect all (role, text) message tuples. @@ -228,24 +289,611 @@ async fn summarize_context_three_requests_and_instructions() { } } - // Exactly one assistant message should remain after compaction and the new user message is present. + // No previous assistant messages should remain and the new user message is present. let assistant_count = messages.iter().filter(|(r, _)| r == "assistant").count(); - assert_eq!( - assistant_count, 1, - "exactly one assistant message should remain after compaction" - ); + assert_eq!(assistant_count, 0, "assistant history should be cleared"); assert!( messages .iter() .any(|(r, t)| r == "user" && t == THIRD_USER_MSG), "third request should include the new user message" ); + let Some((_, bridge_text)) = messages.iter().find(|(role, text)| { + role == "user" + && (text.contains("Here were the user messages") + || text.contains("Here are all the user messages")) + && text.contains(SUMMARY_TEXT) + }) else { + panic!("expected a bridge message containing the summary"); + }; assert!( - !messages.iter().any(|(_, t)| t.contains("hello world")), - "third request should not include the original user input" + bridge_text.contains("hello world"), + "bridge should capture earlier user messages" ); assert!( - !messages.iter().any(|(_, t)| t.contains(SUMMARIZE_TRIGGER)), + !bridge_text.contains(SUMMARIZE_TRIGGER), + "bridge text should not echo the summarize trigger" + ); + assert!( + !messages + .iter() + .any(|(_, text)| text.contains(SUMMARIZE_TRIGGER)), "third request should not include the summarize trigger" ); + + // Shut down Codex to flush rollout entries before inspecting the file. + codex.submit(Op::Shutdown).await.unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await; + + // Verify rollout contains APITurn entries for each API call and a Compacted entry. + println!("rollout path: {}", rollout_path.display()); + let text = std::fs::read_to_string(&rollout_path).unwrap_or_else(|e| { + panic!( + "failed to read rollout file {}: {e}", + rollout_path.display() + ) + }); + let mut api_turn_count = 0usize; + let mut saw_compacted_summary = false; + for line in text.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let Ok(entry): Result = serde_json::from_str(trimmed) else { + continue; + }; + match entry.item { + RolloutItem::TurnContext(_) => { + api_turn_count += 1; + } + RolloutItem::Compacted(ci) => { + if ci.message == SUMMARY_TEXT { + saw_compacted_summary = true; + } + } + _ => {} + } + } + + assert!( + api_turn_count == 3, + "expected three APITurn entries in rollout" + ); + assert!( + saw_compacted_summary, + "expected a Compacted entry containing the summarizer output" + ); +} + +// Windows CI only: bump to 4 workers to prevent SSE/event starvation and test timeouts. +#[cfg_attr(windows, tokio::test(flavor = "multi_thread", worker_threads = 4))] +#[cfg_attr(not(windows), tokio::test(flavor = "multi_thread", worker_threads = 2))] +async fn auto_compact_runs_after_token_limit_hit() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let server = start_mock_server().await; + + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed_with_tokens("r1", 70_000), + ]); + + let sse2 = sse(vec![ + ev_assistant_message("m2", "SECOND_REPLY"), + ev_completed_with_tokens("r2", 330_000), + ]); + + let sse3 = sse(vec![ + ev_assistant_message("m3", AUTO_SUMMARY_TEXT), + ev_completed_with_tokens("r3", 200), + ]); + + let first_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(FIRST_AUTO_MSG) + && !body.contains(SECOND_AUTO_MSG) + && !body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(first_matcher) + .respond_with(sse_response(sse1)) + .mount(&server) + .await; + + let second_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(SECOND_AUTO_MSG) + && body.contains(FIRST_AUTO_MSG) + && !body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(second_matcher) + .respond_with(sse_response(sse2)) + .mount(&server) + .await; + + let third_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(third_matcher) + .respond_with(sse_response(sse3)) + .mount(&server) + .await; + + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + let home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&home); + config.model_provider = model_provider; + config.model_auto_compact_token_limit = Some(200_000); + let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("dummy")); + let codex = conversation_manager + .new_conversation(config) + .await + .unwrap() + .conversation; + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: FIRST_AUTO_MSG.into(), + }], + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: SECOND_AUTO_MSG.into(), + }], + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + // wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + let requests = server.received_requests().await.unwrap(); + assert!( + requests.len() >= 3, + "auto compact should add at least a third request, got {}", + requests.len() + ); + let is_auto_compact = |req: &wiremock::Request| { + std::str::from_utf8(&req.body) + .unwrap_or("") + .contains("You have exceeded the maximum number of tokens") + }; + let auto_compact_count = requests.iter().filter(|req| is_auto_compact(req)).count(); + assert_eq!( + auto_compact_count, 1, + "expected exactly one auto compact request" + ); + let auto_compact_index = requests + .iter() + .enumerate() + .find_map(|(idx, req)| is_auto_compact(req).then_some(idx)) + .expect("auto compact request missing"); + assert_eq!( + auto_compact_index, 2, + "auto compact should add a third request" + ); + + let body3 = requests[auto_compact_index] + .body_json::() + .unwrap(); + let instructions = body3 + .get("instructions") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + assert!( + instructions.contains("You have exceeded the maximum number of tokens"), + "auto compact should reuse summarization instructions" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auto_compact_persists_rollout_entries() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let server = start_mock_server().await; + + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed_with_tokens("r1", 70_000), + ]); + + let sse2 = sse(vec![ + ev_assistant_message("m2", "SECOND_REPLY"), + ev_completed_with_tokens("r2", 330_000), + ]); + + let sse3 = sse(vec![ + ev_assistant_message("m3", AUTO_SUMMARY_TEXT), + ev_completed_with_tokens("r3", 200), + ]); + + let first_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(FIRST_AUTO_MSG) + && !body.contains(SECOND_AUTO_MSG) + && !body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(first_matcher) + .respond_with(sse_response(sse1)) + .mount(&server) + .await; + + let second_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(SECOND_AUTO_MSG) + && body.contains(FIRST_AUTO_MSG) + && !body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(second_matcher) + .respond_with(sse_response(sse2)) + .mount(&server) + .await; + + let third_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(third_matcher) + .respond_with(sse_response(sse3)) + .mount(&server) + .await; + + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + let home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&home); + config.model_provider = model_provider; + let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("dummy")); + let NewConversation { + conversation: codex, + session_configured, + .. + } = conversation_manager.new_conversation(config).await.unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: FIRST_AUTO_MSG.into(), + }], + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: SECOND_AUTO_MSG.into(), + }], + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + codex.submit(Op::Shutdown).await.unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await; + + let rollout_path = session_configured.rollout_path; + let text = std::fs::read_to_string(&rollout_path).unwrap_or_else(|e| { + panic!( + "failed to read rollout file {}: {e}", + rollout_path.display() + ) + }); + + let mut turn_context_count = 0usize; + for line in text.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let Ok(entry): Result = serde_json::from_str(trimmed) else { + continue; + }; + match entry.item { + RolloutItem::TurnContext(_) => { + turn_context_count += 1; + } + RolloutItem::Compacted(_) => {} + _ => {} + } + } + + assert!( + turn_context_count >= 2, + "expected at least two turn context entries, got {turn_context_count}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auto_compact_stops_after_failed_attempt() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let server = start_mock_server().await; + + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed_with_tokens("r1", 500), + ]); + + let sse2 = sse(vec![ + ev_assistant_message("m2", SUMMARY_TEXT), + ev_completed_with_tokens("r2", 50), + ]); + + let sse3 = sse(vec![ + ev_assistant_message("m3", STILL_TOO_BIG_REPLY), + ev_completed_with_tokens("r3", 500), + ]); + + let first_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(FIRST_AUTO_MSG) + && !body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(first_matcher) + .respond_with(sse_response(sse1.clone())) + .mount(&server) + .await; + + let second_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains("You have exceeded the maximum number of tokens") + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(second_matcher) + .respond_with(sse_response(sse2.clone())) + .mount(&server) + .await; + + let third_matcher = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + !body.contains("You have exceeded the maximum number of tokens") + && body.contains(SUMMARY_TEXT) + }; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .and(third_matcher) + .respond_with(sse_response(sse3.clone())) + .mount(&server) + .await; + + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + let home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&home); + config.model_provider = model_provider; + config.model_auto_compact_token_limit = Some(200); + let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("dummy")); + let codex = conversation_manager + .new_conversation(config) + .await + .unwrap() + .conversation; + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: FIRST_AUTO_MSG.into(), + }], + }) + .await + .unwrap(); + + let error_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Error(_))).await; + let EventMsg::Error(ErrorEvent { message }) = error_event else { + panic!("expected error event"); + }; + assert!( + message.contains("limit"), + "error message should include limit information: {message}" + ); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + let requests = server.received_requests().await.unwrap(); + assert_eq!( + requests.len(), + 3, + "auto compact should attempt at most one summarization before erroring" + ); + + let last_body = requests[2].body_json::().unwrap(); + let instructions = last_body + .get("instructions") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + assert!( + !instructions.contains("You have exceeded the maximum number of tokens"), + "third request should be the follow-up turn, not another summarization" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_events() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let server = start_mock_server().await; + + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed_with_tokens("r1", 500), + ]); + let sse2 = sse(vec![ + ev_assistant_message("m2", FIRST_AUTO_SUMMARY), + ev_completed_with_tokens("r2", 50), + ]); + let sse3 = sse(vec![ + ev_function_call(DUMMY_CALL_ID, DUMMY_FUNCTION_NAME, "{}"), + ev_completed_with_tokens("r3", 150), + ]); + let sse4 = sse(vec![ + ev_assistant_message("m4", SECOND_LARGE_REPLY), + ev_completed_with_tokens("r4", 450), + ]); + let sse5 = sse(vec![ + ev_assistant_message("m5", SECOND_AUTO_SUMMARY), + ev_completed_with_tokens("r5", 60), + ]); + let sse6 = sse(vec![ + ev_assistant_message("m6", FINAL_REPLY), + ev_completed_with_tokens("r6", 120), + ]); + + #[derive(Clone)] + struct SeqResponder { + bodies: Arc>, + calls: Arc, + requests: Arc>>>, + } + + impl SeqResponder { + fn new(bodies: Vec) -> Self { + Self { + bodies: Arc::new(bodies), + calls: Arc::new(AtomicUsize::new(0)), + requests: Arc::new(Mutex::new(Vec::new())), + } + } + + fn recorded_requests(&self) -> Vec> { + self.requests.lock().unwrap().clone() + } + } + + impl Respond for SeqResponder { + fn respond(&self, req: &Request) -> ResponseTemplate { + let idx = self.calls.fetch_add(1, Ordering::SeqCst); + self.requests.lock().unwrap().push(req.body.clone()); + let body = self + .bodies + .get(idx) + .unwrap_or_else(|| panic!("unexpected request index {idx}")) + .clone(); + ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(body, "text/event-stream") + } + } + + let responder = SeqResponder::new(vec![sse1, sse2, sse3, sse4, sse5, sse6]); + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with(responder.clone()) + .expect(6) + .mount(&server) + .await; + + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + let home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&home); + config.model_provider = model_provider; + config.model_auto_compact_token_limit = Some(200); + let conversation_manager = ConversationManager::with_auth(CodexAuth::from_api_key("dummy")); + let codex = conversation_manager + .new_conversation(config) + .await + .unwrap() + .conversation; + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: MULTI_AUTO_MSG.into(), + }], + }) + .await + .unwrap(); + + loop { + let event = codex.next_event().await.unwrap(); + if let EventMsg::TaskComplete(_) = &event.msg + && !event.id.starts_with("auto-compact-") + { + break; + } + } + + let request_bodies: Vec = responder + .recorded_requests() + .into_iter() + .map(|body| String::from_utf8(body).unwrap_or_default()) + .collect(); + assert_eq!( + request_bodies.len(), + 6, + "expected six requests including two auto compactions" + ); + assert!( + request_bodies[0].contains(MULTI_AUTO_MSG), + "first request should contain the user input" + ); + assert!( + request_bodies[1].contains("You have exceeded the maximum number of tokens"), + "first auto compact request should use summarization instructions" + ); + assert!( + request_bodies[3].contains(&format!("unsupported call: {DUMMY_FUNCTION_NAME}")), + "function call output should be sent before the second auto compact" + ); + assert!( + request_bodies[4].contains("You have exceeded the maximum number of tokens"), + "second auto compact request should reuse summarization instructions" + ); } diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs new file mode 100644 index 000000000000..34b43ece9146 --- /dev/null +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -0,0 +1,838 @@ +#![allow(clippy::expect_used)] + +//! Integration tests that cover compacting, resuming, and forking conversations. +//! +//! Each test sets up a mocked SSE conversation and drives the conversation through +//! a specific sequence of operations. After every operation we capture the +//! request payload that Codex would send to the model and assert that the +//! model-visible history matches the expected sequence of messages. + +use super::compact::FIRST_REPLY; +use super::compact::SUMMARIZE_TRIGGER; +use super::compact::SUMMARY_TEXT; +use super::compact::ev_assistant_message; +use super::compact::ev_completed; +use super::compact::mount_sse_once; +use super::compact::sse; +use codex_core::CodexAuth; +use codex_core::CodexConversation; +use codex_core::ConversationManager; +use codex_core::ModelProviderInfo; +use codex_core::NewConversation; +use codex_core::built_in_model_providers; +use codex_core::config::Config; +use codex_core::protocol::ConversationPathResponseEvent; +use codex_core::protocol::EventMsg; +use codex_core::protocol::InputItem; +use codex_core::protocol::Op; +use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; +use core_test_support::load_default_config_for_test; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use serde_json::Value; +use serde_json::json; +use std::sync::Arc; +use tempfile::TempDir; +use wiremock::MockServer; + +const AFTER_SECOND_RESUME: &str = "AFTER_SECOND_RESUME"; + +fn network_disabled() -> bool { + std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +/// Scenario: compact an initial conversation, resume it, fork one turn back, and +/// ensure the model-visible history matches expectations at each request. +async fn compact_resume_and_fork_preserve_model_history_view() { + if network_disabled() { + println!("Skipping test because network is disabled in this sandbox"); + return; + } + + // 1. Arrange mocked SSE responses for the initial compact/resume/fork flow. + let server = MockServer::start().await; + mount_initial_flow(&server).await; + + // 2. Start a new conversation and drive it through the compact/resume/fork steps. + let (_home, config, manager, base) = start_test_conversation(&server).await; + + user_turn(&base, "hello world").await; + compact_conversation(&base).await; + user_turn(&base, "AFTER_COMPACT").await; + let base_path = fetch_conversation_path(&base, "base conversation").await; + assert!( + base_path.exists(), + "compact+resume test expects base path {base_path:?} to exist", + ); + + let resumed = resume_conversation(&manager, &config, base_path).await; + user_turn(&resumed, "AFTER_RESUME").await; + let resumed_path = fetch_conversation_path(&resumed, "resumed conversation").await; + assert!( + resumed_path.exists(), + "compact+resume test expects resumed path {resumed_path:?} to exist", + ); + + let forked = fork_conversation(&manager, &config, resumed_path, 1).await; + user_turn(&forked, "AFTER_FORK").await; + + // 3. Capture the requests to the model and validate the history slices. + let requests = gather_request_bodies(&server).await; + + // input after compact is a prefix of input after resume/fork + let input_after_compact = json!(requests[requests.len() - 3]["input"]); + let input_after_resume = json!(requests[requests.len() - 2]["input"]); + let input_after_fork = json!(requests[requests.len() - 1]["input"]); + + let compact_arr = input_after_compact + .as_array() + .expect("input after compact should be an array"); + let resume_arr = input_after_resume + .as_array() + .expect("input after resume should be an array"); + let fork_arr = input_after_fork + .as_array() + .expect("input after fork should be an array"); + + assert!( + compact_arr.len() <= resume_arr.len(), + "after-resume input should have at least as many items as after-compact", + ); + assert_eq!(compact_arr.as_slice(), &resume_arr[..compact_arr.len()]); + eprint!( + "len of compact: {}, len of fork: {}", + compact_arr.len(), + fork_arr.len() + ); + eprintln!("input_after_fork:{}", json!(input_after_fork)); + assert!( + compact_arr.len() <= fork_arr.len(), + "after-fork input should have at least as many items as after-compact", + ); + assert_eq!(compact_arr.as_slice(), &fork_arr[..compact_arr.len()]); + + let prompt = requests[0]["instructions"] + .as_str() + .unwrap_or_default() + .to_string(); + let user_instructions = requests[0]["input"][0]["content"][0]["text"] + .as_str() + .unwrap_or_default() + .to_string(); + let environment_context = requests[0]["input"][1]["content"][0]["text"] + .as_str() + .unwrap_or_default() + .to_string(); + let tool_calls = json!(requests[0]["tools"].as_array()); + let prompt_cache_key = requests[0]["prompt_cache_key"] + .as_str() + .unwrap_or_default() + .to_string(); + let fork_prompt_cache_key = requests[requests.len() - 1]["prompt_cache_key"] + .as_str() + .unwrap_or_default() + .to_string(); + let user_turn_1 = json!( + { + "model": "gpt-5", + "instructions": prompt, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_instructions + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": environment_context + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello world" + } + ] + } + ], + "tools": tool_calls, + "tool_choice": "auto", + "parallel_tool_calls": false, + "reasoning": { + "summary": "auto" + }, + "store": false, + "stream": true, + "include": [ + "reasoning.encrypted_content" + ], + "prompt_cache_key": prompt_cache_key + }); + let compact_1 = json!( + { + "model": "gpt-5", + "instructions": "You have exceeded the maximum number of tokens, please stop coding and instead write a short memento message for the next agent. Your note should: +- Summarize what you finished and what still needs work. If there was a recent update_plan call, repeat its steps verbatim. +- List outstanding TODOs with file paths / line numbers so they're easy to find. +- Flag code that needs more tests (edge cases, performance, integration, etc.). +- Record any open bugs, quirks, or setup steps that will make it easier for the next agent to pick up where you left off.", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_instructions + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": environment_context + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello world" + } + ] + }, + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "FIRST_REPLY" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Start Summarization" + } + ] + } + ], + "tools": [], + "tool_choice": "auto", + "parallel_tool_calls": false, + "reasoning": { + "summary": "auto" + }, + "store": false, + "stream": true, + "include": [ + "reasoning.encrypted_content" + ], + "prompt_cache_key": prompt_cache_key + }); + let user_turn_2_after_compact = json!( + { + "model": "gpt-5", + "instructions": prompt, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_instructions + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": environment_context + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "You were originally given instructions from a user over one or more turns. Here were the user messages: + +hello world + +Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis: + +SUMMARY_ONLY_CONTEXT" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "AFTER_COMPACT" + } + ] + } + ], + "tools": tool_calls, + "tool_choice": "auto", + "parallel_tool_calls": false, + "reasoning": { + "summary": "auto" + }, + "store": false, + "stream": true, + "include": [ + "reasoning.encrypted_content" + ], + "prompt_cache_key": prompt_cache_key + }); + let usert_turn_3_after_resume = json!( + { + "model": "gpt-5", + "instructions": prompt, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_instructions + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": environment_context + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "You were originally given instructions from a user over one or more turns. Here were the user messages: + +hello world + +Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis: + +SUMMARY_ONLY_CONTEXT" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "AFTER_COMPACT" + } + ] + }, + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "AFTER_COMPACT_REPLY" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "AFTER_RESUME" + } + ] + } + ], + "tools": tool_calls, + "tool_choice": "auto", + "parallel_tool_calls": false, + "reasoning": { + "summary": "auto" + }, + "store": false, + "stream": true, + "include": [ + "reasoning.encrypted_content" + ], + "prompt_cache_key": prompt_cache_key + }); + let user_turn_3_after_fork = json!( + { + "model": "gpt-5", + "instructions": prompt, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_instructions + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": environment_context + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "You were originally given instructions from a user over one or more turns. Here were the user messages: + +hello world + +Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis: + +SUMMARY_ONLY_CONTEXT" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "AFTER_COMPACT" + } + ] + }, + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "AFTER_COMPACT_REPLY" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "AFTER_FORK" + } + ] + } + ], + "tools": tool_calls, + "tool_choice": "auto", + "parallel_tool_calls": false, + "reasoning": { + "summary": "auto" + }, + "store": false, + "stream": true, + "include": [ + "reasoning.encrypted_content" + ], + "prompt_cache_key": fork_prompt_cache_key + }); + let expected = json!([ + user_turn_1, + compact_1, + user_turn_2_after_compact, + usert_turn_3_after_resume, + user_turn_3_after_fork + ]); + assert_eq!(requests.len(), 5); + assert_eq!(json!(requests), expected); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +/// Scenario: after the forked branch is compacted, resuming again should reuse +/// the compacted history and only append the new user message. +async fn compact_resume_after_second_compaction_preserves_history() { + if network_disabled() { + println!("Skipping test because network is disabled in this sandbox"); + return; + } + + // 1. Arrange mocked SSE responses for the initial flow plus the second compact. + let server = MockServer::start().await; + mount_initial_flow(&server).await; + mount_second_compact_flow(&server).await; + + // 2. Drive the conversation through compact -> resume -> fork -> compact -> resume. + let (_home, config, manager, base) = start_test_conversation(&server).await; + + user_turn(&base, "hello world").await; + compact_conversation(&base).await; + user_turn(&base, "AFTER_COMPACT").await; + let base_path = fetch_conversation_path(&base, "base conversation").await; + assert!( + base_path.exists(), + "second compact test expects base path {base_path:?} to exist", + ); + + let resumed = resume_conversation(&manager, &config, base_path).await; + user_turn(&resumed, "AFTER_RESUME").await; + let resumed_path = fetch_conversation_path(&resumed, "resumed conversation").await; + assert!( + resumed_path.exists(), + "second compact test expects resumed path {resumed_path:?} to exist", + ); + + let forked = fork_conversation(&manager, &config, resumed_path, 3).await; + user_turn(&forked, "AFTER_FORK").await; + + compact_conversation(&forked).await; + user_turn(&forked, "AFTER_COMPACT_2").await; + let forked_path = fetch_conversation_path(&forked, "forked conversation").await; + assert!( + forked_path.exists(), + "second compact test expects forked path {forked_path:?} to exist", + ); + + let resumed_again = resume_conversation(&manager, &config, forked_path).await; + user_turn(&resumed_again, AFTER_SECOND_RESUME).await; + + let requests = gather_request_bodies(&server).await; + let input_after_compact = json!(requests[requests.len() - 2]["input"]); + let input_after_resume = json!(requests[requests.len() - 1]["input"]); + + // test input after compact before resume is the same as input after resume + let compact_input_array = input_after_compact + .as_array() + .expect("input after compact should be an array"); + let resume_input_array = input_after_resume + .as_array() + .expect("input after resume should be an array"); + assert!( + compact_input_array.len() <= resume_input_array.len(), + "after-resume input should have at least as many items as after-compact" + ); + assert_eq!( + compact_input_array.as_slice(), + &resume_input_array[..compact_input_array.len()] + ); + // hard coded test + let prompt = requests[0]["instructions"] + .as_str() + .unwrap_or_default() + .to_string(); + let user_instructions = requests[0]["input"][0]["content"][0]["text"] + .as_str() + .unwrap_or_default() + .to_string(); + let environment_instructions = requests[0]["input"][1]["content"][0]["text"] + .as_str() + .unwrap_or_default() + .to_string(); + + let expected = json!([ + { + "instructions": prompt, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_instructions + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": environment_instructions + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "You were originally given instructions from a user over one or more turns. Here were the user messages:\n\nAFTER_FORK\n\nAnother language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis:\n\nSUMMARY_ONLY_CONTEXT" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "AFTER_COMPACT_2" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "AFTER_SECOND_RESUME" + } + ] + } + ], + } + ]); + let last_request_after_2_compacts = json!([{ + "instructions": requests[requests.len() -1]["instructions"], + "input": requests[requests.len() -1]["input"], + }]); + assert_eq!(expected, last_request_after_2_compacts); +} + +fn normalize_line_endings(value: &mut Value) { + match value { + Value::String(text) => { + if text.contains('\r') { + *text = text.replace("\r\n", "\n").replace('\r', "\n"); + } + } + Value::Array(items) => { + for item in items { + normalize_line_endings(item); + } + } + Value::Object(map) => { + for item in map.values_mut() { + normalize_line_endings(item); + } + } + _ => {} + } +} + +async fn gather_request_bodies(server: &MockServer) -> Vec { + server + .received_requests() + .await + .expect("mock server should not fail") + .into_iter() + .map(|req| { + let mut value = req.body_json::().expect("valid JSON body"); + normalize_line_endings(&mut value); + value + }) + .collect() +} + +async fn mount_initial_flow(server: &MockServer) { + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed("r1"), + ]); + let sse2 = sse(vec![ + ev_assistant_message("m2", SUMMARY_TEXT), + ev_completed("r2"), + ]); + let sse3 = sse(vec![ + ev_assistant_message("m3", "AFTER_COMPACT_REPLY"), + ev_completed("r3"), + ]); + let sse4 = sse(vec![ev_completed("r4")]); + let sse5 = sse(vec![ev_completed("r5")]); + + let match_first = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains("\"text\":\"hello world\"") + && !body.contains(&format!("\"text\":\"{SUMMARIZE_TRIGGER}\"")) + && !body.contains("\"text\":\"AFTER_COMPACT\"") + && !body.contains("\"text\":\"AFTER_RESUME\"") + && !body.contains("\"text\":\"AFTER_FORK\"") + }; + mount_sse_once(server, match_first, sse1).await; + + let match_compact = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(&format!("\"text\":\"{SUMMARIZE_TRIGGER}\"")) + }; + mount_sse_once(server, match_compact, sse2).await; + + let match_after_compact = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains("\"text\":\"AFTER_COMPACT\"") + && !body.contains("\"text\":\"AFTER_RESUME\"") + && !body.contains("\"text\":\"AFTER_FORK\"") + }; + mount_sse_once(server, match_after_compact, sse3).await; + + let match_after_resume = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains("\"text\":\"AFTER_RESUME\"") + }; + mount_sse_once(server, match_after_resume, sse4).await; + + let match_after_fork = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains("\"text\":\"AFTER_FORK\"") + }; + mount_sse_once(server, match_after_fork, sse5).await; +} + +async fn mount_second_compact_flow(server: &MockServer) { + let sse6 = sse(vec![ + ev_assistant_message("m4", SUMMARY_TEXT), + ev_completed("r6"), + ]); + let sse7 = sse(vec![ev_completed("r7")]); + + let match_second_compact = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(&format!("\"text\":\"{SUMMARIZE_TRIGGER}\"")) && body.contains("AFTER_FORK") + }; + mount_sse_once(server, match_second_compact, sse6).await; + + let match_after_second_resume = |req: &wiremock::Request| { + let body = std::str::from_utf8(&req.body).unwrap_or(""); + body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\"")) + }; + mount_sse_once(server, match_after_second_resume, sse7).await; +} + +async fn start_test_conversation( + server: &MockServer, +) -> (TempDir, Config, ConversationManager, Arc) { + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + let home = TempDir::new().expect("create temp dir"); + let mut config = load_default_config_for_test(&home); + config.model_provider = model_provider; + + let manager = ConversationManager::with_auth(CodexAuth::from_api_key("dummy")); + let NewConversation { conversation, .. } = manager + .new_conversation(config.clone()) + .await + .expect("create conversation"); + + (home, config, manager, conversation) +} + +async fn user_turn(conversation: &Arc, text: &str) { + conversation + .submit(Op::UserInput { + items: vec![InputItem::Text { text: text.into() }], + }) + .await + .expect("submit user turn"); + wait_for_event(conversation, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; +} + +async fn compact_conversation(conversation: &Arc) { + conversation + .submit(Op::Compact) + .await + .expect("compact conversation"); + wait_for_event(conversation, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; +} + +async fn fetch_conversation_path( + conversation: &Arc, + context: &str, +) -> std::path::PathBuf { + conversation + .submit(Op::GetPath) + .await + .expect("request conversation path"); + match wait_for_event(conversation, |ev| { + matches!(ev, EventMsg::ConversationPath(_)) + }) + .await + { + EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path, + _ => panic!("expected ConversationPath event for {context}"), + } +} + +async fn resume_conversation( + manager: &ConversationManager, + config: &Config, + path: std::path::PathBuf, +) -> Arc { + let auth_manager = + codex_core::AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")); + let NewConversation { conversation, .. } = manager + .resume_conversation_from_rollout(config.clone(), path, auth_manager) + .await + .expect("resume conversation"); + conversation +} + +async fn fork_conversation( + manager: &ConversationManager, + config: &Config, + path: std::path::PathBuf, + back_steps: usize, +) -> Arc { + let NewConversation { conversation, .. } = manager + .fork_conversation(back_steps, config.clone(), path) + .await + .expect("fork conversation"); + conversation +} diff --git a/codex-rs/core/tests/suite/exec_stream_events.rs b/codex-rs/core/tests/suite/exec_stream_events.rs index 521823d2d4b8..0b3edf2e1c66 100644 --- a/codex-rs/core/tests/suite/exec_stream_events.rs +++ b/codex-rs/core/tests/suite/exec_stream_events.rs @@ -2,8 +2,11 @@ use std::collections::HashMap; use std::path::PathBuf; +use std::time::Duration; use async_channel::Receiver; +use codex_core::error::CodexErr; +use codex_core::error::SandboxErr; use codex_core::exec::ExecParams; use codex_core::exec::SandboxType; use codex_core::exec::StdoutStream; @@ -170,3 +173,36 @@ async fn test_aggregated_output_interleaves_in_order() { assert_eq!(result.aggregated_output.text, "O1\nE1\nO2\nE2\n"); assert_eq!(result.aggregated_output.truncated_after_lines, None); } + +#[tokio::test] +async fn test_exec_timeout_returns_partial_output() { + let cmd = vec![ + "/bin/sh".to_string(), + "-c".to_string(), + "printf 'before\\n'; sleep 2; printf 'after\\n'".to_string(), + ]; + + let params = ExecParams { + command: cmd, + cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + timeout_ms: Some(200), + env: HashMap::new(), + with_escalated_permissions: None, + justification: None, + }; + + let policy = SandboxPolicy::new_read_only_policy(); + + let result = process_exec_tool_call(params, SandboxType::None, &policy, &None, None).await; + + let Err(CodexErr::Sandbox(SandboxErr::Timeout { output })) = result else { + panic!("expected timeout error"); + }; + + assert_eq!(output.exit_code, 124); + assert_eq!(output.stdout.text, "before\n"); + assert!(output.stderr.text.is_empty()); + assert_eq!(output.aggregated_output.text, "before\n"); + assert!(output.duration >= Duration::from_millis(200)); + assert!(output.timed_out); +} diff --git a/codex-rs/core/tests/suite/fork_conversation.rs b/codex-rs/core/tests/suite/fork_conversation.rs index 92e43dbfeb94..08e3f29d2824 100644 --- a/codex-rs/core/tests/suite/fork_conversation.rs +++ b/codex-rs/core/tests/suite/fork_conversation.rs @@ -1,12 +1,16 @@ use codex_core::CodexAuth; +use codex_core::ContentItem; use codex_core::ConversationManager; use codex_core::ModelProviderInfo; use codex_core::NewConversation; +use codex_core::ResponseItem; use codex_core::built_in_model_providers; -use codex_core::protocol::ConversationHistoryResponseEvent; +use codex_core::protocol::ConversationPathResponseEvent; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; +use codex_core::protocol::RolloutItem; +use codex_core::protocol::RolloutLine; use core_test_support::load_default_config_for_test; use core_test_support::wait_for_event; use tempfile::TempDir; @@ -71,84 +75,121 @@ async fn fork_conversation_twice_drops_to_first_message() { let _ = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; } - // Request history from the base conversation. - codex.submit(Op::GetHistory).await.unwrap(); + // Request history from the base conversation to obtain rollout path. + codex.submit(Op::GetPath).await.unwrap(); let base_history = - wait_for_event(&codex, |ev| matches!(ev, EventMsg::ConversationHistory(_))).await; + wait_for_event(&codex, |ev| matches!(ev, EventMsg::ConversationPath(_))).await; + let base_path = match &base_history { + EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path.clone(), + _ => panic!("expected ConversationHistory event"), + }; - // Capture entries from the base history and compute expected prefixes after each fork. - let entries_after_three = match &base_history { - EventMsg::ConversationHistory(ConversationHistoryResponseEvent { entries, .. }) => { - entries.clone() + // GetHistory flushes before returning the path; no wait needed. + + // Helper: read rollout items (excluding SessionMeta) from a JSONL path. + let read_items = |p: &std::path::Path| -> Vec { + let text = std::fs::read_to_string(p).expect("read rollout file"); + let mut items: Vec = Vec::new(); + for line in text.lines() { + if line.trim().is_empty() { + continue; + } + let v: serde_json::Value = serde_json::from_str(line).expect("jsonl line"); + let rl: RolloutLine = serde_json::from_value(v).expect("rollout line"); + match rl.item { + RolloutItem::SessionMeta(_) => {} + other => items.push(other), + } } - _ => panic!("expected ConversationHistory event"), + items + }; + + // Compute expected prefixes after each fork by truncating base rollout at nth-from-last user input. + let base_items = read_items(&base_path); + let find_user_input_positions = |items: &[RolloutItem]| -> Vec { + let mut pos = Vec::new(); + for (i, it) in items.iter().enumerate() { + if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) = it + && role == "user" + { + // Consider any user message as an input boundary; recorder stores both EventMsg and ResponseItem. + // We specifically look for input items, which are represented as ContentItem::InputText. + if content + .iter() + .any(|c| matches!(c, ContentItem::InputText { .. })) + { + pos.push(i); + } + } + } + pos }; - // History layout for this test: - // [0] user instructions, - // [1] environment context, - // [2] "first" user message, - // [3] "second" user message, - // [4] "third" user message. - - // Fork 1: drops the last user message and everything after. - let expected_after_first = vec![ - entries_after_three[0].clone(), - entries_after_three[1].clone(), - entries_after_three[2].clone(), - entries_after_three[3].clone(), - ]; - - // Fork 2: drops the last user message and everything after. - // [0] user instructions, - // [1] environment context, - // [2] "first" user message, - let expected_after_second = vec![ - entries_after_three[0].clone(), - entries_after_three[1].clone(), - entries_after_three[2].clone(), - ]; - - // Fork once with n=1 → drops the last user message and everything after. + let user_inputs = find_user_input_positions(&base_items); + + // After dropping last user input (n=1), cut strictly before that input if present, else empty. + let cut1 = user_inputs + .get(user_inputs.len().saturating_sub(1)) + .copied() + .unwrap_or(0); + let expected_after_first: Vec = base_items[..cut1].to_vec(); + + // After dropping again (n=1 on fork1), compute expected relative to fork1's rollout. + + // Fork once with n=1 → drops the last user input and everything after. let NewConversation { conversation: codex_fork1, .. } = conversation_manager - .fork_conversation(entries_after_three.clone(), 1, config_for_fork.clone()) + .fork_conversation(1, config_for_fork.clone(), base_path.clone()) .await .expect("fork 1"); - codex_fork1.submit(Op::GetHistory).await.unwrap(); + codex_fork1.submit(Op::GetPath).await.unwrap(); let fork1_history = wait_for_event(&codex_fork1, |ev| { - matches!(ev, EventMsg::ConversationHistory(_)) + matches!(ev, EventMsg::ConversationPath(_)) }) .await; - let entries_after_first_fork = match &fork1_history { - EventMsg::ConversationHistory(ConversationHistoryResponseEvent { entries, .. }) => { - assert!(matches!( - fork1_history, - EventMsg::ConversationHistory(ConversationHistoryResponseEvent { ref entries, .. }) if *entries == expected_after_first - )); - entries.clone() - } + let fork1_path = match &fork1_history { + EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path.clone(), _ => panic!("expected ConversationHistory event after first fork"), }; + // GetHistory on fork1 flushed; the file is ready. + let fork1_items = read_items(&fork1_path); + pretty_assertions::assert_eq!( + serde_json::to_value(&fork1_items).unwrap(), + serde_json::to_value(&expected_after_first).unwrap() + ); + // Fork again with n=1 → drops the (new) last user message, leaving only the first. let NewConversation { conversation: codex_fork2, .. } = conversation_manager - .fork_conversation(entries_after_first_fork.clone(), 1, config_for_fork.clone()) + .fork_conversation(1, config_for_fork.clone(), fork1_path.clone()) .await .expect("fork 2"); - codex_fork2.submit(Op::GetHistory).await.unwrap(); + codex_fork2.submit(Op::GetPath).await.unwrap(); let fork2_history = wait_for_event(&codex_fork2, |ev| { - matches!(ev, EventMsg::ConversationHistory(_)) + matches!(ev, EventMsg::ConversationPath(_)) }) .await; - assert!(matches!( - fork2_history, - EventMsg::ConversationHistory(ConversationHistoryResponseEvent { ref entries, .. }) if *entries == expected_after_second - )); + let fork2_path = match &fork2_history { + EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path.clone(), + _ => panic!("expected ConversationHistory event after second fork"), + }; + // GetHistory on fork2 flushed; the file is ready. + let fork1_items = read_items(&fork1_path); + let fork1_user_inputs = find_user_input_positions(&fork1_items); + let cut_last_on_fork1 = fork1_user_inputs + .get(fork1_user_inputs.len().saturating_sub(1)) + .copied() + .unwrap_or(0); + let expected_after_second: Vec = fork1_items[..cut_last_on_fork1].to_vec(); + let fork2_items = read_items(&fork2_path); + pretty_assertions::assert_eq!( + serde_json::to_value(&fork2_items).unwrap(), + serde_json::to_value(&expected_after_second).unwrap() + ); } diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index aabf83bbe3ca..4c217b63f44e 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -3,11 +3,15 @@ mod cli_stream; mod client; mod compact; +mod compact_resume_fork; mod exec; mod exec_stream_events; mod fork_conversation; mod live_cli; +mod model_overrides; mod prompt_caching; +mod review; +mod rollout_list_find; mod seatbelt; mod stream_error_allows_next_turn; mod stream_no_completed; diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs new file mode 100644 index 000000000000..a186c13ef33c --- /dev/null +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -0,0 +1,92 @@ +use codex_core::CodexAuth; +use codex_core::ConversationManager; +use codex_core::protocol::EventMsg; +use codex_core::protocol::Op; +use codex_core::protocol_config_types::ReasoningEffort; +use core_test_support::load_default_config_for_test; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use tempfile::TempDir; + +const CONFIG_TOML: &str = "config.toml"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn override_turn_context_does_not_persist_when_config_exists() { + let codex_home = TempDir::new().unwrap(); + let config_path = codex_home.path().join(CONFIG_TOML); + let initial_contents = "model = \"gpt-4o\"\n"; + tokio::fs::write(&config_path, initial_contents) + .await + .expect("seed config.toml"); + + let mut config = load_default_config_for_test(&codex_home); + config.model = "gpt-4o".to_string(); + + let conversation_manager = + ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); + let codex = conversation_manager + .new_conversation(config) + .await + .expect("create conversation") + .conversation; + + codex + .submit(Op::OverrideTurnContext { + cwd: None, + approval_policy: None, + sandbox_policy: None, + model: Some("o3".to_string()), + effort: Some(Some(ReasoningEffort::High)), + summary: None, + }) + .await + .expect("submit override"); + + codex.submit(Op::Shutdown).await.expect("request shutdown"); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await; + + let contents = tokio::fs::read_to_string(&config_path) + .await + .expect("read config.toml after override"); + assert_eq!(contents, initial_contents); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn override_turn_context_does_not_create_config_file() { + let codex_home = TempDir::new().unwrap(); + let config_path = codex_home.path().join(CONFIG_TOML); + assert!( + !config_path.exists(), + "test setup should start without config" + ); + + let config = load_default_config_for_test(&codex_home); + + let conversation_manager = + ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); + let codex = conversation_manager + .new_conversation(config) + .await + .expect("create conversation") + .conversation; + + codex + .submit(Op::OverrideTurnContext { + cwd: None, + approval_policy: None, + sandbox_policy: None, + model: Some("o3".to_string()), + effort: Some(Some(ReasoningEffort::Medium)), + summary: None, + }) + .await + .expect("submit override"); + + codex.submit(Op::Shutdown).await.expect("request shutdown"); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await; + + assert!( + !config_path.exists(), + "override should not create config.toml" + ); +} diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index eb685ea3f22e..94fce96ec896 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -391,7 +391,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() { exclude_slash_tmp: true, }), model: Some("o3".to_string()), - effort: Some(ReasoningEffort::High), + effort: Some(Some(ReasoningEffort::High)), summary: Some(ReasoningSummary::Detailed), }) .await @@ -525,7 +525,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() { exclude_slash_tmp: true, }, model: "o3".to_string(), - effort: ReasoningEffort::High, + effort: Some(ReasoningEffort::High), summary: ReasoningSummary::Detailed, }) .await diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs new file mode 100644 index 000000000000..f65913a2ca1b --- /dev/null +++ b/codex-rs/core/tests/suite/review.rs @@ -0,0 +1,598 @@ +use codex_core::CodexAuth; +use codex_core::CodexConversation; +use codex_core::ConversationManager; +use codex_core::ModelProviderInfo; +use codex_core::built_in_model_providers; +use codex_core::config::Config; +use codex_core::protocol::EventMsg; +use codex_core::protocol::ExitedReviewModeEvent; +use codex_core::protocol::InputItem; +use codex_core::protocol::Op; +use codex_core::protocol::ReviewCodeLocation; +use codex_core::protocol::ReviewFinding; +use codex_core::protocol::ReviewLineRange; +use codex_core::protocol::ReviewOutputEvent; +use codex_core::protocol::ReviewRequest; +use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; +use core_test_support::load_default_config_for_test; +use core_test_support::load_sse_fixture_with_id_from_str; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use std::path::PathBuf; +use std::sync::Arc; +use tempfile::TempDir; +use tokio::io::AsyncWriteExt as _; +use uuid::Uuid; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +/// Verify that submitting `Op::Review` spawns a child task and emits +/// EnteredReviewMode -> ExitedReviewMode(None) -> TaskComplete +/// in that order when the model returns a structured review JSON payload. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn review_op_emits_lifecycle_and_review_output() { + // Skip under Codex sandbox network restrictions. + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + // Start mock Responses API server. Return a single assistant message whose + // text is a JSON-encoded ReviewOutputEvent. + let review_json = serde_json::json!({ + "findings": [ + { + "title": "Prefer Stylize helpers", + "body": "Use .dim()/.bold() chaining instead of manual Style where possible.", + "confidence_score": 0.9, + "priority": 1, + "code_location": { + "absolute_file_path": "/tmp/file.rs", + "line_range": {"start": 10, "end": 20} + } + } + ], + "overall_correctness": "good", + "overall_explanation": "All good with some improvements suggested.", + "overall_confidence_score": 0.8 + }) + .to_string(); + let sse_template = r#"[ + {"type":"response.output_item.done", "item":{ + "type":"message", "role":"assistant", + "content":[{"type":"output_text","text":__REVIEW__}] + }}, + {"type":"response.completed", "response": {"id": "__ID__"}} + ]"#; + let review_json_escaped = serde_json::to_string(&review_json).unwrap(); + let sse_raw = sse_template.replace("__REVIEW__", &review_json_escaped); + let server = start_responses_server_with_sse(&sse_raw, 1).await; + let codex_home = TempDir::new().unwrap(); + let codex = new_conversation_for_server(&server, &codex_home, |_| {}).await; + + // Submit review request. + codex + .submit(Op::Review { + review_request: ReviewRequest { + prompt: "Please review my changes".to_string(), + user_facing_hint: "my changes".to_string(), + }, + }) + .await + .unwrap(); + + // Verify lifecycle: Entered -> Exited(Some(review)) -> TaskComplete. + let _entered = wait_for_event(&codex, |ev| matches!(ev, EventMsg::EnteredReviewMode(_))).await; + let closed = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExitedReviewMode(_))).await; + let review = match closed { + EventMsg::ExitedReviewMode(ev) => ev + .review_output + .expect("expected ExitedReviewMode with Some(review_output)"), + other => panic!("expected ExitedReviewMode(..), got {other:?}"), + }; + + // Deep compare full structure using PartialEq (floats are f32 on both sides). + let expected = ReviewOutputEvent { + findings: vec![ReviewFinding { + title: "Prefer Stylize helpers".to_string(), + body: "Use .dim()/.bold() chaining instead of manual Style where possible.".to_string(), + confidence_score: 0.9, + priority: 1, + code_location: ReviewCodeLocation { + absolute_file_path: PathBuf::from("/tmp/file.rs"), + line_range: ReviewLineRange { start: 10, end: 20 }, + }, + }], + overall_correctness: "good".to_string(), + overall_explanation: "All good with some improvements suggested.".to_string(), + overall_confidence_score: 0.8, + }; + assert_eq!(expected, review); + let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + server.verify().await; +} + +/// When the model returns plain text that is not JSON, ensure the child +/// lifecycle still occurs and the plain text is surfaced via +/// ExitedReviewMode(Some(..)) as the overall_explanation. +// Windows CI only: bump to 4 workers to prevent SSE/event starvation and test timeouts. +#[cfg_attr(windows, tokio::test(flavor = "multi_thread", worker_threads = 4))] +#[cfg_attr(not(windows), tokio::test(flavor = "multi_thread", worker_threads = 2))] +async fn review_op_with_plain_text_emits_review_fallback() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let sse_raw = r#"[ + {"type":"response.output_item.done", "item":{ + "type":"message", "role":"assistant", + "content":[{"type":"output_text","text":"just plain text"}] + }}, + {"type":"response.completed", "response": {"id": "__ID__"}} + ]"#; + let server = start_responses_server_with_sse(sse_raw, 1).await; + let codex_home = TempDir::new().unwrap(); + let codex = new_conversation_for_server(&server, &codex_home, |_| {}).await; + + codex + .submit(Op::Review { + review_request: ReviewRequest { + prompt: "Plain text review".to_string(), + user_facing_hint: "plain text review".to_string(), + }, + }) + .await + .unwrap(); + + let _entered = wait_for_event(&codex, |ev| matches!(ev, EventMsg::EnteredReviewMode(_))).await; + let closed = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExitedReviewMode(_))).await; + let review = match closed { + EventMsg::ExitedReviewMode(ev) => ev + .review_output + .expect("expected ExitedReviewMode with Some(review_output)"), + other => panic!("expected ExitedReviewMode(..), got {other:?}"), + }; + + // Expect a structured fallback carrying the plain text. + let expected = ReviewOutputEvent { + overall_explanation: "just plain text".to_string(), + ..Default::default() + }; + assert_eq!(expected, review); + let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + server.verify().await; +} + +/// When the model returns structured JSON in a review, ensure no AgentMessage +/// is emitted; the UI consumes the structured result via ExitedReviewMode. +// Windows CI only: bump to 4 workers to prevent SSE/event starvation and test timeouts. +#[cfg_attr(windows, tokio::test(flavor = "multi_thread", worker_threads = 4))] +#[cfg_attr(not(windows), tokio::test(flavor = "multi_thread", worker_threads = 2))] +async fn review_does_not_emit_agent_message_on_structured_output() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let review_json = serde_json::json!({ + "findings": [ + { + "title": "Example", + "body": "Structured review output.", + "confidence_score": 0.5, + "priority": 1, + "code_location": { + "absolute_file_path": "/tmp/file.rs", + "line_range": {"start": 1, "end": 2} + } + } + ], + "overall_correctness": "ok", + "overall_explanation": "ok", + "overall_confidence_score": 0.5 + }) + .to_string(); + let sse_template = r#"[ + {"type":"response.output_item.done", "item":{ + "type":"message", "role":"assistant", + "content":[{"type":"output_text","text":__REVIEW__}] + }}, + {"type":"response.completed", "response": {"id": "__ID__"}} + ]"#; + let review_json_escaped = serde_json::to_string(&review_json).unwrap(); + let sse_raw = sse_template.replace("__REVIEW__", &review_json_escaped); + let server = start_responses_server_with_sse(&sse_raw, 1).await; + let codex_home = TempDir::new().unwrap(); + let codex = new_conversation_for_server(&server, &codex_home, |_| {}).await; + + codex + .submit(Op::Review { + review_request: ReviewRequest { + prompt: "check structured".to_string(), + user_facing_hint: "check structured".to_string(), + }, + }) + .await + .unwrap(); + + // Drain events until TaskComplete; ensure none are AgentMessage. + use tokio::time::Duration; + use tokio::time::timeout; + let mut saw_entered = false; + let mut saw_exited = false; + loop { + let ev = timeout(Duration::from_secs(5), codex.next_event()) + .await + .expect("timeout waiting for event") + .expect("stream ended unexpectedly"); + match ev.msg { + EventMsg::TaskComplete(_) => break, + EventMsg::AgentMessage(_) => { + panic!("unexpected AgentMessage during review with structured output") + } + EventMsg::EnteredReviewMode(_) => saw_entered = true, + EventMsg::ExitedReviewMode(_) => saw_exited = true, + _ => {} + } + } + assert!(saw_entered && saw_exited, "missing review lifecycle events"); + + server.verify().await; +} + +/// Ensure that when a custom `review_model` is set in the config, the review +/// request uses that model (and not the main chat model). +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn review_uses_custom_review_model_from_config() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + // Minimal stream: just a completed event + let sse_raw = r#"[ + {"type":"response.completed", "response": {"id": "__ID__"}} + ]"#; + let server = start_responses_server_with_sse(sse_raw, 1).await; + let codex_home = TempDir::new().unwrap(); + // Choose a review model different from the main model; ensure it is used. + let codex = new_conversation_for_server(&server, &codex_home, |cfg| { + cfg.model = "gpt-4.1".to_string(); + cfg.review_model = "gpt-5".to_string(); + }) + .await; + + codex + .submit(Op::Review { + review_request: ReviewRequest { + prompt: "use custom model".to_string(), + user_facing_hint: "use custom model".to_string(), + }, + }) + .await + .unwrap(); + + // Wait for completion + let _entered = wait_for_event(&codex, |ev| matches!(ev, EventMsg::EnteredReviewMode(_))).await; + let _closed = wait_for_event(&codex, |ev| { + matches!( + ev, + EventMsg::ExitedReviewMode(ExitedReviewModeEvent { + review_output: None + }) + ) + }) + .await; + let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Assert the request body model equals the configured review model + let request = &server.received_requests().await.unwrap()[0]; + let body = request.body_json::().unwrap(); + assert_eq!(body["model"].as_str().unwrap(), "gpt-5"); + + server.verify().await; +} + +/// When a review session begins, it must not prepend prior chat history from +/// the parent session. The request `input` should contain only the review +/// prompt from the user. +// Windows CI only: bump to 4 workers to prevent SSE/event starvation and test timeouts. +#[cfg_attr(windows, tokio::test(flavor = "multi_thread", worker_threads = 4))] +#[cfg_attr(not(windows), tokio::test(flavor = "multi_thread", worker_threads = 2))] +async fn review_input_isolated_from_parent_history() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + // Mock server for the single review request + let sse_raw = r#"[ + {"type":"response.completed", "response": {"id": "__ID__"}} + ]"#; + let server = start_responses_server_with_sse(sse_raw, 1).await; + + // Seed a parent session history via resume file with both user + assistant items. + let codex_home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&codex_home); + config.model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + let session_file = codex_home.path().join("resume.jsonl"); + { + let mut f = tokio::fs::File::create(&session_file).await.unwrap(); + let convo_id = Uuid::new_v4(); + // Proper session_meta line (enveloped) with a conversation id + let meta_line = serde_json::json!({ + "timestamp": "2024-01-01T00:00:00.000Z", + "type": "session_meta", + "payload": { + "id": convo_id, + "timestamp": "2024-01-01T00:00:00Z", + "instructions": null, + "cwd": ".", + "originator": "test_originator", + "cli_version": "test_version" + } + }); + f.write_all(format!("{meta_line}\n").as_bytes()) + .await + .unwrap(); + + // Prior user message (enveloped response_item) + let user = codex_protocol::models::ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![codex_protocol::models::ContentItem::InputText { + text: "parent: earlier user message".to_string(), + }], + }; + let user_json = serde_json::to_value(&user).unwrap(); + let user_line = serde_json::json!({ + "timestamp": "2024-01-01T00:00:01.000Z", + "type": "response_item", + "payload": user_json + }); + f.write_all(format!("{user_line}\n").as_bytes()) + .await + .unwrap(); + + // Prior assistant message (enveloped response_item) + let assistant = codex_protocol::models::ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![codex_protocol::models::ContentItem::OutputText { + text: "parent: assistant reply".to_string(), + }], + }; + let assistant_json = serde_json::to_value(&assistant).unwrap(); + let assistant_line = serde_json::json!({ + "timestamp": "2024-01-01T00:00:02.000Z", + "type": "response_item", + "payload": assistant_json + }); + f.write_all(format!("{assistant_line}\n").as_bytes()) + .await + .unwrap(); + } + let codex = + resume_conversation_for_server(&server, &codex_home, session_file.clone(), |_| {}).await; + + // Submit review request; it must start fresh (no parent history in `input`). + let review_prompt = "Please review only this".to_string(); + codex + .submit(Op::Review { + review_request: ReviewRequest { + prompt: review_prompt.clone(), + user_facing_hint: review_prompt.clone(), + }, + }) + .await + .unwrap(); + + let _entered = wait_for_event(&codex, |ev| matches!(ev, EventMsg::EnteredReviewMode(_))).await; + let _closed = wait_for_event(&codex, |ev| { + matches!( + ev, + EventMsg::ExitedReviewMode(ExitedReviewModeEvent { + review_output: None + }) + ) + }) + .await; + let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Assert the request `input` contains only the single review user message. + let request = &server.received_requests().await.unwrap()[0]; + let body = request.body_json::().unwrap(); + let expected_input = serde_json::json!([ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": review_prompt}] + } + ]); + assert_eq!(body["input"], expected_input); + + server.verify().await; +} + +/// After a review thread finishes, its conversation should not leak into the +/// parent session. A subsequent parent turn must not include any review +/// messages in its request `input`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn review_history_does_not_leak_into_parent_session() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + // Respond to both the review request and the subsequent parent request. + let sse_raw = r#"[ + {"type":"response.output_item.done", "item":{ + "type":"message", "role":"assistant", + "content":[{"type":"output_text","text":"review assistant output"}] + }}, + {"type":"response.completed", "response": {"id": "__ID__"}} + ]"#; + let server = start_responses_server_with_sse(sse_raw, 2).await; + let codex_home = TempDir::new().unwrap(); + let codex = new_conversation_for_server(&server, &codex_home, |_| {}).await; + + // 1) Run a review turn that produces an assistant message (isolated in child). + codex + .submit(Op::Review { + review_request: ReviewRequest { + prompt: "Start a review".to_string(), + user_facing_hint: "Start a review".to_string(), + }, + }) + .await + .unwrap(); + let _entered = wait_for_event(&codex, |ev| matches!(ev, EventMsg::EnteredReviewMode(_))).await; + let _closed = wait_for_event(&codex, |ev| { + matches!( + ev, + EventMsg::ExitedReviewMode(ExitedReviewModeEvent { + review_output: Some(_) + }) + ) + }) + .await; + let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // 2) Continue in the parent session; request input must not include any review items. + let followup = "back to parent".to_string(); + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: followup.clone(), + }], + }) + .await + .unwrap(); + let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Inspect the second request (parent turn) input contents. + // Parent turns include session initial messages (user_instructions, environment_context). + // Critically, no messages from the review thread should appear. + let requests = server.received_requests().await.unwrap(); + assert_eq!(requests.len(), 2); + let body = requests[1].body_json::().unwrap(); + let input = body["input"].as_array().expect("input array"); + + // Must include the followup as the last item for this turn + let last = input.last().expect("at least one item in input"); + assert_eq!(last["role"].as_str().unwrap(), "user"); + let last_text = last["content"][0]["text"].as_str().unwrap(); + assert_eq!(last_text, followup); + + // Ensure no review-thread content leaked into the parent request + let contains_review_prompt = input + .iter() + .any(|msg| msg["content"][0]["text"].as_str().unwrap_or_default() == "Start a review"); + let contains_review_assistant = input.iter().any(|msg| { + msg["content"][0]["text"].as_str().unwrap_or_default() == "review assistant output" + }); + assert!( + !contains_review_prompt, + "review prompt leaked into parent turn input" + ); + assert!( + !contains_review_assistant, + "review assistant output leaked into parent turn input" + ); + + server.verify().await; +} + +/// Start a mock Responses API server and mount the given SSE stream body. +async fn start_responses_server_with_sse(sse_raw: &str, expected_requests: usize) -> MockServer { + let server = MockServer::start().await; + let sse = load_sse_fixture_with_id_from_str(sse_raw, &Uuid::new_v4().to_string()); + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(sse.clone(), "text/event-stream"), + ) + .expect(expected_requests as u64) + .mount(&server) + .await; + server +} + +/// Create a conversation configured to talk to the provided mock server. +#[expect(clippy::expect_used)] +async fn new_conversation_for_server( + server: &MockServer, + codex_home: &TempDir, + mutator: F, +) -> Arc +where + F: FnOnce(&mut Config), +{ + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + let mut config = load_default_config_for_test(codex_home); + config.model_provider = model_provider; + mutator(&mut config); + let conversation_manager = + ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); + conversation_manager + .new_conversation(config) + .await + .expect("create conversation") + .conversation +} + +/// Create a conversation resuming from a rollout file, configured to talk to the provided mock server. +#[expect(clippy::expect_used)] +async fn resume_conversation_for_server( + server: &MockServer, + codex_home: &TempDir, + resume_path: std::path::PathBuf, + mutator: F, +) -> Arc +where + F: FnOnce(&mut Config), +{ + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + let mut config = load_default_config_for_test(codex_home); + config.model_provider = model_provider; + mutator(&mut config); + let conversation_manager = + ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); + let auth_manager = + codex_core::AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + conversation_manager + .resume_conversation_from_rollout(config, resume_path, auth_manager) + .await + .expect("resume conversation") + .conversation +} diff --git a/codex-rs/core/tests/suite/rollout_list_find.rs b/codex-rs/core/tests/suite/rollout_list_find.rs new file mode 100644 index 000000000000..88409a461693 --- /dev/null +++ b/codex-rs/core/tests/suite/rollout_list_find.rs @@ -0,0 +1,50 @@ +#![allow(clippy::unwrap_used, clippy::expect_used)] +use std::io::Write; +use std::path::PathBuf; + +use codex_core::find_conversation_path_by_id_str; +use tempfile::TempDir; +use uuid::Uuid; + +/// Create sessions/YYYY/MM/DD and write a minimal rollout file containing the +/// provided conversation id in the SessionMeta line. Returns the absolute path. +fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid) -> PathBuf { + let sessions = codex_home.path().join("sessions/2024/01/01"); + std::fs::create_dir_all(&sessions).unwrap(); + + let file = sessions.join(format!("rollout-2024-01-01T00-00-00-{id}.jsonl")); + let mut f = std::fs::File::create(&file).unwrap(); + // Minimal first line: session_meta with the id so content search can find it + writeln!( + f, + "{}", + serde_json::json!({ + "timestamp": "2024-01-01T00:00:00.000Z", + "type": "session_meta", + "payload": { + "id": id, + "timestamp": "2024-01-01T00:00:00Z", + "instructions": null, + "cwd": ".", + "originator": "test", + "cli_version": "test" + } + }) + ) + .unwrap(); + + file +} + +#[tokio::test] +async fn find_locates_rollout_file_by_id() { + let home = TempDir::new().unwrap(); + let id = Uuid::new_v4(); + let expected = write_minimal_rollout_with_id(&home, id); + + let found = find_conversation_path_by_id_str(home.path(), &id.to_string()) + .await + .unwrap(); + + assert_eq!(found.unwrap(), expected); +} diff --git a/codex-rs/docs/codex_mcp_interface.md b/codex-rs/docs/codex_mcp_interface.md new file mode 100644 index 000000000000..1b048085c351 --- /dev/null +++ b/codex-rs/docs/codex_mcp_interface.md @@ -0,0 +1,123 @@ +# Codex MCP Interface [experimental] + +This document describes Codex’s experimental MCP interface: a JSON‑RPC API that runs over the Model Context Protocol (MCP) transport to control a local Codex engine. + +- Status: experimental and subject to change without notice +- Server binary: `codex mcp` (or `codex-mcp-server`) +- Transport: standard MCP over stdio (JSON‑RPC 2.0, line‑delimited) + +## Overview + +Codex exposes a small set of MCP‑compatible methods to create and manage conversations, send user input, receive live events, and handle approval prompts. The types are defined in `protocol/src/mcp_protocol.rs` and re‑used by the MCP server implementation in `mcp-server/`. + +At a glance: + +- Conversations + - `newConversation` → start a Codex session + - `sendUserMessage` / `sendUserTurn` → send user input into a conversation + - `interruptConversation` → stop the current turn + - `listConversations`, `resumeConversation`, `archiveConversation` +- Configuration and info + - `getUserSavedConfig`, `setDefaultModel`, `getUserAgent`, `userInfo` +- Auth + - `loginApiKey`, `loginChatGpt`, `cancelLoginChatGpt`, `logoutChatGpt`, `getAuthStatus` +- Utilities + - `gitDiffToRemote`, `execOneOffCommand` +- Approvals (server → client requests) + - `applyPatchApproval`, `execCommandApproval` +- Notifications (server → client) + - `loginChatGptComplete`, `authStatusChange` + - `codex/event` stream with agent events + +See code for full type definitions and exact shapes: `protocol/src/mcp_protocol.rs`. + +## Starting the server + +Run Codex as an MCP server and connect an MCP client: + +```bash +codex mcp | your_mcp_client +``` + +For a simple inspection UI, you can also try: + +```bash +npx @modelcontextprotocol/inspector codex mcp +``` + +## Conversations + +Start a new session with optional overrides: + +Request `newConversation` params (subset): + +- `model`: string model id (e.g. "o3", "gpt-5") +- `profile`: optional named profile +- `cwd`: optional working directory +- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` | `never` +- `sandbox`: `read-only` | `workspace-write` | `danger-full-access` +- `config`: map of additional config overrides +- `baseInstructions`: optional instruction override +- `includePlanTool` / `includeApplyPatchTool`: booleans + +Response: `{ conversationId, model, reasoningEffort?, rolloutPath }` + +Send input to the active turn: + +- `sendUserMessage` → enqueue items to the conversation +- `sendUserTurn` → structured turn with explicit `cwd`, `approvalPolicy`, `sandboxPolicy`, `model`, optional `effort`, and `summary` + +Interrupt a running turn: `interruptConversation`. + +List/resume/archive: `listConversations`, `resumeConversation`, `archiveConversation`. + +## Event stream + +While a conversation runs, the server sends notifications: + +- `codex/event` with the serialized Codex event payload. The shape matches `core/src/protocol.rs`’s `Event` and `EventMsg` types. Some notifications include a `_meta.requestId` to correlate with the originating request. +- Auth notifications via method names `loginChatGptComplete` and `authStatusChange`. + +Clients should render events and, when present, surface approval requests (see next section). + +## Approvals (server → client) + +When Codex needs approval to apply changes or run commands, the server issues JSON‑RPC requests to the client: + +- `applyPatchApproval { conversationId, callId, fileChanges, reason?, grantRoot? }` +- `execCommandApproval { conversationId, callId, command, cwd, reason? }` + +The client must reply with `{ decision: "allow" | "deny" }` for each request. + +## Auth helpers + +For ChatGPT or API‑key based auth flows, the server exposes helpers: + +- `loginApiKey { apiKey }` +- `loginChatGpt` → returns `{ loginId, authUrl }`; browser completes flow; then `loginChatGptComplete` notification follows +- `cancelLoginChatGpt { loginId }`, `logoutChatGpt`, `getAuthStatus { includeToken?, refreshToken? }` + +## Example: start and send a message + +```json +{ "jsonrpc": "2.0", "id": 1, "method": "newConversation", "params": { "model": "gpt-5", "approvalPolicy": "on-request" } } +``` + +Server responds: + +```json +{ "jsonrpc": "2.0", "id": 1, "result": { "conversationId": "c7b0…", "model": "gpt-5", "rolloutPath": "/path/to/rollout.jsonl" } } +``` + +Then send input: + +```json +{ "jsonrpc": "2.0", "id": 2, "method": "sendUserMessage", "params": { "conversationId": "c7b0…", "items": [{ "type": "text", "text": "Hello Codex" }] } } +``` + +While processing, the server emits `codex/event` notifications containing agent output, approvals, and status updates. + +## Compatibility and stability + +This interface is experimental. Method names, fields, and event shapes may evolve. For the authoritative schema, consult `protocol/src/mcp_protocol.rs` and the corresponding server wiring in `mcp-server/`. + diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index a0005f495db2..26274f3c1a9f 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -39,7 +39,7 @@ tokio = { version = "1", features = [ "signal", ] } tracing = { version = "0.1.41", features = ["log"] } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } [dev-dependencies] assert_cmd = "2" @@ -47,4 +47,6 @@ core_test_support = { path = "../core/tests/common" } libc = "0.2" predicates = "3" tempfile = "3.13.0" +uuid = "1" +walkdir = "2" wiremock = "0.6" diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 51bfc7ee3bb1..bbdafc76ff48 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -6,6 +6,10 @@ use std::path::PathBuf; #[derive(Parser, Debug)] #[command(version)] pub struct Cli { + /// Action to perform. If omitted, runs a new non-interactive session. + #[command(subcommand)] + pub command: Option, + /// Optional image(s) to attach to the initial prompt. #[arg( long = "image", @@ -78,6 +82,28 @@ pub struct Cli { pub prompt: Option, } +#[derive(Debug, clap::Subcommand)] +pub enum Command { + /// Resume a previous session by id or pick the most recent with --last. + Resume(ResumeArgs), +} + +#[derive(Parser, Debug)] +pub struct ResumeArgs { + /// Conversation/session id (UUID). When provided, resumes this session. + /// If omitted, use --last to pick the most recent recorded session. + #[arg(value_name = "SESSION_ID")] + pub session_id: Option, + + /// Resume the most recent recorded session (newest) without specifying an id. + #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")] + pub last: bool, + + /// Prompt to send after resuming the session. If `-` is used, read from stdin. + #[arg(value_name = "PROMPT")] + pub prompt: Option, +} + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] #[value(rename_all = "kebab-case")] pub enum Color { diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index b350ccb99d72..0ca88d2b86e7 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -16,12 +16,7 @@ pub(crate) trait EventProcessor { /// Handle a single event emitted by the agent. fn process_event(&mut self, event: Event) -> CodexStatus; - /// Exit code to use upon completion. Implementations should set a non-zero - /// value when a fatal error was encountered (e.g., upstream streaming - /// failure). - fn exit_code(&self) -> i32 { - 0 - } + // No exit_code method; CLI controls process exit based on core events. } pub(crate) fn handle_last_message(last_agent_message: Option<&str>, output_file: &Path) { diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 90f56237ca39..dc3a9782c561 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -617,13 +617,16 @@ impl EventProcessor for EventProcessorWithHumanOutput { } } } + EventMsg::ConversationPath(_) => {} + EventMsg::TurnAborted(_) => {} + EventMsg::UserMessage(_) => {} + EventMsg::EnteredReviewMode(_) => {} + EventMsg::ExitedReviewMode(_) => {} } CodexStatus::Running } - fn exit_code(&self) -> i32 { - if self.had_error { 1 } else { 0 } - } + // exit_code handled by CLI; suppress unused warnings by omitting method. } // Extend trait with exit_code override diff --git a/codex-rs/exec/src/event_processor_with_json_output.rs b/codex-rs/exec/src/event_processor_with_json_output.rs index fd22bae97ff9..aca069ecdb53 100644 --- a/codex-rs/exec/src/event_processor_with_json_output.rs +++ b/codex-rs/exec/src/event_processor_with_json_output.rs @@ -63,5 +63,5 @@ impl EventProcessor for EventProcessorWithJsonOutput { } } - fn exit_code(&self) -> i32 { if self.had_error { 1 } else { 0 } } + // exit_code handled by CLI; suppress unused warnings by omitting method. } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index d226bb1d969b..0ec5eb57c2f3 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -23,7 +23,6 @@ use codex_core::protocol::Op; use codex_core::protocol::TaskCompleteEvent; use codex_ollama::DEFAULT_OSS_MODEL; use codex_protocol::config_types::SandboxMode; -use codex_protocol::mcp_protocol::AuthMode; use event_processor_with_human_output::EventProcessorWithHumanOutput; use event_processor_with_json_output::EventProcessorWithJsonOutput; use tracing::debug; @@ -31,11 +30,14 @@ use tracing::error; use tracing::info; use tracing_subscriber::EnvFilter; +use crate::cli::Command as ExecCommand; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; +use codex_core::find_conversation_path_by_id_str; pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { let Cli { + command, images, model: model_cli_arg, oss, @@ -43,7 +45,6 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any full_auto, dangerously_bypass_approvals_and_sandbox, cwd, - debug, skip_git_repo_check, color, last_message_file, @@ -51,10 +52,18 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any sandbox_mode: sandbox_mode_cli_arg, prompt, config_overrides, + .. } = cli; - // Determine the prompt based on CLI arg and/or stdin. - let prompt = match prompt { + // Determine the prompt source (parent or subcommand) and read from stdin if needed. + let prompt_arg = match &command { + // Allow prompt before the subcommand by falling back to the parent-level prompt + // when the Resume subcommand did not provide its own prompt. + Some(ExecCommand::Resume(args)) => args.prompt.clone().or(prompt), + None => prompt, + }; + + let prompt = match prompt_arg { Some(p) if p != "-" => p, // Either `-` was passed or no positional arg. maybe_dash => { @@ -139,6 +148,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // Load configuration and determine approval policy let overrides = ConfigOverrides { model, + review_model: None, config_profile, // This CLI is intended to be headless and has no affordances for asking // the user for approval. @@ -149,9 +159,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any codex_linux_sandbox_exe, base_instructions: None, include_plan_tool: None, - disable_response_storage: oss.then_some(true), + include_apply_patch_tool: None, + include_view_image_tool: None, + disable_response_storage: None, + debug: None, show_raw_agent_reasoning: oss.then_some(true), - debug: Some(debug), tools_web_search_request: None, }; // Parse `-c` overrides. @@ -191,14 +203,36 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let conversation_manager = ConversationManager::new(AuthManager::shared( config.codex_home.clone(), - AuthMode::ApiKey, + codex_protocol::mcp_protocol::AuthMode::ApiKey, config.responses_originator_header.clone(), )); + + // Handle resume subcommand by resolving a rollout path and using explicit resume API. let NewConversation { conversation_id: _, conversation, session_configured, - } = conversation_manager.new_conversation(config).await?; + } = if let Some(ExecCommand::Resume(args)) = command { + let resume_path = resolve_resume_path(&config, &args).await?; + + if let Some(path) = resume_path { + conversation_manager + .resume_conversation_from_rollout( + config.clone(), + path, + AuthManager::shared( + config.codex_home.clone(), + codex_protocol::mcp_protocol::AuthMode::ApiKey, + config.responses_originator_header.clone(), + ), + ) + .await? + } else { + conversation_manager.new_conversation(config).await? + } + } else { + conversation_manager.new_conversation(config).await? + }; info!("Codex initialized with event: {session_configured:?}"); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); @@ -280,8 +314,26 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any } } } - // If any fatal Error events occurred, exit non‑zero so CI fails fast. - let exit_code = event_processor.exit_code(); - if exit_code != 0 { std::process::exit(exit_code); } + Ok(()) } + +async fn resolve_resume_path( + config: &Config, + args: &crate::cli::ResumeArgs, +) -> anyhow::Result> { + if args.last { + match codex_core::RolloutRecorder::list_conversations(&config.codex_home, 1, None).await { + Ok(page) => Ok(page.items.first().map(|it| it.path.clone())), + Err(e) => { + error!("Error listing conversations: {e}"); + Ok(None) + } + } + } else if let Some(id_str) = args.session_id.as_deref() { + let path = find_conversation_path_by_id_str(&config.codex_home, id_str).await?; + Ok(path) + } else { + Ok(None) + } +} diff --git a/codex-rs/exec/tests/fixtures/cli_responses_fixture.sse b/codex-rs/exec/tests/fixtures/cli_responses_fixture.sse new file mode 100644 index 000000000000..660c56b1d849 --- /dev/null +++ b/codex-rs/exec/tests/fixtures/cli_responses_fixture.sse @@ -0,0 +1,10 @@ +event: response.created +data: {"type":"response.created","response":{"id":"resp1"}} + +event: response.output_item.done +data: {"type":"response.output_item.done","item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"fixture hello"}]}} + +event: response.completed +data: {"type":"response.completed","response":{"id":"resp1","output":[]}} + + diff --git a/codex-rs/exec/tests/suite/mod.rs b/codex-rs/exec/tests/suite/mod.rs index 75b19ee1b24e..5748fba1e846 100644 --- a/codex-rs/exec/tests/suite/mod.rs +++ b/codex-rs/exec/tests/suite/mod.rs @@ -1,4 +1,5 @@ // Aggregates all former standalone integration tests as modules. mod apply_patch; mod common; +mod resume; mod sandbox; diff --git a/codex-rs/exec/tests/suite/resume.rs b/codex-rs/exec/tests/suite/resume.rs new file mode 100644 index 000000000000..5868ed8eb284 --- /dev/null +++ b/codex-rs/exec/tests/suite/resume.rs @@ -0,0 +1,267 @@ +#![allow(clippy::unwrap_used, clippy::expect_used)] +use anyhow::Context; +use assert_cmd::prelude::*; +use serde_json::Value; +use std::process::Command; +use tempfile::TempDir; +use uuid::Uuid; +use walkdir::WalkDir; + +/// Utility: scan the sessions dir for a rollout file that contains `marker` +/// in any response_item.message.content entry. Returns the absolute path. +fn find_session_file_containing_marker( + sessions_dir: &std::path::Path, + marker: &str, +) -> Option { + for entry in WalkDir::new(sessions_dir) { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + if !entry.file_type().is_file() { + continue; + } + if !entry.file_name().to_string_lossy().ends_with(".jsonl") { + continue; + } + let path = entry.path(); + let Ok(content) = std::fs::read_to_string(path) else { + continue; + }; + // Skip the first meta line and scan remaining JSONL entries. + let mut lines = content.lines(); + if lines.next().is_none() { + continue; + } + for line in lines { + if line.trim().is_empty() { + continue; + } + let Ok(item): Result = serde_json::from_str(line) else { + continue; + }; + if item.get("type").and_then(|t| t.as_str()) == Some("response_item") + && let Some(payload) = item.get("payload") + && payload.get("type").and_then(|t| t.as_str()) == Some("message") + && payload + .get("content") + .map(|c| c.to_string()) + .unwrap_or_default() + .contains(marker) + { + return Some(path.to_path_buf()); + } + } + } + None +} + +/// Extract the conversation UUID from the first SessionMeta line in the rollout file. +fn extract_conversation_id(path: &std::path::Path) -> String { + let content = std::fs::read_to_string(path).unwrap(); + let mut lines = content.lines(); + let meta_line = lines.next().expect("missing meta line"); + let meta: Value = serde_json::from_str(meta_line).expect("invalid meta json"); + meta.get("payload") + .and_then(|p| p.get("id")) + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string() +} + +#[test] +fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> { + let home = TempDir::new()?; + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/cli_responses_fixture.sse"); + + // 1) First run: create a session with a unique marker in the content. + let marker = format!("resume-last-{}", Uuid::new_v4()); + let prompt = format!("echo {marker}"); + + Command::cargo_bin("codex-exec") + .context("should find binary for codex-exec")? + .env("CODEX_HOME", home.path()) + .env("OPENAI_API_KEY", "dummy") + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg(&prompt) + .assert() + .success(); + + // Find the created session file containing the marker. + let sessions_dir = home.path().join("sessions"); + let path = find_session_file_containing_marker(&sessions_dir, &marker) + .expect("no session file found after first run"); + + // 2) Second run: resume the most recent file with a new marker. + let marker2 = format!("resume-last-2-{}", Uuid::new_v4()); + let prompt2 = format!("echo {marker2}"); + + let mut binding = assert_cmd::Command::cargo_bin("codex-exec") + .context("should find binary for codex-exec")?; + let cmd = binding + .env("CODEX_HOME", home.path()) + .env("OPENAI_API_KEY", "dummy") + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg(&prompt2) + .arg("resume") + .arg("--last"); + cmd.assert().success(); + + // Ensure the same file was updated and contains both markers. + let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2) + .expect("no resumed session file containing marker2"); + assert_eq!( + resumed_path, path, + "resume --last should append to existing file" + ); + let content = std::fs::read_to_string(&resumed_path)?; + assert!(content.contains(&marker)); + assert!(content.contains(&marker2)); + Ok(()) +} + +#[test] +fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> { + let home = TempDir::new()?; + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/cli_responses_fixture.sse"); + + // 1) First run: create a session + let marker = format!("resume-by-id-{}", Uuid::new_v4()); + let prompt = format!("echo {marker}"); + + Command::cargo_bin("codex-exec") + .context("should find binary for codex-exec")? + .env("CODEX_HOME", home.path()) + .env("OPENAI_API_KEY", "dummy") + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg(&prompt) + .assert() + .success(); + + let sessions_dir = home.path().join("sessions"); + let path = find_session_file_containing_marker(&sessions_dir, &marker) + .expect("no session file found after first run"); + let session_id = extract_conversation_id(&path); + assert!( + !session_id.is_empty(), + "missing conversation id in meta line" + ); + + // 2) Resume by id + let marker2 = format!("resume-by-id-2-{}", Uuid::new_v4()); + let prompt2 = format!("echo {marker2}"); + + let mut binding = assert_cmd::Command::cargo_bin("codex-exec") + .context("should find binary for codex-exec")?; + let cmd = binding + .env("CODEX_HOME", home.path()) + .env("OPENAI_API_KEY", "dummy") + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg(&prompt2) + .arg("resume") + .arg(&session_id); + cmd.assert().success(); + + let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2) + .expect("no resumed session file containing marker2"); + assert_eq!( + resumed_path, path, + "resume by id should append to existing file" + ); + let content = std::fs::read_to_string(&resumed_path)?; + assert!(content.contains(&marker)); + assert!(content.contains(&marker2)); + Ok(()) +} + +#[test] +fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> { + let home = TempDir::new()?; + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/cli_responses_fixture.sse"); + + let marker = format!("resume-config-{}", Uuid::new_v4()); + let prompt = format!("echo {marker}"); + + Command::cargo_bin("codex-exec") + .context("should find binary for codex-exec")? + .env("CODEX_HOME", home.path()) + .env("OPENAI_API_KEY", "dummy") + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("--sandbox") + .arg("workspace-write") + .arg("--model") + .arg("gpt-5") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg(&prompt) + .assert() + .success(); + + let sessions_dir = home.path().join("sessions"); + let path = find_session_file_containing_marker(&sessions_dir, &marker) + .expect("no session file found after first run"); + + let marker2 = format!("resume-config-2-{}", Uuid::new_v4()); + let prompt2 = format!("echo {marker2}"); + + let output = Command::cargo_bin("codex-exec") + .context("should find binary for codex-exec")? + .env("CODEX_HOME", home.path()) + .env("OPENAI_API_KEY", "dummy") + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("--sandbox") + .arg("workspace-write") + .arg("--model") + .arg("gpt-5-high") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg(&prompt2) + .arg("resume") + .arg("--last") + .output() + .context("resume run should succeed")?; + + assert!(output.status.success(), "resume run failed: {output:?}"); + + let stdout = String::from_utf8(output.stdout)?; + assert!( + stdout.contains("model: gpt-5-high"), + "stdout missing model override: {stdout}" + ); + assert!( + stdout.contains("sandbox: workspace-write"), + "stdout missing sandbox override: {stdout}" + ); + + let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2) + .expect("no resumed session file containing marker2"); + assert_eq!(resumed_path, path, "resume should append to same file"); + + let content = std::fs::read_to_string(&resumed_path)?; + assert!(content.contains(&marker)); + assert!(content.contains(&marker2)); + Ok(()) +} diff --git a/codex-rs/execpolicy/Cargo.toml b/codex-rs/execpolicy/Cargo.toml index e0e3d75e42a6..9f0a25c42274 100644 --- a/codex-rs/execpolicy/Cargo.toml +++ b/codex-rs/execpolicy/Cargo.toml @@ -1,7 +1,7 @@ [package] +edition = "2024" name = "codex-execpolicy" version = { workspace = true } -edition = "2024" [[bin]] name = "codex-execpolicy" @@ -15,9 +15,8 @@ path = "src/lib.rs" workspace = true [dependencies] -anyhow = "1" -starlark = "0.13.0" allocative = "0.3.3" +anyhow = "1" clap = { version = "4", features = ["derive"] } derive_more = { version = "2", features = ["display"] } env_logger = "0.11.5" @@ -25,9 +24,10 @@ log = "0.4" multimap = "0.10.0" path-absolutize = "3.1.1" regex-lite = "0.1" -serde = { version = "1.0.194", features = ["derive"] } -serde_json = "1.0.143" +serde = { version = "1", features = ["derive"] } +serde_json = "1" serde_with = { version = "3", features = ["macros"] } +starlark = "0.13.0" [dev-dependencies] tempfile = "3.13.0" diff --git a/codex-rs/file-search/Cargo.toml b/codex-rs/file-search/Cargo.toml index f02d8402aa44..023e6936c85d 100644 --- a/codex-rs/file-search/Cargo.toml +++ b/codex-rs/file-search/Cargo.toml @@ -17,5 +17,5 @@ clap = { version = "4", features = ["derive"] } ignore = "0.4.23" nucleo-matcher = "0.3.1" serde = { version = "1", features = ["derive"] } -serde_json = "1.0.143" +serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/codex-rs/justfile b/codex-rs/justfile index beaa682daa47..850737efd614 100644 --- a/codex-rs/justfile +++ b/codex-rs/justfile @@ -30,3 +30,14 @@ fix *args: install: rustup show active-toolchain cargo fetch + +# Run `cargo nextest` since it's faster than `cargo test`, though including +# --no-fail-fast is important to ensure all tests are run. +# +# Run `cargo install cargo-nextest` if you don't have it installed. +test: + cargo nextest run --no-fail-fast + +# Run the MCP server +mcp-server-run *args: + cargo run -p codex-mcp-server -- "$@" diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 8faa02d15855..fddb43c38e29 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -121,7 +121,7 @@ async fn test_writable_root() { } #[tokio::test] -#[should_panic(expected = "Sandbox(Timeout)")] +#[should_panic(expected = "Sandbox(Timeout")] async fn test_timeout() { run_cmd(&["sleep", "2"], &[], 50).await; } @@ -156,26 +156,27 @@ async fn assert_network_blocked(cmd: &[&str]) { ) .await; - let (exit_code, stdout, stderr) = match result { - Ok(output) => (output.exit_code, output.stdout.text, output.stderr.text), - Err(CodexErr::Sandbox(SandboxErr::Denied(exit_code, stdout, stderr))) => { - (exit_code, stdout, stderr) - } + let output = match result { + Ok(output) => output, + Err(CodexErr::Sandbox(SandboxErr::Denied { output })) => *output, _ => { panic!("expected sandbox denied error, got: {result:?}"); } }; - dbg!(&stderr); - dbg!(&stdout); - dbg!(&exit_code); + dbg!(&output.stderr.text); + dbg!(&output.stdout.text); + dbg!(&output.exit_code); // A completely missing binary exits with 127. Anything else should also // be non‑zero (EPERM from seccomp will usually bubble up as 1, 2, 13…) // If—*and only if*—the command exits 0 we consider the sandbox breached. - if exit_code == 0 { - panic!("Network sandbox FAILED - {cmd:?} exited 0\nstdout:\n{stdout}\nstderr:\n{stderr}",); + if output.exit_code == 0 { + panic!( + "Network sandbox FAILED - {cmd:?} exited 0\nstdout:\n{}\nstderr:\n{}", + output.stdout.text, output.stderr.text + ); } } diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index d3ccfd621179..6c8c169802e5 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -241,8 +241,8 @@ async fn process_request( &opts.codex_home, api_key.clone(), tokens.id_token.clone(), - Some(tokens.access_token.clone()), - Some(tokens.refresh_token.clone()), + tokens.access_token.clone(), + tokens.refresh_token.clone(), ) .await { @@ -444,8 +444,8 @@ async fn persist_tokens_async( codex_home: &Path, api_key: Option, id_token: String, - access_token: Option, - refresh_token: Option, + access_token: String, + refresh_token: String, ) -> io::Result<()> { // Reuse existing synchronous logic but run it off the async runtime. let codex_home = codex_home.to_path_buf(); @@ -457,43 +457,29 @@ async fn persist_tokens_async( } } - let mut auth = read_or_default(&auth_file); - if let Some(key) = api_key { - auth.openai_api_key = Some(key); - } - let tokens = auth.tokens.get_or_insert_with(TokenData::default); - tokens.id_token = parse_id_token(&id_token).map_err(io::Error::other)?; - // Persist chatgpt_account_id if present in claims + let mut tokens = TokenData { + id_token: parse_id_token(&id_token).map_err(io::Error::other)?, + access_token, + refresh_token, + account_id: None, + }; if let Some(acc) = jwt_auth_claims(&id_token) .get("chatgpt_account_id") .and_then(|v| v.as_str()) { tokens.account_id = Some(acc.to_string()); } - if let Some(at) = access_token { - tokens.access_token = at; - } - if let Some(rt) = refresh_token { - tokens.refresh_token = rt; - } - auth.last_refresh = Some(Utc::now()); + let auth = AuthDotJson { + openai_api_key: api_key, + tokens: Some(tokens), + last_refresh: Some(Utc::now()), + }; codex_core::auth::write_auth_json(&auth_file, &auth) }) .await .map_err(|e| io::Error::other(format!("persist task failed: {e}")))? } -fn read_or_default(path: &Path) -> AuthDotJson { - match codex_core::auth::try_read_auth_json(path) { - Ok(auth) => auth, - Err(_) => AuthDotJson { - openai_api_key: None, - tokens: None, - last_refresh: None, - }, - } -} - fn compose_success_url(port: u16, issuer: &str, id_token: &str, access_token: &str) -> String { let token_claims = jwt_auth_claims(id_token); let access_claims = jwt_auth_claims(access_token); diff --git a/codex-rs/login/tests/suite/login_server_e2e.rs b/codex-rs/login/tests/suite/login_server_e2e.rs index ef6b80fbe153..2da1a62d27f1 100644 --- a/codex-rs/login/tests/suite/login_server_e2e.rs +++ b/codex-rs/login/tests/suite/login_server_e2e.rs @@ -88,6 +88,22 @@ async fn end_to_end_login_flow_persists_auth_json() { let tmp = tempdir().unwrap(); let codex_home = tmp.path().to_path_buf(); + // Seed auth.json with stale API key + tokens that should be overwritten. + let stale_auth = serde_json::json!({ + "OPENAI_API_KEY": "sk-stale", + "tokens": { + "id_token": "stale.header.payload", + "access_token": "stale-access", + "refresh_token": "stale-refresh", + "account_id": "stale-acc" + } + }); + std::fs::write( + codex_home.join("auth.json"), + serde_json::to_string_pretty(&stale_auth).unwrap(), + ) + .unwrap(); + let state = "test_state_123".to_string(); // Run server in background @@ -121,10 +137,10 @@ async fn end_to_end_login_flow_persists_auth_json() { let auth_path = codex_home.join("auth.json"); let data = std::fs::read_to_string(&auth_path).unwrap(); let json: serde_json::Value = serde_json::from_str(&data).unwrap(); - assert!( - !json["OPENAI_API_KEY"].is_null(), - "OPENAI_API_KEY should be set" - ); + // The following assert is here because of the old oauth flow that exchanges tokens for an + // API key. See obtain_api_key in server.rs for details. Once we remove this old mechanism + // from the code, this test should be updated to expect that the API key is no longer present. + assert_eq!(json["OPENAI_API_KEY"], "access-123"); assert_eq!(json["tokens"]["access_token"], "access-123"); assert_eq!(json["tokens"]["refresh_token"], "refresh-123"); assert_eq!(json["tokens"]["account_id"], "acc-123"); diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index 8dbc0e28134f..ae98b4a3c6b4 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -8,8 +8,6 @@ use codex_core::ConversationManager; use codex_core::NewConversation; use codex_core::config::Config; use codex_core::config::ConfigOverrides; -use codex_core::config::ConfigToml; -use codex_core::config::load_config_as_toml; use codex_core::git_info::git_diff_to_remote; use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::Event; @@ -42,7 +40,6 @@ use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::mcp_protocol::EXEC_COMMAND_APPROVAL_METHOD; use codex_protocol::mcp_protocol::ExecCommandApprovalParams; use codex_protocol::mcp_protocol::ExecCommandApprovalResponse; -use codex_protocol::mcp_protocol::GetConfigTomlResponse; use codex_protocol::mcp_protocol::InputItem as WireInputItem; use codex_protocol::mcp_protocol::InterruptConversationParams; use codex_protocol::mcp_protocol::InterruptConversationResponse; @@ -150,68 +147,12 @@ impl CodexMessageProcessor { ClientRequest::GitDiffToRemote { request_id, params } => { self.git_diff_to_origin(request_id, params.cwd).await; } - ClientRequest::GetConfigToml { request_id } => { - self.get_config_toml(request_id).await; - } + _ => { /* ignore unsupported methods */ } } } - async fn get_config_toml(&self, request_id: RequestId) { - let toml_value = match load_config_as_toml(&self._config.codex_home) { - Ok(val) => val, - Err(err) => { - let error = JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to load config.toml: {err}"), - data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; - } - }; - - let cfg: ConfigToml = match toml_value.try_into() { - Ok(cfg) => cfg, - Err(err) => { - let error = JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to parse config.toml: {err}"), - data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; - } - }; - - let profiles: HashMap = cfg - .profiles - .into_iter() - .map(|(k, v)| { - ( - k, - // Define this explicitly here to avoid the need to - // implement `From` - // for the `ConfigProfile` type and introduce a dependency on codex_core - codex_protocol::config_types::ConfigProfile { - model: v.model, - approval_policy: v.approval_policy.map(map_ask_for_approval_to_wire), - model_reasoning_effort: v.model_reasoning_effort.map(map_reasoning_effort_to_wire), - }, - ) - }) - .collect(); - - let response = GetConfigTomlResponse { - approval_policy: cfg.approval_policy.map(map_ask_for_approval_to_wire), - sandbox_mode: cfg.sandbox_mode, - model_reasoning_effort: cfg.model_reasoning_effort.map(map_reasoning_effort_to_wire), - profile: cfg.profile, - profiles: Some(profiles), - }; - - self.outgoing.send_response(request_id, response).await; - } - + // Upstream added utility endpoints (user info, set default model, one-off exec). + // Our fork does not expose them via this server; omit to preserve behavior. async fn process_new_conversation(&self, request_id: RequestId, params: NewConversationParams) { let config = match derive_config_from_params(params, self.codex_linux_sandbox_exe.clone()) { Ok(config) => config, @@ -234,8 +175,11 @@ impl CodexMessageProcessor { .. } = conversation_id; let response = NewConversationResponse { - conversation_id: ConversationId(conversation_id), + conversation_id, model: session_configured.model, + reasoning_effort: None, + // We do not expose the underlying rollout file path in this fork; provide the sessions root. + rollout_path: self._config.codex_home.join("sessions"), }; self.outgoing.send_response(request_id, response).await; } @@ -257,7 +201,7 @@ impl CodexMessageProcessor { } = params; let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { @@ -299,7 +243,7 @@ impl CodexMessageProcessor { let InterruptConversationParams { conversation_id } = params; let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { @@ -325,7 +269,7 @@ impl CodexMessageProcessor { let AddConversationListenerParams { conversation_id } = params; let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { @@ -457,7 +401,7 @@ impl CodexMessageProcessor { let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { @@ -589,6 +533,7 @@ fn derive_config_from_params( } = params; let overrides = ConfigOverrides { model, + review_model: None, config_profile: profile, cwd: cwd.map(PathBuf::from), approval_policy: approval_policy.map(map_ask_for_approval_from_wire), @@ -597,6 +542,8 @@ fn derive_config_from_params( codex_linux_sandbox_exe, base_instructions, include_plan_tool, + include_apply_patch_tool: None, + include_view_image_tool: None, disable_response_storage: None, show_raw_agent_reasoning: None, debug: None, @@ -708,25 +655,4 @@ fn map_ask_for_approval_from_wire(a: codex_protocol::protocol::AskForApproval) - } } -fn map_ask_for_approval_to_wire(a: core_protocol::AskForApproval) -> codex_protocol::protocol::AskForApproval { - match a { - core_protocol::AskForApproval::UnlessTrusted => codex_protocol::protocol::AskForApproval::UnlessTrusted, - core_protocol::AskForApproval::OnFailure => codex_protocol::protocol::AskForApproval::OnFailure, - core_protocol::AskForApproval::OnRequest => codex_protocol::protocol::AskForApproval::OnRequest, - core_protocol::AskForApproval::Never => codex_protocol::protocol::AskForApproval::Never, - } -} - -fn map_reasoning_effort_to_wire( - e: codex_core::config_types::ReasoningEffort, -) -> codex_protocol::config_types::ReasoningEffort { - use codex_core::config_types::ReasoningEffort as CoreRE; - use codex_protocol::config_types::ReasoningEffort as WireRE; - match e { - CoreRE::Minimal => WireRE::Minimal, - CoreRE::Low => WireRE::Low, - CoreRE::Medium => WireRE::Medium, - CoreRE::High => WireRE::High, - CoreRE::None => WireRE::Medium, - } -} +// Unused legacy mappers removed to avoid warnings. diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index a264098e32ce..0ccca2d7a673 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -152,6 +152,7 @@ impl CodexToolCallParam { // Build the `ConfigOverrides` recognized by codex-core. let overrides = codex_core::config::ConfigOverrides { model, + review_model: None, config_profile: profile, cwd: cwd.map(PathBuf::from), approval_policy: approval_policy.map(Into::into), @@ -160,6 +161,8 @@ impl CodexToolCallParam { codex_linux_sandbox_exe, base_instructions, include_plan_tool, + include_apply_patch_tool: None, + include_view_image_tool: None, disable_response_storage: None, show_raw_agent_reasoning: None, debug: None, diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index d5a47f9b580f..de7bb830bf67 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -90,7 +90,7 @@ pub async fn run_codex_tool_session( running_requests_id_to_codex_uuid .lock() .await - .insert(id.clone(), conversation_id); + .insert(id.clone(), conversation_id.into()); let submission = Submission { id: sub_id.clone(), op: Op::UserInput { @@ -280,7 +280,12 @@ async fn run_codex_tool_session_inner( | EventMsg::PlanUpdate(_) | EventMsg::BrowserScreenshotUpdate(_) | EventMsg::AgentStatusUpdate(_) + | EventMsg::TurnAborted(_) + | EventMsg::ConversationPath(_) + | EventMsg::UserMessage(_) | EventMsg::ShutdownComplete + | EventMsg::EnteredReviewMode(_) + | EventMsg::ExitedReviewMode(_) | EventMsg::CustomToolCallBegin(_) | EventMsg::CustomToolCallEnd(_) => { // For now, we do not do anything extra for these diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 90374d10b8c2..6993352e0b6f 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -508,7 +508,11 @@ impl MessageProcessor { let outgoing = self.outgoing.clone(); let running_requests_id_to_codex_uuid = self.running_requests_id_to_codex_uuid.clone(); - let codex = match self.conversation_manager.get_conversation(session_id).await { + let codex = match self + .conversation_manager + .get_conversation(codex_protocol::mcp_protocol::ConversationId(session_id)) + .await + { Ok(c) => c, Err(_) => { tracing::warn!("Session not found for session_id: {session_id}"); @@ -590,7 +594,11 @@ impl MessageProcessor { tracing::info!("session_id: {session_id}"); // Obtain the Codex conversation from the server. - let codex_arc = match self.conversation_manager.get_conversation(session_id).await { + let codex_arc = match self + .conversation_manager + .get_conversation(codex_protocol::mcp_protocol::ConversationId(session_id)) + .await + { Ok(c) => c, Err(_) => { tracing::warn!("Session not found for session_id: {session_id}"); diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 3684e50925ab..476566ad9ac0 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -97,6 +97,9 @@ impl OutgoingMessageSender { } } + /// This is used with the MCP server, but not the more general JSON-RPC app + /// server. Prefer [`OutgoingMessageSender::send_server_notification`] where + /// possible. pub(crate) async fn send_event_as_notification( &self, event: &Event, @@ -124,14 +127,9 @@ impl OutgoingMessageSender { #[allow(dead_code)] pub(crate) async fn send_server_notification(&self, notification: ServerNotification) { - let method = format!("codex/event/{notification}"); - let params = match serde_json::to_value(¬ification) { - Ok(serde_json::Value::Object(mut map)) => map.remove("data"), - _ => None, - }; - let outgoing_message = - OutgoingMessage::Notification(OutgoingNotification { method, params }); - let _ = self.sender.send(outgoing_message); + let _ = self + .sender + .send(OutgoingMessage::AppServerNotification(notification)); } pub(crate) async fn send_notification(&self, notification: OutgoingNotification) { @@ -149,6 +147,10 @@ impl OutgoingMessageSender { pub(crate) enum OutgoingMessage { Request(OutgoingRequest), Notification(OutgoingNotification), + /// AppServerNotification is specific to the case where this is run as an + /// "app server" as opposed to an MCP server. + #[allow(dead_code)] + AppServerNotification(ServerNotification), Response(OutgoingResponse), Error(OutgoingError), } @@ -172,6 +174,21 @@ impl From for JSONRPCMessage { params, }) } + AppServerNotification(notification) => { + let method = notification.to_string(); + let params = match notification.to_params() { + Ok(params) => Some(params), + Err(err) => { + warn!("failed to serialize notification params: {err}"); + None + } + }; + JSONRPCMessage::Notification(JSONRPCNotification { + jsonrpc: JSONRPC_VERSION.into(), + method, + params, + }) + } Response(OutgoingResponse { id, result }) => { JSONRPCMessage::Response(JSONRPCResponse { jsonrpc: JSONRPC_VERSION.into(), @@ -243,8 +260,12 @@ pub(crate) struct OutgoingError { mod tests { use codex_core::protocol::EventMsg; use codex_core::protocol::SessionConfiguredEvent; + use codex_protocol::config_types::ReasoningEffort; + use codex_protocol::mcp_protocol::ConversationId; + use codex_protocol::mcp_protocol::LoginChatGptCompleteNotification; use pretty_assertions::assert_eq; use serde_json::json; + use tempfile::NamedTempFile; use uuid::Uuid; use super::*; @@ -254,13 +275,18 @@ mod tests { let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); + let conversation_id = ConversationId::new(); + let rollout_file = NamedTempFile::new().unwrap(); let event = Event { id: "1".to_string(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: Uuid::new_v4(), + session_id: conversation_id, model: "gpt-4o".to_string(), + reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, history_entry_count: 1000, + initial_messages: None, + rollout_path: rollout_file.path().to_path_buf(), }), }; @@ -277,7 +303,7 @@ mod tests { let Ok(expected_params) = serde_json::to_value(&event) else { panic!("Event must serialize"); }; - assert_eq!(params, Some(expected_params.clone())); + assert_eq!(params, Some(expected_params)); } #[tokio::test] @@ -285,11 +311,16 @@ mod tests { let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); + let conversation_id = ConversationId::new(); + let rollout_file = NamedTempFile::new().unwrap(); let session_configured_event = SessionConfiguredEvent { - session_id: Uuid::new_v4(), + session_id: conversation_id, model: "gpt-4o".to_string(), + reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, history_entry_count: 1000, + initial_messages: None, + rollout_path: rollout_file.path().to_path_buf(), }; let event = Event { id: "1".to_string(), @@ -316,11 +347,38 @@ mod tests { "msg": { "session_id": session_configured_event.session_id, "model": session_configured_event.model, + "reasoning_effort": session_configured_event.reasoning_effort, "history_log_id": session_configured_event.history_log_id, "history_entry_count": session_configured_event.history_entry_count, "type": "session_configured", + "rollout_path": rollout_file.path().to_path_buf(), } }); assert_eq!(params.unwrap(), expected_params); } + + #[test] + fn verify_server_notification_serialization() { + let notification = + ServerNotification::LoginChatGptComplete(LoginChatGptCompleteNotification { + login_id: Uuid::nil(), + success: true, + error: None, + }); + + let jsonrpc_notification: JSONRPCMessage = + OutgoingMessage::AppServerNotification(notification).into(); + assert_eq!( + JSONRPCMessage::Notification(JSONRPCNotification { + jsonrpc: "2.0".into(), + method: "loginChatGptComplete".into(), + params: Some(json!({ + "loginId": Uuid::nil(), + "success": true, + })), + }), + jsonrpc_notification, + "ensure the strum macros serialize the method field correctly" + ); + } } diff --git a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs b/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs index c19d1e8b879f..48f27f7167e2 100644 --- a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs +++ b/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs @@ -52,6 +52,8 @@ pub(crate) async fn handle_create_conversation( codex_linux_sandbox_exe: None, base_instructions, include_plan_tool: None, + include_apply_patch_tool: None, + include_view_image_tool: None, disable_response_storage: None, show_raw_agent_reasoning: None, debug: None, diff --git a/codex-rs/mcp-server/tests/suite/auth.rs b/codex-rs/mcp-server/tests/suite/auth.rs index 415392780f85..9bfd1115fbb5 100644 --- a/codex-rs/mcp-server/tests/suite/auth.rs +++ b/codex-rs/mcp-server/tests/suite/auth.rs @@ -14,11 +14,17 @@ use tokio::time::timeout; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); -// Helper to create a config.toml; mirrors create_conversation.rs -fn create_config_toml(codex_home: &Path) -> std::io::Result<()> { +fn create_config_toml_custom_provider( + codex_home: &Path, + requires_openai_auth: bool, +) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); - std::fs::write( - config_toml, + let requires_line = if requires_openai_auth { + "requires_openai_auth = true\n" + } else { + "" + }; + let contents = format!( r#" model = "mock-model" approval_policy = "never" @@ -32,6 +38,20 @@ base_url = "http://127.0.0.1:0/v1" wire_api = "chat" request_max_retries = 0 stream_max_retries = 0 +{requires_line} +"# + ); + std::fs::write(config_toml, contents) +} + +fn create_config_toml(codex_home: &Path) -> std::io::Result<()> { + let config_toml = codex_home.join("config.toml"); + std::fs::write( + config_toml, + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "danger-full-access" "#, ) } @@ -104,6 +124,47 @@ async fn get_auth_status_with_api_key() { assert_eq!(status.preferred_auth_method, AuthMode::ChatGPT); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_auth_status_with_api_key_when_auth_not_required() { + let codex_home = TempDir::new().unwrap_or_else(|e| panic!("create tempdir: {e}")); + create_config_toml_custom_provider(codex_home.path(), false) + .unwrap_or_else(|err| panic!("write config.toml: {err}")); + + let mut mcp = McpProcess::new(codex_home.path()) + .await + .expect("spawn mcp process"); + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) + .await + .expect("init timeout") + .expect("init failed"); + + login_with_api_key_via_request(&mut mcp, "sk-test-key").await; + + let request_id = mcp + .send_get_auth_status_request(GetAuthStatusParams { + include_token: Some(true), + refresh_token: Some(false), + }) + .await + .expect("send getAuthStatus"); + + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await + .expect("getAuthStatus timeout") + .expect("getAuthStatus response"); + let status: GetAuthStatusResponse = to_response(resp).expect("deserialize status"); + assert_eq!(status.auth_method, None, "expected no auth method"); + assert_eq!(status.auth_token, None, "expected no token"); + assert_eq!( + status.requires_openai_auth, + Some(false), + "requires_openai_auth should be false", + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_auth_status_with_api_key_no_include_token() { let codex_home = TempDir::new().unwrap_or_else(|e| panic!("create tempdir: {e}")); diff --git a/codex-rs/mcp-server/tests/suite/codex_message_processor_flow.rs b/codex-rs/mcp-server/tests/suite/codex_message_processor_flow.rs index 092b135291e2..f978d39cc7c2 100644 --- a/codex-rs/mcp-server/tests/suite/codex_message_processor_flow.rs +++ b/codex-rs/mcp-server/tests/suite/codex_message_processor_flow.rs @@ -318,7 +318,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() { approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::new_read_only_policy(), model: "mock-model".to_string(), - effort: ReasoningEffort::Medium, + effort: Some(ReasoningEffort::Medium), summary: ReasoningSummary::Auto, }) .await diff --git a/codex-rs/mcp-server/tests/suite/set_default_model.rs b/codex-rs/mcp-server/tests/suite/set_default_model.rs index e11b69ed2d0b..7ffee3438d23 100644 --- a/codex-rs/mcp-server/tests/suite/set_default_model.rs +++ b/codex-rs/mcp-server/tests/suite/set_default_model.rs @@ -1,5 +1,6 @@ +use std::path::Path; + use codex_core::config::ConfigToml; -use codex_protocol::config_types::ReasoningEffort; use codex_protocol::mcp_protocol::SetDefaultModelParams; use codex_protocol::mcp_protocol::SetDefaultModelResponse; use mcp_test_support::McpProcess; @@ -14,7 +15,8 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn set_default_model_persists_overrides() { - let codex_home = TempDir::new().unwrap_or_else(|e| panic!("create tempdir: {e}")); + let codex_home = TempDir::new().expect("create tempdir"); + create_config_toml(codex_home.path()).expect("write config.toml"); let mut mcp = McpProcess::new(codex_home.path()) .await @@ -25,8 +27,8 @@ async fn set_default_model_persists_overrides() { .expect("init failed"); let params = SetDefaultModelParams { - model: Some("o4-mini".to_string()), - reasoning_effort: Some(ReasoningEffort::High), + model: Some("gpt-4.1".to_string()), + reasoning_effort: None, }; let request_id = mcp @@ -53,10 +55,22 @@ async fn set_default_model_persists_overrides() { assert_eq!( ConfigToml { - model: Some("o4-mini".to_string()), - model_reasoning_effort: Some(ReasoningEffort::High), + model: Some("gpt-4.1".to_string()), + model_reasoning_effort: None, ..Default::default() }, config_toml, ); } + +// Helper to create a config.toml; mirrors create_conversation.rs +fn create_config_toml(codex_home: &Path) -> std::io::Result<()> { + let config_toml = codex_home.join("config.toml"); + std::fs::write( + config_toml, + r#" +model = "gpt-5" +model_reasoning_effort = "medium" +"#, + ) +} diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 40034bc9989d..14612df68051 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -39,7 +39,7 @@ pub enum ReasoningSummary { /// Controls output length/detail on GPT-5 models via the Responses API. /// Serialized with lowercase values to match the OpenAI API. -#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum Verbosity { diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index de9b546c10cf..fe77d20b6659 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; -use crate::config_types::ConfigProfile; use crate::config_types::ReasoningEffort; use crate::config_types::ReasoningSummary; use crate::config_types::SandboxMode; +use crate::config_types::Verbosity; use crate::protocol::AskForApproval; +use crate::protocol::EventMsg; use crate::protocol::FileChange; use crate::protocol::ReviewDecision; use crate::protocol::SandboxPolicy; @@ -18,16 +19,40 @@ use strum_macros::Display; use ts_rs::TS; use uuid::Uuid; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, TS)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Hash)] #[ts(type = "string")] pub struct ConversationId(pub Uuid); +impl ConversationId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +impl Default for ConversationId { + fn default() -> Self { + Self::new() + } +} + impl Display for ConversationId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } +impl From for ConversationId { + fn from(value: Uuid) -> Self { + Self(value) + } +} + +impl From for Uuid { + fn from(value: ConversationId) -> Self { + value.0 + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)] #[ts(type = "string")] pub struct GitSha(pub String); @@ -54,6 +79,23 @@ pub enum ClientRequest { request_id: RequestId, params: NewConversationParams, }, + /// List recorded Codex conversations (rollouts) with optional pagination and search. + ListConversations { + #[serde(rename = "id")] + request_id: RequestId, + params: ListConversationsParams, + }, + /// Resume a recorded Codex conversation from a rollout file. + ResumeConversation { + #[serde(rename = "id")] + request_id: RequestId, + params: ResumeConversationParams, + }, + ArchiveConversation { + #[serde(rename = "id")] + request_id: RequestId, + params: ArchiveConversationParams, + }, SendUserMessage { #[serde(rename = "id")] request_id: RequestId, @@ -84,6 +126,11 @@ pub enum ClientRequest { request_id: RequestId, params: GitDiffToRemoteParams, }, + LoginApiKey { + #[serde(rename = "id")] + request_id: RequestId, + params: LoginApiKeyParams, + }, LoginChatGpt { #[serde(rename = "id")] request_id: RequestId, @@ -102,9 +149,28 @@ pub enum ClientRequest { request_id: RequestId, params: GetAuthStatusParams, }, - GetConfigToml { + GetUserSavedConfig { + #[serde(rename = "id")] + request_id: RequestId, + }, + SetDefaultModel { + #[serde(rename = "id")] + request_id: RequestId, + params: SetDefaultModelParams, + }, + GetUserAgent { + #[serde(rename = "id")] + request_id: RequestId, + }, + UserInfo { + #[serde(rename = "id")] + request_id: RequestId, + }, + /// Execute a command (argv vector) under the server's sandbox. + ExecOneOffCommand { #[serde(rename = "id")] request_id: RequestId, + params: ExecOneOffCommandParams, }, } @@ -156,6 +222,61 @@ pub struct NewConversationParams { pub struct NewConversationResponse { pub conversation_id: ConversationId, pub model: String, + /// Note this could be ignored by the model. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + pub rollout_path: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[serde(rename_all = "camelCase")] +pub struct ResumeConversationResponse { + pub conversation_id: ConversationId, + pub model: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_messages: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] +#[serde(rename_all = "camelCase")] +pub struct ListConversationsParams { + /// Optional page size; defaults to a reasonable server-side value. + #[serde(skip_serializing_if = "Option::is_none")] + pub page_size: Option, + /// Opaque pagination cursor returned by a previous call. + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct ConversationSummary { + pub conversation_id: ConversationId, + pub path: PathBuf, + pub preview: String, + /// RFC3339 timestamp string for the session start, if available. + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct ListConversationsResponse { + pub items: Vec, + /// Opaque cursor to pass to the next call to continue after the last item. + /// if None, there are no more items to return. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct ResumeConversationParams { + /// Absolute path to the rollout JSONL file. + pub path: PathBuf, + /// Optional overrides to apply when spawning the resumed session. + #[serde(skip_serializing_if = "Option::is_none")] + pub overrides: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] @@ -164,10 +285,32 @@ pub struct AddConversationSubscriptionResponse { pub subscription_id: Uuid, } +/// The [`ConversationId`] must match the `rollout_path`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct ArchiveConversationParams { + pub conversation_id: ConversationId, + pub rollout_path: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct ArchiveConversationResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct RemoveConversationSubscriptionResponse {} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginApiKeyParams { + pub api_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginApiKeyResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LoginChatGptResponse { @@ -218,34 +361,154 @@ pub struct GetAuthStatusParams { pub refresh_token: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct ExecOneOffCommandParams { + /// Command argv to execute. + pub command: Vec, + /// Timeout of the command in milliseconds. + /// If not specified, a sensible default is used server-side. + pub timeout_ms: Option, + /// Optional working directory for the process. Defaults to server config cwd. + #[serde(skip_serializing_if = "Option::is_none")] + pub cwd: Option, + /// Optional explicit sandbox policy overriding the server default. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_policy: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct ExecArbitraryCommandResponse { + pub exit_code: i32, + pub stdout: String, + pub stderr: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GetAuthStatusResponse { #[serde(skip_serializing_if = "Option::is_none")] pub auth_method: Option, - pub preferred_auth_method: AuthMode, #[serde(skip_serializing_if = "Option::is_none")] pub auth_token: Option, + + // Indicates that auth method must be valid to use the server. + // This can be false if using a custom provider that is configured + // with requires_openai_auth == false. + #[serde(skip_serializing_if = "Option::is_none")] + pub requires_openai_auth: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetUserAgentResponse { + pub user_agent: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct UserInfoResponse { + /// Note: `alleged_user_email` is not currently verified. We read it from + /// the local auth.json, which the user could theoretically modify. In the + /// future, we may add logic to verify the email against the server before + /// returning it. + pub alleged_user_email: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetUserSavedConfigResponse { + pub config: UserSavedConfig, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct SetDefaultModelParams { + /// If set to None, this means `model` should be cleared in config.toml. + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + /// If set to None, this means `model_reasoning_effort` should be cleared + /// in config.toml. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] -pub struct GetConfigTomlResponse { +pub struct SetDefaultModelResponse {} + +/// UserSavedConfig contains a subset of the config. It is meant to expose mcp +/// client-configurable settings that can be specified in the NewConversation +/// and SendUserTurn requests. +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct UserSavedConfig { /// Approvals #[serde(skip_serializing_if = "Option::is_none")] pub approval_policy: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_settings: Option, - /// Relevant model configuration + /// Model-specific configuration + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, #[serde(skip_serializing_if = "Option::is_none")] pub model_reasoning_effort: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub model_reasoning_summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub model_verbosity: Option, + + /// Tools + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option, /// Profiles #[serde(skip_serializing_if = "Option::is_none")] pub profile: Option, + #[serde(default)] + pub profiles: HashMap, +} + +/// MCP representation of a [`codex_core::config_profile::ConfigProfile`]. +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct Profile { + pub model: Option, + /// The key in the `model_providers` map identifying the + /// [`ModelProviderInfo`] to use. + pub model_provider: Option, + pub approval_policy: Option, + pub model_reasoning_effort: Option, + pub model_reasoning_summary: Option, + pub model_verbosity: Option, + pub chatgpt_base_url: Option, +} +/// MCP representation of a [`codex_core::config::ToolsToml`]. +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct Tools { + #[serde(skip_serializing_if = "Option::is_none")] + pub web_search: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub view_image: Option, +} + +/// MCP representation of a [`codex_core::config_types::SandboxWorkspaceWrite`]. +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SandboxSettings { + #[serde(default)] + pub writable_roots: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub profiles: Option>, + pub network_access: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_tmpdir_env_var: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_slash_tmp: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] @@ -264,7 +527,8 @@ pub struct SendUserTurnParams { pub approval_policy: AskForApproval, pub sandbox_policy: SandboxPolicy, pub model: String, - pub effort: ReasoningEffort, + #[serde(skip_serializing_if = "Option::is_none")] + pub effort: Option, pub summary: ReasoningSummary, } @@ -398,8 +662,8 @@ pub struct AuthStatusChangeNotification { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] +#[serde(tag = "method", content = "params", rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] pub enum ServerNotification { /// Authentication status changed AuthStatusChange(AuthStatusChangeNotification), @@ -408,6 +672,15 @@ pub enum ServerNotification { LoginChatGptComplete(LoginChatGptCompleteNotification), } +impl ServerNotification { + pub fn to_params(self) -> Result { + match self { + ServerNotification::AuthStatusChange(params) => serde_json::to_value(params), + ServerNotification::LoginChatGptComplete(params) => serde_json::to_value(params), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -442,4 +715,10 @@ mod tests { serde_json::to_value(&request).unwrap(), ); } + + #[test] + fn test_conversation_id_default_is_not_zeroes() { + let id = ConversationId::default(); + assert_ne!(id.0, Uuid::nil()); + } } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 7e358f12e246..c3aebcdd42e0 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -15,6 +15,7 @@ use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::custom_prompts::CustomPrompt; use crate::mcp_protocol::ConversationId; use crate::message_history::HistoryEntry; +use crate::models::ContentItem; use crate::models::ResponseItem; use crate::num_format::format_with_separators; use crate::parse_command::ParsedCommand; @@ -81,7 +82,8 @@ pub enum Op { model: String, /// Will only be honored if the model is configured to use reasoning. - effort: ReasoningEffortConfig, + #[serde(skip_serializing_if = "Option::is_none")] + effort: Option, /// Will only be honored if the model is configured to use reasoning. summary: ReasoningSummaryConfig, @@ -111,8 +113,11 @@ pub enum Op { model: Option, /// Updated reasoning effort (honored only for reasoning-capable models). + /// + /// Use `Some(Some(_))` to set a specific effort, `Some(None)` to clear + /// the effort, or `None` to leave the existing value unchanged. #[serde(skip_serializing_if = "Option::is_none")] - effort: Option, + effort: Option>, /// Updated reasoning summary preference (honored only for reasoning-capable models). #[serde(skip_serializing_if = "Option::is_none")] @@ -149,7 +154,7 @@ pub enum Op { /// Request the full in-memory conversation transcript for the current session. /// Reply is delivered via `EventMsg::ConversationHistory`. - GetHistory, + GetPath, /// Request the list of MCP tools available across all configured servers. /// Reply is delivered via `EventMsg::McpListToolsResponse`. @@ -162,6 +167,10 @@ pub enum Op { /// The agent will use its existing context (either conversation history or previous response id) /// to generate a summary which will be returned as an AgentMessage event. Compact, + + /// Request a code review from the agent. + Review { review_request: ReviewRequest }, + /// Request to shut down codex instance. Shutdown, } @@ -405,6 +414,7 @@ pub struct Event { } /// Response event from the agent +/// NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen. #[derive(Debug, Clone, Deserialize, Serialize, Display, TS)] #[serde(tag = "type", rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] @@ -499,7 +509,18 @@ pub enum EventMsg { /// Notification that the agent is shutting down. ShutdownComplete, - ConversationHistory(ConversationHistoryResponseEvent), + ConversationPath(ConversationPathResponseEvent), + + /// Entered review mode. + EnteredReviewMode(ReviewRequest), + + /// Exited review mode with an optional final result to apply. + ExitedReviewMode(ExitedReviewModeEvent), +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +pub struct ExitedReviewModeEvent { + pub review_output: Option, } // Individual event payload types matching each `EventMsg` variant. @@ -708,12 +729,12 @@ where let (_role, message) = value; let message = message.as_ref(); let trimmed = message.trim(); - if trimmed.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG) - && trimmed.ends_with(ENVIRONMENT_CONTEXT_CLOSE_TAG) + if starts_with_ignore_ascii_case(trimmed, ENVIRONMENT_CONTEXT_OPEN_TAG) + && ends_with_ignore_ascii_case(trimmed, ENVIRONMENT_CONTEXT_CLOSE_TAG) { InputMessageKind::EnvironmentContext - } else if trimmed.starts_with(USER_INSTRUCTIONS_OPEN_TAG) - && trimmed.ends_with(USER_INSTRUCTIONS_CLOSE_TAG) + } else if starts_with_ignore_ascii_case(trimmed, USER_INSTRUCTIONS_OPEN_TAG) + && ends_with_ignore_ascii_case(trimmed, USER_INSTRUCTIONS_CLOSE_TAG) { InputMessageKind::UserInstructions } else { @@ -722,6 +743,26 @@ where } } +fn starts_with_ignore_ascii_case(text: &str, prefix: &str) -> bool { + let text_bytes = text.as_bytes(); + let prefix_bytes = prefix.as_bytes(); + text_bytes.len() >= prefix_bytes.len() + && text_bytes + .iter() + .zip(prefix_bytes.iter()) + .all(|(a, b)| a.eq_ignore_ascii_case(b)) +} + +fn ends_with_ignore_ascii_case(text: &str, suffix: &str) -> bool { + let text_bytes = text.as_bytes(); + let suffix_bytes = suffix.as_bytes(); + text_bytes.len() >= suffix_bytes.len() + && text_bytes[text_bytes.len() - suffix_bytes.len()..] + .iter() + .zip(suffix_bytes.iter()) + .all(|(a, b)| a.eq_ignore_ascii_case(b)) +} + #[derive(Debug, Clone, Deserialize, Serialize, TS)] pub struct AgentMessageDeltaEvent { pub delta: String, @@ -801,9 +842,9 @@ pub struct WebSearchEndEvent { /// Response payload for `Op::GetHistory` containing the current session's /// in-memory transcript. #[derive(Debug, Clone, Deserialize, Serialize, TS)] -pub struct ConversationHistoryResponseEvent { +pub struct ConversationPathResponseEvent { pub conversation_id: ConversationId, - pub entries: Vec, + pub path: PathBuf, } #[derive(Debug, Clone, Deserialize, Serialize, TS)] @@ -828,26 +869,7 @@ impl InitialHistory { InitialHistory::Forked(items) => items.clone(), } } - pub fn get_response_items(&self) -> Vec { - match self { - InitialHistory::New => Vec::new(), - InitialHistory::Resumed(resumed) => resumed - .history - .iter() - .filter_map(|ri| match ri { - RolloutItem::ResponseItem(item) => Some(item.clone()), - _ => None, - }) - .collect(), - InitialHistory::Forked(items) => items - .iter() - .filter_map(|ri| match ri { - RolloutItem::ResponseItem(item) => Some(item.clone()), - _ => None, - }) - .collect(), - } - } + pub fn get_event_msgs(&self) -> Option> { match self { InitialHistory::New => None, @@ -897,9 +919,39 @@ pub struct SessionMetaLine { pub enum RolloutItem { SessionMeta(SessionMetaLine), ResponseItem(ResponseItem), + Compacted(CompactedItem), + TurnContext(TurnContextItem), EventMsg(EventMsg), } +#[derive(Serialize, Deserialize, Clone, Debug, TS)] +pub struct CompactedItem { + pub message: String, +} + +impl From for ResponseItem { + fn from(value: CompactedItem) -> Self { + ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: value.message, + }], + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, TS)] +pub struct TurnContextItem { + pub cwd: PathBuf, + pub approval_policy: AskForApproval, + pub sandbox_policy: SandboxPolicy, + pub model: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub effort: Option, + pub summary: ReasoningSummaryConfig, +} + #[derive(Serialize, Deserialize, Clone)] pub struct RolloutLine { pub timestamp: String, @@ -920,6 +972,57 @@ pub struct GitInfo { pub repository_url: Option, } +/// Review request sent to the review session. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +pub struct ReviewRequest { + pub prompt: String, + pub user_facing_hint: String, +} + +/// Structured review result produced by a child review session. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +pub struct ReviewOutputEvent { + pub findings: Vec, + pub overall_correctness: String, + pub overall_explanation: String, + pub overall_confidence_score: f32, +} + +impl Default for ReviewOutputEvent { + fn default() -> Self { + Self { + findings: Vec::new(), + overall_correctness: String::default(), + overall_explanation: String::default(), + overall_confidence_score: 0.0, + } + } +} + +/// A single review finding describing an observed issue or recommendation. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +pub struct ReviewFinding { + pub title: String, + pub body: String, + pub confidence_score: f32, + pub priority: i32, + pub code_location: ReviewCodeLocation, +} + +/// Location of the code related to a review finding. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +pub struct ReviewCodeLocation { + pub absolute_file_path: PathBuf, + pub line_range: ReviewLineRange, +} + +/// Inclusive line range in a file associated with the finding. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +pub struct ReviewLineRange { + pub start: u32, + pub end: u32, +} + #[derive(Debug, Clone, Deserialize, Serialize, TS)] pub struct ExecCommandBeginEvent { /// Identifier so this can be paired with the ExecCommandEnd event. @@ -1064,6 +1167,10 @@ pub struct SessionConfiguredEvent { /// Tell the client what model is being queried. pub model: String, + /// The effort the model is putting into reasoning about the user's request. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + /// Identifier of the history log file (inode on Unix, 0 otherwise). pub history_log_id: u64, @@ -1152,6 +1259,7 @@ mod tests { msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id: conversation_id, model: "codex-mini-latest".to_string(), + reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, initial_messages: None, @@ -1165,6 +1273,7 @@ mod tests { "type": "session_configured", "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8", "model": "codex-mini-latest", + "reasoning_effort": "medium", "history_log_id": 0, "history_entry_count": 0, "rollout_path": format!("{}", rollout_file.path().display()), diff --git a/codex-rs/tui/frames/blocks/frame_1.txt b/codex-rs/tui/frames/blocks/frame_1.txt new file mode 100644 index 000000000000..8c3263f51841 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_1.txt @@ -0,0 +1,17 @@ + + ▒▓▒▓▒██▒▒██▒ + ▒▒█▓█▒█▓█▒▒░░▒▒ ▒ █▒ + █░█░███ ▒░ ░ █░ ░▒░░░█ + ▓█▒▒████▒ ▓█░▓░█ + ▒▒▓▓█▒░▒░▒▒ ▓░▒▒█ + ░█ █░ ░█▓▓░░█ █▓▒░░█ + █▒ ▓█ █▒░█▓ ░▒ ░▓░ + ░░▒░░ █▓▓░▓░█ ░░ + ░▒░█░ ▓░░▒▒░ ▓░██████▒██ ▒ ░ + ▒░▓█ ▒▓█░ ▓█ ░ ░▒▒▒▓▓███░▓█▓█░ + ▒▒▒ ▒ ▒▒█▓▓░ ░▒████ ▒█ ▓█▓▒▓ + █▒█ █ ░ ██▓█▒░ + ▒▒█░▒█▒ ▒▒▒█░▒█ + ▒██▒▒ ██▓▓▒▓▓▓▒██▒█░█ + ░█ █░░░▒▒▒█▒▓██ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_10.txt b/codex-rs/tui/frames/blocks/frame_10.txt new file mode 100644 index 000000000000..a6fbbf1a4b87 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_10.txt @@ -0,0 +1,17 @@ + + ▒████▒██▒ + ██░███▒░▓▒██ + ▒▒█░░▓░░▓░█▒██ + ░▒▒▓▒░▓▒▓▒███▒▒█ + ▓ ▓░░ ░▒ ██▓▒▓░▓ + ░░ █░█░▓▓▒ ░▒ ░ + ▒ ░█ █░░░░█ ░▓█ + ░░▒█▓█░░▓▒░▓▒░░ + ░▒ ▒▒░▓░░█▒█▓░░ + ░ █░▒█░▒▓▒█▒▒▒░█░ + █ ░░░░░ ▒█ ▒░░ + ▒░██▒██ ▒░ █▓▓ + ░█ ░░░░██▓█▓░▓░ + ▓░██▓░█▓▒ ▓▓█ + ██ ▒█▒▒█▓█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_11.txt b/codex-rs/tui/frames/blocks/frame_11.txt new file mode 100644 index 000000000000..88e3dfa7c583 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_11.txt @@ -0,0 +1,17 @@ + + ███████▒ + ▓ ▓░░░▒▒█ + ▓ ▒▒░░▓▒█▓▒█ + ░▒▒░░▒▓█▒▒▓▓ + ▒ ▓▓▒░█▒█▓▒░░█ + ░█░░░█▒▓▓░▒▓░░ + ██ █░░░░░░▒░▒▒ + ░ ░░▓░░▒▓ ░ ░ + ▓ █░▓░░█▓█░▒░ + ██ ▒░▓▒█ ▓░▒░▒ + █░▓ ░░░░▒▓░▒▒░ + ▒▒▓▓░▒█▓██▓░░ + ▒ █░▒▒▒▒░▓ + ▒█ █░░█▒▓█░ + ▒▒ ███▒█░ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_12.txt b/codex-rs/tui/frames/blocks/frame_12.txt new file mode 100644 index 000000000000..c6c0ef3e87dc --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_12.txt @@ -0,0 +1,17 @@ + + █████▓ + █▒░▒▓░█▒ + ░▓▒██ + ▓█░░░▒▒ ░ + ░ █░░░░▓▓░ + ░█▓▓█▒ ▒░ + ░ ░▓▒░░▒ + ░ ▓█▒░░ + ██ ░▓░░█░░ + ░ ▓░█▓█▒ + ░▓ ░ ▒██▓ + █ █░ ▒█░ + ▓ ██░██▒░ + █▒▓ █░▒░░ + ▒ █░▒▓▓ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_13.txt b/codex-rs/tui/frames/blocks/frame_13.txt new file mode 100644 index 000000000000..7a090e51e330 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_13.txt @@ -0,0 +1,17 @@ + + ▓████ + ░▒▒░░ + ░░▒░ + ░██░▒ + █ ░░ + ▓▓░░ + █ ░░ + █ ░ + ▓█ ▒░▓ + ░ █▒░ + █░▓▓ ░░ + ░▒▒▒░ + ░██░▒ + █▒▒░▒ + █ ▓ ▒ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_14.txt b/codex-rs/tui/frames/blocks/frame_14.txt new file mode 100644 index 000000000000..f5e74d12b7ef --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_14.txt @@ -0,0 +1,17 @@ + + ████▓ + █▓▒▒▓▒ + ░▒░░▓ ░ + ░░▓░ ▒░█ + ░░░▒ ░ + ░█░░ █░ + ░░░░ ▓ █ + ░░▒░░ ▒ + ░░░░ + ▒▓▓ ▓▓ + ▒░ █▓█░ + ░█░░▒▒▒░ + ▓ ░▒▒▒░ + ░▒▓█▒▒▓ + ▒█ █▒▓ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_15.txt b/codex-rs/tui/frames/blocks/frame_15.txt new file mode 100644 index 000000000000..f04599ea27dc --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_15.txt @@ -0,0 +1,17 @@ + + █████░▒ + ░█▒░░▒▓██ + ▓▓░█▒▒░ █░ + ░▓░ ▓▓█▓▒▒░ + ░░▒ ▒▒░░▓ ▒░ + ▒░░▓░░▓▓░ + ░░ ░░░░░░█░ + ░░▓░░█░░░ █▓░ + ░░████░░░▒▓▓░ + ░▒░▓▓░▒░█▓ ▓░ + ░▓░░░░▒░ ░ ▓ + ░██▓▒░░▒▓ ▒ + █░▒█ ▓▓▓░ ▓░ + ░▒░░▒▒▓█▒▓ + ▒▒█▒▒▒▒▓ + ░░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_16.txt b/codex-rs/tui/frames/blocks/frame_16.txt new file mode 100644 index 000000000000..1eb080286ec5 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_16.txt @@ -0,0 +1,17 @@ + + ▒▒█ ███░▒ + ▓▒░░█░░▒░▒▒ + ░▓▓ ▒▓▒▒░░ █▒ + ▓▓▓ ▓█▒▒░▒░░██░ + ░░▓▒▓██▒░░█▓░░▒ + ░░░█░█ ░▒▒ ░ ░▓░ + ▒▒░ ▓░█░░░░▓█ █ ░ + ░▓▓ ░░░░▓░░░ ▓ ░░ + ▒▒░░░█░▓▒░░ ██ ▓ + █ ▒▒█▒▒▒█░▓▒░ █▒░ + ░░░█ ▓█▒░▓ ▓▓░░░ + ░░█ ░░ ░▓▓█ ▓ + ▒░█ ░ ▓█▓▒█░ + ▒░░ ▒█░▓▓█▒░ + █▓▓▒▒▓▒▒▓█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_17.txt b/codex-rs/tui/frames/blocks/frame_17.txt new file mode 100644 index 000000000000..dd5f5c8da5ff --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_17.txt @@ -0,0 +1,17 @@ + + █▒███▓▓░█▒ + ▒▓██░░░█▒█░█ ▒█ + ██▒▓▒▒▒░██ ░░░▒ ▒ + ▓░▓▒▓░ ▒░ █░▓▒░░░▒▒ + ░▓▒ ░ ░ ▓▒▒▒▓▓ █ + ░▒██▓░ █▓▓░ ▓█▒▓░▓▓ + █ ▓▓░ █▓▓░▒ █ ░░▓▒░ + ▓ ▒░ ▓▓░░▓░█░░▒▓█ + █▓█▓▒▒▒█░▒▒░▒▒▓▒░░░ ░ + ░ ▒▓▒▒░▓█▒▓░░▒ ▒███▒ + ▒▒▒▓ ████▒▒░█▓▓▒ ▒█ + ▒░░▒█ ░▓░░░ ▓ + ▒▒▒ █▒▒ ███▓▒▒▓ + █ ░██▒▒█░▒▓█▓░█ + ░█▓▓▒██░█▒██ + ░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_18.txt b/codex-rs/tui/frames/blocks/frame_18.txt new file mode 100644 index 000000000000..a6c93e6c01d2 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_18.txt @@ -0,0 +1,17 @@ + + ▒▒▒█▒▒█▓░█▒ + ▒█ ▒▓███░▒▒█ █▓▓▒ + ▒▓▓░█ █▒ █ ▓▒ █▓▓▒ █ + █░░█▓█▒ █ █▒░▒▓▒░▒▓▒▒▒█ + ▒▒▓▓ ▓░ ▒ █▒▒▓░▓░▒▒▓▒▒▒ + ▓▒░ ██░▓▒▒▒▓███░█▓▓▒▓░▓░ + ░░▒▓▓ █▓█▓░ ▒▓ █░▒░▒█ + ▒▓░░ ▒▒ ░░▓▒ ░▓░ + ▒ █▒▒▒▓▒▓█░░█░█▓▒█ ░█░░ + ▒▒▒░█▒█ ░░▓▒▒▒▒░░░▒▓░░▒ █ + ░▓░▒░ █████░ ▒▒▒▓░▓█▓░▓░ + ▒▒ █▒█ ░░█ ▓█▒█ + ▒▒██▒▒▓ ▒█▒▒▓▒█░ + █░▓████▒▒▒▒██▒▓▒██ + ░░▒▓▒▒█▓█ ▓█ + ░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_19.txt b/codex-rs/tui/frames/blocks/frame_19.txt new file mode 100644 index 000000000000..73341b5d5816 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_19.txt @@ -0,0 +1,17 @@ + + ▒▒▒▒█░█▒▒░▓▒ + ▒█░░░▒▓▒▒▒▒█▒█░███ + ██▓▓▓ ░██░ ░█▓█░█▓▒ + ▓▓░██▒░ ▒▒▒██▒░██ + ░░▓░▓░ █░▒ ▓ ░▒ ░▒█ + ░▒▓██ ▒░█░▓ ▓▓ █▓█░ + ▒▒░░█ ▓█▒▓░██░ ▓▓▓█░ + ░░░░ ░▓ ▒░ █ ░ ░░░ + ░█░▒█▒▓▓▒▒▒░░░░██▓█░▓ ▒ ░░ + ▒▓▓█░▒█▓▒██▒█░█ ▒▒ ▓▒▒▒█▓▓░▒ + █▒ ▓█░ ██ ▒▒▒▓░▓▓ ▓▓█ + ▒▒▒█▒▒ ░▓▓▒▓▓█ + █ ▒▒░░██ █▓▒▓▓░▓░ + █ ▓░█▓░█▒▒▒▓▓█ ▓█░█ + ░▓▒▓▓█▒█▓▒█▓▒ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_2.txt b/codex-rs/tui/frames/blocks/frame_2.txt new file mode 100644 index 000000000000..1c7578c970ef --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_2.txt @@ -0,0 +1,17 @@ + + ▒▓▒▓▒█▒▒▒██▒ + ▒██▓█▓█░░░▒░░▒▒█░██▒ + █░█░▒██░█░░ ░ █▒█▓░░▓░█ + ▒░▓▒▓████▒ ▓█▒░▓░█ + █▒ ▓█▒░▒▒▒▒▒ ▒█░▒░█ + █▓█ ░ ░█▒█▓▒█ ▒▒░█░ + █░██░ ▒▓░▓░▒░█ ▓ ░ ░ + ░ ▒░ █░█░░▓█ ░█▓▓░ + █ ▒░ ▓░▒▒▒░ ▓░█████████░▒░░█ + ▒▒█░ ▓░░█ ▓█ ░▒▒▒▒▒▒▓▓▒▒░█▓ ░ + ▒▒▒ █ █▒▓▓░█ ░ ███████ ░██░░ + █▒▒▓▓█ ░ ██▓▓██ + ▓▒▒▒░██ █▒▒█ ▒░ + ░░▒▓▒▒ ██▓▓▒▓▓▓▒█░▒░░█ + ░████░░▒▒▒▒░▓▓█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_20.txt b/codex-rs/tui/frames/blocks/frame_20.txt new file mode 100644 index 000000000000..3e0c5f0d9ceb --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_20.txt @@ -0,0 +1,17 @@ + + ▒▒█▒░░▒█▒█▒▒ + █▓▒ ▓█▒█▒▒▒░░▒▒█▒██ + ██ ▒██ ░█ ░ ▒ ▒██░█▒ + ▒░ ▒█░█ ▒██░▒▓█▒▒ + ▒░ █░█ ▒▓ ▒░░▒█▒░░▒ + ▓░█░█ ███▓░ ▓ █▒░░▒ + ▓░▓█░ ██ ▓██▒ █▒░▓ + ░▒▒▓░ ▓▓░ █ ░░ ░ + ░▓░░▓█▒▓▒▒▒▒▒▒▒██▓▒▒▒▒█ ▓ ░▒ + █░▒░▒ ▓░░▒▒▒▒░▒ █▒▒ ░▒▒ █▓ ░░ + ▒█▒▒█ █ ▒█▒░░█░ ▓▒ + █ ▒█▓█ ▒▓█▓░▓ + ▒▒▒██░▒ █▓█░▓██ + ▒█▓▓ ░█▒▓▓█▓ ░ ░█▓██ + ░██░▒ ▒▒▒▒▒░█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_21.txt b/codex-rs/tui/frames/blocks/frame_21.txt new file mode 100644 index 000000000000..971877651f3e --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_21.txt @@ -0,0 +1,17 @@ + + ▒▒█▒█▒▒█▒██▒▒ + ███░░▒▒█░▒░█▓▒░▓██▒ + ▓█▒▒██▒ ░ ░▒░██▒░██ + ██░▓ █ ▒█▓██▓██ + ▓█▓█░ █░▓▒▒ ▒▒▒▒█ + ▓ ▓░ ███▒▓▓ ▒▒▒█ + ░█░░ ▒ ▓░█▓█ ▒▓▒ + ░▒ ▒▓ ░█ ░ ░ + ░ ░ ██▓▓▓▓▓███ ▒░█ ░█ ▓▓ ░ + ░ ░▒ ░▒ ▒█░ ▒ ░█░█ ▓ ▓▓ + ▓ ▓ ░░ █░ ██▒█▓ ▓░ █ + ██ ▓▓▒ ▒█ ▓ + █▒ ▒▓▒ ▒▓▓██ █░ + █▒▒ █ ██▓░░▓▓▒█ ▓░ + ███▓█▒▒▒▒█▒▓██░ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_22.txt b/codex-rs/tui/frames/blocks/frame_22.txt new file mode 100644 index 000000000000..2713fd669e24 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_22.txt @@ -0,0 +1,17 @@ + + ▒██▒█▒▒█▒██▒ + ▒█▓█░▓▒▓░▓▒░░▓░█▓██▒ + █▓█▓░▒██░ ░ █▒███▒▒██ + ▓█░██ ██░░░▒█▒ + ▒░░▓█ █▒▓░▒░▓▓▓█░ + ▒░█▓░ █░▓░▓▒▓░ ▒░▒▒░ + ░██▒▓ ░█░▒█▓█ ░░▓░ + ░░▒░░ ░▒░░▒▒ ░▒░ ░ + ░░█ █ █░▒▒▓▓▓▒██▒▒█░▒ ▒█ ▒░▓ + ▒░▒ █▒▒▒█ ▓█ ░▓▓░ ▒█▓▒ ░██ ▓▒▒ + ▒▒▒▒░ ██ ░ ░▓██▒▓▓▓ █░ + ▒█▒▒▒█ ▒██ ░██ + █ █▓ ██▒ ▒▓██ █▒▓ + █▓███ █░▓▒█▓▓▓▒█ ███ + ░ ░▒▓▒▒▒▓▒▒▓▒█░ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_23.txt b/codex-rs/tui/frames/blocks/frame_23.txt new file mode 100644 index 000000000000..39a6c5564446 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_23.txt @@ -0,0 +1,17 @@ + + ▒██▒▒████▒█▒▒ + ▒▒░█░▒▒█▒▒▒█░▒░█░█▒ + █ █░██▓█░ ░▓█░▒▓░░█ + ▓▓░█▓▓░ ▒▓▓▒░░▓▒ + ▓▓░░▓█ █▓████▓█▒░▒ + █▒░ ▓░ ▒█████▓██░░▒░█ + ░░░ ░ ▓▓▓▓ ▒░░ ░██ + ░▓░ ░ ░ ░█▒▒█ ░ █▓░ + ▒ ▒ ░█░▓▒▒▒▒▒▓▒░▒█░▒ ▒▒ ░ ░░░ + ░▒▒▒░ ▒ ▓░▒ ▒░▒▒█░ ▒▒░ + ▓█░ ░ ░ █░▓▓▒░▒▓▒▓░ + █░░▒░▓ █▓░▒▒▓░ + ▒ ░██▓▒▒ ▒▓ ▓█▓█▓ + ▒▒▒█▓██▒░▒▒▒██ ▓▒██░ + ░ █▒▒░▒▒█▒▒██░ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_24.txt b/codex-rs/tui/frames/blocks/frame_24.txt new file mode 100644 index 000000000000..90ccc262f077 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_24.txt @@ -0,0 +1,17 @@ + + ▒░▒▓███▒▒█▒ + █ ▒▓ ░▒▒░▒▒██▒██ + █ █▓▒▓█ ░ ▓░▓█░███ ▒ + ██▓▓█▓░▒█▒░░▓░ ▒█▒░▒▒█ + █ ▓▓▒▓█ ░ ▓▒▒░░░▒░██ + ░█▒█▒░ ███▓ ▓░▓ ▓ ▒ + ░ ░░ █▓▒█▓ ▓▒▒░▒▒░▒ + ░ ▒░░ ░█▒▓▒▒░░▒▓▓░░░ + ░▓ ░▓▓▓▓██░░░██▒██▒░ ░ ░░ + ▒ ▓ █░▓██▓▓██░▓▒▒██░ ░█░ + ▒ █▒░▒█ ░ ▒█▓█▒░▒▓█░ + ▒ ▒██▒ ░ ▓▓▓ + ▒▓█▒░░▓ ▒▒ ▒▓▓▒█ + ▓▓██▒▒ ░░▓▒▒▓░▒▒▓░ + █▓▒██▓▒▒▒▒▒██ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_25.txt b/codex-rs/tui/frames/blocks/frame_25.txt new file mode 100644 index 000000000000..d8fd5b45a8f9 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_25.txt @@ -0,0 +1,17 @@ + + ▒█▒█▓████▒ + █ ███░▒▓▒░█░░█ + ▓░▓▓██ ▓░█▒▒▒░░░▒ + ░██░ ▓ ▒░ ▒░██▒▓ + █▒▒▒█▓█▒▓▓▒░ ░▓▓▒▓█ + ▒█░░░▒██▓▒░▓ ▓░█░▓▓░█ + ░▓░█░ ░▒▒▓▒▒▓░▒▓▒ ░▒░ + ░░░▓░▓ ░▒▒▒▓░▒▒░▒░░▒ + ▒█▒░ ░▒▒▒▒▒▒█░░▒▒░██░▒ + ▓▓ ░▓░█░▒░░▓█▒░▒█▒▓▒░ + ▒░█▓▒░░ ██▓░▒░▓░░ + ░▒ ░▓█▓▒▓██▓▒▓█▓▓░▓ + ▒░▒░▒▒▒█▓▓█▒▓▒░░▓ + ▒▓▓▒▒▒█▒░██ █░█ + ░█ █▒██▒█░█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_26.txt b/codex-rs/tui/frames/blocks/frame_26.txt new file mode 100644 index 000000000000..a4734b4486d8 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_26.txt @@ -0,0 +1,17 @@ + + ▒▓███ ██ + ▓█░▓▓▒░█▓░█ + ▓█ ░▓▒░▒ ▒█ + ▓█ █░░░▒░░▒█▓▒ + ░▒█▒░▓░ █▒▓▓░▒▓ + ▒ ░▓▓▓ █▒▒ ▒▒▓ + ░ ██▒░░▓░░▓▓ █ + ▓▓ ▒░░░▒▒▒░░▓░░ + ░ ▓▒█▓█░█▒▒▓▒░░ + ▓▒░▓█░▒▒██▒▒█░ + ░░ ▓░█ ▒█▓░█▒░░ + ▒▒░░▓▒ ▓▓ ░░░ + █ █░▒ ▒░▓░▓█ + ░ █▒▒ █▒██▓ + ▒▓▓▒█░▒▒█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_27.txt b/codex-rs/tui/frames/blocks/frame_27.txt new file mode 100644 index 000000000000..b99e90e6d43b --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_27.txt @@ -0,0 +1,17 @@ + + ▓█████ + ░▓▓▓░▓▒ + ▓█░ █░▓█░ + ░░░▒░░▓░░ + ░ ░░▒▓█▒ + ░▒▓▒ ░░░░░ + ▒ ░░▒█░░ + ░ ░░░░▒ ░░ + ░▓ ▓ ░█░░░░ + █▒ ▓ ▒░▒█░░ + ░▓ ▒▒███▓█ + ░░██░░▒▓░ + ░▒▒█▒█▓░▒ + ▒▒▒░▒▒▓▓ + █▒ ▒▒▓ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_28.txt b/codex-rs/tui/frames/blocks/frame_28.txt new file mode 100644 index 000000000000..de6db173b464 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_28.txt @@ -0,0 +1,17 @@ + + ▓██▓ + ░█▒░░ + ▒ ▓░░ + ░▓░█░ + ░ ░░ + ░ ▓ ░ + ▒░░ ▒░ + ░▓ ░ + ▓▒ ▒░ + ░░▓▓░░ + ░ ▒░ + ░▒█▒░ + ░▒█░░ + █▒▒▓░ + ░ ▓█░ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_29.txt b/codex-rs/tui/frames/blocks/frame_29.txt new file mode 100644 index 000000000000..d7b871c9c337 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_29.txt @@ -0,0 +1,17 @@ + + ██████ + █░█▓ █▒ + ▒█░░ █ ░ + ░░░░▒▒█▓ + ▒ ░ ░ ░ + ░█░░░ ▒▒ + ░▒▒░░░ ▒ + ░░▒░░ + ░░░█░ ░ + ▒░▒░░ ░ + █░░▓░▒ ▒ + ░▓░░░ ▒░ + ░░░░░░▒░ + ░▒░█▓ ░█ + ░░█ ▓█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_3.txt b/codex-rs/tui/frames/blocks/frame_3.txt new file mode 100644 index 000000000000..833b2b3db2e2 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_3.txt @@ -0,0 +1,17 @@ + + ▒▓▒▓██▒▒▒▒█▒ + ▒██▓▒░░█░ ▒▒░▓▒▒░██▒ + █▓▓█▓░█ ░ ░ ░ ███▓▒░█ + ▓█▓▒░▓██▒ ░▒█ ░░▒ + █▓█░▓▒▓░░█▒▒ ▒▒▒░░▒ + ▓░▒▒▓ ▓█░▒▓▒▒ ░ ▒▒░ + ▒█ ░ ██▒░▒ ░█ ▓█▓░█ + █▓░█░ █▓░ ▓▒░ ░▒░▒░ + ▓ █░ ▓░██░░█▓░▒██▒▒▒██▒░▒ ▓░ + █▒▓▒█ ▓▓█▓▓▓░ ░█░▒▒█ ▒▓█▓▒░░▒░░ + █▒░ ░ ░░██ ███ ███▓▓▓█▓ + ██░ ▒█ ░ ▓▒█▒▓▓ + ▒▒▓▓█▒█ ██▓▓ █░█ + ▒▒██▒██▒▒▓▒▓█▓▒█▓░▒█ + ░███▒▓░▒▒▒▒░▓▓▒ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_30.txt b/codex-rs/tui/frames/blocks/frame_30.txt new file mode 100644 index 000000000000..9c27cf67d0fa --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_30.txt @@ -0,0 +1,17 @@ + + ▒▓ ████ + ▒▓▓░░▒██▒▒ + █▒░█▒▒░██▒ + ░░▒░▓░▒▒░▒ ▒█ + ▒█░░░▒░█░█ ░ + ░█░▒█ █░░░░▓░ + ▒▓░░░▒▒ ▒▓▒░ ▒░ + ░ ██▒░█░ ░▓ ░ + ░▒ ▒░▒░▒▓░█ ░ + ░░▒░▒▒░░ ██ ░ + ▒░░▓▒▒█░░░█░░ + ░█▓▓█▓█▒░░ ░ + ▒░▒░░▓█░░█░▓ + █▒██▒▒▓░█▓█ + ▒▓▓░▒▒▒▓█ + ░░░░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_31.txt b/codex-rs/tui/frames/blocks/frame_31.txt new file mode 100644 index 000000000000..c787451d71c9 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_31.txt @@ -0,0 +1,17 @@ + + ▒▓▓████▒█ + ▒██▓██▒ █▒███ + █░▒▓▓█▒▒░▓ ░▒█▒ + █░▓█▒▒█▓▒█▒▒░▒░░▒ + ▒░░░░█▓█▒▒█ ▒░▓▒▒ + ▓░▒░░▒░█ ▒▓██▓▓░█ ░ + ▓░░ ░▒█░▒▓▒▓▓█░█░▓░ + ▒▒█ ░░ ░▒ ░▒ ░░▒▓░ + ░▒█▒░█▒░░░▓█░░░▒ ░ + ░░░▓▓░░▒▒▒▒▒░▒░░ █ + ▒█▒▓█░█ ▓███░▓░█░▒ + ░░░▒▒▒█ ▒▒█ ░ + ▓░█▒▒ █ ▓ ░█░▓░ + ▓░▒░▓▒░░█░ █░░ + █ ▒░▒██▓▓▓█ + ░░░░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_32.txt b/codex-rs/tui/frames/blocks/frame_32.txt new file mode 100644 index 000000000000..e5e7adf64d40 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_32.txt @@ -0,0 +1,17 @@ + + █████▓▓░█▒ + ▓█░██░▒░░██░░█ + ▓▒█▒▒██▒▓▓░█░█▒███ + █▓▓░▒█░▓▓ ▓ █▒▒░██ █ + ▓▓░█░█▒██░▓ █░█░▒▓▒█▒█ + ▒▓▒▒█▒█░░▓░░█▒ ░█▓ █ + █░ ▓█░█▒░░██░█▒░▓▒▓▓░█▒ + ░░░█▒ ▒░░ ▓█░▓▓▒ ▒░ ░ + ▒░░▓▒ █▒░ ▒▒░███░░░▒░ ▒░ + █ ▒░░█▒█▒▒▒▒▒▒░░█░▓░▓▒ + █▒█░░▓ ░█ ███▒▓▓▓▓▓▓ + ▒█░▒▒▒ █▒░▓█░ + ███░░░█▒ ▒▓▒░▓ █ + ▒▓▒ ░█░▓▒█░▒█ ▒▓ + ░▓▒▒▒██▓█▒ + ░░░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_33.txt b/codex-rs/tui/frames/blocks/frame_33.txt new file mode 100644 index 000000000000..31a607b29cb0 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_33.txt @@ -0,0 +1,17 @@ + + ▒██▒█▒█▒░▓▒ + ▒██░░▒█▒░▓░▓░█░█▓ + ▒▓▒░████▒ ░ █▓░░█ █ + █▒▓░▓▒░█▒ █░░▒▒█ + ▒▓░▓░░░▓▒▒▒ ░█▒▒▒ + ▓▓█ ▒▒▒▒░▒█ ▓▒▓▒▒ + ░░█ ▒██░▒░▒ ░█░░ + █░██ ███▒▓▒█ ▒ ░█ + ░░░ ░ █░ ▓████▓▒▒█░░█▓▒░▒░ + ▒▓░█ ▓▓█▓░░░▒▒▒▒▒░░█▒▒▒░░▓ + ▒▒▒█ ░▓░▓ ▓ ███ ░░█▓▒░ + ▒█▒██ █ ▓▓▓▓▒▓ + █▒ ███▓█ ▒█░█▓█▒█ + ▒░ █▒█░█▓█▒ ▓█▒█░█ + ▒▒██▒▒▒▒██▓▓ + ░░░░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_34.txt b/codex-rs/tui/frames/blocks/frame_34.txt new file mode 100644 index 000000000000..db99cb73d619 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_34.txt @@ -0,0 +1,17 @@ + + ▒█▒████▒░█▒ + ▒███▓▒▓░ ░██▒██▓█▒▒ + ▒▓▓█░█ ▓░█░ ░▒▒▒█ ███ + █▓▒░█▒▓█▒ █░██▒▒ + ▓▓░▒▓▓░ ░ █ ▒▒█▒▒ + █▓▒░░▓ ▒▒ ░▒█▒ ▒█▒░▒ + ░█▒░▒ █▒▒█░▒▒ ░▓░▒ + ▒░▒ ▓ ░█▒░▓ ░ ▓ ▒▒ + ██▓▓ ▓▒▓▓ ▒▒▒██████░▒▒ ░▒░ + ░░▒█▓██▒ ▓▓█░░░▒░▓▒▒▒█▓▒░░░░▒ + ▓▒▒█ ░▒░█▒ ██░░░░▒ █▓█▒░█ + ▓█▒▓▒▒▒ ▓▓▓░▓█ + ▒█░░█▒▓█ ▒█▒ ▒▓█░ + ▓▒▓░ ░██▓██▒█▒█░██▓█ + ░▒▓▒▒▒▒▒▒▓▒█▒▒ + ░░░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_35.txt b/codex-rs/tui/frames/blocks/frame_35.txt new file mode 100644 index 000000000000..814188563de9 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_35.txt @@ -0,0 +1,17 @@ + + ▒██▓▒███▒██▒ + ██▒█▓░███ ░█░▓ ░█▒▒ + ▒▓▓░▓██░▒█ ░ ░ █▒█▓ ░██ + █▓▓█▓█▓█▒ ██▒▒░▒ + ▓▓░░▓▓▒ ▒██ ░▒█░█ + ▓▓▓▓█░ █░▒ ▓▓█▒ ░▒▒░ + ▒ ▓▓ ▒▒ ██▒▓ ░▒▒▒ + ░░░▓ ▓▒▒▓▓█ ▓ ▓ + ▓ █▒ █░░▓▓ ▓░▒▒▒▓▒▒█░░ ░░▒█ + ░█▒▓█ ▓▓▓ ██▓░▓ ▒█▒▒▒▒▓ ░▓█ ░█ + ▓░▒██▓▒▒░▓▒░ ░ ▒▒▒▒█▒▒█▓▓▒█░ + ▓▒▒▓░ ▒▓█ █▒ + ▒▓░▒▓█▓█ █▓▓▒███ + ▒▒ ░█░▓▓░░█░▓▓█ ▒▓▓ + ▒░▓▒▒▒▓▒▒███ ▒ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_36.txt b/codex-rs/tui/frames/blocks/frame_36.txt new file mode 100644 index 000000000000..cde83b56f419 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_36.txt @@ -0,0 +1,17 @@ + + ▒▒█▒████▒██▒ + ▒▒ ▒█▓▓▓█▒█▓██ ███▒ + █▒█▒███▓█ ░░ ░ █░██░██░█ + ▒░ ██▒▒▒▒ ██░▒ ░ + █▓▒▓▒█░▒░▒█▓ ▒▒▓█ + ▓ █▓░ █▒ ░▓█ ▒▒█ + ░ ▓ ░ ▒ ▒▒ ░▒░█ + ░░▒░ ▒▒ ▒▓▓ ▒░ ░ + ░█ ░ ▓▓ ██ ████▒█████▒ ░▒░░ + ▒█░▒ █░▒▒▓░▓ ░░▒▒▒▒▒▒▒░░ ▒▓█░ + █ █░▒ █▒█▓▒ ██▒▒▒▒▒ ░█ ▓ + ██ ▒▓▓ █▓░ ▓ + ▒▓░░█░█ ███ ▓█░ + ██▒ ██▒▒▓░▒█░▓ ▓ █▓██ + ░██▓░▒██▒██████ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_4.txt b/codex-rs/tui/frames/blocks/frame_4.txt new file mode 100644 index 000000000000..7ad27d16e743 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_4.txt @@ -0,0 +1,17 @@ + + ▒▓▒▓█▒▒█▒██▒ + ▒▓ ██░▓ ░▒▒▓█▓░ ▓██▒ + ██▒░░░██ ░ ░▒░▒▒█░▓▒▒▒ + ▓▓░█░ ▓██ ░██▒█▒ + ▓░▓▒░▒░▒▓▒░█ ▒ ▒░█▒ + ▓░░▒░ █▒░░▓█▒ █ ▒▒░█ + ▒░▓░ ███▒█ ░█ █ ▓░ + ░▓▒ █░▓█▒░░ ░░░ + ▒░ ░▒ ▓░▓ ▒▓▓█░███▒▒▒▒██ ░░█ + ░▒▓ ░ █▓▓▓█▒░░▒▒░█▓▒█▓▓▒▓░▓▓ ░ + ░░▓█▒█▒▒█▒▓ ████████▒▓░░░░ + █░▒ ░▒░ █▒▓▓███ + ▒▒█▓▒ █▒ ▒▓▒██▓░▓ + ░░░▒▒██▒▓▓▒▓██▒██▒░█░ + █▒▒░▓░▒▒▒▒▒▓▓█░ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_5.txt b/codex-rs/tui/frames/blocks/frame_5.txt new file mode 100644 index 000000000000..24f984395481 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_5.txt @@ -0,0 +1,17 @@ + + ▒▓▒▓▓█▒▒▒██▒ + ▒█ █▓█▓░░█░▒█▓▒░ ██ + █▒▓▒█░█ ░ ▒▒░█▒ ███ + █░▓░▓░▓▒█ ▓▒░░░░▒ + █▒▓█▓▒▒█░▒▒█ ░ ▒░▒░▒ + ░░░░▓ ▒▒░▒▓▓░▒ █▓░░ + ░▓░ █ ░▒▒░▒ ░█ ██░█░█ + ░▓░▒ █▒▒░▓▒░ █░▒░ + ░█░▒█ ▓▒░ █░█▒▒░█░▒▒▒██▒ ░▓░ + ▒▒░▒██▓██ ░ ▓▓▒▒▒█▒▓█▓░▓█░░ + ▒█░░█░█▒▒▓█░ ██ █░▓░▒▓ + ▒▒█▓▒▒ ░ ▓▒▓██▒ + ▒▓█▒░▒█▒ ▒▒████▓█ + ▒░█░███▒▓░▒▒██▒█▒░▓█ + ▒▓█▒█ ▒▒▒▓▒███░ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_6.txt b/codex-rs/tui/frames/blocks/frame_6.txt new file mode 100644 index 000000000000..fe185a75737c --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_6.txt @@ -0,0 +1,17 @@ + + ▒▓▒▓▓█▒▒██▒▒ + █▒▓▓█░▒██░██▓▒███▒ + ███░░░█ ░ ░▓▒███▓▒▒ + ▓█░█░█▒▒█ ▒█░░░░█ + █▒░░░█▒▒██▒ ▓▒▒░▒█ + ▓▓▓░▓░▒█▓░▒▒░█ ▓▒▒▓░ + ▒ █░░ ▒▒░▓▒▒ ▒█░▒░ + ░ ░░░ ▒░▒░▓░░ ░█▒░░ + ▒▓░▓░ ▓█░░█▓▓█▒░█░▒▒██▒▓▒▓░ + ░░▒█▓▒▒▒▓█ ░▓▒██░░█▓▒▒▒░█░▒ + ▓ ░ ▓░░░▓▓ █ ██ ░▒▒▓░ + █ ▓ ▓█░ █▓▒▓▓░░ + ▓░▒▒███ ▒█▒▒▓███ + ░ ░██ █ ▓░▒▒████ ▓▓█ + ▒▓▓███▒▒▒░▒███ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_7.txt b/codex-rs/tui/frames/blocks/frame_7.txt new file mode 100644 index 000000000000..7441f97e96e5 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_7.txt @@ -0,0 +1,17 @@ + + ▒▓░▓██▒▒██▒ + ██░█▒░███▒▒▒▓ ░██ + █ █░░░█░ ░▒░░ █▓▒██ + ▒▒░░░░▓█ ▒░▒█░▓█ + ░█░█░░▒░▓▒█ ▓ █░░▒ + ░ ▓░░ ░█▒▓░▒ █▓░░░ + ░▒ ░ ▒▒░▒░▒░ ██▒░░ + ▒ ▓░░ ▒█▓░█░░ █ ░░░ + ▓ ░█ █ ▒▓░▒▓░░▓▓▒░░▒▓█▒░░ + ░██░░▒▓░░▓█░▓▒░░▒▒█▒█▓▒░▒░ + ▒ ▒▒▓█░█▒▓ ██████ ▒▓░░ + █▒ ▓▒▓▒░ █ ▓▓▓▓█ + █▓██▒▒▒▒ █▒░██▓██ + ▒▒█▒░█▒▓░▒▒▒██░██▓ + ░█ ░▓░▒▒█▒▓██ + \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_8.txt b/codex-rs/tui/frames/blocks/frame_8.txt new file mode 100644 index 000000000000..ea88b095382b --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_8.txt @@ -0,0 +1,17 @@ + + ▒▒█▒▓██▒██▒ + █ █▓░░░█▒▒ ░ █ + ▒░▒█░▓▓█ █ ░▓░█▒█▒█ + ▒█▒█▓░██░ █ ▒▒░░▒ + █ ▓░▓█▒░▓▒ ▓█▒░░█ + ░██░▒▒▒▒▒░▒█ ▒█░░░ + ░█░░░ █▒▓▒░░░ ░▒░▓░█ + ▒█░░▓ ░█▒▓░██▓ ▓░▓░░ + ▒ ▒░░▒▒ ▓█▒░░▓█████▒░░░ + ▒█▓▒▒░ █░█░░▓░▒▒▒░░▒█ + ▓▓▒▒░▒░░░▓█▒█▒█ ▒█ ▓▒░ + ██ ░▒░░░ ▓█▓▓▓█ + █▒▒█▒▒▒▒ ▒▓▒▒░█▓█ + ▓▓█░██ ▓▓██▓▓▒█░░ + ░░▒██▒░▒██▓▒░ + ░░ \ No newline at end of file diff --git a/codex-rs/tui/frames/blocks/frame_9.txt b/codex-rs/tui/frames/blocks/frame_9.txt new file mode 100644 index 000000000000..9066ba1beda2 --- /dev/null +++ b/codex-rs/tui/frames/blocks/frame_9.txt @@ -0,0 +1,17 @@ + + ▓▒▒█▓██▒█ + ▓█▒▓░░█ ▒ ▒▓▒▒ + ▓ █░░▓█▒▒▒▓ ▒▒░█ + ░░▓▓▒▒ ▒▒█░▒▒░██ + ▓█ ▓▒█ ░██ █▓██▓█░░ + ░ ░░░ ▒░▒▓▒▒ ░█░█░░░ + ░ ░█▒░██░▒▒█ ▓█▓ ░░░ + ░ ░▓▒█▒░░░▒▓▒▒▒░ ░░ + █░ ▓░ ░░░░█░░█░░░ + ░▒░░░▒█░▒░▒░░░░▒▒░░░ + ░▒▓▒▒░▓ ████░░ ▓▒░ + ▒░░░▒█░ █▓ ▒▓░░ + ▒█▒░▒▒ ▓▓▒▓░▓█ + ▒▓ ▒▒░█▓█▒▓▓█░░ + █▓▒ █▒▒░▓█▓ + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_1.txt b/codex-rs/tui/frames/codex/frame_1.txt new file mode 100644 index 000000000000..63249f424211 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_1.txt @@ -0,0 +1,17 @@ + + eoeddccddcoe + edoocecocedxxde ecce + oxcxccccee eccecxdxxxc + dceeccooe ocxdxo + eedocexeeee coxeeo + xc ce xcodxxo coexxo + cecoc cexcocxe xox + xxexe oooxdxc cex + xdxce dxxeexcoxcccccceco dc x + exdc edce oc xcxeeeodoooxoooox + eeece eeoooe eecccc eccoodeo + ceo co e ococex + eeoeece edecxecc + ecoee ccdddddodcceoxc + ecccxxxeeeoedccc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_10.txt b/codex-rs/tui/frames/codex/frame_10.txt new file mode 100644 index 000000000000..fe5e51b98459 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_10.txt @@ -0,0 +1,17 @@ + + eccccecce + ccecccexoeco + eeoxxoxxoxceoo + xeeoexdeoeocceeo + o dxxcxe cooeoxo + xe cxcxooe eecx + e xcccxxxxc xoo + c xxecocxxoeeoexx + c xe eexdxxcecdxx + x oxeoxeoeceeexce + o cxxxxxcc eocexe + eecoeocc exccooo + xc xxxxcodooxoe + deccoxcde ooc + co eceeodc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_11.txt b/codex-rs/tui/frames/codex/frame_11.txt new file mode 100644 index 000000000000..48e507a84a1b --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_11.txt @@ -0,0 +1,17 @@ + + occcccce + oc dxxxeeo + oceexxdecoeo + xeexxddoedoo + ecodexcecdexxo + xcexxceddxeoxx + cc oxxxxxxexde + x xxoxxeo xcx + o cxoxxcocxex + cc exodocoxexe + ceo xxxxdoxeex + eeooxecoccdxe + e cxeeeexdc + ec cxxoeoce + ee cccece + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_12.txt b/codex-rs/tui/frames/codex/frame_12.txt new file mode 100644 index 000000000000..29de69516a39 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_12.txt @@ -0,0 +1,17 @@ + + ccccco + odeeoxoe + c xoeco + ocxxxddcx + x cxxxxoox + xcoocecexc + x xoexxe + x ocexxc + co xoxxcxx + x oxcdce + xo xcdcco + o cx eox + o ccxocex + ceocoxexe + e cxeoo + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_13.txt b/codex-rs/tui/frames/codex/frame_13.txt new file mode 100644 index 000000000000..67fe336a1374 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_13.txt @@ -0,0 +1,17 @@ + + occco + xeexx + xeexc + xccxe + c xx + cdoxx + o xx + c cx + oc exo + xc cdx + ceoo xe + xeeex + xcoxe + ceexd + o ocd + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_14.txt b/codex-rs/tui/frames/codex/frame_14.txt new file mode 100644 index 000000000000..f8d32cd6d19d --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_14.txt @@ -0,0 +1,17 @@ + + ccccd + ooeeoe + xexxo x + xxoxcexo + xxxe x + xcxx cx + xxxx o c + xxexe e + xxxx c + ceoo do + exccooox + xcxxeeex + o cxddde + xeoceeo + ec cdo + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_15.txt b/codex-rs/tui/frames/codex/frame_15.txt new file mode 100644 index 000000000000..2e14341237a2 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_15.txt @@ -0,0 +1,17 @@ + + cccccxe + eodxxedco + ooxcdexccx + xoe ooooeex + xxdcdexxocex + exxoxxoox c + xx xxxxxxox + xxoxxcxxx cox + xxcoocxxxeodx + xexdoxexco ox + xoxxxxex e d + xccoexxeo d + cxeo oooe de + xexxeeoceo + eeceeeeo + ee \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_16.txt b/codex-rs/tui/frames/codex/frame_16.txt new file mode 100644 index 000000000000..c90ce92cb6d0 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_16.txt @@ -0,0 +1,17 @@ + + edcccccxe + oexxcxxexde + xooceodexx ce + ooo dceexexxccx + xxdeoccdxxcoxee + xxxcxc xed x xox + eex oeoxxxxocco x + xod xexxoxxxcd ex + eexxxcxoexxccc o + cceeoddecxoex oex + xxxcccocexdcdoxxe + xxc xe eooo o + exc x oooeox + exxcecxoocex + cdoeddeedc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_17.txt b/codex-rs/tui/frames/codex/frame_17.txt new file mode 100644 index 000000000000..e1f2bb6d96c1 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_17.txt @@ -0,0 +1,17 @@ + + odcccddxoe + edccxxxcdcxoceo + oceoeddecocxxxece + oxoeoxcee cxdexxxde + xoe x xcoedeoo o + edcooe odox oodoxoo + c dox oooxe ccxxodx + ocdx ooxxoxoxxddc + oocoeddcxeexeedexxx x + xcedeexoceoxxe eccce + eeeoccccccceexcooe ec + exxec eoxxe d + eee cee ocooeeo + o xccdeceedcdxc + ecdoeocxcecc + e \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_18.txt b/codex-rs/tui/frames/codex/frame_18.txt new file mode 100644 index 000000000000..be64251770d9 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_18.txt @@ -0,0 +1,17 @@ + + eddcddcdxoe + eccedoccxeeoccdde + eodxcccdcocoeccooe c + oxxcooecc ceeeodxedeeeo + eeoo ox ecceeoxoxeedeee + oex ooxoeeeoocoxcooeoeox + xxedo cocoxceoccxdxdo + ceoxx eecxxde xdxc + ecc oedddddcxxoxcoeo xcxe + eeexcec xxoeeeexxxedxee o + xoxeeccccccce eeeoxocoeoe + ee oeo eeccocec + eecceeo eceeoeoe + cxoccccdddecceoeoc + cxxeoeeooccdcc + e \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_19.txt b/codex-rs/tui/frames/codex/frame_19.txt new file mode 100644 index 000000000000..89041571213b --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_19.txt @@ -0,0 +1,17 @@ + + eeddcxcddxoe + ecxxxeodddeceoxcoo + ocddocxcce ecdoecde + odxcoee eddcoexco + xxoeoe oxecocxe xeo + xeocc excxo oo cocx + edxxc oceoxcoe odocx + xxxx xdcexco x xxx + xcxeoddddddxxxxccdcxd e cxx + edooxdcoecceoeo ee deeeoooxe + cecocxcccccccc eeeoxoo ooc + eeecee eooeooc + c eexxco oddooxde + ccoxcoxceeddocc dcxc + cxoedoceooecoe + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_2.txt b/codex-rs/tui/frames/codex/frame_2.txt new file mode 100644 index 000000000000..a3c0663db464 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_2.txt @@ -0,0 +1,17 @@ + + eoeddcdddcoe + ecoocdcxxxdxxdecxcce + oxcxeccxcee eccdcoxxdxo + exoeoccooe ooexoxo + oecocexeeeee eoxexo + cocce xcecoec eexcx + oxccx eoxdxexo ocxcx + xc ee oxcxxdc xcoox + cccdx dxeeexcoxccccccccoxexxc + edcx oxxc oc xdeeeeeooeexco x + eee c ceooxc ecccccccccxocxx + ceeooo e ocdooc + oeeexco odec exc + exedeecccdddddodceexxc + eccccxxeeeexdocc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_20.txt b/codex-rs/tui/frames/codex/frame_20.txt new file mode 100644 index 000000000000..cea5393f7585 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_20.txt @@ -0,0 +1,17 @@ + + eecdxxdcdoee + oddcdoeodddxxeececo + oocecccxcc ecececcxce + excecxc eocxeocee + ex oxc eo exxecexxe + oeoxc cccdxco cexxe + dxdcx oc occe oexo + xeeoe ccddxco xxcx + xoxxdoddddddddeocdeeeec o xe + cxexec oeeeeeexe ceecxde oo xx + eoeecccccccccc eodxxox oe + c ecoo eocoxo + eeecoxe odcedcc + eooocxceddodcxceoocc + eccxe deeeexccc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_21.txt b/codex-rs/tui/frames/codex/frame_21.txt new file mode 100644 index 000000000000..efa6d610d9fb --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_21.txt @@ -0,0 +1,17 @@ + + eeodcddcdcoee + occeeeecxdxcdeeocce + dceeccece eexcceeco + ocxdcc eodcodco + oooce oxoee eeeeo + ocox occeoo eeeo + xcxe e oeooc edec + ee ed cxo x x + x x ocdddddccc exocxo do x + x xe xe eox ececxo ocoo + d co eeccc ce cceod oe o + cc dde ecc o + ce eoe eodcc oe + cde ccccdxxdddccc oe + cccdceeeeoedcce + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_22.txt b/codex-rs/tui/frames/codex/frame_22.txt new file mode 100644 index 000000000000..91c9c2ecaaec --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_22.txt @@ -0,0 +1,17 @@ + + eocdcddcdcoe + ecocxoeoxoexxdxcocce + odcdxecce ecceccceeco + dcxccc ooxxxece + exxoc oeoxdxoodcx + excoe oxoxdeoe exedx + xcceo xcxecoc xxox + xxdxe xexxee xexcx + xxoco cxddddddcceecxe eo exdc + exd ceeeo oocxoox ecdecxoo oed + eeeex cccccccce edcceooocoe + eceeeo ecocxoc + cccd cce eococceo + cdccoccxddcddodccccoc + cxcxedeeeodeodce + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_23.txt b/codex-rs/tui/frames/codex/frame_23.txt new file mode 100644 index 000000000000..5b5f1be139dd --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_23.txt @@ -0,0 +1,17 @@ + + eocedccccdcee + edxcxeeoeddoxexcxce + occxcodce cxdcxedxxo + odxcdoe eddexxde + ooxeoc ooooccocexe + oexcoe ecccccoccxxexo + exxcx odoo exe c xcc + xox x xcxoeeo x cox + ece xcxddddddddxecxecee x xxx + xeeexcdc oee exeeox eex + ocx x eccccccc ceoddxeoeoe + oxxexo ooxeeoe + e xocoee eocdcoco + edecdccexddecccoecce + cx cdexeeceecce + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_24.txt b/codex-rs/tui/frames/codex/frame_24.txt new file mode 100644 index 000000000000..c0269d8eda63 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_24.txt @@ -0,0 +1,17 @@ + + exedcccddoe + oceocxeexddcoecc + occdeoccx oedcxcco e + ocooooxdoeexoe ecexeec + o ooeoo eccoeexeeexoc + xoecee cooo oxd oce + x xx ooeoocoeexeexe + x exx xodoeexxeooexx + xo xddddccxxxccecoex x xx + e o cxoooddooxoeeccx xcx + e cexeccccccce eoocexdooe + e eoce x codo + eoceexo edceodec + oocoeecxxddddxeeoe + cdeccdeeeddcc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_25.txt b/codex-rs/tui/frames/codex/frame_25.txt new file mode 100644 index 000000000000..5b040665d0b7 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_25.txt @@ -0,0 +1,17 @@ + + ecdcdcccce + o coceedexcxxo + oxoooocoxcedexxxe + xccx o dx cexoceo + oeeeoocedoexc xooeoc + eoxxxeccoexd oxoxooxo + xoxcx xeeoeeoxeoecxdx + xxxoxoc xedeoxeexdxxe + ecexcxeeddddcxxeexccxe + oocxoxoxexxdcexecdoex + excoexecccccccoxexoxe + xecxdcdeoocdeooooxo + eeexeeecdooeoexxo + eodeeecdxcc cxc + xoccecoecxc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_26.txt b/codex-rs/tui/frames/codex/frame_26.txt new file mode 100644 index 000000000000..1592c09e8cfa --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_26.txt @@ -0,0 +1,17 @@ + + edccccco + ocxdoexcdxo + occcxdexecceo + dccoxxxexxecoe + xeoexoxcceodxed + e cxodocceeceeo + x ccdxxoxxddcc + oo exxxeedxxoxx + x oecdcxcddoexx + oexooxeeoceecx + xecoxcceooecexx + eexxoe oocxxe + c cxe eeoxoo + xcceecceccd + eodecxeec + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_27.txt b/codex-rs/tui/frames/codex/frame_27.txt new file mode 100644 index 000000000000..5279157c040e --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_27.txt @@ -0,0 +1,17 @@ + + dcccco + xddoxoe + dce cxocx + xxxexxdxx + x exeocd + xeoecexxxe + d cxxecxx + x exxxdcxx + xo o xcxxxx + cd ocexecxx + xo eecccoc + xxccxxeox + xddcdooxe + eeexedoo + cec eeo + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_28.txt b/codex-rs/tui/frames/codex/frame_28.txt new file mode 100644 index 000000000000..ea695865f4a8 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_28.txt @@ -0,0 +1,17 @@ + + occd + xcexe + d dxe + xoecx + x xx + x ocx + exx ex + xoccx + oe ex + xxodxx + x ex + xdcdx + xdcxx + ceeox + x ocx + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_29.txt b/codex-rs/tui/frames/codex/frame_29.txt new file mode 100644 index 000000000000..328d426a4159 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_29.txt @@ -0,0 +1,17 @@ + + ccccco + oxco ce + eoxx ccx + xxxxeeoo + e xcx x + xoxxx ee + xeexxx e + xxdxx + xxxcx e + exdxx e + cxxoxe d + xoxxx ex + xxxxexex + xdxcocxc + xxc oo + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_3.txt b/codex-rs/tui/frames/codex/frame_3.txt new file mode 100644 index 000000000000..3e9206577af5 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_3.txt @@ -0,0 +1,17 @@ + + eoddccddddoe + ecooexxcxcddxdeexcce + odocdxccce ecx cccoexo + ocoexdoce edc xxe + cocxoeoxxcee eeexxe + oxeeo ooxedee x eex + dc x ccexecxo ocoxo + ooxox ooxcoex xexdx + occx dxccxxcoxdcceeeccexecdx + oedeo oocoddx xcxeeo doodeexexe + cex x cxxcoc cccccccccoooooo + ccx ec e oeceoo + deooceo ocdocoxc + decoecceddddoddcdeecc + ecccedxeeeexdoec + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_30.txt b/codex-rs/tui/frames/codex/frame_30.txt new file mode 100644 index 000000000000..b9da98c5c378 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_30.txt @@ -0,0 +1,17 @@ + + edcccco + eodxxeccde + ccexoeexcoe + xxexoxeexe eo + dcxxeexoxo x + xcxec cxxxxox + eoxxxee eoex de + cx ccdxoxcxo e + cxecexdxeoxo e + cxxexeexx co e + exxdeecxxxcxx + xcoooocexxc x + exexxocxxoxo + oeocdeoxooc + eooxeeedc + eeee \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_31.txt b/codex-rs/tui/frames/codex/frame_31.txt new file mode 100644 index 000000000000..baef07474cb8 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_31.txt @@ -0,0 +1,17 @@ + + eodccccdo + eccdcoeccecco + oxeooodeeocxece + oxoceecdeoeexexxe + exxxxcoceeocexoee + dxeexexccedcoooxocx + oxx xecxeododcxcxox + eeo xxcxe xeccxxeox + xeoexcexxxocxxxe x + cxxxooxxeeeeexexx c + eceocxo occceoxcxe + xxxeeeo edc x + dxcde o o xceoe + dxexoexeoxcoxe + ccdxeccoodc + eeee \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_32.txt b/codex-rs/tui/frames/codex/frame_32.txt new file mode 100644 index 000000000000..c0997d9a140c --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_32.txt @@ -0,0 +1,17 @@ + + occccddxoe + dcxccxexxccxxo + oecdeocedoecxcecco + cooxeoedo o oeexco o + ooxoxceccxd ceoxeoeceo + eoeeoecxedxxce xco c + cxcdoecexxooxodeoeooxce + xxxoe cexxcocxdoecexcce + exxoe cexceexcccxxxdxcde + ccceexceceeeeeexxcxdxoe + oecxxo xccccccedooooo + eoxeee oexocx + cccxxxce eoexo o + eoecxcxddceecceo + xddeeococecc + eee \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_33.txt b/codex-rs/tui/frames/codex/frame_33.txt new file mode 100644 index 000000000000..cd8691c15021 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_33.txt @@ -0,0 +1,17 @@ + + eocdcdcdxoe + eccxxecdxdxoxcxco + eoexcccodce ccoxxcco + oeoxoexoe cxxeec + eoxoexxoeee xceee + ooo eeeeeeo oeoee + xxc eocxexe xcxx + cxoo occeodo ecxc + xxx x oe ocooodddcxxcoexex + eoxccodooexxeeeeexxceeexxo + edeo xoxo o ccccccc xxooee + ececoco oododo + ceccocdo ecxoocec + exccecxodcecdoecxc + cddcoeeeeccdoc + eeee \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_34.txt b/codex-rs/tui/frames/codex/frame_34.txt new file mode 100644 index 000000000000..ef8eabf7dc0b --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_34.txt @@ -0,0 +1,17 @@ + + eodccccdxoe + ecccoedxcxccdccdode + eoooxccdxce eeeeoccco + ooexceooe cxocee + ooxeoox xc o eecee + ooexeo eecxece eoexe + xcexe ceecxee xdxe + exdcd xcexocx o ee + ocooc oeooceddccccccxeec xee + xxeooooecoocxxxexoeeeooexxexe + oeeo xexce ccceeeee oooexc + ooeddee odoxoc + ecexcedo ecdceooe + oeoxcxcodocdcdceccdc + cxeddeeeeeddcde + eee \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_35.txt b/codex-rs/tui/frames/codex/frame_35.txt new file mode 100644 index 000000000000..1c53d2373f21 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_35.txt @@ -0,0 +1,17 @@ + + eocddcccdcoe + ocdcoxccccxcxdcxcde + eooxdccxecce eccdcocxco + ooocoodoe cceexe + ooxxooe ceco xecxo + dodoce cxecooce xeex + e oo ee cceo xeee + xxxd oeedoc o co + o oe oxxodcoxddededcxx xxdc + xoedc oodcccoxd eoeeeeocxoc xc + oeeocoeexoee eceeeeceecooeox + coeeox eoc oe + edeedodo odoeccc + ceecxcxodxxcxdocceodc + cexddeeoeecccce + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_36.txt b/codex-rs/tui/frames/codex/frame_36.txt new file mode 100644 index 000000000000..4928a2a9d071 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_36.txt @@ -0,0 +1,17 @@ + + eecdccccdcoe + edccecodocecdcccccce + oeceoccoccee eccxccxocxo + exccceeee ccxecx + ooedecxeeeoo eeoc + o ooe ce cxoo ceec + x d e e cee xexo + xxex ee eoo ex x + xccx oo occcocceccccce xexx + ecxe oxeeoxo exeeeeeeexx eoce + c cxe cecoe ccceeeeec xoco + cocedo ooxco + eoxxcxo occcooe + coecccdedxecxdcocodcc + eccoxeooeooccccc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_4.txt b/codex-rs/tui/frames/codex/frame_4.txt new file mode 100644 index 000000000000..a5ae50eeae4b --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_4.txt @@ -0,0 +1,17 @@ + + eoddcddcdcoe + eocccxo xedocdxcocoe + ocdxxxccce eexeecxoeee + ooxoxcoco cxccece + oxoexdxedexo ecexce + oxxex cexxoce c edxo + cexde ccceccxo o cdx + xoe oxocexx xxx + ex xe dxoceoocxccceeeeoo xxc + xeo x oooooexedexooeodoedxoocx + exdceoeeoeo ccccccccceoxxxe + oxecxee oedoccc + eeode oe eoeocdxo + xxxdecceddddocdccexce + ceexdxeeeeedoce + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_5.txt b/codex-rs/tui/frames/codex/frame_5.txt new file mode 100644 index 000000000000..47abf7a0af69 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_5.txt @@ -0,0 +1,17 @@ + + eodddcdddcoe + ecccocoxxcxdcdexcco + ceoecxcce cedxce oco + oxoxoxodo oexxxxe + oeocoeecxeeo e exexe + xxxxo eexedoxe coxx + xox c eeexecxo ccxcxo + xoxec oedxoex cxex + xoxec oexcoxcdexcxeeecoecxox + eexeoooccc xc ooedeodoooxocxe + eoxxoxoeeoce ccccccccoxdxeo + eecoee e oeocoe + eocexdce edcoccdc + excxoccedxdeocdcexdc + eocecceeeoeocce + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_6.txt b/codex-rs/tui/frames/codex/frame_6.txt new file mode 100644 index 000000000000..ba04c52772f3 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_6.txt @@ -0,0 +1,17 @@ + + eodddcddccee + oedocxdccxccdeocce + oooxxxcc ecxodcccoee + ooxcxcedo ecxxxxo + cdxxxceecce oeexeo + dooxoeecdxeexo odeox + e cxx eexoee ecxex + x xxx exdxoxx xcexx + eoxox ocxxcdocexcxeecceoeox + xxeooeeedc xodcoxxodddexoxe + o x oxxxdoc cccccccceeeox + o d ooe odeooxe + oeeecco eceeococ + ecxcocc dxeeoccc ddc + eodccoeeeeeocc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_7.txt b/codex-rs/tui/frames/codex/frame_7.txt new file mode 100644 index 000000000000..f7dd0de9b609 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_7.txt @@ -0,0 +1,17 @@ + + eoxdccddcoe + ocecexcccdded eco + c oxxxce eexe coeco + eexxxxdc execxoo + ecxcxxexoeo o cxxe + x dxxc ecedxe ooxxx + eecx eexexex ccexx + ecoxx dcoxcxe c xxx + ocxc oceoxeoxxddexxddcexx + xocxxddxxocxoexxeeododexex + e deocxceo cccccccceoxx + ce oeoee ocoodoc + cdoceeee oexococc + eecexcedxeeeccxcco + cxccxdxeeoedcc + \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_8.txt b/codex-rs/tui/frames/codex/frame_8.txt new file mode 100644 index 000000000000..e3f93702f726 --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_8.txt @@ -0,0 +1,17 @@ + + eecedccdcoe + occoxxxcdd x cc + exdoxooc ocxoxceceo + ececoxocx c dexxe + c oxooexoe ocexxo + xcoxeeeeexec ecxxx + xcxxx cedexex xexoxo + eoxxo xceoxoco oeoxx + e exxee ocdxxococooexxx + ecodexcoxoxxdxdeexxdc + ooeexexxxocececceccoex + cocxexee oooooc + ceeceeee eoeexcoc + odcxoc ddccdodoxe + xxeccexeocode + ee \ No newline at end of file diff --git a/codex-rs/tui/frames/codex/frame_9.txt b/codex-rs/tui/frames/codex/frame_9.txt new file mode 100644 index 000000000000..210e417d435c --- /dev/null +++ b/codex-rs/tui/frames/codex/frame_9.txt @@ -0,0 +1,17 @@ + + odecoccdo + oceoxxccd eoee + o oxxoceedo eexo + c xxodde eeoxeexco + occdeccxco coccdcxx + x xxxcexedee xcxcxxx + e xcexccxeeocooo exx + x xoeoexxxeodeex xx + coxc oxcxxxxcxxoxxe + xexxxeoxexexxxxeexxx + c eeoeexocccccxxcoex + exxxeoe oo eoxe + ecexee odedxoc + eoceexcocdddcxe + coe ceexdcoc + \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_1.txt b/codex-rs/tui/frames/default/frame_1.txt new file mode 100644 index 000000000000..64a140d2b9cf --- /dev/null +++ b/codex-rs/tui/frames/default/frame_1.txt @@ -0,0 +1,17 @@ +                                       +             _._:=++==+,_              +         _=,/*\+/+\=||=_ _"+_          +       ,|*|+**"^`   `"*`"~=~||+        +      ;*_\*',,_            /*|;|,      +     \^;/'^|\`\\            ".|\\,     +    ~* +`  |*/;||,           '.\||,    +   +^"-*    '\|*/"|_          ! |/|    +   ||_|`     ,//|;|*            "`|    +   |=~'`    ;||^\|".~++++++_+, =" |    +    _~;*  _;+` /* |"|___.:,,,|/,/,|    +    \^_"^ ^\,./`   `^*''* ^*"/,;_/     +     *^, ", `              ,'/*_|      +       ^\,`\+_          _=_+|_+"       +         ^*,\_!*+:;=;;.=*+_,|*         +           `*"*|~~___,_;+*"            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_10.txt b/codex-rs/tui/frames/default/frame_10.txt new file mode 100644 index 000000000000..9d45417346bf --- /dev/null +++ b/codex-rs/tui/frames/default/frame_10.txt @@ -0,0 +1,17 @@ +                                       +              _+***\++_                +             *'`+*+\~/_*,              +            ^_,||/~~-~+\,,             +           |__/\|;_.\,''\\,            +           / ;||"|^  /_/|/            +          |` '|*~//\   `_"|            +          \  ~*"*||~|*   |/,           +          "  ||\+/+||-_ .\||           +          "  ~\ \\|;~~+\+;||           +          |  ,|\,|_/_*___|*`           +           , "|||||""!\,"\|`           +           \`',\,*"  "",//            +            |' |||~*,:,/|/`            +             ;`**/|+;_!//'             +              *, _*\_,;*               +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_11.txt b/codex-rs/tui/frames/default/frame_11.txt new file mode 100644 index 000000000000..769e5ae76d77 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_11.txt @@ -0,0 +1,17 @@ +                                       +               ,****++_                +              /" ;|||\\,               +             /"__||;\*/\,              +             |__||=;,_=//              +            _".;\|+\';_||,             +            |+`||+_;;|_/||             +            ** ,||||||_|=\             +            |  ||/||\/ |"|             +            /  '|/||*/+|_|             +            ** _|/=,"/|_|^             +            '`- ||||=/|\\|             +             \_-/|_*/**;|`             +             !_ *|\\^_|;"              +              \+!*||,_/*`              +               \_ '*+_+`               +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_12.txt b/codex-rs/tui/frames/default/frame_12.txt new file mode 100644 index 000000000000..50cfd73302d8 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_12.txt @@ -0,0 +1,17 @@ +                                       +                +***+.                 +               ,=`_/|,\                +               "  |/\+,                +               /+~||=="|               +              | '~|||./|               +              |'..*^"_|"               +              |   ~/\||\               +              |   /+\||"               +              *, ~/||+|~               +              |   /|*;*_               +              |.  |"=**/               +               ,  *|!_,|               +               / **|,*\|               +               '^/",|\|`               +                \ '~\./                +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_13.txt b/codex-rs/tui/frames/default/frame_13.txt new file mode 100644 index 000000000000..04ed71335c19 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_13.txt @@ -0,0 +1,17 @@ +                                       +                 /***,                 +                 |__||                 +                 |`_|"                 +                 |**|_                 +                 *  ||                 +                 ":-||                 +                 ,  ||                 +                 +  "|                 +                /+  _~.                +                |"  +=|                +                '`.. ~`                +                 |___|                 +                 |+,|_                 +                 *__|=                 +                 , ."=                 +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_14.txt b/codex-rs/tui/frames/default/frame_14.txt new file mode 100644 index 000000000000..66e91f7187b1 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_14.txt @@ -0,0 +1,17 @@ +                                       +                 +***;                 +                 ,/__.\                +                |_||. |                +                ||/|"^~,               +                |||\   |               +                ~*||  '|               +                |||| . *               +                ||\|`  \               +                |||~   "               +                "^//  ;/               +                \|"",.,|               +                |*~|___|               +                /!"|===`               +                |\/*__/                +                 _* '=/                +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_15.txt b/codex-rs/tui/frames/default/frame_15.txt new file mode 100644 index 000000000000..9d8132e3c410 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_15.txt @@ -0,0 +1,17 @@ +                                       +                 ++***~_               +                `,=||^:*,              +               //|*=\|"*|              +               |/` //,.__|             +              ||="=\||/"^|             +              \||-||//|  "             +              ||   ||||~~,|            +              ||/~|+||| '-|            +              ||+,,*|||_.:|            +              |_|;/|\~*. .|            +              |/||||_| ` ;             +              |**.^~|\-  =             +              '|\, ///` ;`             +               |^||\\.+\/              +                \^*^___/               +                   ``                  \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_16.txt b/codex-rs/tui/frames/default/frame_16.txt new file mode 100644 index 000000000000..7217fe58b8e3 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_16.txt @@ -0,0 +1,17 @@ +                                       +                _=+"**+~_              +               /^||*||\|=\             +              |//"\/=\|| '\            +             /// ;' \|\||**|           +             ||;_ =||*/|`\           +            |||*|  /|= !| ~.|          +            \\|  ,||||/*", |          +            |/; |`||/|||"; `|          +            \\|~|+~/^||"*+  /          +            *"__,==\*|._| ,_|          +            |||+""/*\|;";.~|`          +             ||* |   `//,  /           +             \|*  |  /,/_,|            +              \|~"_*~//+_|             +               ':._=:__;*              +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_17.txt b/codex-rs/tui/frames/default/frame_17.txt new file mode 100644 index 000000000000..0d873df75182 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_17.txt @@ -0,0 +1,17 @@ +                                       +                ,=+++;;~,_             +             _;**|~~*=*|,"^,           +            ,*\/_==`+,"|||_"\          +           /|/_/|"   |;\~||=\         +           |/_ ~     "/\=\//  ,        +          `=*,/`   ,:/| /,=/|./        +          *!;/|   ,//|_ *"||/=|        +          -"=|!   !//||/ ,||=;*        +          ,/*/\==+~\_|\^:\||| |        +           |"_;__|/*\/||\!\+'+\        +          \\\/"""****\_|*//\ \'        +           \||_*       `/||` ;         +            _\\!*\_   ,',/^_/          +             , ~*+=\+`_;*:|'           +              `+;/_,+~*_+*             +                   `                   \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_18.txt b/codex-rs/tui/frames/default/frame_18.txt new file mode 100644 index 000000000000..a474a4f3d032 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_18.txt @@ -0,0 +1,17 @@ +                                       +               _==+==+;~,_             +            _+"_;,++~__,"+;;_          +          _/:|*"*=" "._"+//\ *         +         ,||*.,^" _/=~\;\\\,       +        _\// /|   _\/~/|_\;\\_       +        /\| ,, _/,*,|'-/^/`/~!      +        ||\:/     +/*/|"_/"*|=|=,      +        "\-~|     ^\"||;^   |;|"       +       \"" ,\==;=;+~|,|*/\, |*|`       +        _\\|*\* ~|/__\_~||_;~`\ ,      +        |/|\`""*****` \__/|/*/`-`      +         \\!,\,         ``*"/*_'       +          \^*+^^.      _*^_/\,`        +           '|.**++===^'*_/_,*          +             "~|_/__,.+";+"            +                    `                  \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_19.txt b/codex-rs/tui/frames/default/frame_19.txt new file mode 100644 index 000000000000..e83b78bd3ba7 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_19.txt @@ -0,0 +1,17 @@ +                                       +              __==+~+==~._             +           _+|||\/===_*_,|*,,          +         ,*;;/"|+*`    `*:,`*;\        +        /;|*,^`         _==+,^|*,      +       ||/`/`         ,|^"/"|\ |\,     +      |\/*'         _|*~/!./ '.*|      +      ^=|~'        /*^/|+,`   /:/+|    +      ||||         |;"\|",    | |||    +      |*|\,=;;===~~~|+*;*|;   \ "||    +      ^;/,|=*/^*+\,`, ^_ :\_\,/.|_     +      '\"/+|"""""**"   ^\_/|// //'     +       \\^*_\             `//_//'      +        '!_\~~*,        ,;=./|;`       +          '".|*/~+__=;/*" ;*~'         +             "~._:-'_,.^*-^            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_2.txt b/codex-rs/tui/frames/default/frame_2.txt new file mode 100644 index 000000000000..ac205dd4a51f --- /dev/null +++ b/codex-rs/tui/frames/default/frame_2.txt @@ -0,0 +1,17 @@ +                                       +             _._:=+===+,_              +         _+,/*;+||~=~|=_'|*+_          +       ,|*|\**~*``  `"*=*/||;|,        +     _|/_/*',,_           -,\|/|,      +     ,^"/*^|\_\\_           ^,|\|,     +    '/+"`  |*\+/\+           \\|*|     +   ,|'*|    ^/|;|_|,          /"|"|    +   |" \`     ,|*||;*          |'/.|    +   *""=|    ;|^^_|".~++++++++,|_|~*    +    _='|  /||' /* |=\____..__|+/!|!    +    \\\ * *\..|'   `"*******"|,*||     +     '\_./, `              ,+;/,*      +       .\__|+,          ,=_+!_|"       +        `~^;__"*+:;=;;.=*`_||*         +           `*+*+~~____~;/*"            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_20.txt b/codex-rs/tui/frames/default/frame_20.txt new file mode 100644 index 000000000000..bff8cc065f94 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_20.txt @@ -0,0 +1,17 @@ +                                       +              __+=~~=+=,__             +          ,;=";,_,===||_^*\+,          +        ,,"_+*"~*"    `"^"\+*|+_       +      _|"_*|*           _,+|\/*\\      +     _| ,|*           _/!_||^*\||\     +     /`,|'           +*';|"/  '\~|\    +    ;|;+|          ,* .+*^     ,\|/    +    |_^/`          "";:|",     |~"|    +    |/||;,=;======_,';^^\\*    / |\    +    '|^|_" /``____|\  *\\"|=\ ,/ ||    +     \,^\'"""""""*"     \,=||,| /\     +      * ^*/,               _/*.|/      +        \_^*,~_          ,;*`;*'       +          ^,-."~+^;;,:"~"`,/*'         +            `*+~_!=____|*""            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_21.txt b/codex-rs/tui/frames/default/frame_21.txt new file mode 100644 index 000000000000..b23aadbc7c70 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_21.txt @@ -0,0 +1,17 @@ +                                       +             __,=+==+=+,__             +          ,+*``__+~=~+;_`-*+_          +        ;*^_+*^"`     `^~*+_`*,        +      ,*|;"'             _,;*,;*,      +     /,/*`             ,|/_\ \\_^,     +    /"/|             ,**_//    \\^,    +    |'|`           _!/`,/'     \;\"    +     `\            \; "|,       | |    +    | |  ,+;;;;;+++  ^|,"|,    ;/ |    +    | ~\ |_      _,|   ^"`*|,  /"./    +     ; ". ``"""  '`     '*_,; /` ,     +     '+ :;_                 _*" /      +       *_  ^-_          _.;*' ,`       +         *=_ *"*+:~~;:=*""  -`         +            *++;+____,_;+*`            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_22.txt b/codex-rs/tui/frames/default/frame_22.txt new file mode 100644 index 000000000000..ccc8480d8b14 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_22.txt @@ -0,0 +1,17 @@ +                                       +             _,+=+==+=+,_              +         _+/*|._/|/\||;|+/*+_          +       ,;*;~\**`    `"*\**+__+,        +      ;*~**"            ,,|||_*\       +     \|~/'            ,_/|=|./;'|      +    \|*/`           ,~/|;^/` \|\=|     +   |+*\/           ~+|\*/'    ~|/|     +   ||=|`           |\|~^\     |^|"|    +   ||,", +~==;;;=++_\*|_!\,   _|;"!    +   ^|= *_\\,!/,"~//|  \*;_"|,,!/\=     +    \\\_| """""**"`    `:*+_///",`     +     \*\_\,               _*,"|,'      +      '"+; ++_         _.*,"*_/        +        ':*+,"*~;=+;;/=*""',*          +           "~"~_;___-=_.=*`            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_23.txt b/codex-rs/tui/frames/default/frame_23.txt new file mode 100644 index 000000000000..406ced01b086 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_23.txt @@ -0,0 +1,17 @@ +                                       +            _,+_=+*++=+__              +         _=|+|\_,_==,|_|*|+_           +       ,"+|',;*`    "~:+|\;||,         +      /;|*;/`          _;;\||;\        +     //|`/'          ,/,,'*/*\|\       +    ,\|"/`         _***''/*'|~\|,      +    `||"|         /:/. _|`  "!|'*      +    ~/| |         |"|,_\,   | */|      +    ^"\ |+~;=====;=|_*|_"\_ | |~|      +    |\\_|"="       /`\ \|_\,~ \_|      +     /'| | `"""""""   '`/;=|_/^/`      +      ,||_|.             ,/|\^/`       +       \ |,'/__       _.";*/+/         +         \=\+;*+\~==_++"-_+*`          +           "~!*=\~__+__**`             +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_24.txt b/codex-rs/tui/frames/default/frame_24.txt new file mode 100644 index 000000000000..73f563939029 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_24.txt @@ -0,0 +1,17 @@ +                                       +             _~_;++*==,_               +          ,"_/"|__~==+,_'+             +        ,"';\/+"~ .`:*~**, \           +       ,'//,/|=,\`~/`!_*\|\_'          +      , //\/,    `""/\_|``\|,'         +      ~,\+\`       *,,/!.|;!/"\        +     |  |~!      ,/_,/"/^\|\^|^        +     | \||       |,=/\_|~_/.`||        +     |. |;;;:++~~~++_*,_| |  ||        +      _ / !*|/,,;;,,|.^\+*| |*|        +      \ '\|\*""""""` \,.*\|=/,`        +       \ \,*\           |!"/;/         +        \.*\`|.      _="_/:_*          +          .-*,\^"~~:==;|^_/`           +            *:_*+;\__==+*              +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_25.txt b/codex-rs/tui/frames/default/frame_25.txt new file mode 100644 index 000000000000..6fb0cbc16cf7 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_25.txt @@ -0,0 +1,17 @@ +                                       +             _+=*;++++_                +           ,!*,*`_;\|*||,              +          /|//,,".|+\=\|||_            +         |+*| /! =| "\|,*\/            +        ,__\,/'^;/_|" |//\/*           +        \,~|~_*+.^|: /|,|//|,          +        |/|*| |__/\_/|\/_"|=|          +        |||/|."!~_=\/|\_~=||\          +       ^+\|"|__====+~|\\|+*|\          +        /-"|/|,|_||;*_|\*=/\|          +        \~*/\|`"""""'+/|\|/|`          +         |_"|;+;\-,*:_/,//|/           +          ^`^|_\_*;/,^/_||/            +           \.;\\_*=|**!*|*             +             ~,"*\+,_+|*               +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_26.txt b/codex-rs/tui/frames/default/frame_26.txt new file mode 100644 index 000000000000..8bd6052839db --- /dev/null +++ b/codex-rs/tui/frames/default/frame_26.txt @@ -0,0 +1,17 @@ +                                       +              _;***"+,                 +             /*|;/\|+;|,               +            /*""|;\|\""\,              +           ;*",||~_||_+/^              +           |_,\|.~"*\/;|\;             +           \ "|/:/"*_\"\\/             +          |!  *'=||/||;;"+             +          /-  \|||^^=||/||             +          |  .\*;+~+==/\||             +          ! ._|/,|__,*\\*|             +           |`"/|*"\,/`+\||             +           \_~|/\   //"||`             +            *  *|\ ^`/|/,              +             |"*\\"*_**;               +              \/:^*~_\*                +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_27.txt b/codex-rs/tui/frames/default/frame_27.txt new file mode 100644 index 000000000000..e8630695b8d3 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_27.txt @@ -0,0 +1,17 @@ +                                       +                ;***+,                 +               |;:/|/\                 +              ;'` *|/'|                +              |~~^||;|~                +              |  `|_-'=                +              ~_._"`|||`               +              = "||_*||!               +             |  `||~="||               +             |. .!|+||||               +             '= ."_|_*||               +              |- _^**+/'               +              ||++||_/|                +              |==+=,/|^                +               \__|\=//                +               '\" ^\/                 +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_28.txt b/codex-rs/tui/frames/default/frame_28.txt new file mode 100644 index 000000000000..3313d8b9bf76 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_28.txt @@ -0,0 +1,17 @@ +                                       +                 /**;                  +                 |+_|`                 +                 = ;|`                 +                 |-`*|                 +                 |  ~|                 +                 | -"|                 +                ^|~ _|                 +                 |-""|                 +                /\  _|                 +                ||.:~|                 +                 |  _|                 +                 |=+=|                 +                 |=*||                 +                 *__/|                 +                 ~ .+|                 +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_29.txt b/codex-rs/tui/frames/default/frame_29.txt new file mode 100644 index 000000000000..2ae088f1b905 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_29.txt @@ -0,0 +1,17 @@ +                                       +                 +****,                +                ,|*/!*\                +                ^,|| '"|               +                ||||^\,/               +                \ ~"|  |               +                |,~|| __               +                |\\||| ^               +                ||=~|                  +                ||~*|   `              +                _|=||   `              +                *||/|^ =               +                |/~~| _|               +                ~|||`~_|               +                |=|*/"|'               +                 ~|* .,                +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_3.txt b/codex-rs/tui/frames/default/frame_3.txt new file mode 100644 index 000000000000..727e25a8e89b --- /dev/null +++ b/codex-rs/tui/frames/default/frame_3.txt @@ -0,0 +1,17 @@ +                                       +             _.=;++====,_              +         _+,/\||+|"==|;_^|*+_          +       ,;/*;|*""`   `"~!**+/^|,        +      /+/\|;,+_          `=*!||\       +     '/*|/^/||*\_           \^\||_     +    /|\\/  .,|\;\\           | \\|     +    =* |    '*\|_"|,          .*/|,    +   ,-|,|     ,-|"/\|          ~_|=|    +    -"'|    ;|*+~|*-~=++___++_~^";|    +    ,\:\, ./*/;;| |*|__,!=.,;\`|\|`    +    '^| | "||+,"   "***"""**,///,/     +     '*~ \+ `              /^+^//      +       =^//*\,          ,+;/",|'       +         =^+,_**^=;=;,:=*;`_+"         +           `*+*_:~____~;/^"            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_30.txt b/codex-rs/tui/frames/default/frame_30.txt new file mode 100644 index 000000000000..99eeebce339d --- /dev/null +++ b/codex-rs/tui/frames/default/frame_30.txt @@ -0,0 +1,17 @@ +                                       +                 _;"**+,               +               _/;||\*'=\              +               "'^|,\\|+,\             +              ||\|/|_\|\ \,            +              =*||`\|,|,  |            +             |*|^+  *||||.|            +             \/|||\_ \/\| =`           +             "| '+=~,|"|-  `           +             "|_"\~=~\/|,  `           +             "||\|__~|!+,  `           +             !\||;\_*~||+~|            +              |*//,/*\||" |            +              \|\||/*~|,~/             +               ,^,+=^/|,/'             +                \-.|^__;'              +                  ````                 \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_31.txt b/codex-rs/tui/frames/default/frame_31.txt new file mode 100644 index 000000000000..8d9adf28b24a --- /dev/null +++ b/codex-rs/tui/frames/default/frame_31.txt @@ -0,0 +1,17 @@ +                                       +                _.:*+*+=,              +              _+*;+,_"+\'*,            +             ,|\//,=_`."|_*\           +            ,~/+__*;_,\\|\~|\          +            ^||||+-*\_,"\|/__          +           ;|^`~_|'"\;*,./|,"|         +           /|| |^*|\.=/;*|*|/|         +           \\, |~"|\ |^""|~\.|         +           |\,_|'^~|~/+~~|_  |         +           "||~//||___\_|\|| *         +           ^*_/+|, /***`/|'~_!         +            |||\\\,     _=* |          +             ;|*=_!,  . |*`/`          +              :|\|/_|`,|",|`           +               '"=~_+*/.;*             +                   ````                \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_32.txt b/codex-rs/tui/frames/default/frame_32.txt new file mode 100644 index 000000000000..4175a7a66ef7 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_32.txt @@ -0,0 +1,17 @@ +                                       +                ,++++;;~,_             +              ;*~**~\||**|~,           +            /^*=^,+^:-`*|*\'*,         +           '//|_,`;- - ,_\|+,!,        +          //|,|*\'*|; '`,~\/\*\,       +          \/\\,\*|`:||+_   |+/ *       +         *|";,`'\||,,|,=`/_//|'_       +         |||,_  "_||"/'|;-_"\|""`      +         \||-_ '_|"__|+++~~~=|"=`      +         '""_`|*\'\_____||*|;~-_       +           ,\*||/ |*""***^;///./       +           \,|^\\        ,\|/'~        +           '**||~+_    _/_|/ ,         +             \-\"~+|;=*`_+"_/          +               ~;=__,+/*_""            +                   ```                 \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_33.txt b/codex-rs/tui/frames/default/frame_33.txt new file mode 100644 index 000000000000..dbd9568018a1 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_33.txt @@ -0,0 +1,17 @@ +                                       +               _,+=+=+=~._             +            _+*||\*=~:|-|*|*.          +          _/\|+*+,="`  "*/||+",        +         ,\/|/_|,_        *||\_*       +        _.|/`||/\^\         |+\\^      +        //,   \_\\`\,        /\/_\     +        ||+ !  \,*|_|\       |*||      +        *|,,   ,''\/=,       \"|*      +        ||| | ,` /*,,,;==+~~+/_|\~     +        ^/|'"/:,/`~|_____||*^^^~|.     +        \=\, |/|/ / """"*** ||,/^`     +         \+\*,",           //;/=/      +          *\"*,*;,      _+|,/*\'       +            \~"*\+|,;+_";,\*~*         +              "==+,___^+*;-"           +                   ````                \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_34.txt b/codex-rs/tui/frames/default/frame_34.txt new file mode 100644 index 000000000000..7fc67a92dbcd --- /dev/null +++ b/codex-rs/tui/frames/default/frame_34.txt @@ -0,0 +1,17 @@ +                                       +               _,=++++=~,_             +           _+*+/\;|"~+*=**;,=_         +         _//,|*":~*`   `^\\,"**,       +        ,/_|*_/,_          *|,*\\      +       //|\-/|!|"!,          \\*\^     +      ,/\|`/ \\"|\*\          \,\|\    +      |*^~_   '\_*|_^          |;~_    +      \|=":    |*_|/"|         / _\    +      ,'/."   /\//"_==++++++~__" ~^`   +      ||\,/,,^"//'~||_~.___,/_||`|\    +       /_\, |\|*^!  "**````^ ,/,\|'    +        /,\;=\_             /;.|/'     +         \+`|+\;,        _+="_/,`      +           -\/~"|+,;,+=*=*`+*:'        +             "~\;=_____;=*=^           +                   ```                 \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_35.txt b/codex-rs/tui/frames/default/frame_35.txt new file mode 100644 index 000000000000..570f34f0de5d --- /dev/null +++ b/codex-rs/tui/frames/default/frame_35.txt @@ -0,0 +1,17 @@ +                                       +              _,+;=+++=+,_             +           ,*=*.~**+"|*~:"~*=_         +        _//|;+*|^*"`  `"*=*."|*,       +       ,//+/,;,_            *+_\|_     +      //~|//\ "\*,            |\*|,    +     ;/;/+` '|_"..*_           |\\|    +     \ !./    \\ '*_.           |^\\   +     ~|~:      /\\;/'           / "/   +     / ,_    ,||/;"/|==_;_=+~~  |~=*   +     |,^;'  //;"*+/|; \,____/"~/' |'   +      /`\,'/\_|/^`  `"^^^^*^^*//_,|    +       ".^\/~               _/* ,^     +        \;`^;,:,          ,;/_**'      +          "^_"~*~-:~~+~;/*"_/;"        +             "^~;=__/__++*"^           +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_36.txt b/codex-rs/tui/frames/default/frame_36.txt new file mode 100644 index 000000000000..74d83c8e7025 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_36.txt @@ -0,0 +1,17 @@ +                                       +              __+=++++=+,_             +          _=""\+/;/+\+;++"**+_         +        ,\'\,+*-*"`` `"*~*+|,*|,       +      _|"*+____            '*~\"|      +     ,/_;\'|\`\,.             ^\.*     +     / ,/`  *_ "|/,            "\^*    +    | ;!`     !\ "\\            |^|,   +    ||\~      _\ _//!           \| |   +    |'"|     // ,*"',++_+++++_  |\~|   +     _*|\  ,|__/~/ !`~_______|| \/'`   +     ' *|\ +_+/^     "**^^^^^" |,"/    +      ',"\;.                 ,/|"/     +        \/||+~,           ,++"/,`      +          *,_"**=^;~_+~;"-",;+'        +            `*+/~_,,_,,++**"           +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_4.txt b/codex-rs/tui/frames/default/frame_4.txt new file mode 100644 index 000000000000..06dbce99c076 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_4.txt @@ -0,0 +1,17 @@ +                                       +             _.=;+==+=+,_              +         _-"+*|/!|\=/*;|"/*,_          +       ,*=|||+*"`   `^~\^*|/\\_        +      //|,|".+,          "|**\*\       +     /|/_|=|\;^|,          \"\|*\      +    /||^|  '_||/*\          ' \=|,     +    "\|;`   '**\+"|,         , ";|     +     |.\     ,|/*^||           |||     +    _|!|_   ;|/"^//+~+++____,, ||*     +    |\/!| ,///,_|`=\|,._,:/^;|//"|     +     `|;'\,\\,\/   "*******'^-|||`     +      ,|\"|_`             ,^;/**'      +       \\,:^!,_        _.^,*;|/        +         ~||=_**\;;=;,+=*+\|*`         +            *\\~:~_____;-*`            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_5.txt b/codex-rs/tui/frames/default/frame_5.txt new file mode 100644 index 000000000000..6b1ce1244799 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_5.txt @@ -0,0 +1,17 @@ +                                       +             _.=;;+===+,_              +         _+"+/*/||+~=+;_|"+,           +        *_/\+|*"`   "^=|*\!,*,         +      ,|/|/|.=,          -^||||_       +     ,\/*/^_+|_\,         ` \|\|_      +     ~|||/ \_|\;/|_          +/||      +    |/|!+   `\_|\"|,        ''~+|,     +    |/|_"    ,_=|/_|          +|_|     +    |,|^*   /_|",|*=_~+~___+,_"|.|     +     _\|\,,/+*" |"!//\=^,=.,/|/*|`     +     \,||,~,\\/+`  "**""""",~;|\/      +      \\+/\\ `            /_/*,^       +       ^/*\|=+_        _=+,*';*        +         ^|*|,*+\;~=_,+=*^~;*          +            ^-*_*"___-_,+*`            +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_6.txt b/codex-rs/tui/frames/default/frame_6.txt new file mode 100644 index 000000000000..7724f483dc68 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_6.txt @@ -0,0 +1,17 @@ +                                       +             _.=;;+==++__              +          ,^;/*|=*+|++;_,*+_           +        ,,,|||*"   `"~.=*+*/\_         +       /,|*|*_=,        \+||||,        +      '=|||*\\+*\         .\\|\,       +     ;-/|/`^+;|\_|,        .=\/|       +     _ +~|   !^\|/^\        ^+|_|      +     ~ |~|   _|=~/||        ~+\||      +     _.|-|  /'||*;/+_~+~__++_.\/|      +     ||\,/_^_;+ |/=*,||,;==\|,|\!      +      / | /~||;/"  "*"""'*"`\\/|       +       , ; /,`           ,:_//|`       +        .`\_**,       _+^_/*,+         +         `"~*,"+!;~__,+**!:;'          +            ^-;*+,___`_,+*             +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_7.txt b/codex-rs/tui/frames/default/frame_7.txt new file mode 100644 index 000000000000..0d0f43072c61 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_7.txt @@ -0,0 +1,17 @@ +                                       +             _.~;*+==+,_               +          ,*`+\|+*+==\;!`*,            +         * ,|||*`  `^~`!*/_*,          +        \\|||~;+       ^~\*|/,         +       `'|*||^|/\,       . *||\        +      | ;||" `'\;|\       ,-|||        +       `\"|  ^_|\|\|       **^||       +      _"/~|   =+/|*|`      ' |||       +       /"~* ,"_/|\/~~;;_~~=;*\||       +      |,'||=:~|/'|.\||\\,=,;\|\|       +       \ =^/*|*_/  ******""_/||        +       '_ /_/_`         ,"-/;/'        +        ';,+\\\_      ,^~,*/*'         +          \_'\|*\;~___+*|*+/           +            "~*"~:~__,_;**             +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_8.txt b/codex-rs/tui/frames/default/frame_8.txt new file mode 100644 index 000000000000..2e8019c06126 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_8.txt @@ -0,0 +1,17 @@ +                                       +             __+_;++=+,_               +           ,"*/|||*==!~ "+             +         _|=,|//*!,"~/~*\+^,           +        _*\*/|,+|     ' =^||\          +        ' /|/,\|/\      .'\||,         +       |',|\^^_\|_*      \+|||         +       |*||| '_;\|`|      ^|/|,        +       \,||/  |+\/|,*.    .`/||        +       \ \||_^ !/*=|~/+,+,,\~||        +       !  \*/=_|",|,||;|=__||='        +        //\\|\|||/'^*^*"\*"/\|         +        ',"|\|``        .,///'         +         '\_*\\\_    _/\_|+/*          +           .:'|,*!;;+*;/=,|`           +             ~~_**\|_,+/=`             +                  ``                   \ No newline at end of file diff --git a/codex-rs/tui/frames/default/frame_9.txt b/codex-rs/tui/frames/default/frame_9.txt new file mode 100644 index 000000000000..128e91500789 --- /dev/null +++ b/codex-rs/tui/frames/default/frame_9.txt @@ -0,0 +1,17 @@ +                                       +              .=^*/++=,                +            /*_/||*"=!_-\_             +           / ,||/*^^=/!\_~,            +          " ||/;=_ _^,|\^|+,           +         /*";\*"|*,  +:+||           +         | |||"^|\;*   '|*|||          +         ` ~*\ **|\\," / `||          +        |  ~/_ ~||_/= | !||          +        !  ",|" /|"|~~|+|~,||`         +         |_|||_,|^|_||||__|||          +         " `^/\\|/"****||"/\|          +          \~||\,`     ,/ ^/|`          +           \+\|\\    /;_;|/*           +            ^-"\_|*/+=;;*|`            +             '-_ *\\|;+/"              +                                       \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_1.txt b/codex-rs/tui/frames/dots/frame_1.txt new file mode 100644 index 000000000000..36964a48647b --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_1.txt @@ -0,0 +1,17 @@ + + ○◉○◉○●●○○●●○ + ○○●◉●○●◉●○○··○○ ○ ●○ + ●·●·●●● ○· · ●· ·○···● + ◉●○○●●●●○ ◉●·◉·● + ○○◉◉●○·○·○○ ◉·○○● + ·● ●· ·●◉◉··● ●◉○··● + ●○ ◉● ●○·●◉ ·○ ·◉· + ··○·· ●◉◉·◉·● ·· + ·○·●· ◉··○○· ◉·●●●●●●○●● ○ · + ○·◉● ○◉●· ◉● · ·○○○◉◉●●●·◉●◉●· + ○○○ ○ ○○●◉◉· ·○●●●● ○● ◉●◉○◉ + ●○● ● · ●●◉●○· + ○○●·○●○ ○○○●·○● + ○●●○○ ●●◉◉○◉◉◉○●●○●·● + ·● ●···○○○●○◉●● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_10.txt b/codex-rs/tui/frames/dots/frame_10.txt new file mode 100644 index 000000000000..3c687d7f64ff --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_10.txt @@ -0,0 +1,17 @@ + + ○●●●●○●●○ + ●●·●●●○·◉○●● + ○○●··◉··◉·●○●● + ·○○◉○·◉○◉○●●●○○● + ◉ ◉·· ·○ ●●◉○◉·◉ + ·· ●·●·◉◉○ ·○ · + ○ ·● ●····● ·◉● + ··○●◉●··◉○·◉○·· + ·○ ○○·◉··●○●◉·· + · ●·○●·○◉○●○○○·●· + ● ····· ○● ○·· + ○·●●○●● ○· ●◉◉ + ·● ····●●◉●◉·◉· + ◉·●●◉·●◉○ ◉◉● + ●● ○●○○●◉● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_11.txt b/codex-rs/tui/frames/dots/frame_11.txt new file mode 100644 index 000000000000..c2548db4b3c4 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_11.txt @@ -0,0 +1,17 @@ + + ●●●●●●●○ + ◉ ◉···○○● + ◉ ○○··◉○●◉○● + ·○○··○◉●○○◉◉ + ○ ◉◉○·●○●◉○··● + ·●···●○◉◉·○◉·· + ●● ●······○·○○ + · ··◉··○◉ · · + ◉ ●·◉··●◉●·○· + ●● ○·◉○● ◉·○·○ + ●·◉ ····○◉·○○· + ○○◉◉·○●◉●●◉·· + ○ ●·○○○○·◉ + ○● ●··●○◉●· + ○○ ●●●○●· + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_12.txt b/codex-rs/tui/frames/dots/frame_12.txt new file mode 100644 index 000000000000..30b03392bf4d --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_12.txt @@ -0,0 +1,17 @@ + + ●●●●●◉ + ●○·○◉·●○ + ·◉○●● + ◉●···○○ · + · ●····◉◉· + ·●◉◉●○ ○· + · ·◉○··○ + · ◉●○·· + ●● ·◉··●·· + · ◉·●◉●○ + ·◉ · ○●●◉ + ● ●· ○●· + ◉ ●●·●●○· + ●○◉ ●·○·· + ○ ●·○◉◉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_13.txt b/codex-rs/tui/frames/dots/frame_13.txt new file mode 100644 index 000000000000..cb95f3763d3c --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_13.txt @@ -0,0 +1,17 @@ + + ◉●●●● + ·○○·· + ··○· + ·●●·○ + ● ·· + ◉◉·· + ● ·· + ● · + ◉● ○·◉ + · ●○· + ●·◉◉ ·· + ·○○○· + ·●●·○ + ●○○·○ + ● ◉ ○ + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_14.txt b/codex-rs/tui/frames/dots/frame_14.txt new file mode 100644 index 000000000000..3a8ed60b8ff1 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_14.txt @@ -0,0 +1,17 @@ + + ●●●●◉ + ●◉○○◉○ + ·○··◉ · + ··◉· ○·● + ···○ · + ·●·· ●· + ···· ◉ ● + ··○·· ○ + ···· + ○◉◉ ◉◉ + ○· ●◉●· + ·●··○○○· + ◉ ·○○○· + ·○◉●○○◉ + ○● ●○◉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_15.txt b/codex-rs/tui/frames/dots/frame_15.txt new file mode 100644 index 000000000000..c57b4af0ee5d --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_15.txt @@ -0,0 +1,17 @@ + + ●●●●●·○ + ·●○··○◉●● + ◉◉·●○○· ●· + ·◉· ◉◉●◉○○· + ··○ ○○··◉ ○· + ○··◉··◉◉· + ·· ······●· + ··◉··●··· ●◉· + ··●●●●···○◉◉· + ·○·◉◉·○·●◉ ◉· + ·◉····○· · ◉ + ·●●◉○··○◉ ○ + ●·○● ◉◉◉· ◉· + ·○··○○◉●○◉ + ○○●○○○○◉ + ·· \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_16.txt b/codex-rs/tui/frames/dots/frame_16.txt new file mode 100644 index 000000000000..18ae0e09ee31 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_16.txt @@ -0,0 +1,17 @@ + + ○○● ●●●·○ + ◉○··●··○·○○ + ·◉◉ ○◉○○·· ●○ + ◉◉◉ ◉●○○·○··●●· + ··◉○◉●●○··●◉··○ + ···●·● ·○○ · ·◉· + ○○· ◉·●····◉● ● · + ·◉◉ ····◉··· ◉ ·· + ○○···●·◉○·· ●● ◉ + ● ○○●○○○●·◉○· ●○· + ···● ◉●○·◉ ◉◉··· + ··● ·· ·◉◉● ◉ + ○·● · ◉●◉○●· + ○·· ○●·◉◉●○· + ●◉◉○○◉○○◉● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_17.txt b/codex-rs/tui/frames/dots/frame_17.txt new file mode 100644 index 000000000000..a470b4ba8df4 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_17.txt @@ -0,0 +1,17 @@ + + ●○●●●◉◉·●○ + ○◉●●···●○●·● ○● + ●●○◉○○○·●● ···○ ○ + ◉·◉○◉· ○· ●·◉○···○○ + ·◉○ · · ◉○○○◉◉ ● + ·○●●◉· ●◉◉· ◉●○◉·◉◉ + ● ◉◉· ●◉◉·○ ● ··◉○· + ◉ ○· ◉◉··◉·●··○◉● + ●◉●◉○○○●·○○·○○◉○··· · + · ○◉○○·◉●○◉··○ ○●●●○ + ○○○◉ ●●●●○○·●◉◉○ ○● + ○··○● ·◉··· ◉ + ○○○ ●○○ ●●●◉○○◉ + ● ·●●○○●·○◉●◉·● + ·●◉◉○●●·●○●● + · \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_18.txt b/codex-rs/tui/frames/dots/frame_18.txt new file mode 100644 index 000000000000..c0354b393310 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_18.txt @@ -0,0 +1,17 @@ + + ○○○●○○●◉·●○ + ○● ○◉●●●·○○● ●◉◉○ + ○◉◉·● ●○ ● ◉○ ●◉◉○ ● + ●··●◉●○ ● ●○·○◉○·○◉○○○● + ○○◉◉ ◉· ○ ●○○◉·◉·○○◉○○○ + ◉○· ●●·◉○○○◉●●●·●◉◉○◉·◉· + ··○◉◉ ●◉●◉· ○◉ ●·○·○● + ○◉·· ○○ ··◉○ ·◉· + ○ ●○○○◉○◉●··●·●◉○● ·●·· + ○○○·●○● ··◉○○○○···○◉··○ ● + ·◉·○· ●●●●●· ○○○◉·◉●◉·◉· + ○○ ●○● ··● ◉●○● + ○○●●○○◉ ○●○○◉○●· + ●·◉●●●●○○○○●●○◉○●● + ··○◉○○●◉● ◉● + · \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_19.txt b/codex-rs/tui/frames/dots/frame_19.txt new file mode 100644 index 000000000000..c9ded5683883 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_19.txt @@ -0,0 +1,17 @@ + + ○○○○●·●○○·◉○ + ○●···○◉○○○○●○●·●●● + ●●◉◉◉ ·●●· ·●◉●·●◉○ + ◉◉·●●○· ○○○●●○·●● + ··◉·◉· ●·○ ◉ ·○ ·○● + ·○◉●● ○·●·◉ ◉◉ ●◉●· + ○○··● ◉●○◉·●●· ◉◉◉●· + ···· ·◉ ○· ● · ··· + ·●·○●○◉◉○○○····●●◉●·◉ ○ ·· + ○◉◉●·○●◉○●●○●·● ○○ ◉○○○●◉◉·○ + ●○ ◉●· ●● ○○○◉·◉◉ ◉◉● + ○○○●○○ ·◉◉○◉◉● + ● ○○··●● ●◉○◉◉·◉· + ● ◉·●◉·●○○○◉◉● ◉●·● + ·◉○◉◉●○●◉○●◉○ + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_2.txt b/codex-rs/tui/frames/dots/frame_2.txt new file mode 100644 index 000000000000..6e7a27fb2941 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_2.txt @@ -0,0 +1,17 @@ + + ○◉○◉○●○○○●●○ + ○●●◉●◉●···○··○○●·●●○ + ●·●·○●●·●·· · ●○●◉··◉·● + ○·◉○◉●●●●○ ◉●○·◉·● + ●○ ◉●○·○○○○○ ○●·○·● + ●◉● · ·●○●◉○● ○○·●· + ●·●●· ○◉·◉·○·● ◉ · · + · ○· ●·●··◉● ·●◉◉· + ● ○· ◉·○○○· ◉·●●●●●●●●●·○··● + ○○●· ◉··● ◉● ·○○○○○○◉◉○○·●◉ · + ○○○ ● ●○◉◉·● · ●●●●●●● ·●●·· + ●○○◉◉● · ●●◉◉●● + ◉○○○·●● ●○○● ○· + ··○◉○○ ●●◉◉○◉◉◉○●·○··● + ·●●●●··○○○○·◉◉● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_20.txt b/codex-rs/tui/frames/dots/frame_20.txt new file mode 100644 index 000000000000..d9809e733cce --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_20.txt @@ -0,0 +1,17 @@ + + ○○●○··○●○●○○ + ●◉○ ◉●○●○○○··○○●○●● + ●● ○●● ·● · ○ ○●●·●○ + ○· ○●·● ○●●·○◉●○○ + ○· ●·● ○◉ ○··○●○··○ + ◉·●·● ●●●◉· ◉ ●○··○ + ◉·◉●· ●● ◉●●○ ●○·◉ + ·○○◉· ◉◉· ● ·· · + ·◉··◉●○◉○○○○○○○●●◉○○○○● ◉ ·○ + ●·○·○ ◉··○○○○·○ ●○○ ·○○ ●◉ ·· + ○●○○● ● ○●○··●· ◉○ + ● ○●◉● ○◉●◉·◉ + ○○○●●·○ ●◉●·◉●● + ○●◉◉ ·●○◉◉●◉ · ·●◉●● + ·●●·○ ○○○○○·● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_21.txt b/codex-rs/tui/frames/dots/frame_21.txt new file mode 100644 index 000000000000..0821f12d752a --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_21.txt @@ -0,0 +1,17 @@ + + ○○●○●○○●○●●○○ + ●●●··○○●·○·●◉○·◉●●○ + ◉●○○●●○ · ·○·●●○·●● + ●●·◉ ● ○●◉●●◉●● + ◉●◉●· ●·◉○○ ○○○○● + ◉ ◉· ●●●○◉◉ ○○○● + ·●·· ○ ◉·●◉● ○◉○ + ·○ ○◉ ·● · · + · · ●●◉◉◉◉◉●●● ○·● ·● ◉◉ · + · ·○ ·○ ○●· ○ ·●·● ◉ ◉◉ + ◉ ◉ ·· ●· ●●○●◉ ◉· ● + ●● ◉◉○ ○● ◉ + ●○ ○◉○ ○◉◉●● ●· + ●○○ ● ●●◉··◉◉○● ◉· + ●●●◉●○○○○●○◉●●· + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_22.txt b/codex-rs/tui/frames/dots/frame_22.txt new file mode 100644 index 000000000000..d67334980197 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_22.txt @@ -0,0 +1,17 @@ + + ○●●○●○○●○●●○ + ○●◉●·◉○◉·◉○··◉·●◉●●○ + ●◉●◉·○●●· · ●○●●●○○●● + ◉●·●● ●●···○●○ + ○··◉● ●○◉·○·◉◉◉●· + ○·●◉· ●·◉·◉○◉· ○·○○· + ·●●○◉ ·●·○●◉● ··◉· + ··○·· ·○··○○ ·○· · + ··● ● ●·○○◉◉◉○●●○○●·○ ○● ○·◉ + ○·○ ●○○○● ◉● ·◉◉· ○●◉○ ·●● ◉○○ + ○○○○· ●● · ·◉●●○◉◉◉ ●· + ○●○○○● ○●● ·●● + ● ●◉ ●●○ ○◉●● ●○◉ + ●◉●●● ●·◉○●◉◉◉○● ●●● + · ·○◉○○○◉○○◉○●· + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_23.txt b/codex-rs/tui/frames/dots/frame_23.txt new file mode 100644 index 000000000000..180ab1678424 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_23.txt @@ -0,0 +1,17 @@ + + ○●●○○●●●●○●○○ + ○○·●·○○●○○○●·○·●·●○ + ● ●·●●◉●· ·◉●·○◉··● + ◉◉·●◉◉· ○◉◉○··◉○ + ◉◉··◉● ●◉●●●●◉●○·○ + ●○· ◉· ○●●●●●◉●●··○·● + ··· · ◉◉◉◉ ○·· ·●● + ·◉· · · ·●○○● · ●◉· + ○ ○ ·●·◉○○○○○◉○·○●·○ ○○ · ··· + ·○○○· ○ ◉·○ ○·○○●· ○○· + ◉●· · · ●·◉◉○·○◉○◉· + ●··○·◉ ●◉·○○◉· + ○ ·●●◉○○ ○◉ ◉●◉●◉ + ○○○●◉●●○·○○○●● ◉○●●· + · ●○○·○○●○○●●· + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_24.txt b/codex-rs/tui/frames/dots/frame_24.txt new file mode 100644 index 000000000000..3244b1c6f925 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_24.txt @@ -0,0 +1,17 @@ + + ○·○◉●●●○○●○ + ● ○◉ ·○○·○○●●○●● + ● ●◉○◉● · ◉·◉●·●●● ○ + ●●◉◉●◉·○●○··◉· ○●○·○○● + ● ◉◉○◉● · ◉○○···○·●● + ·●○●○· ●●●◉ ◉·◉ ◉ ○ + · ·· ●◉○●◉ ◉○○·○○·○ + · ○·· ·●○◉○○··○◉◉··· + ·◉ ·◉◉◉◉●●···●●○●●○· · ·· + ○ ◉ ●·◉●●◉◉●●·◉○○●●· ·●· + ○ ●○·○● · ○●◉●○·○◉●· + ○ ○●●○ · ◉◉◉ + ○◉●○··◉ ○○ ○◉◉○● + ◉◉●●○○ ··◉○○◉·○○◉· + ●◉○●●◉○○○○○●● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_25.txt b/codex-rs/tui/frames/dots/frame_25.txt new file mode 100644 index 000000000000..c04ef18b74fa --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_25.txt @@ -0,0 +1,17 @@ + + ○●○●◉●●●●○ + ● ●●●·○◉○·●··● + ◉·◉◉●● ◉·●○○○···○ + ·●●· ◉ ○· ○·●●○◉ + ●○○○●◉●○◉◉○· ·◉◉○◉● + ○●···○●●◉○·◉ ◉·●·◉◉·● + ·◉·●· ·○○◉○○◉·○◉○ ·○· + ···◉·◉ ·○○○◉·○○·○··○ + ○●○· ·○○○○○○●··○○·●●·○ + ◉◉ ·◉·●·○··◉●○·○●○◉○· + ○·●◉○·· ●●◉·○·◉·· + ·○ ·◉●◉○◉●●◉○◉●◉◉·◉ + ○·○·○○○●◉◉●○◉○··◉ + ○◉◉○○○●○·●● ●·● + ·● ●○●●○●·● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_26.txt b/codex-rs/tui/frames/dots/frame_26.txt new file mode 100644 index 000000000000..1ecc43beef26 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_26.txt @@ -0,0 +1,17 @@ + + ○◉●●● ●● + ◉●·◉◉○·●◉·● + ◉● ·◉○·○ ○● + ◉● ●···○··○●◉○ + ·○●○·◉· ●○◉◉·○◉ + ○ ·◉◉◉ ●○○ ○○◉ + · ●●○··◉··◉◉ ● + ◉◉ ○···○○○··◉·· + · ◉○●◉●·●○○◉○·· + ◉○·◉●·○○●●○○●· + ·· ◉·● ○●◉·●○·· + ○○··◉○ ◉◉ ··· + ● ●·○ ○·◉·◉● + · ●○○ ●○●●◉ + ○◉◉○●·○○● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_27.txt b/codex-rs/tui/frames/dots/frame_27.txt new file mode 100644 index 000000000000..83e62da52e22 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_27.txt @@ -0,0 +1,17 @@ + + ◉●●●●● + ·◉◉◉·◉○ + ◉●· ●·◉●· + ···○··◉·· + · ··○◉●○ + ·○◉○ ····· + ○ ··○●·· + · ····○ ·· + ·◉ ◉ ·●···· + ●○ ◉ ○·○●·· + ·◉ ○○●●●◉● + ··●●··○◉· + ·○○●○●◉·○ + ○○○·○○◉◉ + ●○ ○○◉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_28.txt b/codex-rs/tui/frames/dots/frame_28.txt new file mode 100644 index 000000000000..6d460c936de0 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_28.txt @@ -0,0 +1,17 @@ + + ◉●●◉ + ·●○·· + ○ ◉·· + ·◉·●· + · ·· + · ◉ · + ○·· ○· + ·◉ · + ◉○ ○· + ··◉◉·· + · ○· + ·○●○· + ·○●·· + ●○○◉· + · ◉●· + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_29.txt b/codex-rs/tui/frames/dots/frame_29.txt new file mode 100644 index 000000000000..d0d6b3c286dc --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_29.txt @@ -0,0 +1,17 @@ + + ●●●●●● + ●·●◉ ●○ + ○●·· ● · + ····○○●◉ + ○ · · · + ·●··· ○○ + ·○○··· ○ + ··○·· + ···●· · + ○·○·· · + ●··◉·○ ○ + ·◉··· ○· + ······○· + ·○·●◉ ·● + ··● ◉● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_3.txt b/codex-rs/tui/frames/dots/frame_3.txt new file mode 100644 index 000000000000..062da3ed89fc --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_3.txt @@ -0,0 +1,17 @@ + + ○◉○◉●●○○○○●○ + ○●●◉○··●· ○○·◉○○·●●○ + ●◉◉●◉·● · · · ●●●◉○·● + ◉●◉○·◉●●○ ·○● ··○ + ●◉●·◉○◉··●○○ ○○○··○ + ◉·○○◉ ◉●·○◉○○ · ○○· + ○● · ●●○·○ ·● ◉●◉·● + ●◉·●· ●◉· ◉○· ·○·○· + ◉ ●· ◉·●●··●◉·○●●○○○●●○·○ ◉· + ●○◉○● ◉◉●◉◉◉· ·●·○○● ○◉●◉○··○·· + ●○· · ··●● ●●● ●●●◉◉◉●◉ + ●●· ○● · ◉○●○◉◉ + ○○◉◉●○● ●●◉◉ ●·● + ○○●●○●●○○◉○◉●◉○●◉·○● + ·●●●○◉·○○○○·◉◉○ + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_30.txt b/codex-rs/tui/frames/dots/frame_30.txt new file mode 100644 index 000000000000..4bf02ade3d83 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_30.txt @@ -0,0 +1,17 @@ + + ○◉ ●●●● + ○◉◉··○●●○○ + ●○·●○○·●●○ + ··○·◉·○○·○ ○● + ○●···○·●·● · + ·●·○● ●····◉· + ○◉···○○ ○◉○· ○· + · ●●○·●· ·◉ · + ·○ ○·○·○◉·● · + ··○·○○·· ●● · + ○··◉○○●···●·· + ·●◉◉●◉●○·· · + ○·○··◉●··●·◉ + ●○●●○○◉·●◉● + ○◉◉·○○○◉● + ···· \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_31.txt b/codex-rs/tui/frames/dots/frame_31.txt new file mode 100644 index 000000000000..99385ee51fa5 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_31.txt @@ -0,0 +1,17 @@ + + ○◉◉●●●●○● + ○●●◉●●○ ●○●●● + ●·○◉◉●○○·◉ ·○●○ + ●·◉●○○●◉○●○○·○··○ + ○····●◉●○○● ○·◉○○ + ◉·○··○·● ○◉●●◉◉·● · + ◉·· ·○●·○◉○◉◉●·●·◉· + ○○● ·· ·○ ·○ ··○◉· + ·○●○·●○···◉●···○ · + ···◉◉··○○○○○·○·· ● + ○●○◉●·● ◉●●●·◉·●·○ + ···○○○● ○○● · + ◉·●○○ ● ◉ ·●·◉· + ◉·○·◉○··●· ●·· + ● ○·○●●◉◉◉● + ···· \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_32.txt b/codex-rs/tui/frames/dots/frame_32.txt new file mode 100644 index 000000000000..771e9c9106b0 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_32.txt @@ -0,0 +1,17 @@ + + ●●●●●◉◉·●○ + ◉●·●●·○··●●··● + ◉○●○○●●○◉◉·●·●○●●● + ●◉◉·○●·◉◉ ◉ ●○○·●● ● + ◉◉·●·●○●●·◉ ●·●·○◉○●○● + ○◉○○●○●··◉··●○ ·●◉ ● + ●· ◉●·●○··●●·●○·◉○◉◉·●○ + ···●○ ○·· ◉●·◉◉○ ○· · + ○··◉○ ●○· ○○·●●●···○· ○· + ● ○··●○●○○○○○○··●·◉·◉○ + ●○●··◉ ·● ●●●○◉◉◉◉◉◉ + ○●·○○○ ●○·◉●· + ●●●···●○ ○◉○·◉ ● + ○◉○ ·●·◉○●·○● ○◉ + ·◉○○○●●◉●○ + ··· \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_33.txt b/codex-rs/tui/frames/dots/frame_33.txt new file mode 100644 index 000000000000..4d36c1eb6f26 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_33.txt @@ -0,0 +1,17 @@ + + ○●●○●○●○·◉○ + ○●●··○●○·◉·◉·●·●◉ + ○◉○·●●●●○ · ●◉··● ● + ●○◉·◉○·●○ ●··○○● + ○◉·◉···◉○○○ ·●○○○ + ◉◉● ○○○○·○● ◉○◉○○ + ··● ○●●·○·○ ·●·· + ●·●● ●●●○◉○● ○ ·● + ··· · ●· ◉●●●●◉○○●··●◉○·○· + ○◉·● ◉◉●◉···○○○○○··●○○○··◉ + ○○○● ·◉·◉ ◉ ●●● ··●◉○· + ○●○●● ● ◉◉◉◉○◉ + ●○ ●●●◉● ○●·●◉●○● + ○· ●○●·●◉●○ ◉●○●·● + ○○●●○○○○●●◉◉ + ···· \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_34.txt b/codex-rs/tui/frames/dots/frame_34.txt new file mode 100644 index 000000000000..4cbd99c14353 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_34.txt @@ -0,0 +1,17 @@ + + ○●○●●●●○·●○ + ○●●●◉○◉· ·●●○●●◉●○○ + ○◉◉●·● ◉·●· ·○○○● ●●● + ●◉○·●○◉●○ ●·●●○○ + ◉◉·○◉◉· · ● ○○●○○ + ●◉○··◉ ○○ ·○●○ ○●○·○ + ·●○·○ ●○○●·○○ ·◉·○ + ○·○ ◉ ·●○·◉ · ◉ ○○ + ●●◉◉ ◉○◉◉ ○○○●●●●●●·○○ ·○· + ··○●◉●●○ ◉◉●···○·◉○○○●◉○····○ + ◉○○● ·○·●○ ●●····○ ●◉●○·● + ◉●○◉○○○ ◉◉◉·◉● + ○●··●○◉● ○●○ ○◉●· + ◉○◉· ·●●◉●●○●○●·●●◉● + ·○◉○○○○○○◉○●○○ + ··· \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_35.txt b/codex-rs/tui/frames/dots/frame_35.txt new file mode 100644 index 000000000000..5ccdf711b5bc --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_35.txt @@ -0,0 +1,17 @@ + + ○●●◉○●●●○●●○ + ●●○●◉·●●● ·●·◉ ·●○○ + ○◉◉·◉●●·○● · · ●○●◉ ·●● + ●◉◉●◉●◉●○ ●●○○·○ + ◉◉··◉◉○ ○●● ·○●·● + ◉◉◉◉●· ●·○ ◉◉●○ ·○○· + ○ ◉◉ ○○ ●●○◉ ·○○○ + ···◉ ◉○○◉◉● ◉ ◉ + ◉ ●○ ●··◉◉ ◉·○○○◉○○●·· ··○● + ·●○◉● ◉◉◉ ●●◉·◉ ○●○○○○◉ ·◉● ·● + ◉·○●●◉○○·◉○· · ○○○○●○○●◉◉○●· + ◉○○◉· ○◉● ●○ + ○◉·○◉●◉● ●◉◉○●●● + ○○ ·●·◉◉··●·◉◉● ○◉◉ + ○·◉○○○◉○○●●● ○ + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_36.txt b/codex-rs/tui/frames/dots/frame_36.txt new file mode 100644 index 000000000000..6a26abaea68e --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_36.txt @@ -0,0 +1,17 @@ + + ○○●○●●●●○●●○ + ○○ ○●◉◉◉●○●◉●● ●●●○ + ●○●○●●●◉● ·· · ●·●●·●●·● + ○· ●●○○○○ ●●·○ · + ●◉○◉○●·○·○●◉ ○○◉● + ◉ ●◉· ●○ ·◉● ○○● + · ◉ · ○ ○○ ·○·● + ··○· ○○ ○◉◉ ○· · + ·● · ◉◉ ●● ●●●●○●●●●●○ ·○·· + ○●·○ ●·○○◉·◉ ··○○○○○○○·· ○◉●· + ● ●·○ ●○●◉○ ●●○○○○○ ·● ◉ + ●● ○◉◉ ●◉· ◉ + ○◉··●·● ●●● ◉●· + ●●○ ●●○○◉·○●·◉ ◉ ●◉●● + ·●●◉·○●●○●●●●●● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_4.txt b/codex-rs/tui/frames/dots/frame_4.txt new file mode 100644 index 000000000000..b4496013b5e6 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_4.txt @@ -0,0 +1,17 @@ + + ○◉○◉●○○●○●●○ + ○◉ ●●·◉ ·○○◉●◉· ◉●●○ + ●●○···●● · ·○·○○●·◉○○○ + ◉◉·●· ◉●● ·●●○●○ + ◉·◉○·○·○◉○·● ○ ○·●○ + ◉··○· ●○··◉●○ ● ○○·● + ○·◉· ●●●○● ·● ● ◉· + ·◉○ ●·◉●○·· ··· + ○· ·○ ◉·◉ ○◉◉●·●●●○○○○●● ··● + ·○◉ · ●◉◉◉●○··○○·●◉○●◉◉○◉·◉◉ · + ··◉●○●○○●○◉ ●●●●●●●●○◉···· + ●·○ ·○· ●○◉◉●●● + ○○●◉○ ●○ ○◉○●●◉·◉ + ···○○●●○◉◉○◉●●○●●○·●· + ●○○·◉·○○○○○◉◉●· + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_5.txt b/codex-rs/tui/frames/dots/frame_5.txt new file mode 100644 index 000000000000..0905c495b266 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_5.txt @@ -0,0 +1,17 @@ + + ○◉○◉◉●○○○●●○ + ○● ●◉●◉··●·○●◉○· ●● + ●○◉○●·● · ○○·●○ ●●● + ●·◉·◉·◉○● ◉○····○ + ●○◉●◉○○●·○○● · ○·○·○ + ····◉ ○○·○◉◉·○ ●◉·· + ·◉· ● ·○○·○ ·● ●●·●·● + ·◉·○ ●○○·◉○· ●·○· + ·●·○● ◉○· ●·●○○·●·○○○●●○ ·◉· + ○○·○●●◉●● · ◉◉○○○●○◉●◉·◉●·· + ○●··●·●○○◉●· ●● ●·◉·○◉ + ○○●◉○○ · ◉○◉●●○ + ○◉●○·○●○ ○○●●●●◉● + ○·●·●●●○◉·○○●●○●○·◉● + ○◉●○● ○○○◉○●●●· + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_6.txt b/codex-rs/tui/frames/dots/frame_6.txt new file mode 100644 index 000000000000..3f96b6676177 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_6.txt @@ -0,0 +1,17 @@ + + ○◉○◉◉●○○●●○○ + ●○◉◉●·○●●·●●◉○●●●○ + ●●●···● · ·◉○●●●◉○○ + ◉●·●·●○○● ○●····● + ●○···●○○●●○ ◉○○·○● + ◉◉◉·◉·○●◉·○○·● ◉○○◉· + ○ ●·· ○○·◉○○ ○●·○· + · ··· ○·○·◉·· ·●○·· + ○◉·◉· ◉●··●◉◉●○·●·○○●●○◉○◉· + ··○●◉○○○◉● ·◉○●●··●◉○○○·●·○ + ◉ · ◉···◉◉ ● ●● ·○○◉· + ● ◉ ◉●· ●◉○◉◉·· + ◉·○○●●● ○●○○◉●●● + · ·●● ● ◉·○○●●●● ◉◉● + ○◉◉●●●○○○·○●●● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_7.txt b/codex-rs/tui/frames/dots/frame_7.txt new file mode 100644 index 000000000000..aa52e1b869d1 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_7.txt @@ -0,0 +1,17 @@ + + ○◉·◉●●○○●●○ + ●●·●○·●●●○○○◉ ·●● + ● ●···●· ·○·· ●◉○●● + ○○····◉● ○·○●·◉● + ·●·●··○·◉○● ◉ ●··○ + · ◉·· ·●○◉·○ ●◉··· + ·○ · ○○·○·○· ●●○·· + ○ ◉·· ○●◉·●·· ● ··· + ◉ ·● ● ○◉·○◉··◉◉○··○◉●○·· + ·●●··○◉··◉●·◉○··○○●○●◉○·○· + ○ ○○◉●·●○◉ ●●●●●● ○◉·· + ●○ ◉○◉○· ● ◉◉◉◉● + ●◉●●○○○○ ●○·●●◉●● + ○○●○·●○◉·○○○●●·●●◉ + ·● ·◉·○○●○◉●● + \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_8.txt b/codex-rs/tui/frames/dots/frame_8.txt new file mode 100644 index 000000000000..5791ce70e480 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_8.txt @@ -0,0 +1,17 @@ + + ○○●○◉●●○●●○ + ● ●◉···●○○ · ● + ○·○●·◉◉● ● ·◉·●○●○● + ○●○●◉·●●· ● ○○··○ + ● ◉·◉●○·◉○ ◉●○··● + ·●●·○○○○○·○● ○●··· + ·●··· ●○◉○··· ·○·◉·● + ○●··◉ ·●○◉·●●◉ ◉·◉·· + ○ ○··○○ ◉●○··◉●●●●●○··· + ○●◉○○· ●·●··◉·○○○··○● + ◉◉○○·○···◉●○●○● ○● ◉○· + ●● ·○··· ◉●◉◉◉● + ●○○●○○○○ ○◉○○·●◉● + ◉◉●·●● ◉◉●●◉◉○●·· + ··○●●○·○●●◉○· + ·· \ No newline at end of file diff --git a/codex-rs/tui/frames/dots/frame_9.txt b/codex-rs/tui/frames/dots/frame_9.txt new file mode 100644 index 000000000000..35588ee1ee76 --- /dev/null +++ b/codex-rs/tui/frames/dots/frame_9.txt @@ -0,0 +1,17 @@ + + ◉○○●◉●●○● + ◉●○◉··● ○ ○◉○○ + ◉ ●··◉●○○○◉ ○○·● + ··◉◉○○ ○○●·○○·●● + ◉● ◉○● ·●● ●◉●●◉●·· + · ··· ○·○◉○○ ·●·●··· + · ·●○·●●·○○● ◉●◉ ··· + · ·◉○●○···○◉○○○· ·· + ●· ◉· ····●··●··· + ·○···○●·○·○····○○··· + ·○◉○○·◉ ●●●●·· ◉○· + ○···○●· ●◉ ○◉·· + ○●○·○○ ◉◉○◉·◉● + ○◉ ○○·●◉●○◉◉●·· + ●◉○ ●○○·◉●◉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_1.txt b/codex-rs/tui/frames/hash/frame_1.txt new file mode 100644 index 000000000000..45adbbac2479 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_1.txt @@ -0,0 +1,17 @@ + + -.-A*##**##- + -*#A**#A#**..*- -█#- + #.*.#**█-- -█*-█.*...# + **-**█##- A*.*.# + *-*A█-.*-** █..**# + .* #- .*A*..# █.*..# + #-█-* █*.*A█.- .A. + ..-.- #AA.*.* █-. + .*.█- *..-*.█..######-## *█ . + -.** -*#- A* .█.---.A###.A#A#. + *--█- -*#.A- --*██* -*█A#*-A + *-# █# - #█A*-. + -*#-*#- -*-#.-#█ + -*#*- *#A****.**#-#.* + -*█*...---#-*#*█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_10.txt b/codex-rs/tui/frames/hash/frame_10.txt new file mode 100644 index 000000000000..0e9a76d4d8f1 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_10.txt @@ -0,0 +1,17 @@ + + -#****##- + *█-#*#*.A-*# + --#..A..-.#*## + .--A*.*-.*#██**# + A *..█.- █#A-A.A + .- █.*.AA* --█. + * .*█*....* .A# + █ ..*#A#..---.*.. + █ .* **.*..#*#*.. + . #.*#.-A-*---.*- + # █.....██ *#█*.- + *-█#*#*█ -.██#AA + .█ ....*#A#A.A- + *-**A.#*- AA█ + *# -**-#** + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_11.txt b/codex-rs/tui/frames/hash/frame_11.txt new file mode 100644 index 000000000000..b7e743b218b0 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_11.txt @@ -0,0 +1,17 @@ + + #****##- + A█ *...**# + A█--..***A*# + .--..**#-*AA + -█.**.#*█*-..# + .#-..#-**.-A.. + ** #......-.** + . ..A..*A .█. + A █.A..*A#.-. + ** -.A*#█A.-.- + █-- ....*A.**. + *--A.-*A***.- + - *.**--.*█ + *# *..#-A*- + *- █*#-#- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_12.txt b/codex-rs/tui/frames/hash/frame_12.txt new file mode 100644 index 000000000000..0c6c85043f94 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_12.txt @@ -0,0 +1,17 @@ + + #***#. + #*--A.#* + █ .A*## + A#...**█. + . █.....A. + .█..*-█-.█ + . .A*..* + . A#*..█ + *# .A..#.. + . A.***- + .. .█***A + # *. -#. + A **.#**. + █-A█#.*.- + * █.*.A + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_13.txt b/codex-rs/tui/frames/hash/frame_13.txt new file mode 100644 index 000000000000..097cd508d7ec --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_13.txt @@ -0,0 +1,17 @@ + + A***# + .--.. + .--.█ + .**.- + * .. + █A-.. + # .. + # █. + A# -.. + .█ #*. + █-.. .- + .---. + .##.- + *--.* + # .█* + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_14.txt b/codex-rs/tui/frames/hash/frame_14.txt new file mode 100644 index 000000000000..8eca90950407 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_14.txt @@ -0,0 +1,17 @@ + + #**** + #A--.* + .-... . + ..A.█-.# + ...* . + .*.. █. + .... . * + ..*.- * + .... █ + █-AA *A + *.██#.#. + .*..---. + A █.***- + .*A*--A + -* █*A + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_15.txt b/codex-rs/tui/frames/hash/frame_15.txt new file mode 100644 index 000000000000..cbf646ab35c0 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_15.txt @@ -0,0 +1,17 @@ + + ##***.- + -#*..-A*# + AA.***.█*. + .A- AA#.--. + ..*█**..A█-. + *..-..AA. █ + .. ......#. + ..A..#... █-. + ..###*...-.A. + .-.*A.*.*. .. + .A....-. - * + .**.-..*- * + █.*# AAA- *- + .-..**.#*A + *-*----A + -- \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_16.txt b/codex-rs/tui/frames/hash/frame_16.txt new file mode 100644 index 000000000000..82698755af17 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_16.txt @@ -0,0 +1,17 @@ + + -*#█**#.- + A-..*..*.** + .AA█*A**.. █* + AAA **-*.*..**. + ..*-A*█*..*A.-* + ...*.█ .** . ... + **. A-#....A*█# . + .A* .-..A...█* -. + **...#.A-..█*# A + *█--#****..-. #-. + ...#██A**.*█*...- + ..* .- -AA# A + *.* . A#A-#. + *..█-*.AA#-. + █A.-*A--** + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_17.txt b/codex-rs/tui/frames/hash/frame_17.txt new file mode 100644 index 000000000000..57d02179e707 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_17.txt @@ -0,0 +1,17 @@ + + #*###**.#- + -***...***.#█-# + #**A-**-##█...-█* + A.A-A.█-- █.**...** + .A- . .█A***AA # + -**#A- #AA. A#*A..A + * *A. #AA.- *█..A*. + -█*. AA..A.#..*** + #A*A***#.*-.*-A*... . + .█-*--.A**A..* *#█#* + ***A███*****-.*AA* *█ + *..-* -A..- * + -** **- #█#A--A + # .*#**#--**A.█ + -#*A-##.*-#* + - \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_18.txt b/codex-rs/tui/frames/hash/frame_18.txt new file mode 100644 index 000000000000..ef524a0ed910 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_18.txt @@ -0,0 +1,17 @@ + + -**#**#*.#- + -#█-*###.--#█#**- + -AA.*█**█#█.-█#AA* * + #..*.#-█* █*--A*.*****# + -*AA A. -█#-*A.A.-****- + A*. ##..---A#*#.█-A-A-A. + ..*AA #A*A.█-A█*.*.*# + █*-.. -*█..*- .*.█ + *██ #******#..#.*A*# .*.- + -**.*** ..A--*-...-*.-* # + .A.*-██*****- *--A.A*A--- + ** #*# --*█A*-█ + *-*#--. -*--A*#- + █..**##***-█*-A-#* + █..-A--#.#█*#█ + - \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_19.txt b/codex-rs/tui/frames/hash/frame_19.txt new file mode 100644 index 000000000000..80a9abf0128d --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_19.txt @@ -0,0 +1,17 @@ + + --**#.#**..- + -#...*A***-*-#.*## + #***A█.#*- -*A#-*** + A*.*#-- -**##-.*# + ..A-A- #.-█A█.* .*# + .*A*█ -.*.A .A █.*. + -*..█ A*-A.##- AAA#. + .... .*█*.█# . ... + .*.*#******....#***.* * █.. + -*A#.**A-*#*#-# -- A*-*#A..- + █*█A#.█████**█ -*-A.AA AA█ + **-*-* -AA-AA█ + █ -*..*# #**.A.*- + ██..*A.#--**A*█ **.█ + █..-A-█-#.-*-- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_2.txt b/codex-rs/tui/frames/hash/frame_2.txt new file mode 100644 index 000000000000..843df90f283f --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_2.txt @@ -0,0 +1,17 @@ + + -.-A*#***##- + -##A**#...*..*-█.*#- + #.*.***.*-- -█***A..*.# + -.A-A*█##- -#*.A.# + #-█A*-.*-**- -#.*.# + █A#█- .**#A*# **.*. + #.█*. -A.*.-.# A█.█. + .█ *- #.*..** .█A.. + *██*. *.---.█..#########.-..* + -*█. A..█ A* .**----..--.#A . + *** * **...█ -█*******█.#*.. + █*-.A# - ##*A#* + .*--.## #*-# -.█ + -.-*--█*#A****.**--..* + -*#*#..----.*A*█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_20.txt b/codex-rs/tui/frames/hash/frame_20.txt new file mode 100644 index 000000000000..b588df38946d --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_20.txt @@ -0,0 +1,17 @@ + + --#*..*#*#-- + #**█*#-#***..--**## + ##█-#*█.*█ -█-█*#*.#- + -.█-*.* -##.*A*** + -. #.* -A -..-**..* + A-#.█ #*█*.█A █*..* + *.*#. #* .#*- #*.A + .--A- ██*A.█# ..█. + .A..*#********-#█*--*** A .* + █.-.-█ A------.* ***█.** #A .. + *#-*████████*█ *#*..#. A* + * -*A# -A*..A + *--*#.- #**-**█ + -#-.█.#-**#A█.█-#A*█ + -*#.- *----.*██ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_21.txt b/codex-rs/tui/frames/hash/frame_21.txt new file mode 100644 index 000000000000..0d1fc7ec26ed --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_21.txt @@ -0,0 +1,17 @@ + + --#*#**#*##-- + ##*----#.*.#*---*#- + **--#*-█- --.*#--*# + #*.*██ -#**#**# + A#A*- #.A-* **--# + A█A. #**-AA **-# + .█.- - A-#A█ ***█ + -* ** █.# . . + . . ##*****### -.#█.# *A . + . .* .- -#. -█-*.# A█.A + * █. --███ █- █*-#* A- # + █# A*- -*█ A + *- --- -.**█ #- + **- *█*#A..*A**██ -- + *##*#----#-*#*- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_22.txt b/codex-rs/tui/frames/hash/frame_22.txt new file mode 100644 index 000000000000..8fbfdb571382 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_22.txt @@ -0,0 +1,17 @@ + + -##*#**#*##- + -#A*..-A.A*..*.#A*#- + #***.***- -█****#--## + **.**█ ##...-** + *..A█ #-A.*..A*█. + *.*A- #.A.*-A- *.**. + .#**A .#.**A█ ..A. + ..*.- .*..-* .-.█. + ..#█# #.******##-**.- *# -.*█ + -.* *-**# A#█.AA. ***-█.## A** + ***-. █████**█- -A*#-AAA█#- + ***-*# -*#█.#█ + ██#* ##- -.*#█*-A + █A*##█*.**#**A**███#* + █.█.-*----*-.**- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_23.txt b/codex-rs/tui/frames/hash/frame_23.txt new file mode 100644 index 000000000000..ef2f8adb7095 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_23.txt @@ -0,0 +1,17 @@ + + -##-*#*##*#-- + -*.#.*-#-**#.-.*.#- + #█#.█#**- █.A#.**..# + A*.**A- -***..** + AA.-A█ #A##█*A**.* + #*.█A- -***██A*█..*.# + -..█. AAA. -.- █ .█* + .A. . .█.#-*# . *A. + -█* .#.********.-*.-█*- . ... + .**-.█*█ A-* *.-*#. *-. + A█. . -███████ █-A**.-A-A- + #..-.. #A.*-A- + * .#█A-- -.█**A#A + ***#**#*.**-##█--#*- + █. ***.--#--**- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_24.txt b/codex-rs/tui/frames/hash/frame_24.txt new file mode 100644 index 000000000000..09a7fd520cb9 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_24.txt @@ -0,0 +1,17 @@ + + -.-*##***#- + #█-A█.--.**##-█# + #██**A#█. .-A*.**# * + #█AA#A.*#*-.A- -**.*-█ + # AA*A# -██A*-.--*.#█ + .#*#*- *##A ..* A█* + . .. #A-#A█A-*.*-.- + . *.. .#*A*-..-A.-.. + .. .***A##...##-*#-. . .. + - A *.A##**##..-*#*. .*. + * █*.**██████- *#.**.*A#- + * *#** . █A*A + *.**-.. -*█-AA-* + .-*#*-█..A***.--A- + *A-*#**--**#* + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_25.txt b/codex-rs/tui/frames/hash/frame_25.txt new file mode 100644 index 000000000000..af8bb947f60c --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_25.txt @@ -0,0 +1,17 @@ + + -#***####- + # *#*--**.*..# + A.AA##█..#***...- + .#*. A *. █*.#**A + #--*#A█-*A-.█ .AA*A* + *#...-*#.-.A A.#.AA.# + .A.*. .--A*-A.*A-█.*. + ...A..█ .-**A.*-.*..* + -#*.█.--****#..**.#*.* + A-█.A.#.-..**-.***A*. + *.*A*.-██████#A.*.A.- + .-█.*#**-#*A-A#AA.A + ---.-*-**A#-A-..A + *.***-**.** *.* + .#█**##-#.* + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_26.txt b/codex-rs/tui/frames/hash/frame_26.txt new file mode 100644 index 000000000000..7ff85c300af4 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_26.txt @@ -0,0 +1,17 @@ + + -****█## + A*.*A*.#*.# + A*██.**.*██*# + **█#...-..-#A- + .-#*...█**A*.** + * █.AAA█*-*█**A + . *█*..A..**█# + A- *...--*..A.. + . .***#.#**A*.. + .-.A#.--#****. + .-█A.*█*#A-#*.. + *-..A* AA█..- + * *.* --A.A# + .█***█*-*** + *AA-*.-** + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_27.txt b/codex-rs/tui/frames/hash/frame_27.txt new file mode 100644 index 000000000000..06e988b0761d --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_27.txt @@ -0,0 +1,17 @@ + + ****## + .*AA.A* + *█- *.A█. + ...-..*.. + . -.--█* + .-.-█-...- + * █..-*.. + . -...*█.. + .. . .#.... + █* .█-.-*.. + .- --**#A█ + ..##..-A. + .**#*#A.- + *--.**AA + █*█ -*A + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_28.txt b/codex-rs/tui/frames/hash/frame_28.txt new file mode 100644 index 000000000000..0e258181458f --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_28.txt @@ -0,0 +1,17 @@ + + A*** + .#-.- + * *.- + .--*. + . .. + . -█. + -.. -. + .-██. + A* -. + ...A.. + . -. + .*#*. + .**.. + *--A. + . .#. + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_29.txt b/codex-rs/tui/frames/hash/frame_29.txt new file mode 100644 index 000000000000..7f2ddab00a4f --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_29.txt @@ -0,0 +1,17 @@ + + #****# + #.*A ** + -#.. ██. + ....-*#A + * .█. . + .#... -- + .**... - + ..*.. + ...*. - + -.*.. - + *..A.- * + .A... -. + ....-.-. + .*.*A█.█ + ..* .# + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_3.txt b/codex-rs/tui/frames/hash/frame_3.txt new file mode 100644 index 000000000000..8cce426bb4a5 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_3.txt @@ -0,0 +1,17 @@ + + -.**##****#- + -##A*..#.█**.*--.*#- + #*A**.*██- -█. **#A-.# + A#A*.*##- -** ..* + █A*.A-A..**- *-*..- + A.**A .#.**** . **. + ** . █**.-█.# .*A.# + #-.#. #-.█A*. .-.*. + -██. *.*#..*-.*##---##-.-█*. + #*A*# .A*A**. .*.--# *.#**-.*.- + █-. . █..##█ █***███**#AAA#A + █*. *# - A-#-AA + *-AA**# ##*A█#.█ + *-##-**-****#A***--#█ + -*#*-A.----.*A-█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_30.txt b/codex-rs/tui/frames/hash/frame_30.txt new file mode 100644 index 000000000000..24a2165e45b9 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_30.txt @@ -0,0 +1,17 @@ + + -*█**## + -A*..**█** + ██-.#**.##* + ..*.A.-*.* *# + **..-*.#.# . + .*.-# *...... + *A...*- *A*. *- + █. █#*.#.█.- - + █.-█*.*.*A.# - + █..*.--.. ## - + *..**-*...#.. + .*AA#A**..█ . + *.*..A*..#.A + #-##*-A.#A█ + *-..---*█ + ---- \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_31.txt b/codex-rs/tui/frames/hash/frame_31.txt new file mode 100644 index 000000000000..65f139ab962f --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_31.txt @@ -0,0 +1,17 @@ + + -.A*#*#*# + -#**##-█#*█*# + #.*AA#*--.█.-** + #.A#--**-#**.*..* + -....#-**-#█*.A-- + *.--.-.██***#.A.#█. + A.. .-*.*.*A**.*.A. + **# ..█.* .-██..*.. + .*#-.█-...A#...- . + █...AA..---*-.*.. * + -*-A#.# A***-A.█.- + ...***# -** . + *.**- # . .*-A- + A.*.A-.-#.█#.- + ██*.-#*A.** + ---- \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_32.txt b/codex-rs/tui/frames/hash/frame_32.txt new file mode 100644 index 000000000000..6cbec21aecaa --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_32.txt @@ -0,0 +1,17 @@ + + #####**.#- + **.**.*..**..# + A-**-##-A--*.**█*# + █AA.-#-*- - #-*.## # + AA.#.**█*.* █-#.*A***# + *A**#**.-A..#- .#A * + *.█*#-█*..##.#*-A-AA.█- + ...#- █-..█A█.*--█*.██- + *..-- █-.█--.###...*.█*- + ███--.**█*-----..*.*.-- + #**..A .*██***-*AAA.A + *#.-** #*.A█. + █**...#- -A-.A # + *-*█.#.***--#█-A + .**--##A*-██ + --- \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_33.txt b/codex-rs/tui/frames/hash/frame_33.txt new file mode 100644 index 000000000000..a661feb2aff3 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_33.txt @@ -0,0 +1,17 @@ + + -##*#*#*..- + -#*..***.A.-.*.*. + -A*.#*##*█- █*A..#█# + #*A.A-.#- *..*-* + -..A-..A*-* .#**- + AA# *-**-*# A*A-* + ..# *#*.-.* .*.. + *.## #██*A*# *█.* + ... . #- A*###***#..#A-.*. + -A.██AA#A-..-----..*---... + ***# .A.A A ████*** ..#A-- + *#**#█# AA*A*A + **█*#**# -#.#A**█ + *.█**#.#*#-█*#**.* + █**##----#**-█ + ---- \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_34.txt b/codex-rs/tui/frames/hash/frame_34.txt new file mode 100644 index 000000000000..3427025326ca --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_34.txt @@ -0,0 +1,17 @@ + + -#*####*.#- + -#*#A**.█.#*****#*- + -AA#.*█A.*- --**#█**# + #A-.*-A#- *.#*** + AA.*-A. .█ # ****- + #A*.-A **█.*** *#*.* + .*-.- █*-*.-- .*.- + *.*█A .*-.A█. A -* + #█A.█ A*AA█-**######.--█ .-- + ..*#A##-█AA█...-..---#A-..-.* + A-*# .*.*- █**----- #A#*.█ + A#****- A*..A█ + *#-.#**# -#*█-A#- + -*A.█.##*##****-#*A█ + █.***-----****- + --- \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_35.txt b/codex-rs/tui/frames/hash/frame_35.txt new file mode 100644 index 000000000000..e0919ec5d0e5 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_35.txt @@ -0,0 +1,17 @@ + + -##**###*##- + #***..**#█.*.A█.**- + -AA.*#*.-*█- -█***.█.*# + #AA#A#*#- *#-*.- + AA..AA* █**# .**.# + *A*A#- █.-█..*- .**. + * .A ** █*-. .-** + ...A A***A█ A █A + A #- #..A*█A.**-*-*#.. ..** + .#-*█ AA*█*#A.* *#----A█.A█ .█ + A-*#█A*-.A-- -█----*--*AA-#. + █.-*A. -A* #- + **--*#A# #*A-**█ + █--█.*.-A..#.*A*█-A*█ + █-.**--A--##*█- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_36.txt b/codex-rs/tui/frames/hash/frame_36.txt new file mode 100644 index 000000000000..0355f68b47c3 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_36.txt @@ -0,0 +1,17 @@ + + --#*####*##- + -*██*#A*A#*#*##█**#- + #*█*##*-*█-- -█*.*#.#*.# + -.█*#---- █*.*█. + #A-**█.*-*#. -*.* + A #A- *- █.A# █*-* + . * - * █** .-.# + ..*. -* -AA *. . + .██. AA #*██###-#####- .*.. + -*.* #.--A.A -.-------.. *A█- + █ *.* #-#A- █**-----█ .#█A + █#█**. #A.█A + *A..#.# ###█A#- + *#-█***-*.-#.*█-█#*#█ + -*#A.-##-####**█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_4.txt b/codex-rs/tui/frames/hash/frame_4.txt new file mode 100644 index 000000000000..2b4b7c670bb4 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_4.txt @@ -0,0 +1,17 @@ + + -.**#**#*##- + --█#*.A .**A**.█A*#- + #**...#*█- --.*-*.A**- + AA.#.█.## █.***** + A.A-.*.**-.# *█*.** + A..-. █-..A** █ **.# + █*.*- █***#█.# # █*. + ..* #.A*-.. ... + -. .- *.A█-AA#.###----## ..* + .*A . #AAA#-.-**.#.-#AA-*.AA█. + -.*█*#**#*A █*******█--...- + #.*█.-- #-*A**█ + **#A- #- -.-#**.A + ...*-*******##**#*.*- + ***.A.-----*-*- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_5.txt b/codex-rs/tui/frames/hash/frame_5.txt new file mode 100644 index 000000000000..c71575690bbe --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_5.txt @@ -0,0 +1,17 @@ + + -.***#***##- + -#█#A*A..#.*#*-.█## + *-A*#.*█- █-*.** #*# + #.A.A..*# --....- + #*A*A--#.-*# - *.*.- + ....A *-.**A.- #A.. + .A. # -*-.*█.# ██.#.# + .A.-█ #-*.A-. #.-. + .#.-* A-.█#.**-.#.---##-█... + -*.*##A#*█ .█ AA**-#*.#A.A*.- + *#..#.#**A#- █**█████#.*.*A + **#A** - A-A*#- + -A**.*#- -*##*█** + -.*.#*#**.*-##**-.** + --*-*█-----##*- + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_6.txt b/codex-rs/tui/frames/hash/frame_6.txt new file mode 100644 index 000000000000..799e3a1cf5ac --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_6.txt @@ -0,0 +1,17 @@ + + -.***#**##-- + #-*A*.**#.##*-#*#- + ###...*█ -█..**#*A*- + A#.*.*-*# *#....# + █*...***#** .**.*# + *-A.A--#*.*-.# .**A. + - #.. -*.A-* -#.-. + . ... -.*.A.. .#*.. + -..-. A█..**A#-.#.--##-.*A. + ..*#A---*# .A**#..#****.#.* + A . A...*A█ █*████*█-**A. + # * A#- #A-AA.- + .-*-**# -#--A*## + -█.*#█# *.--##** A*█ + --**##-----##* + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_7.txt b/codex-rs/tui/frames/hash/frame_7.txt new file mode 100644 index 000000000000..4a3f9f202fbd --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_7.txt @@ -0,0 +1,17 @@ + + -..**#**##- + #*-#*.#*#**** -*# + * #...*- --.- *A-*# + **....*# -.**.A# + -█.*..-.A*# . *..* + . *..█ -█**.* #-... + -*█. --.*.*. **-.. + -█A.. *#A.*.- █ ... + A█.* #█-A.*A..**-..****.. + .#█..*A..A█..*..**#*#**.*. + * *-A*.*-A ******██-A.. + █- A-A-- #█-A*A█ + █*##***- #-.#*A*█ + *-█*.***.---#*.*#A + █.*█.A.--#-*** + \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_8.txt b/codex-rs/tui/frames/hash/frame_8.txt new file mode 100644 index 000000000000..4bc5a6f1186d --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_8.txt @@ -0,0 +1,17 @@ + + --#-*##*##- + #█*A...*** . █# + -.*#.AA* #█.A.**#-# + -***A.##. █ *-..* + █ A.A#*.A* .█*..# + .█#.*---*.-* *#... + .*... █-**.-. .-.A.# + *#..A .#*A.#*. .-A.. + * *..-- A**..A#####*... + **A*-.█#.#..*.*--..*█ + AA**.*...A█-*-*█**█A*. + █#█.*.-- .#AAA█ + █*-****- -A*-.#A* + .A█.#* **#**A*#.- + ..-***.-##A*- + -- \ No newline at end of file diff --git a/codex-rs/tui/frames/hash/frame_9.txt b/codex-rs/tui/frames/hash/frame_9.txt new file mode 100644 index 000000000000..db3507db59c7 --- /dev/null +++ b/codex-rs/tui/frames/hash/frame_9.txt @@ -0,0 +1,17 @@ + + .*-*A##*# + A*-A..*█* --*- + A #..A*--*A *-.# + █ ..A**- --#.*-.## + A*█***█.*# *.█#A#.. + . ...█-.**-- .█.*... + - .**.**.**#█.#A -.. + . .A-#*...-A**-. .. + █#.█ A.█....#..#..- + .-...-#.-.-....--... + █ --A**.A█****..█A*. + *...*#- #A -A.- + *#*.** A*-*.A* + --█*-.*A#****.- + █-- ***.*#A█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_1.txt b/codex-rs/tui/frames/hbars/frame_1.txt new file mode 100644 index 000000000000..ab8be3eb1e16 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_1.txt @@ -0,0 +1,17 @@ + + ▂▅▂▅▄▇▇▄▄▇▆▂ + ▂▄▆▅▇▃▇▅▇▃▄▁▁▄▂ ▂█▇▂ + ▆▁▇▁▇▇▇█▃▂ ▂█▇▂█▁▄▁▁▁▇ + ▄▇▂▃▇█▆▆▂ ▅▇▁▄▁▆ + ▃▃▄▅█▃▁▃▂▃▃ █▅▁▃▃▆ + ▁▇ ▇▂ ▁▇▅▄▁▁▆ █▅▃▁▁▆ + ▇▃█▆▇ █▃▁▇▅█▁▂ ▁▅▁ + ▁▁▂▁▂ ▆▅▅▁▄▁▇ █▂▁ + ▁▄▁█▂ ▄▁▁▃▃▁█▅▁▇▇▇▇▇▇▂▇▆ ▄█ ▁ + ▂▁▄▇ ▂▄▇▂ ▅▇ ▁█▁▂▂▂▅▅▆▆▆▁▅▆▅▆▁ + ▃▃▂█▃ ▃▃▆▅▅▂ ▂▃▇██▇ ▃▇█▅▆▄▂▅ + ▇▃▆ █▆ ▂ ▆█▅▇▂▁ + ▃▃▆▂▃▇▂ ▂▄▂▇▁▂▇█ + ▃▇▆▃▂ ▇▇▅▄▄▄▄▅▄▇▇▂▆▁▇ + ▂▇█▇▁▁▁▂▂▂▆▂▄▇▇█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_10.txt b/codex-rs/tui/frames/hbars/frame_10.txt new file mode 100644 index 000000000000..5e565ce40b91 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_10.txt @@ -0,0 +1,17 @@ + + ▂▇▇▇▇▃▇▇▂ + ▇█▂▇▇▇▃▁▅▂▇▆ + ▃▂▆▁▁▅▁▁▆▁▇▃▆▆ + ▁▂▂▅▃▁▄▂▅▃▆██▃▃▆ + ▅ ▄▁▁█▁▃ █▆▅▂▅▁▅ + ▁▂ █▁▇▁▅▅▃ ▂▂█▁ + ▃ ▁▇█▇▁▁▁▁▇ ▁▅▆ + █ ▁▁▃▇▅▇▁▁▆▂▂▅▃▁▁ + █ ▁▃ ▃▃▁▄▁▁▇▃▇▄▁▁ + ▁ ▆▁▃▆▁▂▅▂▇▂▂▂▁▇▂ + ▆ █▁▁▁▁▁██ ▃▆█▃▁▂ + ▃▂█▆▃▆▇█ ▂▁██▆▅▅ + ▁█ ▁▁▁▁▇▆▅▆▅▁▅▂ + ▄▂▇▇▅▁▇▄▂ ▅▅█ + ▇▆ ▂▇▃▂▆▄▇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_11.txt b/codex-rs/tui/frames/hbars/frame_11.txt new file mode 100644 index 000000000000..5305252a8d14 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_11.txt @@ -0,0 +1,17 @@ + + ▆▇▇▇▇▇▇▂ + ▅█ ▄▁▁▁▃▃▆ + ▅█▂▂▁▁▄▃▇▅▃▆ + ▁▂▂▁▁▄▄▆▂▄▅▅ + ▂█▅▄▃▁▇▃█▄▂▁▁▆ + ▁▇▂▁▁▇▂▄▄▁▂▅▁▁ + ▇▇ ▆▁▁▁▁▁▁▂▁▄▃ + ▁ ▁▁▅▁▁▃▅ ▁█▁ + ▅ █▁▅▁▁▇▅▇▁▂▁ + ▇▇ ▂▁▅▄▆█▅▁▂▁▃ + █▂▆ ▁▁▁▁▄▅▁▃▃▁ + ▃▂▆▅▁▂▇▅▇▇▄▁▂ + ▂ ▇▁▃▃▃▂▁▄█ + ▃▇ ▇▁▁▆▂▅▇▂ + ▃▂ █▇▇▂▇▂ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_12.txt b/codex-rs/tui/frames/hbars/frame_12.txt new file mode 100644 index 000000000000..cebfe226e1ee --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_12.txt @@ -0,0 +1,17 @@ + + ▇▇▇▇▇▅ + ▆▄▂▂▅▁▆▃ + █ ▁▅▃▇▆ + ▅▇▁▁▁▄▄█▁ + ▁ █▁▁▁▁▅▅▁ + ▁█▅▅▇▃█▂▁█ + ▁ ▁▅▃▁▁▃ + ▁ ▅▇▃▁▁█ + ▇▆ ▁▅▁▁▇▁▁ + ▁ ▅▁▇▄▇▂ + ▁▅ ▁█▄▇▇▅ + ▆ ▇▁ ▂▆▁ + ▅ ▇▇▁▆▇▃▁ + █▃▅█▆▁▃▁▂ + ▃ █▁▃▅▅ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_13.txt b/codex-rs/tui/frames/hbars/frame_13.txt new file mode 100644 index 000000000000..566cc4ffa308 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_13.txt @@ -0,0 +1,17 @@ + + ▅▇▇▇▆ + ▁▂▂▁▁ + ▁▂▂▁█ + ▁▇▇▁▂ + ▇ ▁▁ + █▅▆▁▁ + ▆ ▁▁ + ▇ █▁ + ▅▇ ▂▁▅ + ▁█ ▇▄▁ + █▂▅▅ ▁▂ + ▁▂▂▂▁ + ▁▇▆▁▂ + ▇▂▂▁▄ + ▆ ▅█▄ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_14.txt b/codex-rs/tui/frames/hbars/frame_14.txt new file mode 100644 index 000000000000..380790e11c9d --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_14.txt @@ -0,0 +1,17 @@ + + ▇▇▇▇▄ + ▆▅▂▂▅▃ + ▁▂▁▁▅ ▁ + ▁▁▅▁█▃▁▆ + ▁▁▁▃ ▁ + ▁▇▁▁ █▁ + ▁▁▁▁ ▅ ▇ + ▁▁▃▁▂ ▃ + ▁▁▁▁ █ + █▃▅▅ ▄▅ + ▃▁██▆▅▆▁ + ▁▇▁▁▂▂▂▁ + ▅ █▁▄▄▄▂ + ▁▃▅▇▂▂▅ + ▂▇ █▄▅ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_15.txt b/codex-rs/tui/frames/hbars/frame_15.txt new file mode 100644 index 000000000000..47d169e98bc1 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_15.txt @@ -0,0 +1,17 @@ + + ▇▇▇▇▇▁▂ + ▂▆▄▁▁▃▅▇▆ + ▅▅▁▇▄▃▁█▇▁ + ▁▅▂ ▅▅▆▅▂▂▁ + ▁▁▄█▄▃▁▁▅█▃▁ + ▃▁▁▆▁▁▅▅▁ █ + ▁▁ ▁▁▁▁▁▁▆▁ + ▁▁▅▁▁▇▁▁▁ █▆▁ + ▁▁▇▆▆▇▁▁▁▂▅▅▁ + ▁▂▁▄▅▁▃▁▇▅ ▅▁ + ▁▅▁▁▁▁▂▁ ▂ ▄ + ▁▇▇▅▃▁▁▃▆ ▄ + █▁▃▆ ▅▅▅▂ ▄▂ + ▁▃▁▁▃▃▅▇▃▅ + ▃▃▇▃▂▂▂▅ + ▂▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_16.txt b/codex-rs/tui/frames/hbars/frame_16.txt new file mode 100644 index 000000000000..3b1fb1fc5d4b --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_16.txt @@ -0,0 +1,17 @@ + + ▂▄▇█▇▇▇▁▂ + ▅▃▁▁▇▁▁▃▁▄▃ + ▁▅▅█▃▅▄▃▁▁ █▃ + ▅▅▅ ▄▇▂▃▁▃▁▁▇▇▁ + ▁▁▄▂▅▇█▄▁▁▇▅▁▂▃ + ▁▁▁▇▁█ ▁▃▄ ▁ ▁▅▁ + ▃▃▁ ▅▂▆▁▁▁▁▅▇█▆ ▁ + ▁▅▄ ▁▂▁▁▅▁▁▁█▄ ▂▁ + ▃▃▁▁▁▇▁▅▃▁▁█▇▇ ▅ + ▇█▂▂▆▄▄▃▇▁▅▂▁ ▆▂▁ + ▁▁▁▇██▅▇▃▁▄█▄▅▁▁▂ + ▁▁▇ ▁▂ ▂▅▅▆ ▅ + ▃▁▇ ▁ ▅▆▅▂▆▁ + ▃▁▁█▂▇▁▅▅▇▂▁ + █▅▅▂▄▅▂▂▄▇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_17.txt b/codex-rs/tui/frames/hbars/frame_17.txt new file mode 100644 index 000000000000..93817e2eadde --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_17.txt @@ -0,0 +1,17 @@ + + ▆▄▇▇▇▄▄▁▆▂ + ▂▄▇▇▁▁▁▇▄▇▁▆█▃▆ + ▆▇▃▅▂▄▄▂▇▆█▁▁▁▂█▃ + ▅▁▅▂▅▁█▂▂ █▁▄▃▁▁▁▄▃ + ▁▅▂ ▁ ▁█▅▃▄▃▅▅ ▆ + ▂▄▇▆▅▂ ▆▅▅▁ ▅▆▄▅▁▅▅ + ▇ ▄▅▁ ▆▅▅▁▂ ▇█▁▁▅▄▁ + ▆█▄▁ ▅▅▁▁▅▁▆▁▁▄▄▇ + ▆▅▇▅▃▄▄▇▁▃▂▁▃▃▅▃▁▁▁ ▁ + ▁█▂▄▂▂▁▅▇▃▅▁▁▃ ▃▇█▇▃ + ▃▃▃▅███▇▇▇▇▃▂▁▇▅▅▃ ▃█ + ▃▁▁▂▇ ▂▅▁▁▂ ▄ + ▂▃▃ ▇▃▂ ▆█▆▅▃▂▅ + ▆ ▁▇▇▄▃▇▂▂▄▇▅▁█ + ▂▇▄▅▂▆▇▁▇▂▇▇ + ▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_18.txt b/codex-rs/tui/frames/hbars/frame_18.txt new file mode 100644 index 000000000000..03d2c5e94b83 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_18.txt @@ -0,0 +1,17 @@ + + ▂▄▄▇▄▄▇▄▁▆▂ + ▂▇█▂▄▆▇▇▁▂▂▆█▇▄▄▂ + ▂▅▅▁▇█▇▄█▆█▅▂█▇▅▅▃ ▇ + ▆▁▁▇▅▆▃█▇ █▃▂▂▅▄▁▃▄▃▃▃▆ + ▂▃▅▅ ▅▁ ▂█▇▂▃▅▁▅▁▂▃▄▃▃▂ + ▅▃▁ ▆▆▁▅▂▂▂▅▆▇▆▁█▆▅▃▅▂▅▁ + ▁▁▃▅▅ ▇▅▇▅▁█▂▅█▇▁▄▁▄▆ + █▃▆▁▁ ▃▃█▁▁▄▃ ▁▄▁█ + ▃██ ▆▃▄▄▄▄▄▇▁▁▆▁▇▅▃▆ ▁▇▁▂ + ▂▃▃▁▇▃▇ ▁▁▅▂▂▃▂▁▁▁▂▄▁▂▃ ▆ + ▁▅▁▃▂██▇▇▇▇▇▂ ▃▂▂▅▁▅▇▅▂▆▂ + ▃▃ ▆▃▆ ▂▂▇█▅▇▂█ + ▃▃▇▇▃▃▅ ▂▇▃▂▅▃▆▂ + █▁▅▇▇▇▇▄▄▄▃█▇▂▅▂▆▇ + █▁▁▂▅▂▂▆▅▇█▄▇█ + ▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_19.txt b/codex-rs/tui/frames/hbars/frame_19.txt new file mode 100644 index 000000000000..f82677617003 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_19.txt @@ -0,0 +1,17 @@ + + ▂▂▄▄▇▁▇▄▄▁▅▂ + ▂▇▁▁▁▃▅▄▄▄▂▇▂▆▁▇▆▆ + ▆▇▄▄▅█▁▇▇▂ ▂▇▅▆▂▇▄▃ + ▅▄▁▇▆▃▂ ▂▄▄▇▆▃▁▇▆ + ▁▁▅▂▅▂ ▆▁▃█▅█▁▃ ▁▃▆ + ▁▃▅▇█ ▂▁▇▁▅ ▅▅ █▅▇▁ + ▃▄▁▁█ ▅▇▃▅▁▇▆▂ ▅▅▅▇▁ + ▁▁▁▁ ▁▄█▃▁█▆ ▁ ▁▁▁ + ▁▇▁▃▆▄▄▄▄▄▄▁▁▁▁▇▇▄▇▁▄ ▃ █▁▁ + ▃▄▅▆▁▄▇▅▃▇▇▃▆▂▆ ▃▂ ▅▃▂▃▆▅▅▁▂ + █▃█▅▇▁█████▇▇█ ▃▃▂▅▁▅▅ ▅▅█ + ▃▃▃▇▂▃ ▂▅▅▂▅▅█ + █ ▂▃▁▁▇▆ ▆▄▄▅▅▁▄▂ + ██▅▁▇▅▁▇▂▂▄▄▅▇█ ▄▇▁█ + █▁▅▂▅▆█▂▆▅▃▇▆▃ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_2.txt b/codex-rs/tui/frames/hbars/frame_2.txt new file mode 100644 index 000000000000..d4efa4def0ec --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_2.txt @@ -0,0 +1,17 @@ + + ▂▅▂▅▄▇▄▄▄▇▆▂ + ▂▇▆▅▇▄▇▁▁▁▄▁▁▄▂█▁▇▇▂ + ▆▁▇▁▃▇▇▁▇▂▂ ▂█▇▄▇▅▁▁▄▁▆ + ▂▁▅▂▅▇█▆▆▂ ▆▆▃▁▅▁▆ + ▆▃█▅▇▃▁▃▂▃▃▂ ▃▆▁▃▁▆ + █▅▇█▂ ▁▇▃▇▅▃▇ ▃▃▁▇▁ + ▆▁█▇▁ ▃▅▁▄▁▂▁▆ ▅█▁█▁ + ▁█ ▃▂ ▆▁▇▁▁▄▇ ▁█▅▅▁ + ▇██▄▁ ▄▁▃▃▂▁█▅▁▇▇▇▇▇▇▇▇▆▁▂▁▁▇ + ▂▄█▁ ▅▁▁█ ▅▇ ▁▄▃▂▂▂▂▅▅▂▂▁▇▅ ▁ + ▃▃▃ ▇ ▇▃▅▅▁█ ▂█▇▇▇▇▇▇▇█▁▆▇▁▁ + █▃▂▅▅▆ ▂ ▆▇▄▅▆▇ + ▅▃▂▂▁▇▆ ▆▄▂▇ ▂▁█ + ▂▁▃▄▂▂█▇▇▅▄▄▄▄▅▄▇▂▂▁▁▇ + ▂▇▇▇▇▁▁▂▂▂▂▁▄▅▇█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_20.txt b/codex-rs/tui/frames/hbars/frame_20.txt new file mode 100644 index 000000000000..30c29f51c9bf --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_20.txt @@ -0,0 +1,17 @@ + + ▂▂▇▄▁▁▄▇▄▆▂▂ + ▆▄▄█▄▆▂▆▄▄▄▁▁▂▃▇▃▇▆ + ▆▆█▂▇▇█▁▇█ ▂█▃█▃▇▇▁▇▂ + ▂▁█▂▇▁▇ ▂▆▇▁▃▅▇▃▃ + ▂▁ ▆▁▇ ▂▅ ▂▁▁▃▇▃▁▁▃ + ▅▂▆▁█ ▇▇█▄▁█▅ █▃▁▁▃ + ▄▁▄▇▁ ▆▇ ▅▇▇▃ ▆▃▁▅ + ▁▂▃▅▂ ██▄▅▁█▆ ▁▁█▁ + ▁▅▁▁▄▆▄▄▄▄▄▄▄▄▂▆█▄▃▃▃▃▇ ▅ ▁▃ + █▁▃▁▂█ ▅▂▂▂▂▂▂▁▃ ▇▃▃█▁▄▃ ▆▅ ▁▁ + ▃▆▃▃████████▇█ ▃▆▄▁▁▆▁ ▅▃ + ▇ ▃▇▅▆ ▂▅▇▅▁▅ + ▃▂▃▇▆▁▂ ▆▄▇▂▄▇█ + ▃▆▆▅█▁▇▃▄▄▆▅█▁█▂▆▅▇█ + ▂▇▇▁▂ ▄▂▂▂▂▁▇██ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_21.txt b/codex-rs/tui/frames/hbars/frame_21.txt new file mode 100644 index 000000000000..b6a6c2c109cb --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_21.txt @@ -0,0 +1,17 @@ + + ▂▂▆▄▇▄▄▇▄▇▆▂▂ + ▆▇▇▂▂▂▂▇▁▄▁▇▄▂▂▆▇▇▂ + ▄▇▃▂▇▇▃█▂ ▂▃▁▇▇▂▂▇▆ + ▆▇▁▄██ ▂▆▄▇▆▄▇▆ + ▅▆▅▇▂ ▆▁▅▂▃ ▃▃▂▃▆ + ▅█▅▁ ▆▇▇▂▅▅ ▃▃▃▆ + ▁█▁▂ ▂ ▅▂▆▅█ ▃▄▃█ + ▂▃ ▃▄ █▁▆ ▁ ▁ + ▁ ▁ ▆▇▄▄▄▄▄▇▇▇ ▃▁▆█▁▆ ▄▅ ▁ + ▁ ▁▃ ▁▂ ▂▆▁ ▃█▂▇▁▆ ▅█▅▅ + ▄ █▅ ▂▂███ █▂ █▇▂▆▄ ▅▂ ▆ + █▇ ▅▄▂ ▂▇█ ▅ + ▇▂ ▃▆▂ ▂▅▄▇█ ▆▂ + ▇▄▂ ▇█▇▇▅▁▁▄▅▄▇██ ▆▂ + ▇▇▇▄▇▂▂▂▂▆▂▄▇▇▂ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_22.txt b/codex-rs/tui/frames/hbars/frame_22.txt new file mode 100644 index 000000000000..38195cd38b3f --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_22.txt @@ -0,0 +1,17 @@ + + ▂▆▇▄▇▄▄▇▄▇▆▂ + ▂▇▅▇▁▅▂▅▁▅▃▁▁▄▁▇▅▇▇▂ + ▆▄▇▄▁▃▇▇▂ ▂█▇▃▇▇▇▂▂▇▆ + ▄▇▁▇▇█ ▆▆▁▁▁▂▇▃ + ▃▁▁▅█ ▆▂▅▁▄▁▅▅▄█▁ + ▃▁▇▅▂ ▆▁▅▁▄▃▅▂ ▃▁▃▄▁ + ▁▇▇▃▅ ▁▇▁▃▇▅█ ▁▁▅▁ + ▁▁▄▁▂ ▁▃▁▁▃▃ ▁▃▁█▁ + ▁▁▆█▆ ▇▁▄▄▄▄▄▄▇▇▂▃▇▁▂ ▃▆ ▂▁▄█ + ▃▁▄ ▇▂▃▃▆ ▅▆█▁▅▅▁ ▃▇▄▂█▁▆▆ ▅▃▄ + ▃▃▃▂▁ █████▇▇█▂ ▂▅▇▇▂▅▅▅█▆▂ + ▃▇▃▂▃▆ ▂▇▆█▁▆█ + ██▇▄ ▇▇▂ ▂▅▇▆█▇▂▅ + █▅▇▇▆█▇▁▄▄▇▄▄▅▄▇███▆▇ + █▁█▁▂▄▂▂▂▆▄▂▅▄▇▂ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_23.txt b/codex-rs/tui/frames/hbars/frame_23.txt new file mode 100644 index 000000000000..a81cac3ef209 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_23.txt @@ -0,0 +1,17 @@ + + ▂▆▇▂▄▇▇▇▇▄▇▂▂ + ▂▄▁▇▁▃▂▆▂▄▄▆▁▂▁▇▁▇▂ + ▆█▇▁█▆▄▇▂ █▁▅▇▁▃▄▁▁▆ + ▅▄▁▇▄▅▂ ▂▄▄▃▁▁▄▃ + ▅▅▁▂▅█ ▆▅▆▆█▇▅▇▃▁▃ + ▆▃▁█▅▂ ▂▇▇▇██▅▇█▁▁▃▁▆ + ▂▁▁█▁ ▅▅▅▅ ▂▁▂ █ ▁█▇ + ▁▅▁ ▁ ▁█▁▆▂▃▆ ▁ ▇▅▁ + ▃█▃ ▁▇▁▄▄▄▄▄▄▄▄▁▂▇▁▂█▃▂ ▁ ▁▁▁ + ▁▃▃▂▁█▄█ ▅▂▃ ▃▁▂▃▆▁ ▃▂▁ + ▅█▁ ▁ ▂███████ █▂▅▄▄▁▂▅▃▅▂ + ▆▁▁▂▁▅ ▆▅▁▃▃▅▂ + ▃ ▁▆█▅▂▂ ▂▅█▄▇▅▇▅ + ▃▄▃▇▄▇▇▃▁▄▄▂▇▇█▆▂▇▇▂ + █▁ ▇▄▃▁▂▂▇▂▂▇▇▂ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_24.txt b/codex-rs/tui/frames/hbars/frame_24.txt new file mode 100644 index 000000000000..791f93b59144 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_24.txt @@ -0,0 +1,17 @@ + + ▂▁▂▄▇▇▇▄▄▆▂ + ▆█▂▅█▁▂▂▁▄▄▇▆▂█▇ + ▆██▄▃▅▇█▁ ▅▂▅▇▁▇▇▆ ▃ + ▆█▅▅▆▅▁▄▆▃▂▁▅▂ ▂▇▃▁▃▂█ + ▆ ▅▅▃▅▆ ▂██▅▃▂▁▂▂▃▁▆█ + ▁▆▃▇▃▂ ▇▆▆▅ ▅▁▄ ▅█▃ + ▁ ▁▁ ▆▅▂▆▅█▅▃▃▁▃▃▁▃ + ▁ ▃▁▁ ▁▆▄▅▃▂▁▁▂▅▅▂▁▁ + ▁▅ ▁▄▄▄▅▇▇▁▁▁▇▇▂▇▆▂▁ ▁ ▁▁ + ▂ ▅ ▇▁▅▆▆▄▄▆▆▁▅▃▃▇▇▁ ▁▇▁ + ▃ █▃▁▃▇██████▂ ▃▆▅▇▃▁▄▅▆▂ + ▃ ▃▆▇▃ ▁ █▅▄▅ + ▃▅▇▃▂▁▅ ▂▄█▂▅▅▂▇ + ▅▆▇▆▃▃█▁▁▅▄▄▄▁▃▂▅▂ + ▇▅▂▇▇▄▃▂▂▄▄▇▇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_25.txt b/codex-rs/tui/frames/hbars/frame_25.txt new file mode 100644 index 000000000000..565fdb82ead0 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_25.txt @@ -0,0 +1,17 @@ + + ▂▇▄▇▄▇▇▇▇▂ + ▆ ▇▆▇▂▂▄▃▁▇▁▁▆ + ▅▁▅▅▆▆█▅▁▇▃▄▃▁▁▁▂ + ▁▇▇▁ ▅ ▄▁ █▃▁▆▇▃▅ + ▆▂▂▃▆▅█▃▄▅▂▁█ ▁▅▅▃▅▇ + ▃▆▁▁▁▂▇▇▅▃▁▅ ▅▁▆▁▅▅▁▆ + ▁▅▁▇▁ ▁▂▂▅▃▂▅▁▃▅▂█▁▄▁ + ▁▁▁▅▁▅█ ▁▂▄▃▅▁▃▂▁▄▁▁▃ + ▃▇▃▁█▁▂▂▄▄▄▄▇▁▁▃▃▁▇▇▁▃ + ▅▆█▁▅▁▆▁▂▁▁▄▇▂▁▃▇▄▅▃▁ + ▃▁▇▅▃▁▂██████▇▅▁▃▁▅▁▂ + ▁▂█▁▄▇▄▃▆▆▇▅▂▅▆▅▅▁▅ + ▃▂▃▁▂▃▂▇▄▅▆▃▅▂▁▁▅ + ▃▅▄▃▃▂▇▄▁▇▇ ▇▁▇ + ▁▆█▇▃▇▆▂▇▁▇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_26.txt b/codex-rs/tui/frames/hbars/frame_26.txt new file mode 100644 index 000000000000..e37d671dc4be --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_26.txt @@ -0,0 +1,17 @@ + + ▂▄▇▇▇█▇▆ + ▅▇▁▄▅▃▁▇▄▁▆ + ▅▇██▁▄▃▁▃██▃▆ + ▄▇█▆▁▁▁▂▁▁▂▇▅▃ + ▁▂▆▃▁▅▁█▇▃▅▄▁▃▄ + ▃ █▁▅▅▅█▇▂▃█▃▃▅ + ▁ ▇█▄▁▁▅▁▁▄▄█▇ + ▅▆ ▃▁▁▁▃▃▄▁▁▅▁▁ + ▁ ▅▃▇▄▇▁▇▄▄▅▃▁▁ + ▅▂▁▅▆▁▂▂▆▇▃▃▇▁ + ▁▂█▅▁▇█▃▆▅▂▇▃▁▁ + ▃▂▁▁▅▃ ▅▅█▁▁▂ + ▇ ▇▁▃ ▃▂▅▁▅▆ + ▁█▇▃▃█▇▂▇▇▄ + ▃▅▅▃▇▁▂▃▇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_27.txt b/codex-rs/tui/frames/hbars/frame_27.txt new file mode 100644 index 000000000000..d3dbefa97544 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_27.txt @@ -0,0 +1,17 @@ + + ▄▇▇▇▇▆ + ▁▄▅▅▁▅▃ + ▄█▂ ▇▁▅█▁ + ▁▁▁▃▁▁▄▁▁ + ▁ ▂▁▂▆█▄ + ▁▂▅▂█▂▁▁▁▂ + ▄ █▁▁▂▇▁▁ + ▁ ▂▁▁▁▄█▁▁ + ▁▅ ▅ ▁▇▁▁▁▁ + █▄ ▅█▂▁▂▇▁▁ + ▁▆ ▂▃▇▇▇▅█ + ▁▁▇▇▁▁▂▅▁ + ▁▄▄▇▄▆▅▁▃ + ▃▂▂▁▃▄▅▅ + █▃█ ▃▃▅ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_28.txt b/codex-rs/tui/frames/hbars/frame_28.txt new file mode 100644 index 000000000000..0ae0f54e0b06 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_28.txt @@ -0,0 +1,17 @@ + + ▅▇▇▄ + ▁▇▂▁▂ + ▄ ▄▁▂ + ▁▆▂▇▁ + ▁ ▁▁ + ▁ ▆█▁ + ▃▁▁ ▂▁ + ▁▆██▁ + ▅▃ ▂▁ + ▁▁▅▅▁▁ + ▁ ▂▁ + ▁▄▇▄▁ + ▁▄▇▁▁ + ▇▂▂▅▁ + ▁ ▅▇▁ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_29.txt b/codex-rs/tui/frames/hbars/frame_29.txt new file mode 100644 index 000000000000..d333f278dce9 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_29.txt @@ -0,0 +1,17 @@ + + ▇▇▇▇▇▆ + ▆▁▇▅ ▇▃ + ▃▆▁▁ ██▁ + ▁▁▁▁▃▃▆▅ + ▃ ▁█▁ ▁ + ▁▆▁▁▁ ▂▂ + ▁▃▃▁▁▁ ▃ + ▁▁▄▁▁ + ▁▁▁▇▁ ▂ + ▂▁▄▁▁ ▂ + ▇▁▁▅▁▃ ▄ + ▁▅▁▁▁ ▂▁ + ▁▁▁▁▂▁▂▁ + ▁▄▁▇▅█▁█ + ▁▁▇ ▅▆ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_3.txt b/codex-rs/tui/frames/hbars/frame_3.txt new file mode 100644 index 000000000000..5d0b07202ae9 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_3.txt @@ -0,0 +1,17 @@ + + ▂▅▄▄▇▇▄▄▄▄▆▂ + ▂▇▆▅▃▁▁▇▁█▄▄▁▄▂▃▁▇▇▂ + ▆▄▅▇▄▁▇██▂ ▂█▁ ▇▇▇▅▃▁▆ + ▅▇▅▃▁▄▆▇▂ ▂▄▇ ▁▁▃ + █▅▇▁▅▃▅▁▁▇▃▂ ▃▃▃▁▁▂ + ▅▁▃▃▅ ▅▆▁▃▄▃▃ ▁ ▃▃▁ + ▄▇ ▁ █▇▃▁▂█▁▆ ▅▇▅▁▆ + ▆▆▁▆▁ ▆▆▁█▅▃▁ ▁▂▁▄▁ + ▆██▁ ▄▁▇▇▁▁▇▆▁▄▇▇▂▂▂▇▇▂▁▃█▄▁ + ▆▃▅▃▆ ▅▅▇▅▄▄▁ ▁▇▁▂▂▆ ▄▅▆▄▃▂▁▃▁▂ + █▃▁ ▁ █▁▁▇▆█ █▇▇▇███▇▇▆▅▅▅▆▅ + █▇▁ ▃▇ ▂ ▅▃▇▃▅▅ + ▄▃▅▅▇▃▆ ▆▇▄▅█▆▁█ + ▄▃▇▆▂▇▇▃▄▄▄▄▆▅▄▇▄▂▂▇█ + ▂▇▇▇▂▅▁▂▂▂▂▁▄▅▃█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_30.txt b/codex-rs/tui/frames/hbars/frame_30.txt new file mode 100644 index 000000000000..7ceb36d37ac9 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_30.txt @@ -0,0 +1,17 @@ + + ▂▄█▇▇▇▆ + ▂▅▄▁▁▃▇█▄▃ + ██▃▁▆▃▃▁▇▆▃ + ▁▁▃▁▅▁▂▃▁▃ ▃▆ + ▄▇▁▁▂▃▁▆▁▆ ▁ + ▁▇▁▃▇ ▇▁▁▁▁▅▁ + ▃▅▁▁▁▃▂ ▃▅▃▁ ▄▂ + █▁ █▇▄▁▆▁█▁▆ ▂ + █▁▂█▃▁▄▁▃▅▁▆ ▂ + █▁▁▃▁▂▂▁▁ ▇▆ ▂ + ▃▁▁▄▃▂▇▁▁▁▇▁▁ + ▁▇▅▅▆▅▇▃▁▁█ ▁ + ▃▁▃▁▁▅▇▁▁▆▁▅ + ▆▃▆▇▄▃▅▁▆▅█ + ▃▆▅▁▃▂▂▄█ + ▂▂▂▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_31.txt b/codex-rs/tui/frames/hbars/frame_31.txt new file mode 100644 index 000000000000..419be30ed969 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_31.txt @@ -0,0 +1,17 @@ + + ▂▅▅▇▇▇▇▄▆ + ▂▇▇▄▇▆▂█▇▃█▇▆ + ▆▁▃▅▅▆▄▂▂▅█▁▂▇▃ + ▆▁▅▇▂▂▇▄▂▆▃▃▁▃▁▁▃ + ▃▁▁▁▁▇▆▇▃▂▆█▃▁▅▂▂ + ▄▁▃▂▁▂▁██▃▄▇▆▅▅▁▆█▁ + ▅▁▁ ▁▃▇▁▃▅▄▅▄▇▁▇▁▅▁ + ▃▃▆ ▁▁█▁▃ ▁▃██▁▁▃▅▁ + ▁▃▆▂▁█▃▁▁▁▅▇▁▁▁▂ ▁ + █▁▁▁▅▅▁▁▂▂▂▃▂▁▃▁▁ ▇ + ▃▇▂▅▇▁▆ ▅▇▇▇▂▅▁█▁▂ + ▁▁▁▃▃▃▆ ▂▄▇ ▁ + ▄▁▇▄▂ ▆ ▅ ▁▇▂▅▂ + ▅▁▃▁▅▂▁▂▆▁█▆▁▂ + ██▄▁▂▇▇▅▅▄▇ + ▂▂▂▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_32.txt b/codex-rs/tui/frames/hbars/frame_32.txt new file mode 100644 index 000000000000..1234a419b0c7 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_32.txt @@ -0,0 +1,17 @@ + + ▆▇▇▇▇▄▄▁▆▂ + ▄▇▁▇▇▁▃▁▁▇▇▁▁▆ + ▅▃▇▄▃▆▇▃▅▆▂▇▁▇▃█▇▆ + █▅▅▁▂▆▂▄▆ ▆ ▆▂▃▁▇▆ ▆ + ▅▅▁▆▁▇▃█▇▁▄ █▂▆▁▃▅▃▇▃▆ + ▃▅▃▃▆▃▇▁▂▅▁▁▇▂ ▁▇▅ ▇ + ▇▁█▄▆▂█▃▁▁▆▆▁▆▄▂▅▂▅▅▁█▂ + ▁▁▁▆▂ █▂▁▁█▅█▁▄▆▂█▃▁██▂ + ▃▁▁▆▂ █▂▁█▂▂▁▇▇▇▁▁▁▄▁█▄▂ + ███▂▂▁▇▃█▃▂▂▂▂▂▁▁▇▁▄▁▆▂ + ▆▃▇▁▁▅ ▁▇██▇▇▇▃▄▅▅▅▅▅ + ▃▆▁▃▃▃ ▆▃▁▅█▁ + █▇▇▁▁▁▇▂ ▂▅▂▁▅ ▆ + ▃▆▃█▁▇▁▄▄▇▂▂▇█▂▅ + ▁▄▄▂▂▆▇▅▇▂██ + ▂▂▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_33.txt b/codex-rs/tui/frames/hbars/frame_33.txt new file mode 100644 index 000000000000..780eb104ef3e --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_33.txt @@ -0,0 +1,17 @@ + + ▂▆▇▄▇▄▇▄▁▅▂ + ▂▇▇▁▁▃▇▄▁▅▁▆▁▇▁▇▅ + ▂▅▃▁▇▇▇▆▄█▂ █▇▅▁▁▇█▆ + ▆▃▅▁▅▂▁▆▂ ▇▁▁▃▂▇ + ▂▅▁▅▂▁▁▅▃▃▃ ▁▇▃▃▃ + ▅▅▆ ▃▂▃▃▂▃▆ ▅▃▅▂▃ + ▁▁▇ ▃▆▇▁▂▁▃ ▁▇▁▁ + ▇▁▆▆ ▆██▃▅▄▆ ▃█▁▇ + ▁▁▁ ▁ ▆▂ ▅▇▆▆▆▄▄▄▇▁▁▇▅▂▁▃▁ + ▃▅▁██▅▅▆▅▂▁▁▂▂▂▂▂▁▁▇▃▃▃▁▁▅ + ▃▄▃▆ ▁▅▁▅ ▅ ████▇▇▇ ▁▁▆▅▃▂ + ▃▇▃▇▆█▆ ▅▅▄▅▄▅ + ▇▃█▇▆▇▄▆ ▂▇▁▆▅▇▃█ + ▃▁█▇▃▇▁▆▄▇▂█▄▆▃▇▁▇ + █▄▄▇▆▂▂▂▃▇▇▄▆█ + ▂▂▂▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_34.txt b/codex-rs/tui/frames/hbars/frame_34.txt new file mode 100644 index 000000000000..4bf69e69eb4f --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_34.txt @@ -0,0 +1,17 @@ + + ▂▆▄▇▇▇▇▄▁▆▂ + ▂▇▇▇▅▃▄▁█▁▇▇▄▇▇▄▆▄▂ + ▂▅▅▆▁▇█▅▁▇▂ ▂▃▃▃▆█▇▇▆ + ▆▅▂▁▇▂▅▆▂ ▇▁▆▇▃▃ + ▅▅▁▃▆▅▁ ▁█ ▆ ▃▃▇▃▃ + ▆▅▃▁▂▅ ▃▃█▁▃▇▃ ▃▆▃▁▃ + ▁▇▃▁▂ █▃▂▇▁▂▃ ▁▄▁▂ + ▃▁▄█▅ ▁▇▂▁▅█▁ ▅ ▂▃ + ▆█▅▅█ ▅▃▅▅█▂▄▄▇▇▇▇▇▇▁▂▂█ ▁▃▂ + ▁▁▃▆▅▆▆▃█▅▅█▁▁▁▂▁▅▂▂▂▆▅▂▁▁▂▁▃ + ▅▂▃▆ ▁▃▁▇▃ █▇▇▂▂▂▂▃ ▆▅▆▃▁█ + ▅▆▃▄▄▃▂ ▅▄▅▁▅█ + ▃▇▂▁▇▃▄▆ ▂▇▄█▂▅▆▂ + ▆▃▅▁█▁▇▆▄▆▇▄▇▄▇▂▇▇▅█ + █▁▃▄▄▂▂▂▂▂▄▄▇▄▃ + ▂▂▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_35.txt b/codex-rs/tui/frames/hbars/frame_35.txt new file mode 100644 index 000000000000..86dde2ad3418 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_35.txt @@ -0,0 +1,17 @@ + + ▂▆▇▄▄▇▇▇▄▇▆▂ + ▆▇▄▇▅▁▇▇▇█▁▇▁▅█▁▇▄▂ + ▂▅▅▁▄▇▇▁▃▇█▂ ▂█▇▄▇▅█▁▇▆ + ▆▅▅▇▅▆▄▆▂ ▇▇▂▃▁▂ + ▅▅▁▁▅▅▃ █▃▇▆ ▁▃▇▁▆ + ▄▅▄▅▇▂ █▁▂█▅▅▇▂ ▁▃▃▁ + ▃ ▅▅ ▃▃ █▇▂▅ ▁▃▃▃ + ▁▁▁▅ ▅▃▃▄▅█ ▅ █▅ + ▅ ▆▂ ▆▁▁▅▄█▅▁▄▄▂▄▂▄▇▁▁ ▁▁▄▇ + ▁▆▃▄█ ▅▅▄█▇▇▅▁▄ ▃▆▂▂▂▂▅█▁▅█ ▁█ + ▅▂▃▆█▅▃▂▁▅▃▂ ▂█▃▃▃▃▇▃▃▇▅▅▂▆▁ + █▅▃▃▅▁ ▂▅▇ ▆▃ + ▃▄▂▃▄▆▅▆ ▆▄▅▂▇▇█ + █▃▂█▁▇▁▆▅▁▁▇▁▄▅▇█▂▅▄█ + █▃▁▄▄▂▂▅▂▂▇▇▇█▃ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_36.txt b/codex-rs/tui/frames/hbars/frame_36.txt new file mode 100644 index 000000000000..bccadcf7b782 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_36.txt @@ -0,0 +1,17 @@ + + ▂▂▇▄▇▇▇▇▄▇▆▂ + ▂▄██▃▇▅▄▅▇▃▇▄▇▇█▇▇▇▂ + ▆▃█▃▆▇▇▆▇█▂▂ ▂█▇▁▇▇▁▆▇▁▆ + ▂▁█▇▇▂▂▂▂ █▇▁▃█▁ + ▆▅▂▄▃█▁▃▂▃▆▅ ▃▃▅▇ + ▅ ▆▅▂ ▇▂ █▁▅▆ █▃▃▇ + ▁ ▄ ▂ ▃ █▃▃ ▁▃▁▆ + ▁▁▃▁ ▂▃ ▂▅▅ ▃▁ ▁ + ▁██▁ ▅▅ ▆▇██▆▇▇▂▇▇▇▇▇▂ ▁▃▁▁ + ▂▇▁▃ ▆▁▂▂▅▁▅ ▂▁▂▂▂▂▂▂▂▁▁ ▃▅█▂ + █ ▇▁▃ ▇▂▇▅▃ █▇▇▃▃▃▃▃█ ▁▆█▅ + █▆█▃▄▅ ▆▅▁█▅ + ▃▅▁▁▇▁▆ ▆▇▇█▅▆▂ + ▇▆▂█▇▇▄▃▄▁▂▇▁▄█▆█▆▄▇█ + ▂▇▇▅▁▂▆▆▂▆▆▇▇▇▇█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_4.txt b/codex-rs/tui/frames/hbars/frame_4.txt new file mode 100644 index 000000000000..5867215a96d4 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_4.txt @@ -0,0 +1,17 @@ + + ▂▅▄▄▇▄▄▇▄▇▆▂ + ▂▆█▇▇▁▅ ▁▃▄▅▇▄▁█▅▇▆▂ + ▆▇▄▁▁▁▇▇█▂ ▂▃▁▃▃▇▁▅▃▃▂ + ▅▅▁▆▁█▅▇▆ █▁▇▇▃▇▃ + ▅▁▅▂▁▄▁▃▄▃▁▆ ▃█▃▁▇▃ + ▅▁▁▃▁ █▂▁▁▅▇▃ █ ▃▄▁▆ + █▃▁▄▂ █▇▇▃▇█▁▆ ▆ █▄▁ + ▁▅▃ ▆▁▅▇▃▁▁ ▁▁▁ + ▂▁ ▁▂ ▄▁▅█▃▅▅▇▁▇▇▇▂▂▂▂▆▆ ▁▁▇ + ▁▃▅ ▁ ▆▅▅▅▆▂▁▂▄▃▁▆▅▂▆▅▅▃▄▁▅▅█▁ + ▂▁▄█▃▆▃▃▆▃▅ █▇▇▇▇▇▇▇█▃▆▁▁▁▂ + ▆▁▃█▁▂▂ ▆▃▄▅▇▇█ + ▃▃▆▅▃ ▆▂ ▂▅▃▆▇▄▁▅ + ▁▁▁▄▂▇▇▃▄▄▄▄▆▇▄▇▇▃▁▇▂ + ▇▃▃▁▅▁▂▂▂▂▂▄▆▇▂ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_5.txt b/codex-rs/tui/frames/hbars/frame_5.txt new file mode 100644 index 000000000000..d0cd750b8a71 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_5.txt @@ -0,0 +1,17 @@ + + ▂▅▄▄▄▇▄▄▄▇▆▂ + ▂▇█▇▅▇▅▁▁▇▁▄▇▄▂▁█▇▆ + ▇▂▅▃▇▁▇█▂ █▃▄▁▇▃ ▆▇▆ + ▆▁▅▁▅▁▅▄▆ ▆▃▁▁▁▁▂ + ▆▃▅▇▅▃▂▇▁▂▃▆ ▂ ▃▁▃▁▂ + ▁▁▁▁▅ ▃▂▁▃▄▅▁▂ ▇▅▁▁ + ▁▅▁ ▇ ▂▃▂▁▃█▁▆ ██▁▇▁▆ + ▁▅▁▂█ ▆▂▄▁▅▂▁ ▇▁▂▁ + ▁▆▁▃▇ ▅▂▁█▆▁▇▄▂▁▇▁▂▂▂▇▆▂█▁▅▁ + ▂▃▁▃▆▆▅▇▇█ ▁█ ▅▅▃▄▃▆▄▅▆▅▁▅▇▁▂ + ▃▆▁▁▆▁▆▃▃▅▇▂ █▇▇█████▆▁▄▁▃▅ + ▃▃▇▅▃▃ ▂ ▅▂▅▇▆▃ + ▃▅▇▃▁▄▇▂ ▂▄▇▆▇█▄▇ + ▃▁▇▁▆▇▇▃▄▁▄▂▆▇▄▇▃▁▄▇ + ▃▆▇▂▇█▂▂▂▆▂▆▇▇▂ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_6.txt b/codex-rs/tui/frames/hbars/frame_6.txt new file mode 100644 index 000000000000..2fde73afab15 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_6.txt @@ -0,0 +1,17 @@ + + ▂▅▄▄▄▇▄▄▇▇▂▂ + ▆▃▄▅▇▁▄▇▇▁▇▇▄▂▆▇▇▂ + ▆▆▆▁▁▁▇█ ▂█▁▅▄▇▇▇▅▃▂ + ▅▆▁▇▁▇▂▄▆ ▃▇▁▁▁▁▆ + █▄▁▁▁▇▃▃▇▇▃ ▅▃▃▁▃▆ + ▄▆▅▁▅▂▃▇▄▁▃▂▁▆ ▅▄▃▅▁ + ▂ ▇▁▁ ▃▃▁▅▃▃ ▃▇▁▂▁ + ▁ ▁▁▁ ▂▁▄▁▅▁▁ ▁▇▃▁▁ + ▂▅▁▆▁ ▅█▁▁▇▄▅▇▂▁▇▁▂▂▇▇▂▅▃▅▁ + ▁▁▃▆▅▂▃▂▄▇ ▁▅▄▇▆▁▁▆▄▄▄▃▁▆▁▃ + ▅ ▁ ▅▁▁▁▄▅█ █▇████▇█▂▃▃▅▁ + ▆ ▄ ▅▆▂ ▆▅▂▅▅▁▂ + ▅▂▃▂▇▇▆ ▂▇▃▂▅▇▆▇ + ▂█▁▇▆█▇ ▄▁▂▂▆▇▇▇ ▅▄█ + ▃▆▄▇▇▆▂▂▂▂▂▆▇▇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_7.txt b/codex-rs/tui/frames/hbars/frame_7.txt new file mode 100644 index 000000000000..f9b4ed92190a --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_7.txt @@ -0,0 +1,17 @@ + + ▂▅▁▄▇▇▄▄▇▆▂ + ▆▇▂▇▃▁▇▇▇▄▄▃▄ ▂▇▆ + ▇ ▆▁▁▁▇▂ ▂▃▁▂ ▇▅▂▇▆ + ▃▃▁▁▁▁▄▇ ▃▁▃▇▁▅▆ + ▂█▁▇▁▁▃▁▅▃▆ ▅ ▇▁▁▃ + ▁ ▄▁▁█ ▂█▃▄▁▃ ▆▆▁▁▁ + ▂▃█▁ ▃▂▁▃▁▃▁ ▇▇▃▁▁ + ▂█▅▁▁ ▄▇▅▁▇▁▂ █ ▁▁▁ + ▅█▁▇ ▆█▂▅▁▃▅▁▁▄▄▂▁▁▄▄▇▃▁▁ + ▁▆█▁▁▄▅▁▁▅█▁▅▃▁▁▃▃▆▄▆▄▃▁▃▁ + ▃ ▄▃▅▇▁▇▂▅ ▇▇▇▇▇▇██▂▅▁▁ + █▂ ▅▂▅▂▂ ▆█▆▅▄▅█ + █▄▆▇▃▃▃▂ ▆▃▁▆▇▅▇█ + ▃▂█▃▁▇▃▄▁▂▂▂▇▇▁▇▇▅ + █▁▇█▁▅▁▂▂▆▂▄▇▇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_8.txt b/codex-rs/tui/frames/hbars/frame_8.txt new file mode 100644 index 000000000000..44c448de8a3f --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_8.txt @@ -0,0 +1,17 @@ + + ▂▂▇▂▄▇▇▄▇▆▂ + ▆█▇▅▁▁▁▇▄▄ ▁ █▇ + ▂▁▄▆▁▅▅▇ ▆█▁▅▁▇▃▇▃▆ + ▂▇▃▇▅▁▆▇▁ █ ▄▃▁▁▃ + █ ▅▁▅▆▃▁▅▃ ▅█▃▁▁▆ + ▁█▆▁▃▃▃▂▃▁▂▇ ▃▇▁▁▁ + ▁▇▁▁▁ █▂▄▃▁▂▁ ▁▃▁▅▁▆ + ▃▆▁▁▅ ▁▇▃▅▁▆▇▅ ▅▂▅▁▁ + ▃ ▃▁▁▂▃ ▅▇▄▁▁▅▇▆▇▆▆▃▁▁▁ + ▃▇▅▄▂▁█▆▁▆▁▁▄▁▄▂▂▁▁▄█ + ▅▅▃▃▁▃▁▁▁▅█▃▇▃▇█▃▇█▅▃▁ + █▆█▁▃▁▂▂ ▅▆▅▅▅█ + █▃▂▇▃▃▃▂ ▂▅▃▂▁▇▅▇ + ▅▅█▁▆▇ ▄▄▇▇▄▅▄▆▁▂ + ▁▁▂▇▇▃▁▂▆▇▅▄▂ + ▂▂ \ No newline at end of file diff --git a/codex-rs/tui/frames/hbars/frame_9.txt b/codex-rs/tui/frames/hbars/frame_9.txt new file mode 100644 index 000000000000..a18a8a231c35 --- /dev/null +++ b/codex-rs/tui/frames/hbars/frame_9.txt @@ -0,0 +1,17 @@ + + ▅▄▃▇▅▇▇▄▆ + ▅▇▂▅▁▁▇█▄ ▂▆▃▂ + ▅ ▆▁▁▅▇▃▃▄▅ ▃▂▁▆ + █ ▁▁▅▄▄▂ ▂▃▆▁▃▃▁▇▆ + ▅▇█▄▃▇█▁▇▆ ▇▅█▇▅▇▁▁ + ▁ ▁▁▁█▃▁▃▄▂▂ ▁█▁▇▁▁▁ + ▂ ▁▇▃▁▇▇▁▃▃▆█▅▆▅ ▂▁▁ + ▁ ▁▅▂▆▃▁▁▁▂▅▄▃▂▁ ▁▁ + █▆▁█ ▅▁█▁▁▁▁▇▁▁▆▁▁▂ + ▁▂▁▁▁▂▆▁▃▁▂▁▁▁▁▂▂▁▁▁ + █ ▂▃▅▃▃▁▅█▇▇▇▇▁▁█▅▃▁ + ▃▁▁▁▃▆▂ ▆▅ ▃▅▁▂ + ▃▇▃▁▃▃ ▅▄▂▄▁▅▇ + ▃▆█▃▂▁▇▅▇▄▄▄▇▁▂ + █▆▂ ▇▃▃▁▄▇▅█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_1.txt b/codex-rs/tui/frames/openai/frame_1.txt new file mode 100644 index 000000000000..1019a11c9581 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_1.txt @@ -0,0 +1,17 @@ + + aeaenppnnppa + anpeonpepnniina aopa + pioipoooaa aooaoiniiip + noanooppa eoinip + naneoainann oeinnp + io pa ioeniip oeniip + paopo onioeoia iei + iiaia peeinio oai + inioa niianioeippppppapp no i + aino anpa eo ioiaaaeepppiepepi + naaoa anpeea aaoooo aooepnae + oap op a poeoai + anpanpa anapiapo + aopna opennnnenopapio + aoooiiiaaapanpoo + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_10.txt b/codex-rs/tui/frames/openai/frame_10.txt new file mode 100644 index 000000000000..942f59e944f4 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_10.txt @@ -0,0 +1,17 @@ + + apooonppa + ooapopnieaop + aapiieiipipnpp + iaaeninaenpoonnp + e niioia opeaeie + ia oioieen aaoi + n ioooiiiio iep + o iinpepiipaaenii + o in nniniipnpnii + i pinpiaeaoaaaioa + p oiiiiioo nponia + naopnpoo aioopee + io iiiiopepeiea + naooeipna eeo + op aonapno + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_11.txt b/codex-rs/tui/frames/openai/frame_11.txt new file mode 100644 index 000000000000..ef0aff76e0f2 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_11.txt @@ -0,0 +1,17 @@ + + pooooppa + eo niiinnp + eoaaiinnoenp + iaaiinnpanee + aoennipnonaiip + ipaiipanniaeii + oo piiiiiiainn + i iieiine ioi + e oieiioepiai + oo aienpoeiaia + oap iiiineinni + napeiaoeoonia + a oinnaaino + np oiipaeoa + na oopapa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_12.txt b/codex-rs/tui/frames/openai/frame_12.txt new file mode 100644 index 000000000000..8940e05bd671 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_12.txt @@ -0,0 +1,17 @@ + + pooope + pnaaeipn + o ienpp + epiiinnoi + i oiiiieei + ioeeoaoaio + i ieniin + i epniio + op ieiipii + i eionoa + ie ionooe + p oi api + e ooiponi + oaeopinia + n oinee + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_13.txt b/codex-rs/tui/frames/openai/frame_13.txt new file mode 100644 index 000000000000..c73afab740d6 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_13.txt @@ -0,0 +1,17 @@ + + eooop + iaaii + iaaio + iooia + o ii + oepii + p ii + p oi + ep aie + io pni + oaee ia + iaaai + ippia + oaain + p eon + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_14.txt b/codex-rs/tui/frames/openai/frame_14.txt new file mode 100644 index 000000000000..8a273a1666ac --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_14.txt @@ -0,0 +1,17 @@ + + pooon + peaaen + iaiie i + iieioaip + iiin i + ioii oi + iiii e o + iinia n + iiii o + oaee ne + nioopepi + ioiiaaai + e oinnna + ineoaae + ao one + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_15.txt b/codex-rs/tui/frames/openai/frame_15.txt new file mode 100644 index 000000000000..5a0e8f1b549f --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_15.txt @@ -0,0 +1,17 @@ + + ppoooia + apniiaeop + eeionniooi + iea eepeaai + iinonniieoai + niipiieei o + ii iiiiiipi + iieiipiii opi + iipppoiiiaeei + iaineinioe ei + ieiiiiai a n + iooeaiinp n + oinp eeea na + iaiinnepne + naoaaaae + aa \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_16.txt b/codex-rs/tui/frames/openai/frame_16.txt new file mode 100644 index 000000000000..06c519f60289 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_16.txt @@ -0,0 +1,17 @@ + + anpooopia + eaiioiininn + ieeonennii on + eee noaniniiooi + iinaeooniioeian + iiioio inn i iei + nni eapiiiieoop i + ien iaiieiiion ai + nniiipieaiioop e + ooaapnnnoieai pai + iiipooeoninoneiia + iio ia aeep e + nio i epeapi + niioaoieepai + oeeaneaano + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_17.txt b/codex-rs/tui/frames/openai/frame_17.txt new file mode 100644 index 000000000000..0bd4ef6dfc52 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_17.txt @@ -0,0 +1,17 @@ + + pnpppnnipa + anooiiionoipoap + poneannappoiiiaon + eieaeioaa oinniiinn + iea i ioennnee p + anopea peei epneiee + o nei peeia ooiieni + poni eeiieipiinno + peoennnpinainaeniii i + ioanaaieoneiin npopn + nnneooooooonaioeen no + niiao aeiia n + ann ona popeaae + p iopnnpaanoeio + apneappioapo + a \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_18.txt b/codex-rs/tui/frames/openai/frame_18.txt new file mode 100644 index 000000000000..de59f344efee --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_18.txt @@ -0,0 +1,17 @@ + + annpnnpnipa + apoanpppiaapopnna + aeeiooonopoeaopeen o + piioepaoo onaaeninnnnnp + anee ei aopaneieiannnna + eni ppieaaaepopiopeaeaei + iinee peoeioaeooininp + onpii anoiina inio + noo pnnnnnnpiipioenp ioia + anniono iieaanaiiianian p + ieinaoooooooa naaeieoeapa + nn pnp aaooeoao + naopaae aoaaenpa + oieooppnnnaooaeapo + oiiaeaapeponpo + a \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_19.txt b/codex-rs/tui/frames/openai/frame_19.txt new file mode 100644 index 000000000000..ade566235932 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_19.txt @@ -0,0 +1,17 @@ + + aannpipnniea + apiiinennnaoapiopp + ponneoipoa aoepaonn + eniopaa annppaiop + iieaea piaoeoin inp + ineoo aioie ee oeoi + aniio eoaeippa eeepi + iiii inoniop i iii + ioinpnnnnnniiiiponoin n oii + anepinoeaopnpap aa enanpeeia + onoepioooooooo anaeiee eeo + nnaoan aeeaeeo + o aniiop pnneeina + ooeioeipaanneoo noio + oieaepoapeaopa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_2.txt b/codex-rs/tui/frames/openai/frame_2.txt new file mode 100644 index 000000000000..be49360bbf5d --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_2.txt @@ -0,0 +1,17 @@ + + aeaenpnnnppa + appeonpiiiniinaoiopa + pioinooioaa aoonoeiinip + aieaeooppa ppnieip + paoeoainanna apinip + oepoa ionpenp nnioi + piooi aeiniaip eoioi + io na pioiino ioeei + oooni niaaaioeipppppppppiaiio + anoi eiio eo innaaaaeeaaipe i + nnn o oneeio aoooooooooipoii + onaeep a ppnepo + enaaipp pnap aio + aianaaoopennnnenoaaiio + aopopiiaaaaineoo + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_20.txt b/codex-rs/tui/frames/openai/frame_20.txt new file mode 100644 index 000000000000..6eaf358e88da --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_20.txt @@ -0,0 +1,17 @@ + + aapniinpnpaa + pnnonpapnnniiaaonpp + ppoapooioo aoaonpoipa + aioaoio appineonn + ai pio ae aiiaoniin + eapio poonioe oniin + ninpi po epoa pnie + iaaea ooneiop iioi + ieiinpnnnnnnnnaponaanno e in + oiaiao eaaaaaain onnoinn pe ii + npanoooooooooo npniipi en + o aoep aeoeie + naaopia pnoanoo + appeoipannpeoioapeoo + aopia naaaaiooo + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_21.txt b/codex-rs/tui/frames/openai/frame_21.txt new file mode 100644 index 000000000000..5f317f375c5d --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_21.txt @@ -0,0 +1,17 @@ + + aapnpnnpnppaa + ppoaaaapinipnaapopa + noaapoaoa aaiopaaop + poinoo apnopnop + epeoa piean nnaap + eoei pooaee nnap + ioia a eapeo nnno + an nn oip i i + i i ppnnnnnppp aipoip ne i + i in ia api aoaoip eoee + n oe aaooo oa ooapn ea p + op ena aoo e + oa apa aenoo pa + ona ooopeiinenooo pa + oppnpaaaapanpoa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_22.txt b/codex-rs/tui/frames/openai/frame_22.txt new file mode 100644 index 000000000000..74b75b91135d --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_22.txt @@ -0,0 +1,17 @@ + + appnpnnpnppa + apeoieaeieniinipeopa + pnoninooa aoonoopaapp + noiooo ppiiiaon + niieo paeinieenoi + nioea pieinaea ninni + ipone ipinoeo iiei + iinia iniian iaioi + iipop pinnnnnnppanoia np aino + ain oannp epoieei nonaoipp enn + nnnai ooooooooa aeopaeeeopa + nonanp aopoipo + oopn ppa aeopooae + oeoppooinnpnnenoooopo + oioianaaapnaenoa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_23.txt b/codex-rs/tui/frames/openai/frame_23.txt new file mode 100644 index 000000000000..35e7fe2210d2 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_23.txt @@ -0,0 +1,17 @@ + + appanpoppnpaa + anipinapannpiaioipa + popiopnoa oiepinniip + enionea annniinn + eeiaeo peppooeonin + pnioea aoooooeooiinip + aiioi eeee aia o ioo + iei i ioipanp i oei + aon ipinnnnnnnniaoiaona i iii + innaiono ean nianpi nai + eoi i aooooooo oaenniaeaea + piiaie peinaea + n ipoeaa aeonoepe + nnnpnopninnappopapoa + oi onniaapaaooa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_24.txt b/codex-rs/tui/frames/openai/frame_24.txt new file mode 100644 index 000000000000..a74ea1f0bb7d --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_24.txt @@ -0,0 +1,17 @@ + + aianpponnpa + poaeoiaainnppaop + poonnepoi eaeoioop n + poeepeinpnaiea aoninao + p eenep aooenaiaanipo + ipnpna oppe ein eon + i ii peapeoeaninaia + i nii ipnenaiiaeeaii + ie innneppiiippaopai i ii + a e oieppnnppieanpoi ioi + n oninoooooooa npeoninepa + n npon i oene + neonaie anoaeeao + epopnaoiiennniaaea + oeaopnnaannpo + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_25.txt b/codex-rs/tui/frames/openai/frame_25.txt new file mode 100644 index 000000000000..c2c5b30b296d --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_25.txt @@ -0,0 +1,17 @@ + + apnonppppa + p opoaannioiip + eieeppoeipnnniiia + ipoi e ni onipone + paanpeoaneaio ieeneo + npiiiaopeaie eipieeip + ieioi iaaenaeineaoini + iiieieo ianneinainiin + apnioiaannnnpiinnipoin + epoieipiaiinoainoneni + nioeniaoooooopeinieia + iaoinpnnppoeaepeeie + aaaianaonepaeaiie + nennnaonioo oio + ipoonppapio + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_26.txt b/codex-rs/tui/frames/openai/frame_26.txt new file mode 100644 index 000000000000..09a947d35d60 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_26.txt @@ -0,0 +1,17 @@ + + anoooopp + eoinenipnip + eoooinninoonp + noopiiiaiiapea + iapnieiooneninn + n oieeeooanonne + i ooniieiinnop + ep niiiaaniieii + i enonpipnnenii + eaiepiaaponnoi + iaoeioonpeapnii + naiien eeoiia + o oin aaeiep + ioonnooaoon + neeaoiano + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_27.txt b/codex-rs/tui/frames/openai/frame_27.txt new file mode 100644 index 000000000000..b3fef11ac8c2 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_27.txt @@ -0,0 +1,17 @@ + + nooopp + ineeien + noa oieoi + iiiaiinii + i aiapon + iaeaoaiiia + n oiiaoii + i aiiinoii + ie e ipiiii + on eoaiaoii + ip aaoopeo + iippiiaei + innpnpeia + naainnee + ono ane + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_28.txt b/codex-rs/tui/frames/openai/frame_28.txt new file mode 100644 index 000000000000..11fdcec52077 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_28.txt @@ -0,0 +1,17 @@ + + eoon + ipaia + n nia + ipaoi + i ii + i poi + aii ai + ipooi + en ai + iieeii + i ai + inpni + inoii + oaaei + i epi + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_29.txt b/codex-rs/tui/frames/openai/frame_29.txt new file mode 100644 index 000000000000..2dc6c667532d --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_29.txt @@ -0,0 +1,17 @@ + + poooop + pioe on + apii ooi + iiiianpe + n ioi i + ipiii aa + inniii a + iinii + iiioi a + ainii a + oiieia n + ieiii ai + iiiiaiai + inioeoio + iio ep + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_3.txt b/codex-rs/tui/frames/openai/frame_3.txt new file mode 100644 index 000000000000..9026d59a4307 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_3.txt @@ -0,0 +1,17 @@ + + aennppnnnnpa + appeniipionninaaiopa + pneonioooa aoi oopeaip + epeninppa ano iin + oeoieaeiiona naniia + einne epinnnn i nni + no i ooniaoip eoeip + ppipi ppioeni iaini + pooi niopiiopinppaaappaiaoni + pnenp eeoenni ioiaap nepnnainia + oai i oiippo ooooooooopeeepe + ooi np a eapaee + naeeonp ppneopio + nappaooannnnpenonaapo + aopoaeiaaaaineao + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_30.txt b/codex-rs/tui/frames/openai/frame_30.txt new file mode 100644 index 000000000000..73b4906d0eca --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_30.txt @@ -0,0 +1,17 @@ + + anooopp + aeniinoonn + ooaipnnippn + iinieianin np + noiianipip i + ioiap oiiiiei + neiiina neni na + oi opnipioip a + oiaoninineip a + oiiniaaii pp a + niinnaoiiipii + ioeepeoniio i + niniieoiipie + pappnaeipeo + npeiaaano + aaaa \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_31.txt b/codex-rs/tui/frames/openai/frame_31.txt new file mode 100644 index 000000000000..cc71fce92000 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_31.txt @@ -0,0 +1,17 @@ + + aeeopopnp + aponppaopnoop + pineepnaaeoiaon + piepaaonapnniniin + aiiiipponaponieaa + niaaiaioonnopeeipoi + eii iaoinenenoioiei + nnp iioin iaooiinei + inpaioaiiiepiiia i + oiiieeiiaaanainii o + aoaepip eoooaeioia + iiinnnp ano i + niona p e ioaea + einieaiapiopia + ooniapoeeno + aaaa \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_32.txt b/codex-rs/tui/frames/openai/frame_32.txt new file mode 100644 index 000000000000..c0d6573da78e --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_32.txt @@ -0,0 +1,17 @@ + + pppppnnipa + noiooiniiooiip + eaonappaepaoionoop + oeeiapanp p panipp p + eeipionooin oapinenonp + nennpnoiaeiipa ipe o + oionpaoniippipnaeaeeioa + iiipa oaiioeoinpaoniooa + niipa oaioaaipppiiiniona + oooaaiononaaaaaiioinipa + pnoiie iooooooaneeeee + npiann pnieoi + oooiiipa aeaie p + npnoipinnoaapoae + innaappeoaoo + aaa \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_33.txt b/codex-rs/tui/frames/openai/frame_33.txt new file mode 100644 index 000000000000..56ef96d36a80 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_33.txt @@ -0,0 +1,17 @@ + + appnpnpniea + apoiinonieipioioe + aenipoppnoa ooeiipop + pneieaipa oiinao + aeieaiienan ipnna + eep nannanp enean + iip npoiain ioii + oipp poonenp noio + iii i pa eopppnnnpiipeaini + aeiooeepeaiiaaaaaiioaaaiie + nnnp ieie e ooooooo iipeaa + npnopop eenene + onooponp apipeono + nioonpipnpaonpnoio + onnppaaaaponpo + aaaa \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_34.txt b/codex-rs/tui/frames/openai/frame_34.txt new file mode 100644 index 000000000000..b6e87c62f1cf --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_34.txt @@ -0,0 +1,17 @@ + + apnppppnipa + apopennioiponoonpna + aeepiooeioa aannpooop + peaioaepa oiponn + eeinpei io p nnona + peniae nnoinon npnin + ioaia onaoiaa inia + ninoe ioaieoi e an + poeeo eneeoannppppppiaao iaa + iinpeppaoeeoiiiaieaaapeaiiain + eanp inioa oooaaaaa pepnio + epnnnna eneieo + npaipnnp apnoaepa + pneioippnppnonoapoeo + oinnnaaaaannona + aaa \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_35.txt b/codex-rs/tui/frames/openai/frame_35.txt new file mode 100644 index 000000000000..899d6766b791 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_35.txt @@ -0,0 +1,17 @@ + + appnnpppnppa + ponoeioopoioieoiona + aeeinpoiaooa aoonoeoiop + peepepnpa opania + eeiieen onop inoip + nenepa oiaoeeoa inni + n ee nn ooae iann + iiie ennneo e oe + e pa piienoeinnananpii iino + ipano eenoopein npaaaaeoieo io + eanpoenaieaa aoaaaaoaaoeeapi + oeanei aeo pa + nnaanpep pneaooo + oaaoioipeiipineooaeno + oainnaaeaappooa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_36.txt b/codex-rs/tui/frames/openai/frame_36.txt new file mode 100644 index 000000000000..9a23d2ddd6dd --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_36.txt @@ -0,0 +1,17 @@ + + aapnppppnppa + anoonpenepnpnppooopa + pnonppopooaa aooiopipoip + aioopaaaa ooinoi + peannoinanpe aneo + e pea oa oiep onao + i n a n onn iaip + iini an aee ni i + iooi ee pooopppapppppa inii + aoin piaaeie aiaaaaaaaii neoa + o oin papea oooaaaaao ipoe + oponne peioe + neiipip pppoepa + opaooonaniapinopopnpo + aopeiappappppooo + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_4.txt b/codex-rs/tui/frames/openai/frame_4.txt new file mode 100644 index 000000000000..0c76cc5ce83e --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_4.txt @@ -0,0 +1,17 @@ + + aennpnnpnppa + apopoie inneonioeopa + poniiipooa aainaoienna + eeipioepp oioonon + eieaininnaip nonion + eiiai oaiieon o nnip + onina ooonpoip p oni + ien pieoaii iii + ai ia nieoaeepipppaaaapp iio + ine i peeepaiannipeapeeanieeoi + ainonpnnpne oooooooooapiiia + pinoiaa paneooo + nnpea pa aeaponie + iiinaoonnnnnppnopnioa + onnieiaaaaanpoa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_5.txt b/codex-rs/tui/frames/openai/frame_5.txt new file mode 100644 index 000000000000..2b06cade095a --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_5.txt @@ -0,0 +1,17 @@ + + aennnpnnnppa + apopeoeiipinpnaiopp + oaenpiooa oanion pop + pieieienp paiiiia + pneoeaapianp a ninia + iiiie nainneia peii + iei p anainoip ooipip + ieiao panieai piai + ipiao eaiopionaipiaaappaoiei + aninppepoo io eennapnepeieoia + npiipipnnepa oooooooopinine + nnpenn a eaeopa + aeoninpa anppoono + aioipopnninappnoaino + apoaooaaapappoa + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_6.txt b/codex-rs/tui/frames/openai/frame_6.txt new file mode 100644 index 000000000000..2ca8bb0bc79c --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_6.txt @@ -0,0 +1,17 @@ + + aennnpnnppaa + paneoinopippnapopa + pppiiioo aoienopoena + epioioanp npiiiip + oniiionnpon enninp + npeieaapninaip ennei + a pii aniean apiai + i iii ainieii ipnii + aeipi eoiionepaipiaappaenei + iinpeaaanp ienopiipnnnnipin + e i eiiineo ooooooooannei + p n epa peaeeia + eanaoop apaaeopp + aoiopop niaappoo eno + apnoppaaaaappo + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_7.txt b/codex-rs/tui/frames/openai/frame_7.txt new file mode 100644 index 000000000000..f66ddaf5a652 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_7.txt @@ -0,0 +1,17 @@ + + aeinopnnppa + poapnipopnnnn aop + o piiioa aaia oeaop + nniiiinp ainoiep + aoioiiaienp e oiin + i niio aonnin ppiii + anoi aainini ooaii + aoeii npeioia o iii + eoio poaeineiinnaiinnonii + ipoiineiieoieniinnpnpnnini + n naeoioae ooooooooaeii + oa eaeaa popeneo + onppnnna paipoeoo + naonionniaaapoiope + oiooieiaapanoo + \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_8.txt b/codex-rs/tui/frames/openai/frame_8.txt new file mode 100644 index 000000000000..e54163d2c8a3 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_8.txt @@ -0,0 +1,17 @@ + + aapanppnppa + pooeiiionn i op + ainpieeo poieionpap + aonoeippi o naiin + o eiepnien eoniip + iopinaaaniao npiii + ioiii oanniai iaieip + npiie ipneipoe eaeii + n niiaa eoniiepppppniii + noenaiopipiininaaiino + eenniniiieoaoaoonooeni + opoiniaa epeeeo + onaonnna aenaipeo + eeoipo nnponenpia + iiaooniappena + aa \ No newline at end of file diff --git a/codex-rs/tui/frames/openai/frame_9.txt b/codex-rs/tui/frames/openai/frame_9.txt new file mode 100644 index 000000000000..a339de111841 --- /dev/null +++ b/codex-rs/tui/frames/openai/frame_9.txt @@ -0,0 +1,17 @@ + + enaoeppnp + eoaeiioon apna + e piieoaane naip + o iienna aapinaipp + eoonnooiop oeopepii + i iiioainnaa ioioiii + a ioniooinnpoepe aii + i ieapniiiaennai ii + opio eioiiiipiipiia + iaiiiapiaiaiiiiaaiii + o aaennieoooooiioeni + niiinpa pe aeia + npninn enanieo + aponaioepnnnoia + opa onninpeo + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_1.txt b/codex-rs/tui/frames/shapes/frame_1.txt new file mode 100644 index 000000000000..244e2470b4f1 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_1.txt @@ -0,0 +1,17 @@ + + ◆△◆△●□□●●□▲◆ + ◆●▲△□○□△□○●◇◇●◆ ◆■□◆ + ▲◇□◇□□□■○◆ ◆■□◆■◇●◇◇◇□ + ●□◆○□■▲▲◆ △□◇●◇▲ + ○○●△■○◇○◆○○ ■△◇○○▲ + ◇□ □◆ ◇□△●◇◇▲ ■△○◇◇▲ + □○■▲□ ■○◇□△■◇◆ ◇△◇ + ◇◇◆◇◆ ▲△△◇●◇□ ■◆◇ + ◇●◇■◆ ●◇◇○○◇■△◇□□□□□□◆□▲ ●■ ◇ + ◆◇●□ ◆●□◆ △□ ◇■◇◆◆◆△△▲▲▲◇△▲△▲◇ + ○○◆■○ ○○▲△△◆ ◆○□■■□ ○□■△▲●◆△ + □○▲ ■▲ ◆ ▲■△□◆◇ + ○○▲◆○□◆ ◆●◆□◇◆□■ + ○□▲○◆ □□△●●●●△●□□◆▲◇□ + ◆□■□◇◇◇◆◆◆▲◆●□□■ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_10.txt b/codex-rs/tui/frames/shapes/frame_10.txt new file mode 100644 index 000000000000..f306dffc087e --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_10.txt @@ -0,0 +1,17 @@ + + ◆□□□□○□□◆ + □■◆□□□○◇△◆□▲ + ○◆▲◇◇△◇◇▲◇□○▲▲ + ◇◆◆△○◇●◆△○▲■■○○▲ + △ ●◇◇■◇○ ■▲△◆△◇△ + ◇◆ ■◇□◇△△○ ◆◆■◇ + ○ ◇□■□◇◇◇◇□ ◇△▲ + ■ ◇◇○□△□◇◇▲◆◆△○◇◇ + ■ ◇○ ○○◇●◇◇□○□●◇◇ + ◇ ▲◇○▲◇◆△◆□◆◆◆◇□◆ + ▲ ■◇◇◇◇◇■■ ○▲■○◇◆ + ○◆■▲○▲□■ ◆◇■■▲△△ + ◇■ ◇◇◇◇□▲△▲△◇△◆ + ●◆□□△◇□●◆ △△■ + □▲ ◆□○◆▲●□ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_11.txt b/codex-rs/tui/frames/shapes/frame_11.txt new file mode 100644 index 000000000000..dcf944902b34 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_11.txt @@ -0,0 +1,17 @@ + + ▲□□□□□□◆ + △■ ●◇◇◇○○▲ + △■◆◆◇◇●○□△○▲ + ◇◆◆◇◇●●▲◆●△△ + ◆■△●○◇□○■●◆◇◇▲ + ◇□◆◇◇□◆●●◇◆△◇◇ + □□ ▲◇◇◇◇◇◇◆◇●○ + ◇ ◇◇△◇◇○△ ◇■◇ + △ ■◇△◇◇□△□◇◆◇ + □□ ◆◇△●▲■△◇◆◇○ + ■◆▲ ◇◇◇◇●△◇○○◇ + ○◆▲△◇◆□△□□●◇◆ + ◆ □◇○○○◆◇●■ + ○□ □◇◇▲◆△□◆ + ○◆ ■□□◆□◆ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_12.txt b/codex-rs/tui/frames/shapes/frame_12.txt new file mode 100644 index 000000000000..d8d1fbf334f3 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_12.txt @@ -0,0 +1,17 @@ + + □□□□□△ + ▲●◆◆△◇▲○ + ■ ◇△○□▲ + △□◇◇◇●●■◇ + ◇ ■◇◇◇◇△△◇ + ◇■△△□○■◆◇■ + ◇ ◇△○◇◇○ + ◇ △□○◇◇■ + □▲ ◇△◇◇□◇◇ + ◇ △◇□●□◆ + ◇△ ◇■●□□△ + ▲ □◇ ◆▲◇ + △ □□◇▲□○◇ + ■○△■▲◇○◇◆ + ○ ■◇○△△ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_13.txt b/codex-rs/tui/frames/shapes/frame_13.txt new file mode 100644 index 000000000000..1387fc9b9124 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_13.txt @@ -0,0 +1,17 @@ + + △□□□▲ + ◇◆◆◇◇ + ◇◆◆◇■ + ◇□□◇◆ + □ ◇◇ + ■△▲◇◇ + ▲ ◇◇ + □ ■◇ + △□ ◆◇△ + ◇■ □●◇ + ■◆△△ ◇◆ + ◇◆◆◆◇ + ◇□▲◇◆ + □◆◆◇● + ▲ △■● + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_14.txt b/codex-rs/tui/frames/shapes/frame_14.txt new file mode 100644 index 000000000000..70a5070ba9b2 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_14.txt @@ -0,0 +1,17 @@ + + □□□□● + ▲△◆◆△○ + ◇◆◇◇△ ◇ + ◇◇△◇■○◇▲ + ◇◇◇○ ◇ + ◇□◇◇ ■◇ + ◇◇◇◇ △ □ + ◇◇○◇◆ ○ + ◇◇◇◇ ■ + ■○△△ ●△ + ○◇■■▲△▲◇ + ◇□◇◇◆◆◆◇ + △ ■◇●●●◆ + ◇○△□◆◆△ + ◆□ ■●△ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_15.txt b/codex-rs/tui/frames/shapes/frame_15.txt new file mode 100644 index 000000000000..584e0e043a9b --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_15.txt @@ -0,0 +1,17 @@ + + □□□□□◇◆ + ◆▲●◇◇○△□▲ + △△◇□●○◇■□◇ + ◇△◆ △△▲△◆◆◇ + ◇◇●■●○◇◇△■○◇ + ○◇◇▲◇◇△△◇ ■ + ◇◇ ◇◇◇◇◇◇▲◇ + ◇◇△◇◇□◇◇◇ ■▲◇ + ◇◇□▲▲□◇◇◇◆△△◇ + ◇◆◇●△◇○◇□△ △◇ + ◇△◇◇◇◇◆◇ ◆ ● + ◇□□△○◇◇○▲ ● + ■◇○▲ △△△◆ ●◆ + ◇○◇◇○○△□○△ + ○○□○◆◆◆△ + ◆◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_16.txt b/codex-rs/tui/frames/shapes/frame_16.txt new file mode 100644 index 000000000000..af6c83685534 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_16.txt @@ -0,0 +1,17 @@ + + ◆●□■□□□◇◆ + △○◇◇□◇◇○◇●○ + ◇△△■○△●○◇◇ ■○ + △△△ ●□◆○◇○◇◇□□◇ + ◇◇●◆△□■●◇◇□△◇◆○ + ◇◇◇□◇■ ◇○● ◇ ◇△◇ + ○○◇ △◆▲◇◇◇◇△□■▲ ◇ + ◇△● ◇◆◇◇△◇◇◇■● ◆◇ + ○○◇◇◇□◇△○◇◇■□□ △ + □■◆◆▲●●○□◇△◆◇ ▲◆◇ + ◇◇◇□■■△□○◇●■●△◇◇◆ + ◇◇□ ◇◆ ◆△△▲ △ + ○◇□ ◇ △▲△◆▲◇ + ○◇◇■◆□◇△△□◆◇ + ■△△◆●△◆◆●□ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_17.txt b/codex-rs/tui/frames/shapes/frame_17.txt new file mode 100644 index 000000000000..4a158cf6094a --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_17.txt @@ -0,0 +1,17 @@ + + ▲●□□□●●◇▲◆ + ◆●□□◇◇◇□●□◇▲■○▲ + ▲□○△◆●●◆□▲■◇◇◇◆■○ + △◇△◆△◇■◆◆ ■◇●○◇◇◇●○ + ◇△◆ ◇ ◇■△○●○△△ ▲ + ◆●□▲△◆ ▲△△◇ △▲●△◇△△ + □ ●△◇ ▲△△◇◆ □■◇◇△●◇ + ▲■●◇ △△◇◇△◇▲◇◇●●□ + ▲△□△○●●□◇○◆◇○○△○◇◇◇ ◇ + ◇■◆●◆◆◇△□○△◇◇○ ○□■□○ + ○○○△■■■□□□□○◆◇□△△○ ○■ + ○◇◇◆□ ◆△◇◇◆ ● + ◆○○ □○◆ ▲■▲△○◆△ + ▲ ◇□□●○□◆◆●□△◇■ + ◆□●△◆▲□◇□◆□□ + ◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_18.txt b/codex-rs/tui/frames/shapes/frame_18.txt new file mode 100644 index 000000000000..16bf8c1b581a --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_18.txt @@ -0,0 +1,17 @@ + + ◆●●□●●□●◇▲◆ + ◆□■◆●▲□□◇◆◆▲■□●●◆ + ◆△△◇□■□●■▲■△◆■□△△○ □ + ▲◇◇□△▲○■□ ■○◆◆△●◇○●○○○▲ + ◆○△△ △◇ ◆■□◆○△◇△◇◆○●○○◆ + △○◇ ▲▲◇△◆◆◆△▲□▲◇■▲△○△◆△◇ + ◇◇○△△ □△□△◇■◆△■□◇●◇●▲ + ■○▲◇◇ ○○■◇◇●○ ◇●◇■ + ○■■ ▲○●●●●●□◇◇▲◇□△○▲ ◇□◇◆ + ◆○○◇□○□ ◇◇△◆◆○◆◇◇◇◆●◇◆○ ▲ + ◇△◇○◆■■□□□□□◆ ○◆◆△◇△□△◆▲◆ + ○○ ▲○▲ ◆◆□■△□◆■ + ○○□□○○△ ◆□○◆△○▲◆ + ■◇△□□□□●●●○■□◆△◆▲□ + ■◇◇◆△◆◆▲△□■●□■ + ◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_19.txt b/codex-rs/tui/frames/shapes/frame_19.txt new file mode 100644 index 000000000000..e1bc51ae1be4 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_19.txt @@ -0,0 +1,17 @@ + + ◆◆●●□◇□●●◇△◆ + ◆□◇◇◇○△●●●◆□◆▲◇□▲▲ + ▲□●●△■◇□□◆ ◆□△▲◆□●○ + △●◇□▲○◆ ◆●●□▲○◇□▲ + ◇◇△◆△◆ ▲◇○■△■◇○ ◇○▲ + ◇○△□■ ◆◇□◇△ △△ ■△□◇ + ○●◇◇■ △□○△◇□▲◆ △△△□◇ + ◇◇◇◇ ◇●■○◇■▲ ◇ ◇◇◇ + ◇□◇○▲●●●●●●◇◇◇◇□□●□◇● ○ ■◇◇ + ○●△▲◇●□△○□□○▲◆▲ ○◆ △○◆○▲△△◇◆ + ■○■△□◇■■■■■□□■ ○○◆△◇△△ △△■ + ○○○□◆○ ◆△△◆△△■ + ■ ◆○◇◇□▲ ▲●●△△◇●◆ + ■■△◇□△◇□◆◆●●△□■ ●□◇■ + ■◇△◆△▲■◆▲△○□▲○ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_2.txt b/codex-rs/tui/frames/shapes/frame_2.txt new file mode 100644 index 000000000000..af71459f5e9a --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_2.txt @@ -0,0 +1,17 @@ + + ◆△◆△●□●●●□▲◆ + ◆□▲△□●□◇◇◇●◇◇●◆■◇□□◆ + ▲◇□◇○□□◇□◆◆ ◆■□●□△◇◇●◇▲ + ◆◇△◆△□■▲▲◆ ▲▲○◇△◇▲ + ▲○■△□○◇○◆○○◆ ○▲◇○◇▲ + ■△□■◆ ◇□○□△○□ ○○◇□◇ + ▲◇■□◇ ○△◇●◇◆◇▲ △■◇■◇ + ◇■ ○◆ ▲◇□◇◇●□ ◇■△△◇ + □■■●◇ ●◇○○◆◇■△◇□□□□□□□□▲◇◆◇◇□ + ◆●■◇ △◇◇■ △□ ◇●○◆◆◆◆△△◆◆◇□△ ◇ + ○○○ □ □○△△◇■ ◆■□□□□□□□■◇▲□◇◇ + ■○◆△△▲ ◆ ▲□●△▲□ + △○◆◆◇□▲ ▲●◆□ ◆◇■ + ◆◇○●◆◆■□□△●●●●△●□◆◆◇◇□ + ◆□□□□◇◇◆◆◆◆◇●△□■ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_20.txt b/codex-rs/tui/frames/shapes/frame_20.txt new file mode 100644 index 000000000000..c5eb01382d64 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_20.txt @@ -0,0 +1,17 @@ + + ◆◆□●◇◇●□●▲◆◆ + ▲●●■●▲◆▲●●●◇◇◆○□○□▲ + ▲▲■◆□□■◇□■ ◆■○■○□□◇□◆ + ◆◇■◆□◇□ ◆▲□◇○△□○○ + ◆◇ ▲◇□ ◆△ ◆◇◇○□○◇◇○ + △◆▲◇■ □□■●◇■△ ■○◇◇○ + ●◇●□◇ ▲□ △□□○ ▲○◇△ + ◇◆○△◆ ■■●△◇■▲ ◇◇■◇ + ◇△◇◇●▲●●●●●●●●◆▲■●○○○○□ △ ◇○ + ■◇○◇◆■ △◆◆◆◆◆◆◇○ □○○■◇●○ ▲△ ◇◇ + ○▲○○■■■■■■■■□■ ○▲●◇◇▲◇ △○ + □ ○□△▲ ◆△□△◇△ + ○◆○□▲◇◆ ▲●□◆●□■ + ○▲▲△■◇□○●●▲△■◇■◆▲△□■ + ◆□□◇◆ ●◆◆◆◆◇□■■ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_21.txt b/codex-rs/tui/frames/shapes/frame_21.txt new file mode 100644 index 000000000000..944b99f05819 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_21.txt @@ -0,0 +1,17 @@ + + ◆◆▲●□●●□●□▲◆◆ + ▲□□◆◆◆◆□◇●◇□●◆◆▲□□◆ + ●□○◆□□○■◆ ◆○◇□□◆◆□▲ + ▲□◇●■■ ◆▲●□▲●□▲ + △▲△□◆ ▲◇△◆○ ○○◆○▲ + △■△◇ ▲□□◆△△ ○○○▲ + ◇■◇◆ ◆ △◆▲△■ ○●○■ + ◆○ ○● ■◇▲ ◇ ◇ + ◇ ◇ ▲□●●●●●□□□ ○◇▲■◇▲ ●△ ◇ + ◇ ◇○ ◇◆ ◆▲◇ ○■◆□◇▲ △■△△ + ● ■△ ◆◆■■■ ■◆ ■□◆▲● △◆ ▲ + ■□ △●◆ ◆□■ △ + □◆ ○▲◆ ◆△●□■ ▲◆ + □●◆ □■□□△◇◇●△●□■■ ▲◆ + □□□●□◆◆◆◆▲◆●□□◆ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_22.txt b/codex-rs/tui/frames/shapes/frame_22.txt new file mode 100644 index 000000000000..60ea930d46d7 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_22.txt @@ -0,0 +1,17 @@ + + ◆▲□●□●●□●□▲◆ + ◆□△□◇△◆△◇△○◇◇●◇□△□□◆ + ▲●□●◇○□□◆ ◆■□○□□□◆◆□▲ + ●□◇□□■ ▲▲◇◇◇◆□○ + ○◇◇△■ ▲◆△◇●◇△△●■◇ + ○◇□△◆ ▲◇△◇●○△◆ ○◇○●◇ + ◇□□○△ ◇□◇○□△■ ◇◇△◇ + ◇◇●◇◆ ◇○◇◇○○ ◇○◇■◇ + ◇◇▲■▲ □◇●●●●●●□□◆○□◇◆ ○▲ ◆◇●■ + ○◇● □◆○○▲ △▲■◇△△◇ ○□●◆■◇▲▲ △○● + ○○○◆◇ ■■■■■□□■◆ ◆△□□◆△△△■▲◆ + ○□○◆○▲ ◆□▲■◇▲■ + ■■□● □□◆ ◆△□▲■□◆△ + ■△□□▲■□◇●●□●●△●□■■■▲□ + ■◇■◇◆●◆◆◆▲●◆△●□◆ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_23.txt b/codex-rs/tui/frames/shapes/frame_23.txt new file mode 100644 index 000000000000..5d340640bf39 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_23.txt @@ -0,0 +1,17 @@ + + ◆▲□◆●□□□□●□◆◆ + ◆●◇□◇○◆▲◆●●▲◇◆◇□◇□◆ + ▲■□◇■▲●□◆ ■◇△□◇○●◇◇▲ + △●◇□●△◆ ◆●●○◇◇●○ + △△◇◆△■ ▲△▲▲■□△□○◇○ + ▲○◇■△◆ ◆□□□■■△□■◇◇○◇▲ + ◆◇◇■◇ △△△△ ◆◇◆ ■ ◇■□ + ◇△◇ ◇ ◇■◇▲◆○▲ ◇ □△◇ + ○■○ ◇□◇●●●●●●●●◇◆□◇◆■○◆ ◇ ◇◇◇ + ◇○○◆◇■●■ △◆○ ○◇◆○▲◇ ○◆◇ + △■◇ ◇ ◆■■■■■■■ ■◆△●●◇◆△○△◆ + ▲◇◇◆◇△ ▲△◇○○△◆ + ○ ◇▲■△◆◆ ◆△■●□△□△ + ○●○□●□□○◇●●◆□□■▲◆□□◆ + ■◇ □●○◇◆◆□◆◆□□◆ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_24.txt b/codex-rs/tui/frames/shapes/frame_24.txt new file mode 100644 index 000000000000..558224147dc3 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_24.txt @@ -0,0 +1,17 @@ + + ◆◇◆●□□□●●▲◆ + ▲■◆△■◇◆◆◇●●□▲◆■□ + ▲■■●○△□■◇ △◆△□◇□□▲ ○ + ▲■△△▲△◇●▲○◆◇△◆ ◆□○◇○◆■ + ▲ △△○△▲ ◆■■△○◆◇◆◆○◇▲■ + ◇▲○□○◆ □▲▲△ △◇● △■○ + ◇ ◇◇ ▲△◆▲△■△○○◇○○◇○ + ◇ ○◇◇ ◇▲●△○◆◇◇◆△△◆◇◇ + ◇△ ◇●●●△□□◇◇◇□□◆□▲◆◇ ◇ ◇◇ + ◆ △ □◇△▲▲●●▲▲◇△○○□□◇ ◇□◇ + ○ ■○◇○□■■■■■■◆ ○▲△□○◇●△▲◆ + ○ ○▲□○ ◇ ■△●△ + ○△□○◆◇△ ◆●■◆△△◆□ + △▲□▲○○■◇◇△●●●◇○◆△◆ + □△◆□□●○◆◆●●□□ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_25.txt b/codex-rs/tui/frames/shapes/frame_25.txt new file mode 100644 index 000000000000..38d32507640a --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_25.txt @@ -0,0 +1,17 @@ + + ◆□●□●□□□□◆ + ▲ □▲□◆◆●○◇□◇◇▲ + △◇△△▲▲■△◇□○●○◇◇◇◆ + ◇□□◇ △ ●◇ ■○◇▲□○△ + ▲◆◆○▲△■○●△◆◇■ ◇△△○△□ + ○▲◇◇◇◆□□△○◇△ △◇▲◇△△◇▲ + ◇△◇□◇ ◇◆◆△○◆△◇○△◆■◇●◇ + ◇◇◇△◇△■ ◇◆●○△◇○◆◇●◇◇○ + ○□○◇■◇◆◆●●●●□◇◇○○◇□□◇○ + △▲■◇△◇▲◇◆◇◇●□◆◇○□●△○◇ + ○◇□△○◇◆■■■■■■□△◇○◇△◇◆ + ◇◆■◇●□●○▲▲□△◆△▲△△◇△ + ○◆○◇◆○◆□●△▲○△◆◇◇△ + ○△●○○◆□●◇□□ □◇□ + ◇▲■□○□▲◆□◇□ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_26.txt b/codex-rs/tui/frames/shapes/frame_26.txt new file mode 100644 index 000000000000..4aac44389a95 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_26.txt @@ -0,0 +1,17 @@ + + ◆●□□□■□▲ + △□◇●△○◇□●◇▲ + △□■■◇●○◇○■■○▲ + ●□■▲◇◇◇◆◇◇◆□△○ + ◇◆▲○◇△◇■□○△●◇○● + ○ ■◇△△△■□◆○■○○△ + ◇ □■●◇◇△◇◇●●■□ + △▲ ○◇◇◇○○●◇◇△◇◇ + ◇ △○□●□◇□●●△○◇◇ + △◆◇△▲◇◆◆▲□○○□◇ + ◇◆■△◇□■○▲△◆□○◇◇ + ○◆◇◇△○ △△■◇◇◆ + □ □◇○ ○◆△◇△▲ + ◇■□○○■□◆□□● + ○△△○□◇◆○□ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_27.txt b/codex-rs/tui/frames/shapes/frame_27.txt new file mode 100644 index 000000000000..9896590f7977 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_27.txt @@ -0,0 +1,17 @@ + + ●□□□□▲ + ◇●△△◇△○ + ●■◆ □◇△■◇ + ◇◇◇○◇◇●◇◇ + ◇ ◆◇◆▲■● + ◇◆△◆■◆◇◇◇◆ + ● ■◇◇◆□◇◇ + ◇ ◆◇◇◇●■◇◇ + ◇△ △ ◇□◇◇◇◇ + ■● △■◆◇◆□◇◇ + ◇▲ ◆○□□□△■ + ◇◇□□◇◇◆△◇ + ◇●●□●▲△◇○ + ○◆◆◇○●△△ + ■○■ ○○△ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_28.txt b/codex-rs/tui/frames/shapes/frame_28.txt new file mode 100644 index 000000000000..16b349dc3d5f --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_28.txt @@ -0,0 +1,17 @@ + + △□□● + ◇□◆◇◆ + ● ●◇◆ + ◇▲◆□◇ + ◇ ◇◇ + ◇ ▲■◇ + ○◇◇ ◆◇ + ◇▲■■◇ + △○ ◆◇ + ◇◇△△◇◇ + ◇ ◆◇ + ◇●□●◇ + ◇●□◇◇ + □◆◆△◇ + ◇ △□◇ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_29.txt b/codex-rs/tui/frames/shapes/frame_29.txt new file mode 100644 index 000000000000..24be1563b27a --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_29.txt @@ -0,0 +1,17 @@ + + □□□□□▲ + ▲◇□△ □○ + ○▲◇◇ ■■◇ + ◇◇◇◇○○▲△ + ○ ◇■◇ ◇ + ◇▲◇◇◇ ◆◆ + ◇○○◇◇◇ ○ + ◇◇●◇◇ + ◇◇◇□◇ ◆ + ◆◇●◇◇ ◆ + □◇◇△◇○ ● + ◇△◇◇◇ ◆◇ + ◇◇◇◇◆◇◆◇ + ◇●◇□△■◇■ + ◇◇□ △▲ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_3.txt b/codex-rs/tui/frames/shapes/frame_3.txt new file mode 100644 index 000000000000..3f55b79ac59e --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_3.txt @@ -0,0 +1,17 @@ + + ◆△●●□□●●●●▲◆ + ◆□▲△○◇◇□◇■●●◇●◆○◇□□◆ + ▲●△□●◇□■■◆ ◆■◇ □□□△○◇▲ + △□△○◇●▲□◆ ◆●□ ◇◇○ + ■△□◇△○△◇◇□○◆ ○○○◇◇◆ + △◇○○△ △▲◇○●○○ ◇ ○○◇ + ●□ ◇ ■□○◇◆■◇▲ △□△◇▲ + ▲▲◇▲◇ ▲▲◇■△○◇ ◇◆◇●◇ + ▲■■◇ ●◇□□◇◇□▲◇●□□◆◆◆□□◆◇○■●◇ + ▲○△○▲ △△□△●●◇ ◇□◇◆◆▲ ●△▲●○◆◇○◇◆ + ■○◇ ◇ ■◇◇□▲■ ■□□□■■■□□▲△△△▲△ + ■□◇ ○□ ◆ △○□○△△ + ●○△△□○▲ ▲□●△■▲◇■ + ●○□▲◆□□○●●●●▲△●□●◆◆□■ + ◆□□□◆△◇◆◆◆◆◇●△○■ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_30.txt b/codex-rs/tui/frames/shapes/frame_30.txt new file mode 100644 index 000000000000..54886a319d09 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_30.txt @@ -0,0 +1,17 @@ + + ◆●■□□□▲ + ◆△●◇◇○□■●○ + ■■○◇▲○○◇□▲○ + ◇◇○◇△◇◆○◇○ ○▲ + ●□◇◇◆○◇▲◇▲ ◇ + ◇□◇○□ □◇◇◇◇△◇ + ○△◇◇◇○◆ ○△○◇ ●◆ + ■◇ ■□●◇▲◇■◇▲ ◆ + ■◇◆■○◇●◇○△◇▲ ◆ + ■◇◇○◇◆◆◇◇ □▲ ◆ + ○◇◇●○◆□◇◇◇□◇◇ + ◇□△△▲△□○◇◇■ ◇ + ○◇○◇◇△□◇◇▲◇△ + ▲○▲□●○△◇▲△■ + ○▲△◇○◆◆●■ + ◆◆◆◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_31.txt b/codex-rs/tui/frames/shapes/frame_31.txt new file mode 100644 index 000000000000..b3989b89df99 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_31.txt @@ -0,0 +1,17 @@ + + ◆△△□□□□●▲ + ◆□□●□▲◆■□○■□▲ + ▲◇○△△▲●◆◆△■◇◆□○ + ▲◇△□◆◆□●◆▲○○◇○◇◇○ + ○◇◇◇◇□▲□○◆▲■○◇△◆◆ + ●◇○◆◇◆◇■■○●□▲△△◇▲■◇ + △◇◇ ◇○□◇○△●△●□◇□◇△◇ + ○○▲ ◇◇■◇○ ◇○■■◇◇○△◇ + ◇○▲◆◇■○◇◇◇△□◇◇◇◆ ◇ + ■◇◇◇△△◇◇◆◆◆○◆◇○◇◇ □ + ○□◆△□◇▲ △□□□◆△◇■◇◆ + ◇◇◇○○○▲ ◆●□ ◇ + ●◇□●◆ ▲ △ ◇□◆△◆ + △◇○◇△◆◇◆▲◇■▲◇◆ + ■■●◇◆□□△△●□ + ◆◆◆◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_32.txt b/codex-rs/tui/frames/shapes/frame_32.txt new file mode 100644 index 000000000000..919eee3b0fdf --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_32.txt @@ -0,0 +1,17 @@ + + ▲□□□□●●◇▲◆ + ●□◇□□◇○◇◇□□◇◇▲ + △○□●○▲□○△▲◆□◇□○■□▲ + ■△△◇◆▲◆●▲ ▲ ▲◆○◇□▲ ▲ + △△◇▲◇□○■□◇● ■◆▲◇○△○□○▲ + ○△○○▲○□◇◆△◇◇□◆ ◇□△ □ + □◇■●▲◆■○◇◇▲▲◇▲●◆△◆△△◇■◆ + ◇◇◇▲◆ ■◆◇◇■△■◇●▲◆■○◇■■◆ + ○◇◇▲◆ ■◆◇■◆◆◇□□□◇◇◇●◇■●◆ + ■■■◆◆◇□○■○◆◆◆◆◆◇◇□◇●◇▲◆ + ▲○□◇◇△ ◇□■■□□□○●△△△△△ + ○▲◇○○○ ▲○◇△■◇ + ■□□◇◇◇□◆ ◆△◆◇△ ▲ + ○▲○■◇□◇●●□◆◆□■◆△ + ◇●●◆◆▲□△□◆■■ + ◆◆◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_33.txt b/codex-rs/tui/frames/shapes/frame_33.txt new file mode 100644 index 000000000000..c5598aa7a739 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_33.txt @@ -0,0 +1,17 @@ + + ◆▲□●□●□●◇△◆ + ◆□□◇◇○□●◇△◇▲◇□◇□△ + ◆△○◇□□□▲●■◆ ■□△◇◇□■▲ + ▲○△◇△◆◇▲◆ □◇◇○◆□ + ◆△◇△◆◇◇△○○○ ◇□○○○ + △△▲ ○◆○○◆○▲ △○△◆○ + ◇◇□ ○▲□◇◆◇○ ◇□◇◇ + □◇▲▲ ▲■■○△●▲ ○■◇□ + ◇◇◇ ◇ ▲◆ △□▲▲▲●●●□◇◇□△◆◇○◇ + ○△◇■■△△▲△◆◇◇◆◆◆◆◆◇◇□○○○◇◇△ + ○●○▲ ◇△◇△ △ ■■■■□□□ ◇◇▲△○◆ + ○□○□▲■▲ △△●△●△ + □○■□▲□●▲ ◆□◇▲△□○■ + ○◇■□○□◇▲●□◆■●▲○□◇□ + ■●●□▲◆◆◆○□□●▲■ + ◆◆◆◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_34.txt b/codex-rs/tui/frames/shapes/frame_34.txt new file mode 100644 index 000000000000..5a44de825612 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_34.txt @@ -0,0 +1,17 @@ + + ◆▲●□□□□●◇▲◆ + ◆□□□△○●◇■◇□□●□□●▲●◆ + ◆△△▲◇□■△◇□◆ ◆○○○▲■□□▲ + ▲△◆◇□◆△▲◆ □◇▲□○○ + △△◇○▲△◇ ◇■ ▲ ○○□○○ + ▲△○◇◆△ ○○■◇○□○ ○▲○◇○ + ◇□○◇◆ ■○◆□◇◆○ ◇●◇◆ + ○◇●■△ ◇□◆◇△■◇ △ ◆○ + ▲■△△■ △○△△■◆●●□□□□□□◇◆◆■ ◇○◆ + ◇◇○▲△▲▲○■△△■◇◇◇◆◇△◆◆◆▲△◆◇◇◆◇○ + △◆○▲ ◇○◇□○ ■□□◆◆◆◆○ ▲△▲○◇■ + △▲○●●○◆ △●△◇△■ + ○□◆◇□○●▲ ◆□●■◆△▲◆ + ▲○△◇■◇□▲●▲□●□●□◆□□△■ + ■◇○●●◆◆◆◆◆●●□●○ + ◆◆◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_35.txt b/codex-rs/tui/frames/shapes/frame_35.txt new file mode 100644 index 000000000000..1c1728676b20 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_35.txt @@ -0,0 +1,17 @@ + + ◆▲□●●□□□●□▲◆ + ▲□●□△◇□□□■◇□◇△■◇□●◆ + ◆△△◇●□□◇○□■◆ ◆■□●□△■◇□▲ + ▲△△□△▲●▲◆ □□◆○◇◆ + △△◇◇△△○ ■○□▲ ◇○□◇▲ + ●△●△□◆ ■◇◆■△△□◆ ◇○○◇ + ○ △△ ○○ ■□◆△ ◇○○○ + ◇◇◇△ △○○●△■ △ ■△ + △ ▲◆ ▲◇◇△●■△◇●●◆●◆●□◇◇ ◇◇●□ + ◇▲○●■ △△●■□□△◇● ○▲◆◆◆◆△■◇△■ ◇■ + △◆○▲■△○◆◇△○◆ ◆■○○○○□○○□△△◆▲◇ + ■△○○△◇ ◆△□ ▲○ + ○●◆○●▲△▲ ▲●△◆□□■ + ■○◆■◇□◇▲△◇◇□◇●△□■◆△●■ + ■○◇●●◆◆△◆◆□□□■○ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_36.txt b/codex-rs/tui/frames/shapes/frame_36.txt new file mode 100644 index 000000000000..0cac995ed7ab --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_36.txt @@ -0,0 +1,17 @@ + + ◆◆□●□□□□●□▲◆ + ◆●■■○□△●△□○□●□□■□□□◆ + ▲○■○▲□□▲□■◆◆ ◆■□◇□□◇▲□◇▲ + ◆◇■□□◆◆◆◆ ■□◇○■◇ + ▲△◆●○■◇○◆○▲△ ○○△□ + △ ▲△◆ □◆ ■◇△▲ ■○○□ + ◇ ● ◆ ○ ■○○ ◇○◇▲ + ◇◇○◇ ◆○ ◆△△ ○◇ ◇ + ◇■■◇ △△ ▲□■■▲□□◆□□□□□◆ ◇○◇◇ + ◆□◇○ ▲◇◆◆△◇△ ◆◇◆◆◆◆◆◆◆◇◇ ○△■◆ + ■ □◇○ □◆□△○ ■□□○○○○○■ ◇▲■△ + ■▲■○●△ ▲△◇■△ + ○△◇◇□◇▲ ▲□□■△▲◆ + □▲◆■□□●○●◇◆□◇●■▲■▲●□■ + ◆□□△◇◆▲▲◆▲▲□□□□■ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_4.txt b/codex-rs/tui/frames/shapes/frame_4.txt new file mode 100644 index 000000000000..31e55f9cb8c8 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_4.txt @@ -0,0 +1,17 @@ + + ◆△●●□●●□●□▲◆ + ◆▲■□□◇△ ◇○●△□●◇■△□▲◆ + ▲□●◇◇◇□□■◆ ◆○◇○○□◇△○○◆ + △△◇▲◇■△□▲ ■◇□□○□○ + △◇△◆◇●◇○●○◇▲ ○■○◇□○ + △◇◇○◇ ■◆◇◇△□○ ■ ○●◇▲ + ■○◇●◆ ■□□○□■◇▲ ▲ ■●◇ + ◇△○ ▲◇△□○◇◇ ◇◇◇ + ◆◇ ◇◆ ●◇△■○△△□◇□□□◆◆◆◆▲▲ ◇◇□ + ◇○△ ◇ ▲△△△▲◆◇◆●○◇▲△◆▲△△○●◇△△■◇ + ◆◇●■○▲○○▲○△ ■□□□□□□□■○▲◇◇◇◆ + ▲◇○■◇◆◆ ▲○●△□□■ + ○○▲△○ ▲◆ ◆△○▲□●◇△ + ◇◇◇●◆□□○●●●●▲□●□□○◇□◆ + □○○◇△◇◆◆◆◆◆●▲□◆ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_5.txt b/codex-rs/tui/frames/shapes/frame_5.txt new file mode 100644 index 000000000000..a8ae0ab81937 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_5.txt @@ -0,0 +1,17 @@ + + ◆△●●●□●●●□▲◆ + ◆□■□△□△◇◇□◇●□●◆◇■□▲ + □◆△○□◇□■◆ ■○●◇□○ ▲□▲ + ▲◇△◇△◇△●▲ ▲○◇◇◇◇◆ + ▲○△□△○◆□◇◆○▲ ◆ ○◇○◇◆ + ◇◇◇◇△ ○◆◇○●△◇◆ □△◇◇ + ◇△◇ □ ◆○◆◇○■◇▲ ■■◇□◇▲ + ◇△◇◆■ ▲◆●◇△◆◇ □◇◆◇ + ◇▲◇○□ △◆◇■▲◇□●◆◇□◇◆◆◆□▲◆■◇△◇ + ◆○◇○▲▲△□□■ ◇■ △△○●○▲●△▲△◇△□◇◆ + ○▲◇◇▲◇▲○○△□◆ ■□□■■■■■▲◇●◇○△ + ○○□△○○ ◆ △◆△□▲○ + ○△□○◇●□◆ ◆●□▲□■●□ + ○◇□◇▲□□○●◇●◆▲□●□○◇●□ + ○▲□◆□■◆◆◆▲◆▲□□◆ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_6.txt b/codex-rs/tui/frames/shapes/frame_6.txt new file mode 100644 index 000000000000..e0b1f854547b --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_6.txt @@ -0,0 +1,17 @@ + + ◆△●●●□●●□□◆◆ + ▲○●△□◇●□□◇□□●◆▲□□◆ + ▲▲▲◇◇◇□■ ◆■◇△●□□□△○◆ + △▲◇□◇□◆●▲ ○□◇◇◇◇▲ + ■●◇◇◇□○○□□○ △○○◇○▲ + ●▲△◇△◆○□●◇○◆◇▲ △●○△◇ + ◆ □◇◇ ○○◇△○○ ○□◇◆◇ + ◇ ◇◇◇ ◆◇●◇△◇◇ ◇□○◇◇ + ◆△◇▲◇ △■◇◇□●△□◆◇□◇◆◆□□◆△○△◇ + ◇◇○▲△◆○◆●□ ◇△●□▲◇◇▲●●●○◇▲◇○ + △ ◇ △◇◇◇●△■ ■□■■■■□■◆○○△◇ + ▲ ● △▲◆ ▲△◆△△◇◆ + △◆○◆□□▲ ◆□○◆△□▲□ + ◆■◇□▲■□ ●◇◆◆▲□□□ △●■ + ○▲●□□▲◆◆◆◆◆▲□□ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_7.txt b/codex-rs/tui/frames/shapes/frame_7.txt new file mode 100644 index 000000000000..7e69d68d573f --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_7.txt @@ -0,0 +1,17 @@ + + ◆△◇●□□●●□▲◆ + ▲□◆□○◇□□□●●○● ◆□▲ + □ ▲◇◇◇□◆ ◆○◇◆ □△◆□▲ + ○○◇◇◇◇●□ ○◇○□◇△▲ + ◆■◇□◇◇○◇△○▲ △ □◇◇○ + ◇ ●◇◇■ ◆■○●◇○ ▲▲◇◇◇ + ◆○■◇ ○◆◇○◇○◇ □□○◇◇ + ◆■△◇◇ ●□△◇□◇◆ ■ ◇◇◇ + △■◇□ ▲■◆△◇○△◇◇●●◆◇◇●●□○◇◇ + ◇▲■◇◇●△◇◇△■◇△○◇◇○○▲●▲●○◇○◇ + ○ ●○△□◇□◆△ □□□□□□■■◆△◇◇ + ■◆ △◆△◆◆ ▲■▲△●△■ + ■●▲□○○○◆ ▲○◇▲□△□■ + ○◆■○◇□○●◇◆◆◆□□◇□□△ + ■◇□■◇△◇◆◆▲◆●□□ + \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_8.txt b/codex-rs/tui/frames/shapes/frame_8.txt new file mode 100644 index 000000000000..b7bddd4156ac --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_8.txt @@ -0,0 +1,17 @@ + + ◆◆□◆●□□●□▲◆ + ▲■□△◇◇◇□●● ◇ ■□ + ◆◇●▲◇△△□ ▲■◇△◇□○□○▲ + ◆□○□△◇▲□◇ ■ ●○◇◇○ + ■ △◇△▲○◇△○ △■○◇◇▲ + ◇■▲◇○○○◆○◇◆□ ○□◇◇◇ + ◇□◇◇◇ ■◆●○◇◆◇ ◇○◇△◇▲ + ○▲◇◇△ ◇□○△◇▲□△ △◆△◇◇ + ○ ○◇◇◆○ △□●◇◇△□▲□▲▲○◇◇◇ + ○□△●◆◇■▲◇▲◇◇●◇●◆◆◇◇●■ + △△○○◇○◇◇◇△■○□○□■○□■△○◇ + ■▲■◇○◇◆◆ △▲△△△■ + ■○◆□○○○◆ ◆△○◆◇□△□ + △△■◇▲□ ●●□□●△●▲◇◆ + ◇◇◆□□○◇◆▲□△●◆ + ◆◆ \ No newline at end of file diff --git a/codex-rs/tui/frames/shapes/frame_9.txt b/codex-rs/tui/frames/shapes/frame_9.txt new file mode 100644 index 000000000000..4342d3c81e55 --- /dev/null +++ b/codex-rs/tui/frames/shapes/frame_9.txt @@ -0,0 +1,17 @@ + + △●○□△□□●▲ + △□◆△◇◇□■● ◆▲○◆ + △ ▲◇◇△□○○●△ ○◆◇▲ + ■ ◇◇△●●◆ ◆○▲◇○○◇□▲ + △□■●○□■◇□▲ □△■□△□◇◇ + ◇ ◇◇◇■○◇○●◆◆ ◇■◇□◇◇◇ + ◆ ◇□○◇□□◇○○▲■△▲△ ◆◇◇ + ◇ ◇△◆▲○◇◇◇◆△●○◆◇ ◇◇ + ■▲◇■ △◇■◇◇◇◇□◇◇▲◇◇◆ + ◇◆◇◇◇◆▲◇○◇◆◇◇◇◇◆◆◇◇◇ + ■ ◆○△○○◇△■□□□□◇◇■△○◇ + ○◇◇◇○▲◆ ▲△ ○△◇◆ + ○□○◇○○ △●◆●◇△□ + ○▲■○◆◇□△□●●●□◇◆ + ■▲◆ □○○◇●□△■ + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_1.txt b/codex-rs/tui/frames/slug/frame_1.txt new file mode 100644 index 000000000000..514dc8ac49cc --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_1.txt @@ -0,0 +1,17 @@ + + d-dcottoottd + dot5pot5tooeeod dgtd + tepetppgde egpegxoxeet + cpdoppttd 5pecet + odc5pdeoeoo g-eoot + xp te ep5ceet p-oeet + tdg-p poep5ged g e5e + eedee t55ecep gee + eoxpe ceedoeg-xttttttdtt og e + dxcp dcte 5p egeddd-cttte5t5te + oddgd dot-5e edpppp dpg5tcd5 + pdt gt e tp5pde + doteotd dodtedtg + dptodgptccocc-optdtep + epgpexxdddtdctpg + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_10.txt b/codex-rs/tui/frames/slug/frame_10.txt new file mode 100644 index 000000000000..bd3b8fafff42 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_10.txt @@ -0,0 +1,17 @@ + + dtpppottd + ppetptox5dpt + ddtee5xx-xtott + edd5oecd-otppoot + 5 ceeged pt5d5e5 + ee pepx55o gedge + o xpgpeexep e5t + g eeot5tee-de-oee + g xo ooecxxtotcee + e teoted5dpdddepe + t geeeeegggotgoee + oeptotpg dxggt55 + ep eeexptct5e5e + cepp5etcdg55p + pt dpodtcp + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_11.txt b/codex-rs/tui/frames/slug/frame_11.txt new file mode 100644 index 000000000000..9eaf147a6a09 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_11.txt @@ -0,0 +1,17 @@ + + tppppttd + 5g ceeeoot + 5gddeecop5ot + eddeeoctdo55 + dg-coetopcdeet + eteeetdcced5ee + pp teeeeeedeoo + e ee5eeo5 ege + 5 pe5eep5tede + pp de5otg5eded + pe- eeeeo5eooe + od-5edp5ppcee + gd peooddecg + otgpeetd5pe + od pptdte + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_12.txt b/codex-rs/tui/frames/slug/frame_12.txt new file mode 100644 index 000000000000..11163a99b9b1 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_12.txt @@ -0,0 +1,17 @@ + + tpppt- + toed5eto + g e5ott + 5txeeooge + e pxeee-5e + ep--pdgdeg + e x5oeeo + e 5toeeg + pt x5eetex + e 5epcpd + e- egopp5 + t pegdte + 5 ppetpoe + pd5gteoee + o pxo-5 + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_13.txt b/codex-rs/tui/frames/slug/frame_13.txt new file mode 100644 index 000000000000..eb072e40ad2c --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_13.txt @@ -0,0 +1,17 @@ + + 5pppt + eddee + eedeg + epped + p ee + gc-ee + t ee + t ge + 5t dx- + eg toe + pe-- xe + eddde + etted + pddeo + t -go + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_14.txt b/codex-rs/tui/frames/slug/frame_14.txt new file mode 100644 index 000000000000..100f30930230 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_14.txt @@ -0,0 +1,17 @@ + + tpppc + t5dd-o + edee- e + ee5egdxt + eeeo e + xpee pe + eeee - p + eeoee o + eeex g + gd55 c5 + oeggt-te + epxeddde + 5ggeoooe + eo5pdd5 + dp po5 + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_15.txt b/codex-rs/tui/frames/slug/frame_15.txt new file mode 100644 index 000000000000..5761f309d46f --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_15.txt @@ -0,0 +1,17 @@ + + ttpppxd + etoeedcpt + 55epooegpe + e5e 55t-dde + eeogooee5gde + oee-ee55e g + ee eeeexxte + ee5xeteee p-e + eetttpeeed-ce + edec5eoxp- -e + e5eeeede e c + epp-dxeo- o + peot 555e ce + edeeoo-to5 + odpdddd5 + ee \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_16.txt b/codex-rs/tui/frames/slug/frame_16.txt new file mode 100644 index 000000000000..f9001140ed83 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_16.txt @@ -0,0 +1,17 @@ + + dotgpptxd + 5deepeeoeoo + e55go5ooee po + 555 cpdoeoeeppe + eecd5ppoeep5eeo + eeepep eoo ge x-e + ooe 5eteeee5pgt e + e5c eeee5eeegc ee + ooexetx5deegpt 5 + pgddtooope-de tde + eeetgg5poecgc-xee + eep ee e55t 5 + oep e 5t5dte + oexgdpx55tde + pc-docddcp + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_17.txt b/codex-rs/tui/frames/slug/frame_17.txt new file mode 100644 index 000000000000..696d932d409c --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_17.txt @@ -0,0 +1,17 @@ + + totttccxtd + dcppexxpopetgdt + tpo5dooettgeeedgo + 5e5d5egde pecoxeeoo + e5d x eg5ooo55 t + eopt5e tc5e 5to5e-5 + pgc5e t55ed pgee5oe + -goeg g55ee5eteeocp + t5p5oootxodeodcoeee e + egdcdde5po5eeogotpto + ooo5gggppppodep55o op + oeedp e5eee c + doogpod tpt5dd5 + t xptootedcpcep + etc5dttxpdtp + e \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_18.txt b/codex-rs/tui/frames/slug/frame_18.txt new file mode 100644 index 000000000000..abb0da53d29a --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_18.txt @@ -0,0 +1,17 @@ + + dootootcxtd + dtgdctttxddtgtccd + d5cepgpogtg-dgt55o p + teep-tdgp poed5oxocooot + do55 5e dgtdo5x5edocood + 5oe ttx-ddd5tptep-5d5e5xg + eeoc5 t5p5egd5gpeoeot + go-xe dogeecd eceg + ogg tooococtxetep5ot epee + dooepop xe5ddodxeedcxeo t + e5eoeggpppppe odd5e5p5e-e + oogtot eepg5pdp + odptdd- dpdd5ote + pe-ppttooodppd5dtp + gxed5ddt-tgctg + e \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_19.txt b/codex-rs/tui/frames/slug/frame_19.txt new file mode 100644 index 000000000000..ffc4d2b47551 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_19.txt @@ -0,0 +1,17 @@ + + ddootxtoox-d + dteeeo5ooodpdteptt + tpcc5getpe epctepco + 5ceptde doottdept + ee5e5e tedg5geo eot + eo5pp depx5g-5 p-pe + doexp 5pd5ette 5c5te + eeee ecgoegt e eee + epeotoccoooxxxetpcpec o gee + dc5teop5dptotet dd codot5-ed + pog5tegggggppg dod5e55 55p + oodpdo e55d55p + pgdoxxpt tco-5ece + pg-ep5xtddoc5pg cpxp + gx-dc-pdt-dp-d + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_2.txt b/codex-rs/tui/frames/slug/frame_2.txt new file mode 100644 index 000000000000..f4419e3d693a --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_2.txt @@ -0,0 +1,17 @@ + + d-dcotooottd + dtt5pcteexoxeodpeptd + tepeoppxpee egpop5eecet + de5d5ppttd -toe5et + tdg5pdeodood dteoet + p5tge epot5ot ooepe + teppe d5ecedet 5gege + eg oe tepeecp ep5-e + pggoe cedddeg-xtttttttttedexp + dope 5eep 5p eoodddd--ddet5geg + ooo p po--ep egpppppppgetpee + pod-5t e ttc5tp + -oddett todtgdeg + exdcddgptccocc-opedeep + eptptxxddddxc5pg + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_20.txt b/codex-rs/tui/frames/slug/frame_20.txt new file mode 100644 index 000000000000..0039bd880b1c --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_20.txt @@ -0,0 +1,17 @@ + + ddtoxxototdd + tcogctdtoooeeddpott + ttgdtpgxpg egdgotpetd + degdpep dtteo5poo + de tep d5gdeedpoeeo + 5etep tppceg5 poxeo + cecte tp -tpd toe5 + edd5e ggccegt exge + e5eectocoooooodtpcddoop 5 eo + pededg 5eeddddeo poogeoo t5 ee + otdopgggggggpg otoeete 5o + p dp5t d5p-e5 + oddptxd tcpecpp + dt--gxtdcctcgxget5pp + eptxdgoddddepgg + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_21.txt b/codex-rs/tui/frames/slug/frame_21.txt new file mode 100644 index 000000000000..87e3597d5d8f --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_21.txt @@ -0,0 +1,17 @@ + + ddtotootottdd + ttpeeddtxoxtcde-ptd + cpddtpdge edxptdept + tpecgp dtcptcpt + 5t5pe te5do ooddt + 5g5e tppd55 oodt + epee dg5et5p ocog + eo oc get e e + e e ttcccccttt detget c5 e + e xo ed dte dgepet 5g-5 + c g- eeggg pe ppdtc 5e t + pt ccd dpg 5 + pd d-d d-cpp te + pod pgptcxxccopgg -e + pttctddddtdctpe + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_22.txt b/codex-rs/tui/frames/slug/frame_22.txt new file mode 100644 index 000000000000..8dfe7daaab68 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_22.txt @@ -0,0 +1,17 @@ + + dttotootottd + dt5pe-d5e5oeecet5ptd + tcpcxoppe egpopptddtt + cpxppg tteeedpo + oex5p td5eoe-5cpe + oep5e tx5ecd5e oeooe + etpo5 xteop5p xe5e + eeoee eoexdo edege + eetgt txoocccottdopedgot decgg + deo pdootg5tgx55e opcdgettg5oo + ooode gggggppge ecptd555gte + opodot dptgetp + pgtc ttd d-ptgpd5 + pcpttgpxcotcc5opggptp + gxgxdcddd-od-ope + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_23.txt b/codex-rs/tui/frames/slug/frame_23.txt new file mode 100644 index 000000000000..f573acb7142b --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_23.txt @@ -0,0 +1,17 @@ + + dttdotpttotdd + doeteodtdootedepetd + tgteptcpe gxcteoceet + 5cepc5e dccoeeco + 55ee5p t5ttpp5poeo + toeg5e dppppp5ppexoet + eeege 5c5- dee ggepp + x5e e egetdot e p5e + dgo etxcooooocoedpedgod e exe + eoodegog 5eo oedotx ode + 5pe e eggggggg pe5coed5d5e + teede- t5eod5e + o etp5dd d-gcp5t5 + oootcptoxoodttg-dtpe + gxgpooxddtddppe + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_24.txt b/codex-rs/tui/frames/slug/frame_24.txt new file mode 100644 index 000000000000..92833e8c589d --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_24.txt @@ -0,0 +1,17 @@ + + dxdcttpootd + tgd5geddxoottdpt + tgpco5tgx -ecpxppt o + tp55t5eotoex5egdpoeodp + t 55o5t egg5odeeeoetp + xtotoe ptt5g-ecg5go + e exg t5dt5g5doeoded + e oee eto5odexd5-eee + e- eccccttxxxttdptde e ee + d 5 gpe5ttcctte-dotpe epe + o poeopgggggge ot-poeo5te + o otpo egg5c5 + o-poee- dogd5cdp + --ptodgxxcoocedd5e + pcdptcoddootp + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_25.txt b/codex-rs/tui/frames/slug/frame_25.txt new file mode 100644 index 000000000000..d8b8655dacfc --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_25.txt @@ -0,0 +1,17 @@ + + dtopcttttd + tgptpedcoepeet + 5e55ttg-etoooeeed + etpe 5g oe goetpo5 + tddot5pdc5deg e55o5p + otxexdpt-dec 5ete55et + e5epe edd5od5eo5dgeoe + eee5e-ggxdoo5eodxoeeo + dtoegeddooootxeooetpeo + 5-ge5etedeecpdeopo5oe + oxp5oeegggggpt5eoe5ee + edgectco-tpcd5t55e5 + dededodpc5td5dee5 + o-coodpoeppgpep + xtgpottdtep + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_26.txt b/codex-rs/tui/frames/slug/frame_26.txt new file mode 100644 index 000000000000..4be73d44de00 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_26.txt @@ -0,0 +1,17 @@ + + dcpppgtt + 5pec5oetcet + 5pggecoeoggot + cpgteexdeedt5d + edtoe-xgpo5ceoc + o ge5c5gpdogoo5 + eg ppoee5eeccgt + 5- oeeeddoee5ee + e -opctxtoo5oee + g -de5teddtpoope + eeg5epgot5etoee + odxe5o 55geee + p peo de5e5t + egpoogpdppc + o5cdpxdop + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_27.txt b/codex-rs/tui/frames/slug/frame_27.txt new file mode 100644 index 000000000000..f333909d2b75 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_27.txt @@ -0,0 +1,17 @@ + + cppptt + ecc5e5o + cpe pe5pe + exxdeecex + e eed-po + xd-dgeeeee + o geedpeeg + e eeexogee + e- -geteeee + po -gdedpee + e- ddppt5p + eetteed5e + eootot5ed + oddeoo55 + pog do5 + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_28.txt b/codex-rs/tui/frames/slug/frame_28.txt new file mode 100644 index 000000000000..3c0deb542c8a --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_28.txt @@ -0,0 +1,17 @@ + + 5ppc + etdee + o cee + e-epe + e xe + e -ge + dex de + e-gge + 5o de + ee-cxe + e de + eotoe + eopee + pdd5e + x -te + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_29.txt b/codex-rs/tui/frames/slug/frame_29.txt new file mode 100644 index 000000000000..0c6277f4d52f --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_29.txt @@ -0,0 +1,17 @@ + + tppppt + tep5gpo + dtee pge + eeeedot5 + o xge e + etxee dd + eooeee d + eeoxe + eexpe e + deoee e + pee5ed o + e5xxe de + xeeeexde + eoep5gep + xep -t + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_3.txt b/codex-rs/tui/frames/slug/frame_3.txt new file mode 100644 index 000000000000..b1e91736085b --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_3.txt @@ -0,0 +1,17 @@ + + d-octtooootd + dtt5oeetegooecddeptd + tc5pcepgge egxgppt5det + 5t5oecttd eopgeeo + p5pe5d5eepod odoeed + 5eoo5 -teocoo e ooe + op e ppoedget -p5et + t-ete t-eg5oe xdeoe + -gpe ceptxep-xottdddttdxdgce + tocot -5p5cce epeddtgo-tcoeeoee + pde e geettg gpppgggppt555t5 + ppx ot e 5dtd55 + od55pot ttc5gtep + odttdppdococtcopcedtg + eptpdcxddddxc5dg + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_30.txt b/codex-rs/tui/frames/slug/frame_30.txt new file mode 100644 index 000000000000..9dfd28bc20da --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_30.txt @@ -0,0 +1,17 @@ + + dcgpptt + d5ceeoppoo + gpdetooetto + eeoe5edoeo ot + opeeeoetet e + epedt peeee-e + o5eeeod o5oe oe + ge ptoxtege- e + gedgoxoxo5et e + geeoeddxegtt e + goeecodpxeetxe + ep55t5poeeg e + oeoee5pxetx5 + tdttod5et5p + o--edddcp + eeee \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_31.txt b/codex-rs/tui/frames/slug/frame_31.txt new file mode 100644 index 000000000000..1dba8edd8f76 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_31.txt @@ -0,0 +1,17 @@ + + d-cptptot + dtpcttdgtoppt + teo55tode-gedpo + tx5tddpcdtooeoxeo + deeeet-podtgoe5dd + cedexdepgocpt-5etge + 5ee edpeo-o5cpepe5e + oot exgeo edggexo-e + eotdepdxex5txxed e + geex55eedddodeoee p + dpd5tet 5pppe5epxdg + eeeooot dop e + cepodgt - epe5e + ceoe5deetegtee + pgoxdtp5-cp + eeee \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_32.txt b/codex-rs/tui/frames/slug/frame_32.txt new file mode 100644 index 000000000000..33160e71634b --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_32.txt @@ -0,0 +1,17 @@ + + tttttccxtd + cpxppxoeeppext + 5dpodttdc-epepoppt + p55edtec- - tdoettgt + 55etepoppec petxo5opot + o5ootopeeceetd et5 p + pegctepoeettetoe5d55epd + eeetd gdeeg5pec-dgoegge + oee-d pdegddetttxxxoegoe + pggdeepopodddddeepecx-d + topee5 epggpppdc555-5 + otedoo toe5px + pppeextd d5de5 t + o-ogxtecopedtgd5 + xcoddtt5pdgg + eee \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_33.txt b/codex-rs/tui/frames/slug/frame_33.txt new file mode 100644 index 000000000000..ff8827f3d2f3 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_33.txt @@ -0,0 +1,17 @@ + + dttototox-d + dtpeeopoxce-epep- + d5oetpttoge gp5eetgt + to5e5detd peeodp + d-e5eee5odo etood + 55t odooeot 5o5do + eet g otpedeo epee + pett tppo5ot ogep + eee e te 5ptttcootxxt5deox + d5epg5ct5exedddddeepdddxe- + ooot e5e5 5 ggggppp eet5de + otoptgt 55c5o5 + pogptpct dtet5pop + oxgpotetctdgctopxp + goottddddtpc-g + eeee \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_34.txt b/codex-rs/tui/frames/slug/frame_34.txt new file mode 100644 index 000000000000..4b1eb6a5a235 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_34.txt @@ -0,0 +1,17 @@ + + dtottttoxtd + dtpt5ocegxtpoppctod + d55tepgcxpe edootgppt + t5depd5td petpoo + 55eo-5egeggt oopod + t5oee5 oogeopo otoeo + epdxd podpedd ecxd + oeogc epde5ge 5 do + tp5-g 5o55gdoottttttxddg xde + eeot5ttdg55pxeedx-dddt5deeeeo + 5dot eoepdg gppeeeed t5toep + 5tocood 5c-e5p + oteetoct dtogd5te + -o5xgettcttopopetpcp + gxocodddddcopod + eee \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_35.txt b/codex-rs/tui/frames/slug/frame_35.txt new file mode 100644 index 000000000000..f2432dc0adf3 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_35.txt @@ -0,0 +1,17 @@ + + dttcotttottd + tpop-xpptgepxcgxpod + d55ectpedpge egpop-gept + t55t5tctd ptdoed + 55xe55o gopt eopet + c5c5te pedg--pd eooe + o g-5 oo ppd- edoo + xexc 5ooc5p 5 g5 + 5 td tee5cg5eoodcdotxx exop + etdcp 55cgpt5ec otdddd5gx5p ep + 5eotp5ode5de egddddpddp55dte + g-do5x d5p td + ocedctct tc5dppp + gddgxpx-cxxtxc5pgd5cg + gdxcodd5ddttpgd + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_36.txt b/codex-rs/tui/frames/slug/frame_36.txt new file mode 100644 index 000000000000..c84a104e4ac2 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_36.txt @@ -0,0 +1,17 @@ + + ddtottttottd + doggot5c5totcttgpptd + topottp-pgee egpxptetpet + degptdddd ppxoge + t5dcopeoeot- do-p + 5 t5e pd ge5t godp + e cge go goo edet + eeox do d55g oe e + epge 55 tpgptttdtttttd eoxe + dpeo tedd5x5 gexdddddddee o5pe + p peo tdt5d gppdddddg etg5 + ptgoc- t5eg5 + o5eetxt tttg5te + ptdgppodcxdtxcg-gtctp + ept5xdttdttttppg + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_4.txt b/codex-rs/tui/frames/slug/frame_4.txt new file mode 100644 index 000000000000..2eed2c846535 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_4.txt @@ -0,0 +1,17 @@ + + d-octootottd + d-gtpe5geoo5pceg5ptd + tpoeeetpge edxodpe5ood + 55eteg-tt geppopo + 5e5deoeocdet ogoepo + 5eede pdee5po p ooet + goece pppotget t gce + e-o te5pdee eee + deged ce5gd55txtttddddtt eep + eo5ge t555tdeeooet-dtc5dce55ge + eecpotooto5 gppppppppd-eeee + teogede tdc5ppp + ootcdgtd d-dtpce5 + xeeodppoccocttoptoepe + pooxcxdddddc-pe + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_5.txt b/codex-rs/tui/frames/slug/frame_5.txt new file mode 100644 index 000000000000..e0c7693a9eca --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_5.txt @@ -0,0 +1,17 @@ + + d-occtooottd + dtgt5p5eetxotcdegtt + pd5otepge gdoepogtpt + te5e5e-ot -deeeed + to5p5ddtedot e oeoed + xeee5 odeoc5ed t5ee + e5egt eodeoget ppxtet + e5edg tdoe5de tede + etedp 5degtepodxtxdddttdge-e + doeott5tpg egg55oodto-t5e5pee + oteetxtoo5te gppgggggtxceo5 + oot5oo e 5d5ptd + d5poeotd dottppcp + depetptocxodttopdxcp + d-pdpgddd-dttpe + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_6.txt b/codex-rs/tui/frames/slug/frame_6.txt new file mode 100644 index 000000000000..d5ac091f39ce --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_6.txt @@ -0,0 +1,17 @@ + + d-occtoottdd + tdc5peoptettcdtptd + ttteeepg egx-optp5od + 5tepepdot oteeeet + poeeepootpo -ooeot + c-5e5edtceodet -oo5e + d txe gdoe5do dtede + x exe deox5ee xtoee + d-e-e 5peepc5tdxtxddttd-o5e + eeot5dddct e5opteetcoooeteog + 5 e 5xeec5g gpgggppgeoo5e + t c 5te tcd55ee + -eodppt dtdd5ptt + egxptgtgcxddttppgccp + d-cpttdddedttp + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_7.txt b/codex-rs/tui/frames/slug/frame_7.txt new file mode 100644 index 000000000000..02d1f1ae521b --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_7.txt @@ -0,0 +1,17 @@ + + d-xcptoottd + tpetoetptooocgept + p teeepe edxegp5dpt + ooeeexct dxope5t + epepeede5ot - peeo + e ceeg epoceo t-eee + eoge ddeoeoe ppdee + dg5xe ot5epee p eee + 5gxp tgd5eo5xxccdxxocpoee + etpeeocxe5pe-oeeoototcoeoe + o od5pepd5 ppppppggd5ee + pd 5d5de tg-5c5p + pcttoood tdxtp5pp + odpoepocxdddtpept5 + gxpgxcxddtdcpp + \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_8.txt b/codex-rs/tui/frames/slug/frame_8.txt new file mode 100644 index 000000000000..d028ab360ee0 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_8.txt @@ -0,0 +1,17 @@ + + ddtdcttottd + tgp5eeepoogx gt + deote55pgtgx5xpotdt + dpop5ette p odeeo + p 5e5toe5o -poeet + epteodddoedp oteee + epeee pdcoeee ede5et + otee5 eto5etp- -e5ee + o oeedd g5poex5tttttoxee + g op5odegteteeceoddeeop + 55ooeoeee5pdpdpgopg5oe + ptgeoeee -t555p + podpoood d5odet5p + -cpetpgcctpc5otee + xxdppoedtt5oe + ee \ No newline at end of file diff --git a/codex-rs/tui/frames/slug/frame_9.txt b/codex-rs/tui/frames/slug/frame_9.txt new file mode 100644 index 000000000000..2481e07a3576 --- /dev/null +++ b/codex-rs/tui/frames/slug/frame_9.txt @@ -0,0 +1,17 @@ + + -odp5ttot + 5pd5eepgogd-od + 5 tee5pddo5godxt + g ee5cod ddteodett + 5pgcopgept p-ptctee + e eeegdeocdd epepeee + e xpoeppeootg-t5 eee + e x5dtoxeed5oode gee + g gteg 5egexxetexteee + edeeedtededeeeeddeee + g ed5ooe5gppppeeg5oe + oxeeote t5 d5ee + otoeoo 5cdce5p + d-godep5toccpee + p-d pooect5g + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_1.txt b/codex-rs/tui/frames/vbars/frame_1.txt new file mode 100644 index 000000000000..0ca3a5d334c2 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_1.txt @@ -0,0 +1,17 @@ + + ▎▋▎▋▌▉▉▌▌▉▊▎ + ▎▌▊▋▉▍▉▋▉▍▌▏▏▌▎ ▎█▉▎ + ▊▏▉▏▉▉▉█▍▎ ▎█▉▎█▏▌▏▏▏▉ + ▌▉▎▍▉█▊▊▎ ▋▉▏▌▏▊ + ▍▍▌▋█▍▏▍▎▍▍ █▋▏▍▍▊ + ▏▉ ▉▎ ▏▉▋▌▏▏▊ █▋▍▏▏▊ + ▉▍█▊▉ █▍▏▉▋█▏▎ ▏▋▏ + ▏▏▎▏▎ ▊▋▋▏▌▏▉ █▎▏ + ▏▌▏█▎ ▌▏▏▍▍▏█▋▏▉▉▉▉▉▉▎▉▊ ▌█ ▏ + ▎▏▌▉ ▎▌▉▎ ▋▉ ▏█▏▎▎▎▋▋▊▊▊▏▋▊▋▊▏ + ▍▍▎█▍ ▍▍▊▋▋▎ ▎▍▉██▉ ▍▉█▋▊▌▎▋ + ▉▍▊ █▊ ▎ ▊█▋▉▎▏ + ▍▍▊▎▍▉▎ ▎▌▎▉▏▎▉█ + ▍▉▊▍▎ ▉▉▋▌▌▌▌▋▌▉▉▎▊▏▉ + ▎▉█▉▏▏▏▎▎▎▊▎▌▉▉█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_10.txt b/codex-rs/tui/frames/vbars/frame_10.txt new file mode 100644 index 000000000000..b422fb1274ee --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_10.txt @@ -0,0 +1,17 @@ + + ▎▉▉▉▉▍▉▉▎ + ▉█▎▉▉▉▍▏▋▎▉▊ + ▍▎▊▏▏▋▏▏▊▏▉▍▊▊ + ▏▎▎▋▍▏▌▎▋▍▊██▍▍▊ + ▋ ▌▏▏█▏▍ █▊▋▎▋▏▋ + ▏▎ █▏▉▏▋▋▍ ▎▎█▏ + ▍ ▏▉█▉▏▏▏▏▉ ▏▋▊ + █ ▏▏▍▉▋▉▏▏▊▎▎▋▍▏▏ + █ ▏▍ ▍▍▏▌▏▏▉▍▉▌▏▏ + ▏ ▊▏▍▊▏▎▋▎▉▎▎▎▏▉▎ + ▊ █▏▏▏▏▏██ ▍▊█▍▏▎ + ▍▎█▊▍▊▉█ ▎▏██▊▋▋ + ▏█ ▏▏▏▏▉▊▋▊▋▏▋▎ + ▌▎▉▉▋▏▉▌▎ ▋▋█ + ▉▊ ▎▉▍▎▊▌▉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_11.txt b/codex-rs/tui/frames/vbars/frame_11.txt new file mode 100644 index 000000000000..5d4524e29385 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_11.txt @@ -0,0 +1,17 @@ + + ▊▉▉▉▉▉▉▎ + ▋█ ▌▏▏▏▍▍▊ + ▋█▎▎▏▏▌▍▉▋▍▊ + ▏▎▎▏▏▌▌▊▎▌▋▋ + ▎█▋▌▍▏▉▍█▌▎▏▏▊ + ▏▉▎▏▏▉▎▌▌▏▎▋▏▏ + ▉▉ ▊▏▏▏▏▏▏▎▏▌▍ + ▏ ▏▏▋▏▏▍▋ ▏█▏ + ▋ █▏▋▏▏▉▋▉▏▎▏ + ▉▉ ▎▏▋▌▊█▋▏▎▏▍ + █▎▊ ▏▏▏▏▌▋▏▍▍▏ + ▍▎▊▋▏▎▉▋▉▉▌▏▎ + ▎ ▉▏▍▍▍▎▏▌█ + ▍▉ ▉▏▏▊▎▋▉▎ + ▍▎ █▉▉▎▉▎ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_12.txt b/codex-rs/tui/frames/vbars/frame_12.txt new file mode 100644 index 000000000000..f81900edb1cd --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_12.txt @@ -0,0 +1,17 @@ + + ▉▉▉▉▉▋ + ▊▌▎▎▋▏▊▍ + █ ▏▋▍▉▊ + ▋▉▏▏▏▌▌█▏ + ▏ █▏▏▏▏▋▋▏ + ▏█▋▋▉▍█▎▏█ + ▏ ▏▋▍▏▏▍ + ▏ ▋▉▍▏▏█ + ▉▊ ▏▋▏▏▉▏▏ + ▏ ▋▏▉▌▉▎ + ▏▋ ▏█▌▉▉▋ + ▊ ▉▏ ▎▊▏ + ▋ ▉▉▏▊▉▍▏ + █▍▋█▊▏▍▏▎ + ▍ █▏▍▋▋ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_13.txt b/codex-rs/tui/frames/vbars/frame_13.txt new file mode 100644 index 000000000000..4231032a45cc --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_13.txt @@ -0,0 +1,17 @@ + + ▋▉▉▉▊ + ▏▎▎▏▏ + ▏▎▎▏█ + ▏▉▉▏▎ + ▉ ▏▏ + █▋▊▏▏ + ▊ ▏▏ + ▉ █▏ + ▋▉ ▎▏▋ + ▏█ ▉▌▏ + █▎▋▋ ▏▎ + ▏▎▎▎▏ + ▏▉▊▏▎ + ▉▎▎▏▌ + ▊ ▋█▌ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_14.txt b/codex-rs/tui/frames/vbars/frame_14.txt new file mode 100644 index 000000000000..6eab794e0ab2 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_14.txt @@ -0,0 +1,17 @@ + + ▉▉▉▉▌ + ▊▋▎▎▋▍ + ▏▎▏▏▋ ▏ + ▏▏▋▏█▍▏▊ + ▏▏▏▍ ▏ + ▏▉▏▏ █▏ + ▏▏▏▏ ▋ ▉ + ▏▏▍▏▎ ▍ + ▏▏▏▏ █ + █▍▋▋ ▌▋ + ▍▏██▊▋▊▏ + ▏▉▏▏▎▎▎▏ + ▋ █▏▌▌▌▎ + ▏▍▋▉▎▎▋ + ▎▉ █▌▋ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_15.txt b/codex-rs/tui/frames/vbars/frame_15.txt new file mode 100644 index 000000000000..fa9a859bd04c --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_15.txt @@ -0,0 +1,17 @@ + + ▉▉▉▉▉▏▎ + ▎▊▌▏▏▍▋▉▊ + ▋▋▏▉▌▍▏█▉▏ + ▏▋▎ ▋▋▊▋▎▎▏ + ▏▏▌█▌▍▏▏▋█▍▏ + ▍▏▏▊▏▏▋▋▏ █ + ▏▏ ▏▏▏▏▏▏▊▏ + ▏▏▋▏▏▉▏▏▏ █▊▏ + ▏▏▉▊▊▉▏▏▏▎▋▋▏ + ▏▎▏▌▋▏▍▏▉▋ ▋▏ + ▏▋▏▏▏▏▎▏ ▎ ▌ + ▏▉▉▋▍▏▏▍▊ ▌ + █▏▍▊ ▋▋▋▎ ▌▎ + ▏▍▏▏▍▍▋▉▍▋ + ▍▍▉▍▎▎▎▋ + ▎▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_16.txt b/codex-rs/tui/frames/vbars/frame_16.txt new file mode 100644 index 000000000000..1fcc2090a213 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_16.txt @@ -0,0 +1,17 @@ + + ▎▌▉█▉▉▉▏▎ + ▋▍▏▏▉▏▏▍▏▌▍ + ▏▋▋█▍▋▌▍▏▏ █▍ + ▋▋▋ ▌▉▎▍▏▍▏▏▉▉▏ + ▏▏▌▎▋▉█▌▏▏▉▋▏▎▍ + ▏▏▏▉▏█ ▏▍▌ ▏ ▏▋▏ + ▍▍▏ ▋▎▊▏▏▏▏▋▉█▊ ▏ + ▏▋▌ ▏▎▏▏▋▏▏▏█▌ ▎▏ + ▍▍▏▏▏▉▏▋▍▏▏█▉▉ ▋ + ▉█▎▎▊▌▌▍▉▏▋▎▏ ▊▎▏ + ▏▏▏▉██▋▉▍▏▌█▌▋▏▏▎ + ▏▏▉ ▏▎ ▎▋▋▊ ▋ + ▍▏▉ ▏ ▋▊▋▎▊▏ + ▍▏▏█▎▉▏▋▋▉▎▏ + █▋▋▎▌▋▎▎▌▉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_17.txt b/codex-rs/tui/frames/vbars/frame_17.txt new file mode 100644 index 000000000000..1adf01af903f --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_17.txt @@ -0,0 +1,17 @@ + + ▊▌▉▉▉▌▌▏▊▎ + ▎▌▉▉▏▏▏▉▌▉▏▊█▍▊ + ▊▉▍▋▎▌▌▎▉▊█▏▏▏▎█▍ + ▋▏▋▎▋▏█▎▎ █▏▌▍▏▏▏▌▍ + ▏▋▎ ▏ ▏█▋▍▌▍▋▋ ▊ + ▎▌▉▊▋▎ ▊▋▋▏ ▋▊▌▋▏▋▋ + ▉ ▌▋▏ ▊▋▋▏▎ ▉█▏▏▋▌▏ + ▊█▌▏ ▋▋▏▏▋▏▊▏▏▌▌▉ + ▊▋▉▋▍▌▌▉▏▍▎▏▍▍▋▍▏▏▏ ▏ + ▏█▎▌▎▎▏▋▉▍▋▏▏▍ ▍▉█▉▍ + ▍▍▍▋███▉▉▉▉▍▎▏▉▋▋▍ ▍█ + ▍▏▏▎▉ ▎▋▏▏▎ ▌ + ▎▍▍ ▉▍▎ ▊█▊▋▍▎▋ + ▊ ▏▉▉▌▍▉▎▎▌▉▋▏█ + ▎▉▌▋▎▊▉▏▉▎▉▉ + ▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_18.txt b/codex-rs/tui/frames/vbars/frame_18.txt new file mode 100644 index 000000000000..9c46c648214f --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_18.txt @@ -0,0 +1,17 @@ + + ▎▌▌▉▌▌▉▌▏▊▎ + ▎▉█▎▌▊▉▉▏▎▎▊█▉▌▌▎ + ▎▋▋▏▉█▉▌█▊█▋▎█▉▋▋▍ ▉ + ▊▏▏▉▋▊▍█▉ █▍▎▎▋▌▏▍▌▍▍▍▊ + ▎▍▋▋ ▋▏ ▎█▉▎▍▋▏▋▏▎▍▌▍▍▎ + ▋▍▏ ▊▊▏▋▎▎▎▋▊▉▊▏█▊▋▍▋▎▋▏ + ▏▏▍▋▋ ▉▋▉▋▏█▎▋█▉▏▌▏▌▊ + █▍▊▏▏ ▍▍█▏▏▌▍ ▏▌▏█ + ▍██ ▊▍▌▌▌▌▌▉▏▏▊▏▉▋▍▊ ▏▉▏▎ + ▎▍▍▏▉▍▉ ▏▏▋▎▎▍▎▏▏▏▎▌▏▎▍ ▊ + ▏▋▏▍▎██▉▉▉▉▉▎ ▍▎▎▋▏▋▉▋▎▊▎ + ▍▍ ▊▍▊ ▎▎▉█▋▉▎█ + ▍▍▉▉▍▍▋ ▎▉▍▎▋▍▊▎ + █▏▋▉▉▉▉▌▌▌▍█▉▎▋▎▊▉ + █▏▏▎▋▎▎▊▋▉█▌▉█ + ▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_19.txt b/codex-rs/tui/frames/vbars/frame_19.txt new file mode 100644 index 000000000000..572f5ffc3242 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_19.txt @@ -0,0 +1,17 @@ + + ▎▎▌▌▉▏▉▌▌▏▋▎ + ▎▉▏▏▏▍▋▌▌▌▎▉▎▊▏▉▊▊ + ▊▉▌▌▋█▏▉▉▎ ▎▉▋▊▎▉▌▍ + ▋▌▏▉▊▍▎ ▎▌▌▉▊▍▏▉▊ + ▏▏▋▎▋▎ ▊▏▍█▋█▏▍ ▏▍▊ + ▏▍▋▉█ ▎▏▉▏▋ ▋▋ █▋▉▏ + ▍▌▏▏█ ▋▉▍▋▏▉▊▎ ▋▋▋▉▏ + ▏▏▏▏ ▏▌█▍▏█▊ ▏ ▏▏▏ + ▏▉▏▍▊▌▌▌▌▌▌▏▏▏▏▉▉▌▉▏▌ ▍ █▏▏ + ▍▌▋▊▏▌▉▋▍▉▉▍▊▎▊ ▍▎ ▋▍▎▍▊▋▋▏▎ + █▍█▋▉▏█████▉▉█ ▍▍▎▋▏▋▋ ▋▋█ + ▍▍▍▉▎▍ ▎▋▋▎▋▋█ + █ ▎▍▏▏▉▊ ▊▌▌▋▋▏▌▎ + ██▋▏▉▋▏▉▎▎▌▌▋▉█ ▌▉▏█ + █▏▋▎▋▊█▎▊▋▍▉▊▍ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_2.txt b/codex-rs/tui/frames/vbars/frame_2.txt new file mode 100644 index 000000000000..0e0c021f436b --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_2.txt @@ -0,0 +1,17 @@ + + ▎▋▎▋▌▉▌▌▌▉▊▎ + ▎▉▊▋▉▌▉▏▏▏▌▏▏▌▎█▏▉▉▎ + ▊▏▉▏▍▉▉▏▉▎▎ ▎█▉▌▉▋▏▏▌▏▊ + ▎▏▋▎▋▉█▊▊▎ ▊▊▍▏▋▏▊ + ▊▍█▋▉▍▏▍▎▍▍▎ ▍▊▏▍▏▊ + █▋▉█▎ ▏▉▍▉▋▍▉ ▍▍▏▉▏ + ▊▏█▉▏ ▍▋▏▌▏▎▏▊ ▋█▏█▏ + ▏█ ▍▎ ▊▏▉▏▏▌▉ ▏█▋▋▏ + ▉██▌▏ ▌▏▍▍▎▏█▋▏▉▉▉▉▉▉▉▉▊▏▎▏▏▉ + ▎▌█▏ ▋▏▏█ ▋▉ ▏▌▍▎▎▎▎▋▋▎▎▏▉▋ ▏ + ▍▍▍ ▉ ▉▍▋▋▏█ ▎█▉▉▉▉▉▉▉█▏▊▉▏▏ + █▍▎▋▋▊ ▎ ▊▉▌▋▊▉ + ▋▍▎▎▏▉▊ ▊▌▎▉ ▎▏█ + ▎▏▍▌▎▎█▉▉▋▌▌▌▌▋▌▉▎▎▏▏▉ + ▎▉▉▉▉▏▏▎▎▎▎▏▌▋▉█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_20.txt b/codex-rs/tui/frames/vbars/frame_20.txt new file mode 100644 index 000000000000..42c288df9295 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_20.txt @@ -0,0 +1,17 @@ + + ▎▎▉▌▏▏▌▉▌▊▎▎ + ▊▌▌█▌▊▎▊▌▌▌▏▏▎▍▉▍▉▊ + ▊▊█▎▉▉█▏▉█ ▎█▍█▍▉▉▏▉▎ + ▎▏█▎▉▏▉ ▎▊▉▏▍▋▉▍▍ + ▎▏ ▊▏▉ ▎▋ ▎▏▏▍▉▍▏▏▍ + ▋▎▊▏█ ▉▉█▌▏█▋ █▍▏▏▍ + ▌▏▌▉▏ ▊▉ ▋▉▉▍ ▊▍▏▋ + ▏▎▍▋▎ ██▌▋▏█▊ ▏▏█▏ + ▏▋▏▏▌▊▌▌▌▌▌▌▌▌▎▊█▌▍▍▍▍▉ ▋ ▏▍ + █▏▍▏▎█ ▋▎▎▎▎▎▎▏▍ ▉▍▍█▏▌▍ ▊▋ ▏▏ + ▍▊▍▍████████▉█ ▍▊▌▏▏▊▏ ▋▍ + ▉ ▍▉▋▊ ▎▋▉▋▏▋ + ▍▎▍▉▊▏▎ ▊▌▉▎▌▉█ + ▍▊▊▋█▏▉▍▌▌▊▋█▏█▎▊▋▉█ + ▎▉▉▏▎ ▌▎▎▎▎▏▉██ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_21.txt b/codex-rs/tui/frames/vbars/frame_21.txt new file mode 100644 index 000000000000..aa5d4f7274c7 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_21.txt @@ -0,0 +1,17 @@ + + ▎▎▊▌▉▌▌▉▌▉▊▎▎ + ▊▉▉▎▎▎▎▉▏▌▏▉▌▎▎▊▉▉▎ + ▌▉▍▎▉▉▍█▎ ▎▍▏▉▉▎▎▉▊ + ▊▉▏▌██ ▎▊▌▉▊▌▉▊ + ▋▊▋▉▎ ▊▏▋▎▍ ▍▍▎▍▊ + ▋█▋▏ ▊▉▉▎▋▋ ▍▍▍▊ + ▏█▏▎ ▎ ▋▎▊▋█ ▍▌▍█ + ▎▍ ▍▌ █▏▊ ▏ ▏ + ▏ ▏ ▊▉▌▌▌▌▌▉▉▉ ▍▏▊█▏▊ ▌▋ ▏ + ▏ ▏▍ ▏▎ ▎▊▏ ▍█▎▉▏▊ ▋█▋▋ + ▌ █▋ ▎▎███ █▎ █▉▎▊▌ ▋▎ ▊ + █▉ ▋▌▎ ▎▉█ ▋ + ▉▎ ▍▊▎ ▎▋▌▉█ ▊▎ + ▉▌▎ ▉█▉▉▋▏▏▌▋▌▉██ ▊▎ + ▉▉▉▌▉▎▎▎▎▊▎▌▉▉▎ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_22.txt b/codex-rs/tui/frames/vbars/frame_22.txt new file mode 100644 index 000000000000..3b1ce4ecdedb --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_22.txt @@ -0,0 +1,17 @@ + + ▎▊▉▌▉▌▌▉▌▉▊▎ + ▎▉▋▉▏▋▎▋▏▋▍▏▏▌▏▉▋▉▉▎ + ▊▌▉▌▏▍▉▉▎ ▎█▉▍▉▉▉▎▎▉▊ + ▌▉▏▉▉█ ▊▊▏▏▏▎▉▍ + ▍▏▏▋█ ▊▎▋▏▌▏▋▋▌█▏ + ▍▏▉▋▎ ▊▏▋▏▌▍▋▎ ▍▏▍▌▏ + ▏▉▉▍▋ ▏▉▏▍▉▋█ ▏▏▋▏ + ▏▏▌▏▎ ▏▍▏▏▍▍ ▏▍▏█▏ + ▏▏▊█▊ ▉▏▌▌▌▌▌▌▉▉▎▍▉▏▎ ▍▊ ▎▏▌█ + ▍▏▌ ▉▎▍▍▊ ▋▊█▏▋▋▏ ▍▉▌▎█▏▊▊ ▋▍▌ + ▍▍▍▎▏ █████▉▉█▎ ▎▋▉▉▎▋▋▋█▊▎ + ▍▉▍▎▍▊ ▎▉▊█▏▊█ + ██▉▌ ▉▉▎ ▎▋▉▊█▉▎▋ + █▋▉▉▊█▉▏▌▌▉▌▌▋▌▉███▊▉ + █▏█▏▎▌▎▎▎▊▌▎▋▌▉▎ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_23.txt b/codex-rs/tui/frames/vbars/frame_23.txt new file mode 100644 index 000000000000..0b99396129de --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_23.txt @@ -0,0 +1,17 @@ + + ▎▊▉▎▌▉▉▉▉▌▉▎▎ + ▎▌▏▉▏▍▎▊▎▌▌▊▏▎▏▉▏▉▎ + ▊█▉▏█▊▌▉▎ █▏▋▉▏▍▌▏▏▊ + ▋▌▏▉▌▋▎ ▎▌▌▍▏▏▌▍ + ▋▋▏▎▋█ ▊▋▊▊█▉▋▉▍▏▍ + ▊▍▏█▋▎ ▎▉▉▉██▋▉█▏▏▍▏▊ + ▎▏▏█▏ ▋▋▋▋ ▎▏▎ █ ▏█▉ + ▏▋▏ ▏ ▏█▏▊▎▍▊ ▏ ▉▋▏ + ▍█▍ ▏▉▏▌▌▌▌▌▌▌▌▏▎▉▏▎█▍▎ ▏ ▏▏▏ + ▏▍▍▎▏█▌█ ▋▎▍ ▍▏▎▍▊▏ ▍▎▏ + ▋█▏ ▏ ▎███████ █▎▋▌▌▏▎▋▍▋▎ + ▊▏▏▎▏▋ ▊▋▏▍▍▋▎ + ▍ ▏▊█▋▎▎ ▎▋█▌▉▋▉▋ + ▍▌▍▉▌▉▉▍▏▌▌▎▉▉█▊▎▉▉▎ + █▏ ▉▌▍▏▎▎▉▎▎▉▉▎ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_24.txt b/codex-rs/tui/frames/vbars/frame_24.txt new file mode 100644 index 000000000000..5e26d7a27bf8 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_24.txt @@ -0,0 +1,17 @@ + + ▎▏▎▌▉▉▉▌▌▊▎ + ▊█▎▋█▏▎▎▏▌▌▉▊▎█▉ + ▊██▌▍▋▉█▏ ▋▎▋▉▏▉▉▊ ▍ + ▊█▋▋▊▋▏▌▊▍▎▏▋▎ ▎▉▍▏▍▎█ + ▊ ▋▋▍▋▊ ▎██▋▍▎▏▎▎▍▏▊█ + ▏▊▍▉▍▎ ▉▊▊▋ ▋▏▌ ▋█▍ + ▏ ▏▏ ▊▋▎▊▋█▋▍▍▏▍▍▏▍ + ▏ ▍▏▏ ▏▊▌▋▍▎▏▏▎▋▋▎▏▏ + ▏▋ ▏▌▌▌▋▉▉▏▏▏▉▉▎▉▊▎▏ ▏ ▏▏ + ▎ ▋ ▉▏▋▊▊▌▌▊▊▏▋▍▍▉▉▏ ▏▉▏ + ▍ █▍▏▍▉██████▎ ▍▊▋▉▍▏▌▋▊▎ + ▍ ▍▊▉▍ ▏ █▋▌▋ + ▍▋▉▍▎▏▋ ▎▌█▎▋▋▎▉ + ▋▊▉▊▍▍█▏▏▋▌▌▌▏▍▎▋▎ + ▉▋▎▉▉▌▍▎▎▌▌▉▉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_25.txt b/codex-rs/tui/frames/vbars/frame_25.txt new file mode 100644 index 000000000000..5009b8b66d25 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_25.txt @@ -0,0 +1,17 @@ + + ▎▉▌▉▌▉▉▉▉▎ + ▊ ▉▊▉▎▎▌▍▏▉▏▏▊ + ▋▏▋▋▊▊█▋▏▉▍▌▍▏▏▏▎ + ▏▉▉▏ ▋ ▌▏ █▍▏▊▉▍▋ + ▊▎▎▍▊▋█▍▌▋▎▏█ ▏▋▋▍▋▉ + ▍▊▏▏▏▎▉▉▋▍▏▋ ▋▏▊▏▋▋▏▊ + ▏▋▏▉▏ ▏▎▎▋▍▎▋▏▍▋▎█▏▌▏ + ▏▏▏▋▏▋█ ▏▎▌▍▋▏▍▎▏▌▏▏▍ + ▍▉▍▏█▏▎▎▌▌▌▌▉▏▏▍▍▏▉▉▏▍ + ▋▊█▏▋▏▊▏▎▏▏▌▉▎▏▍▉▌▋▍▏ + ▍▏▉▋▍▏▎██████▉▋▏▍▏▋▏▎ + ▏▎█▏▌▉▌▍▊▊▉▋▎▋▊▋▋▏▋ + ▍▎▍▏▎▍▎▉▌▋▊▍▋▎▏▏▋ + ▍▋▌▍▍▎▉▌▏▉▉ ▉▏▉ + ▏▊█▉▍▉▊▎▉▏▉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_26.txt b/codex-rs/tui/frames/vbars/frame_26.txt new file mode 100644 index 000000000000..900a51c3b55b --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_26.txt @@ -0,0 +1,17 @@ + + ▎▌▉▉▉█▉▊ + ▋▉▏▌▋▍▏▉▌▏▊ + ▋▉██▏▌▍▏▍██▍▊ + ▌▉█▊▏▏▏▎▏▏▎▉▋▍ + ▏▎▊▍▏▋▏█▉▍▋▌▏▍▌ + ▍ █▏▋▋▋█▉▎▍█▍▍▋ + ▏ ▉█▌▏▏▋▏▏▌▌█▉ + ▋▊ ▍▏▏▏▍▍▌▏▏▋▏▏ + ▏ ▋▍▉▌▉▏▉▌▌▋▍▏▏ + ▋▎▏▋▊▏▎▎▊▉▍▍▉▏ + ▏▎█▋▏▉█▍▊▋▎▉▍▏▏ + ▍▎▏▏▋▍ ▋▋█▏▏▎ + ▉ ▉▏▍ ▍▎▋▏▋▊ + ▏█▉▍▍█▉▎▉▉▌ + ▍▋▋▍▉▏▎▍▉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_27.txt b/codex-rs/tui/frames/vbars/frame_27.txt new file mode 100644 index 000000000000..0b2e8c7306f4 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_27.txt @@ -0,0 +1,17 @@ + + ▌▉▉▉▉▊ + ▏▌▋▋▏▋▍ + ▌█▎ ▉▏▋█▏ + ▏▏▏▍▏▏▌▏▏ + ▏ ▎▏▎▊█▌ + ▏▎▋▎█▎▏▏▏▎ + ▌ █▏▏▎▉▏▏ + ▏ ▎▏▏▏▌█▏▏ + ▏▋ ▋ ▏▉▏▏▏▏ + █▌ ▋█▎▏▎▉▏▏ + ▏▊ ▎▍▉▉▉▋█ + ▏▏▉▉▏▏▎▋▏ + ▏▌▌▉▌▊▋▏▍ + ▍▎▎▏▍▌▋▋ + █▍█ ▍▍▋ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_28.txt b/codex-rs/tui/frames/vbars/frame_28.txt new file mode 100644 index 000000000000..01ce82b6d3cc --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_28.txt @@ -0,0 +1,17 @@ + + ▋▉▉▌ + ▏▉▎▏▎ + ▌ ▌▏▎ + ▏▊▎▉▏ + ▏ ▏▏ + ▏ ▊█▏ + ▍▏▏ ▎▏ + ▏▊██▏ + ▋▍ ▎▏ + ▏▏▋▋▏▏ + ▏ ▎▏ + ▏▌▉▌▏ + ▏▌▉▏▏ + ▉▎▎▋▏ + ▏ ▋▉▏ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_29.txt b/codex-rs/tui/frames/vbars/frame_29.txt new file mode 100644 index 000000000000..c682a6082c1a --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_29.txt @@ -0,0 +1,17 @@ + + ▉▉▉▉▉▊ + ▊▏▉▋ ▉▍ + ▍▊▏▏ ██▏ + ▏▏▏▏▍▍▊▋ + ▍ ▏█▏ ▏ + ▏▊▏▏▏ ▎▎ + ▏▍▍▏▏▏ ▍ + ▏▏▌▏▏ + ▏▏▏▉▏ ▎ + ▎▏▌▏▏ ▎ + ▉▏▏▋▏▍ ▌ + ▏▋▏▏▏ ▎▏ + ▏▏▏▏▎▏▎▏ + ▏▌▏▉▋█▏█ + ▏▏▉ ▋▊ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_3.txt b/codex-rs/tui/frames/vbars/frame_3.txt new file mode 100644 index 000000000000..6c202bc0c387 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_3.txt @@ -0,0 +1,17 @@ + + ▎▋▌▌▉▉▌▌▌▌▊▎ + ▎▉▊▋▍▏▏▉▏█▌▌▏▌▎▍▏▉▉▎ + ▊▌▋▉▌▏▉██▎ ▎█▏ ▉▉▉▋▍▏▊ + ▋▉▋▍▏▌▊▉▎ ▎▌▉ ▏▏▍ + █▋▉▏▋▍▋▏▏▉▍▎ ▍▍▍▏▏▎ + ▋▏▍▍▋ ▋▊▏▍▌▍▍ ▏ ▍▍▏ + ▌▉ ▏ █▉▍▏▎█▏▊ ▋▉▋▏▊ + ▊▊▏▊▏ ▊▊▏█▋▍▏ ▏▎▏▌▏ + ▊██▏ ▌▏▉▉▏▏▉▊▏▌▉▉▎▎▎▉▉▎▏▍█▌▏ + ▊▍▋▍▊ ▋▋▉▋▌▌▏ ▏▉▏▎▎▊ ▌▋▊▌▍▎▏▍▏▎ + █▍▏ ▏ █▏▏▉▊█ █▉▉▉███▉▉▊▋▋▋▊▋ + █▉▏ ▍▉ ▎ ▋▍▉▍▋▋ + ▌▍▋▋▉▍▊ ▊▉▌▋█▊▏█ + ▌▍▉▊▎▉▉▍▌▌▌▌▊▋▌▉▌▎▎▉█ + ▎▉▉▉▎▋▏▎▎▎▎▏▌▋▍█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_30.txt b/codex-rs/tui/frames/vbars/frame_30.txt new file mode 100644 index 000000000000..a44dbb6ed048 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_30.txt @@ -0,0 +1,17 @@ + + ▎▌█▉▉▉▊ + ▎▋▌▏▏▍▉█▌▍ + ██▍▏▊▍▍▏▉▊▍ + ▏▏▍▏▋▏▎▍▏▍ ▍▊ + ▌▉▏▏▎▍▏▊▏▊ ▏ + ▏▉▏▍▉ ▉▏▏▏▏▋▏ + ▍▋▏▏▏▍▎ ▍▋▍▏ ▌▎ + █▏ █▉▌▏▊▏█▏▊ ▎ + █▏▎█▍▏▌▏▍▋▏▊ ▎ + █▏▏▍▏▎▎▏▏ ▉▊ ▎ + ▍▏▏▌▍▎▉▏▏▏▉▏▏ + ▏▉▋▋▊▋▉▍▏▏█ ▏ + ▍▏▍▏▏▋▉▏▏▊▏▋ + ▊▍▊▉▌▍▋▏▊▋█ + ▍▊▋▏▍▎▎▌█ + ▎▎▎▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_31.txt b/codex-rs/tui/frames/vbars/frame_31.txt new file mode 100644 index 000000000000..70da8799e297 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_31.txt @@ -0,0 +1,17 @@ + + ▎▋▋▉▉▉▉▌▊ + ▎▉▉▌▉▊▎█▉▍█▉▊ + ▊▏▍▋▋▊▌▎▎▋█▏▎▉▍ + ▊▏▋▉▎▎▉▌▎▊▍▍▏▍▏▏▍ + ▍▏▏▏▏▉▊▉▍▎▊█▍▏▋▎▎ + ▌▏▍▎▏▎▏██▍▌▉▊▋▋▏▊█▏ + ▋▏▏ ▏▍▉▏▍▋▌▋▌▉▏▉▏▋▏ + ▍▍▊ ▏▏█▏▍ ▏▍██▏▏▍▋▏ + ▏▍▊▎▏█▍▏▏▏▋▉▏▏▏▎ ▏ + █▏▏▏▋▋▏▏▎▎▎▍▎▏▍▏▏ ▉ + ▍▉▎▋▉▏▊ ▋▉▉▉▎▋▏█▏▎ + ▏▏▏▍▍▍▊ ▎▌▉ ▏ + ▌▏▉▌▎ ▊ ▋ ▏▉▎▋▎ + ▋▏▍▏▋▎▏▎▊▏█▊▏▎ + ██▌▏▎▉▉▋▋▌▉ + ▎▎▎▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_32.txt b/codex-rs/tui/frames/vbars/frame_32.txt new file mode 100644 index 000000000000..ddfb4be3fe2d --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_32.txt @@ -0,0 +1,17 @@ + + ▊▉▉▉▉▌▌▏▊▎ + ▌▉▏▉▉▏▍▏▏▉▉▏▏▊ + ▋▍▉▌▍▊▉▍▋▊▎▉▏▉▍█▉▊ + █▋▋▏▎▊▎▌▊ ▊ ▊▎▍▏▉▊ ▊ + ▋▋▏▊▏▉▍█▉▏▌ █▎▊▏▍▋▍▉▍▊ + ▍▋▍▍▊▍▉▏▎▋▏▏▉▎ ▏▉▋ ▉ + ▉▏█▌▊▎█▍▏▏▊▊▏▊▌▎▋▎▋▋▏█▎ + ▏▏▏▊▎ █▎▏▏█▋█▏▌▊▎█▍▏██▎ + ▍▏▏▊▎ █▎▏█▎▎▏▉▉▉▏▏▏▌▏█▌▎ + ███▎▎▏▉▍█▍▎▎▎▎▎▏▏▉▏▌▏▊▎ + ▊▍▉▏▏▋ ▏▉██▉▉▉▍▌▋▋▋▋▋ + ▍▊▏▍▍▍ ▊▍▏▋█▏ + █▉▉▏▏▏▉▎ ▎▋▎▏▋ ▊ + ▍▊▍█▏▉▏▌▌▉▎▎▉█▎▋ + ▏▌▌▎▎▊▉▋▉▎██ + ▎▎▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_33.txt b/codex-rs/tui/frames/vbars/frame_33.txt new file mode 100644 index 000000000000..7fa5ac29bcac --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_33.txt @@ -0,0 +1,17 @@ + + ▎▊▉▌▉▌▉▌▏▋▎ + ▎▉▉▏▏▍▉▌▏▋▏▊▏▉▏▉▋ + ▎▋▍▏▉▉▉▊▌█▎ █▉▋▏▏▉█▊ + ▊▍▋▏▋▎▏▊▎ ▉▏▏▍▎▉ + ▎▋▏▋▎▏▏▋▍▍▍ ▏▉▍▍▍ + ▋▋▊ ▍▎▍▍▎▍▊ ▋▍▋▎▍ + ▏▏▉ ▍▊▉▏▎▏▍ ▏▉▏▏ + ▉▏▊▊ ▊██▍▋▌▊ ▍█▏▉ + ▏▏▏ ▏ ▊▎ ▋▉▊▊▊▌▌▌▉▏▏▉▋▎▏▍▏ + ▍▋▏██▋▋▊▋▎▏▏▎▎▎▎▎▏▏▉▍▍▍▏▏▋ + ▍▌▍▊ ▏▋▏▋ ▋ ████▉▉▉ ▏▏▊▋▍▎ + ▍▉▍▉▊█▊ ▋▋▌▋▌▋ + ▉▍█▉▊▉▌▊ ▎▉▏▊▋▉▍█ + ▍▏█▉▍▉▏▊▌▉▎█▌▊▍▉▏▉ + █▌▌▉▊▎▎▎▍▉▉▌▊█ + ▎▎▎▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_34.txt b/codex-rs/tui/frames/vbars/frame_34.txt new file mode 100644 index 000000000000..a8c447ff18a5 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_34.txt @@ -0,0 +1,17 @@ + + ▎▊▌▉▉▉▉▌▏▊▎ + ▎▉▉▉▋▍▌▏█▏▉▉▌▉▉▌▊▌▎ + ▎▋▋▊▏▉█▋▏▉▎ ▎▍▍▍▊█▉▉▊ + ▊▋▎▏▉▎▋▊▎ ▉▏▊▉▍▍ + ▋▋▏▍▊▋▏ ▏█ ▊ ▍▍▉▍▍ + ▊▋▍▏▎▋ ▍▍█▏▍▉▍ ▍▊▍▏▍ + ▏▉▍▏▎ █▍▎▉▏▎▍ ▏▌▏▎ + ▍▏▌█▋ ▏▉▎▏▋█▏ ▋ ▎▍ + ▊█▋▋█ ▋▍▋▋█▎▌▌▉▉▉▉▉▉▏▎▎█ ▏▍▎ + ▏▏▍▊▋▊▊▍█▋▋█▏▏▏▎▏▋▎▎▎▊▋▎▏▏▎▏▍ + ▋▎▍▊ ▏▍▏▉▍ █▉▉▎▎▎▎▍ ▊▋▊▍▏█ + ▋▊▍▌▌▍▎ ▋▌▋▏▋█ + ▍▉▎▏▉▍▌▊ ▎▉▌█▎▋▊▎ + ▊▍▋▏█▏▉▊▌▊▉▌▉▌▉▎▉▉▋█ + █▏▍▌▌▎▎▎▎▎▌▌▉▌▍ + ▎▎▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_35.txt b/codex-rs/tui/frames/vbars/frame_35.txt new file mode 100644 index 000000000000..ba905231e1f1 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_35.txt @@ -0,0 +1,17 @@ + + ▎▊▉▌▌▉▉▉▌▉▊▎ + ▊▉▌▉▋▏▉▉▉█▏▉▏▋█▏▉▌▎ + ▎▋▋▏▌▉▉▏▍▉█▎ ▎█▉▌▉▋█▏▉▊ + ▊▋▋▉▋▊▌▊▎ ▉▉▎▍▏▎ + ▋▋▏▏▋▋▍ █▍▉▊ ▏▍▉▏▊ + ▌▋▌▋▉▎ █▏▎█▋▋▉▎ ▏▍▍▏ + ▍ ▋▋ ▍▍ █▉▎▋ ▏▍▍▍ + ▏▏▏▋ ▋▍▍▌▋█ ▋ █▋ + ▋ ▊▎ ▊▏▏▋▌█▋▏▌▌▎▌▎▌▉▏▏ ▏▏▌▉ + ▏▊▍▌█ ▋▋▌█▉▉▋▏▌ ▍▊▎▎▎▎▋█▏▋█ ▏█ + ▋▎▍▊█▋▍▎▏▋▍▎ ▎█▍▍▍▍▉▍▍▉▋▋▎▊▏ + █▋▍▍▋▏ ▎▋▉ ▊▍ + ▍▌▎▍▌▊▋▊ ▊▌▋▎▉▉█ + █▍▎█▏▉▏▊▋▏▏▉▏▌▋▉█▎▋▌█ + █▍▏▌▌▎▎▋▎▎▉▉▉█▍ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_36.txt b/codex-rs/tui/frames/vbars/frame_36.txt new file mode 100644 index 000000000000..246ed3d6924c --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_36.txt @@ -0,0 +1,17 @@ + + ▎▎▉▌▉▉▉▉▌▉▊▎ + ▎▌██▍▉▋▌▋▉▍▉▌▉▉█▉▉▉▎ + ▊▍█▍▊▉▉▊▉█▎▎ ▎█▉▏▉▉▏▊▉▏▊ + ▎▏█▉▉▎▎▎▎ █▉▏▍█▏ + ▊▋▎▌▍█▏▍▎▍▊▋ ▍▍▋▉ + ▋ ▊▋▎ ▉▎ █▏▋▊ █▍▍▉ + ▏ ▌ ▎ ▍ █▍▍ ▏▍▏▊ + ▏▏▍▏ ▎▍ ▎▋▋ ▍▏ ▏ + ▏██▏ ▋▋ ▊▉██▊▉▉▎▉▉▉▉▉▎ ▏▍▏▏ + ▎▉▏▍ ▊▏▎▎▋▏▋ ▎▏▎▎▎▎▎▎▎▏▏ ▍▋█▎ + █ ▉▏▍ ▉▎▉▋▍ █▉▉▍▍▍▍▍█ ▏▊█▋ + █▊█▍▌▋ ▊▋▏█▋ + ▍▋▏▏▉▏▊ ▊▉▉█▋▊▎ + ▉▊▎█▉▉▌▍▌▏▎▉▏▌█▊█▊▌▉█ + ▎▉▉▋▏▎▊▊▎▊▊▉▉▉▉█ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_4.txt b/codex-rs/tui/frames/vbars/frame_4.txt new file mode 100644 index 000000000000..5dcae750bc0f --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_4.txt @@ -0,0 +1,17 @@ + + ▎▋▌▌▉▌▌▉▌▉▊▎ + ▎▊█▉▉▏▋ ▏▍▌▋▉▌▏█▋▉▊▎ + ▊▉▌▏▏▏▉▉█▎ ▎▍▏▍▍▉▏▋▍▍▎ + ▋▋▏▊▏█▋▉▊ █▏▉▉▍▉▍ + ▋▏▋▎▏▌▏▍▌▍▏▊ ▍█▍▏▉▍ + ▋▏▏▍▏ █▎▏▏▋▉▍ █ ▍▌▏▊ + █▍▏▌▎ █▉▉▍▉█▏▊ ▊ █▌▏ + ▏▋▍ ▊▏▋▉▍▏▏ ▏▏▏ + ▎▏ ▏▎ ▌▏▋█▍▋▋▉▏▉▉▉▎▎▎▎▊▊ ▏▏▉ + ▏▍▋ ▏ ▊▋▋▋▊▎▏▎▌▍▏▊▋▎▊▋▋▍▌▏▋▋█▏ + ▎▏▌█▍▊▍▍▊▍▋ █▉▉▉▉▉▉▉█▍▊▏▏▏▎ + ▊▏▍█▏▎▎ ▊▍▌▋▉▉█ + ▍▍▊▋▍ ▊▎ ▎▋▍▊▉▌▏▋ + ▏▏▏▌▎▉▉▍▌▌▌▌▊▉▌▉▉▍▏▉▎ + ▉▍▍▏▋▏▎▎▎▎▎▌▊▉▎ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_5.txt b/codex-rs/tui/frames/vbars/frame_5.txt new file mode 100644 index 000000000000..cab16091cb9b --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_5.txt @@ -0,0 +1,17 @@ + + ▎▋▌▌▌▉▌▌▌▉▊▎ + ▎▉█▉▋▉▋▏▏▉▏▌▉▌▎▏█▉▊ + ▉▎▋▍▉▏▉█▎ █▍▌▏▉▍ ▊▉▊ + ▊▏▋▏▋▏▋▌▊ ▊▍▏▏▏▏▎ + ▊▍▋▉▋▍▎▉▏▎▍▊ ▎ ▍▏▍▏▎ + ▏▏▏▏▋ ▍▎▏▍▌▋▏▎ ▉▋▏▏ + ▏▋▏ ▉ ▎▍▎▏▍█▏▊ ██▏▉▏▊ + ▏▋▏▎█ ▊▎▌▏▋▎▏ ▉▏▎▏ + ▏▊▏▍▉ ▋▎▏█▊▏▉▌▎▏▉▏▎▎▎▉▊▎█▏▋▏ + ▎▍▏▍▊▊▋▉▉█ ▏█ ▋▋▍▌▍▊▌▋▊▋▏▋▉▏▎ + ▍▊▏▏▊▏▊▍▍▋▉▎ █▉▉█████▊▏▌▏▍▋ + ▍▍▉▋▍▍ ▎ ▋▎▋▉▊▍ + ▍▋▉▍▏▌▉▎ ▎▌▉▊▉█▌▉ + ▍▏▉▏▊▉▉▍▌▏▌▎▊▉▌▉▍▏▌▉ + ▍▊▉▎▉█▎▎▎▊▎▊▉▉▎ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_6.txt b/codex-rs/tui/frames/vbars/frame_6.txt new file mode 100644 index 000000000000..e41e013ab0fa --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_6.txt @@ -0,0 +1,17 @@ + + ▎▋▌▌▌▉▌▌▉▉▎▎ + ▊▍▌▋▉▏▌▉▉▏▉▉▌▎▊▉▉▎ + ▊▊▊▏▏▏▉█ ▎█▏▋▌▉▉▉▋▍▎ + ▋▊▏▉▏▉▎▌▊ ▍▉▏▏▏▏▊ + █▌▏▏▏▉▍▍▉▉▍ ▋▍▍▏▍▊ + ▌▊▋▏▋▎▍▉▌▏▍▎▏▊ ▋▌▍▋▏ + ▎ ▉▏▏ ▍▍▏▋▍▍ ▍▉▏▎▏ + ▏ ▏▏▏ ▎▏▌▏▋▏▏ ▏▉▍▏▏ + ▎▋▏▊▏ ▋█▏▏▉▌▋▉▎▏▉▏▎▎▉▉▎▋▍▋▏ + ▏▏▍▊▋▎▍▎▌▉ ▏▋▌▉▊▏▏▊▌▌▌▍▏▊▏▍ + ▋ ▏ ▋▏▏▏▌▋█ █▉████▉█▎▍▍▋▏ + ▊ ▌ ▋▊▎ ▊▋▎▋▋▏▎ + ▋▎▍▎▉▉▊ ▎▉▍▎▋▉▊▉ + ▎█▏▉▊█▉ ▌▏▎▎▊▉▉▉ ▋▌█ + ▍▊▌▉▉▊▎▎▎▎▎▊▉▉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_7.txt b/codex-rs/tui/frames/vbars/frame_7.txt new file mode 100644 index 000000000000..7a88d5ef148f --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_7.txt @@ -0,0 +1,17 @@ + + ▎▋▏▌▉▉▌▌▉▊▎ + ▊▉▎▉▍▏▉▉▉▌▌▍▌ ▎▉▊ + ▉ ▊▏▏▏▉▎ ▎▍▏▎ ▉▋▎▉▊ + ▍▍▏▏▏▏▌▉ ▍▏▍▉▏▋▊ + ▎█▏▉▏▏▍▏▋▍▊ ▋ ▉▏▏▍ + ▏ ▌▏▏█ ▎█▍▌▏▍ ▊▊▏▏▏ + ▎▍█▏ ▍▎▏▍▏▍▏ ▉▉▍▏▏ + ▎█▋▏▏ ▌▉▋▏▉▏▎ █ ▏▏▏ + ▋█▏▉ ▊█▎▋▏▍▋▏▏▌▌▎▏▏▌▌▉▍▏▏ + ▏▊█▏▏▌▋▏▏▋█▏▋▍▏▏▍▍▊▌▊▌▍▏▍▏ + ▍ ▌▍▋▉▏▉▎▋ ▉▉▉▉▉▉██▎▋▏▏ + █▎ ▋▎▋▎▎ ▊█▊▋▌▋█ + █▌▊▉▍▍▍▎ ▊▍▏▊▉▋▉█ + ▍▎█▍▏▉▍▌▏▎▎▎▉▉▏▉▉▋ + █▏▉█▏▋▏▎▎▊▎▌▉▉ + \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_8.txt b/codex-rs/tui/frames/vbars/frame_8.txt new file mode 100644 index 000000000000..bbf2016fabae --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_8.txt @@ -0,0 +1,17 @@ + + ▎▎▉▎▌▉▉▌▉▊▎ + ▊█▉▋▏▏▏▉▌▌ ▏ █▉ + ▎▏▌▊▏▋▋▉ ▊█▏▋▏▉▍▉▍▊ + ▎▉▍▉▋▏▊▉▏ █ ▌▍▏▏▍ + █ ▋▏▋▊▍▏▋▍ ▋█▍▏▏▊ + ▏█▊▏▍▍▍▎▍▏▎▉ ▍▉▏▏▏ + ▏▉▏▏▏ █▎▌▍▏▎▏ ▏▍▏▋▏▊ + ▍▊▏▏▋ ▏▉▍▋▏▊▉▋ ▋▎▋▏▏ + ▍ ▍▏▏▎▍ ▋▉▌▏▏▋▉▊▉▊▊▍▏▏▏ + ▍▉▋▌▎▏█▊▏▊▏▏▌▏▌▎▎▏▏▌█ + ▋▋▍▍▏▍▏▏▏▋█▍▉▍▉█▍▉█▋▍▏ + █▊█▏▍▏▎▎ ▋▊▋▋▋█ + █▍▎▉▍▍▍▎ ▎▋▍▎▏▉▋▉ + ▋▋█▏▊▉ ▌▌▉▉▌▋▌▊▏▎ + ▏▏▎▉▉▍▏▎▊▉▋▌▎ + ▎▎ \ No newline at end of file diff --git a/codex-rs/tui/frames/vbars/frame_9.txt b/codex-rs/tui/frames/vbars/frame_9.txt new file mode 100644 index 000000000000..4e36e6e126f2 --- /dev/null +++ b/codex-rs/tui/frames/vbars/frame_9.txt @@ -0,0 +1,17 @@ + + ▋▌▍▉▋▉▉▌▊ + ▋▉▎▋▏▏▉█▌ ▎▊▍▎ + ▋ ▊▏▏▋▉▍▍▌▋ ▍▎▏▊ + █ ▏▏▋▌▌▎ ▎▍▊▏▍▍▏▉▊ + ▋▉█▌▍▉█▏▉▊ ▉▋█▉▋▉▏▏ + ▏ ▏▏▏█▍▏▍▌▎▎ ▏█▏▉▏▏▏ + ▎ ▏▉▍▏▉▉▏▍▍▊█▋▊▋ ▎▏▏ + ▏ ▏▋▎▊▍▏▏▏▎▋▌▍▎▏ ▏▏ + █▊▏█ ▋▏█▏▏▏▏▉▏▏▊▏▏▎ + ▏▎▏▏▏▎▊▏▍▏▎▏▏▏▏▎▎▏▏▏ + █ ▎▍▋▍▍▏▋█▉▉▉▉▏▏█▋▍▏ + ▍▏▏▏▍▊▎ ▊▋ ▍▋▏▎ + ▍▉▍▏▍▍ ▋▌▎▌▏▋▉ + ▍▊█▍▎▏▉▋▉▌▌▌▉▏▎ + █▊▎ ▉▍▍▏▌▉▋█ + \ No newline at end of file diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 792b5f9115aa..6c7bec8228a4 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -707,7 +707,7 @@ impl App<'_> { widget.register_pasted_image(placeholder, path); } } - AppEvent::CodexEvent(event) => { + AppEvent::CodeEvent(event) => { self.dispatch_codex_event(event); } AppEvent::ExitRequest => { @@ -722,7 +722,7 @@ impl App<'_> { } } // fallthrough handled by break - AppEvent::CodexOp(op) => match &mut self.app_state { + AppEvent::CodeOp(op) => match &mut self.app_state { AppState::Chat { widget } => widget.submit_op(op), AppState::Onboarding { .. } => {} }, @@ -733,7 +733,7 @@ impl App<'_> { if !command.is_prompt_expanding() { let _ = self .app_event_tx - .send(AppEvent::CodexOp(Op::AddToHistory { text: command_text.clone() })); + .send(AppEvent::CodeOp(Op::AddToHistory { text: command_text.clone() })); } // Extract command arguments by removing the slash command from the beginning // e.g., "/browser status" -> "status", "/chrome 9222" -> "9222" @@ -785,7 +785,7 @@ impl App<'_> { SlashCommand::Compact => { if let AppState::Chat { widget } = &mut self.app_state { widget.clear_token_usage(); - self.app_event_tx.send(AppEvent::CodexOp(Op::Compact)); + self.app_event_tx.send(AppEvent::CodeOp(Op::Compact)); } } SlashCommand::Quit => { break 'main; } @@ -895,7 +895,7 @@ impl App<'_> { use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::FileChange; - self.app_event_tx.send(AppEvent::CodexEvent(Event { + self.app_event_tx.send(AppEvent::CodeEvent(Event { id: "1".to_string(), event_seq: 0, // msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { @@ -965,7 +965,7 @@ impl App<'_> { { use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg}; let msg = format!("✅ Switched to worktree: {}", new_cwd.display()); - let _ = self.app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = self.app_event_tx.send(AppEvent::CodeEvent(Event { id: "switch-cwd".to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg }), @@ -1224,7 +1224,8 @@ impl App<'_> { // Clone cfg for the async block to keep original for the event let cfg_for_rt = cfg.clone(); let result = rt.block_on(async move { - server.fork_conversation(items, nth, cfg_for_rt).await + // Fallback: start a new conversation instead of forking + server.new_conversation(cfg_for_rt).await }); if let Ok(new_conv) = result { tx.send(AppEvent::JumpBackForked { cfg, new_conv: crate::app_event::Redacted(new_conv), prefix_items, prefill: prefill_clone }); @@ -1267,7 +1268,7 @@ impl App<'_> { ), order: None, }; - self.app_event_tx.send(AppEvent::CodexEvent(ev)); + self.app_event_tx.send(AppEvent::CodeEvent(ev)); // Prefill composer with the edited text if let AppState::Chat { widget } = &mut self.app_state { diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index 4be52899c67f..613137ba162d 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -100,7 +100,7 @@ impl App { drop_last_messages: usize, ) { self.backtrack.pending = Some((base_id, drop_last_messages, prefill)); - self.app_event_tx.send(crate::app_event::AppEvent::CodexOp( + self.app_event_tx.send(crate::app_event::AppEvent::CodeOp( codex_core::protocol::Op::GetHistory, )); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 2976b0123d80..36492111f96d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -28,7 +28,7 @@ impl fmt::Debug for Redacted { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub(crate) enum AppEvent { - CodexEvent(Event), + CodeEvent(Event), /// Request a redraw which will be debounced by the [`App`]. RequestRedraw, @@ -57,7 +57,7 @@ pub(crate) enum AppEvent { /// Forward an `Op` to the Agent. Using an `AppEvent` for this avoids /// bubbling channels through layers of widgets. - CodexOp(codex_core::protocol::Op), + CodeOp(codex_core::protocol::Op), /// Dispatch a recognized slash command from the UI (composer) to the app /// layer so it can be handled centrally. Includes the full command text. @@ -168,3 +168,5 @@ pub(crate) enum AppEvent { CancelRunningTask, } + +// No helper constructor; use `AppEvent::CodeEvent(ev)` directly to avoid shadowing. diff --git a/codex-rs/tui/src/app_event_sender.rs b/codex-rs/tui/src/app_event_sender.rs index faa8c8bee464..cc6b83c4b744 100644 --- a/codex-rs/tui/src/app_event_sender.rs +++ b/codex-rs/tui/src/app_event_sender.rs @@ -29,7 +29,7 @@ impl AppEventSender { pub(crate) fn send(&self, event: AppEvent) { // Record inbound events for high-fidelity session replay. // Avoid double-logging Ops; those are logged at the point of submission. - if !matches!(event, AppEvent::CodexOp(_)) { + if !matches!(event, AppEvent::CodeOp(_)) { session_log::log_inbound_app_event(&event); } let is_high = matches!( diff --git a/codex-rs/tui/src/backtrack_helpers.rs b/codex-rs/tui/src/backtrack_helpers.rs index d37fa541a06b..4b78e4d8b95d 100644 --- a/codex-rs/tui/src/backtrack_helpers.rs +++ b/codex-rs/tui/src/backtrack_helpers.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use ratatui::text::Line; /// Convenience: compute the highlight range for the Nth last user message. diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index d858219a39db..d50a883ebfb1 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -383,7 +383,8 @@ impl ChatComposer { // Calculate hint/popup height let hint_height = match &self.active_popup { ActivePopup::None => 1u16, - ActivePopup::Command(c) => c.calculate_required_height(), + // Account for the 2-space left indent inside the popup renderer. + ActivePopup::Command(c) => c.calculate_required_height(width.saturating_sub(2)), ActivePopup::File(c) => c.calculate_required_height(), }; @@ -405,7 +406,7 @@ impl ChatComposer { 1 } else { match &self.active_popup { - ActivePopup::Command(popup) => popup.calculate_required_height(), + ActivePopup::Command(popup) => popup.calculate_required_height(area.width.saturating_sub(2)), ActivePopup::File(popup) => popup.calculate_required_height(), ActivePopup::None => 1, } @@ -1474,7 +1475,7 @@ impl ChatComposer { impl WidgetRef for ChatComposer { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let popup_height = match &self.active_popup { - ActivePopup::Command(popup) => popup.calculate_required_height(), + ActivePopup::Command(popup) => popup.calculate_required_height(area.width.saturating_sub(2)), ActivePopup::File(popup) => popup.calculate_required_height(), ActivePopup::None => 1, }; @@ -2105,7 +2106,7 @@ mod tests { true, sender, false, - "Ask Codex to do anything".to_string(), + "Ask Code to do anything".to_string(), false, ); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs index 66f097f6017e..3d0a1f26b09d 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs @@ -231,7 +231,7 @@ impl ChatComposerHistory { offset: global_idx, log_id, }; - app_event_tx.send(AppEvent::CodexOp(op)); + app_event_tx.send(AppEvent::CodeOp(op)); } None } @@ -258,9 +258,9 @@ mod tests { assert!(history.should_handle_navigation("", 0)); assert!(history.navigate_up("", &tx).is_none()); // don't replace the text yet - // Verify that an AppEvent::CodexOp with the correct GetHistoryEntryRequest was sent. + // Verify that an AppEvent::CodeOp with the correct GetHistoryEntryRequest was sent. let event = rx.try_recv().expect("expected AppEvent to be sent"); - let AppEvent::CodexOp(history_request1) = event else { + let AppEvent::CodeOp(history_request1) = event else { panic!("unexpected event variant"); }; assert_eq!( @@ -282,7 +282,7 @@ mod tests { // Verify second CodexOp event for offset 1. let event2 = rx.try_recv().expect("expected second event"); - let AppEvent::CodexOp(history_request_2) = event2 else { + let AppEvent::CodeOp(history_request_2) = event2 else { panic!("unexpected event variant"); }; assert_eq!( diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index d5455bef0b7e..149c211df18d 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -99,10 +99,37 @@ impl CommandPopup { .ensure_visible(matches_len, MAX_POPUP_ROWS.min(matches_len)); } - /// Determine the preferred height of the popup. This is the number of - /// rows required to show at most MAX_POPUP_ROWS commands. - pub(crate) fn calculate_required_height(&self) -> u16 { - self.filtered_items().len().clamp(1, MAX_POPUP_ROWS) as u16 + /// Determine the preferred height of the popup for a given width. + /// Accounts for wrapped descriptions so that long tooltips don't overflow. + pub(crate) fn calculate_required_height(&self, width: u16) -> u16 { + use super::selection_popup_common::GenericDisplayRow; + use super::selection_popup_common::measure_rows_height; + let matches = self.filtered(); + let rows_all: Vec = if matches.is_empty() { + Vec::new() + } else { + matches + .into_iter() + .map(|(item, indices, _)| match item { + CommandItem::Builtin(cmd) => GenericDisplayRow { + name: format!("/{}", cmd.command()), + match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), + is_current: false, + description: Some(cmd.description().to_string()), + name_color: Some(crate::colors::primary()), + }, + CommandItem::UserPrompt(i) => GenericDisplayRow { + name: format!("/{}", self.prompts[i].name), + match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), + is_current: false, + description: Some("send saved prompt".to_string()), + name_color: Some(crate::colors::primary()), + }, + }) + .collect() + }; + + measure_rows_height(&rows_all, &self.state, MAX_POPUP_ROWS, width) } /// Compute fuzzy-filtered matches over built-in commands and user prompts, diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index e142b8bf2777..5d53a7eb0d7c 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -174,7 +174,7 @@ impl BottomPane<'_> { // send an interrupt even while the composer has focus. if matches!(key_event.code, crossterm::event::KeyCode::Esc) && self.is_task_running { // Send Op::Interrupt directly when a task is running so Esc can cancel. - self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); + self.app_event_tx.send(AppEvent::CodeOp(Op::Interrupt)); self.request_redraw(); return InputResult::None; } @@ -935,7 +935,7 @@ mod tests_removed { for x in 0..area.width { row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' ')); } - if row.contains("Ask Codex") { + if row.contains("Ask Code") { found_composer = true; break; } diff --git a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs index 641f973ec3ba..ae562fcecabb 100644 --- a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -11,6 +11,7 @@ use ratatui::widgets::Table; use ratatui::widgets::Widget; use super::scroll_state::ScrollState; +use unicode_width::UnicodeWidthStr; /// A generic representation of a display row for selection popups. pub(crate) struct GenericDisplayRow { @@ -120,3 +121,64 @@ pub(crate) fn render_rows( table.render(area, buf); } + +/// Estimate the required height (in terminal rows) to render up to +/// `max_results` rows for the provided `rows_all`, taking wrapping into +/// account for the given content `width` and the current `state` window. +/// +/// This mirrors the selection windowing logic in `render_rows` so that the +/// composer can allocate an appropriate hint/popup height prior to render. +pub(crate) fn measure_rows_height( + rows_all: &[GenericDisplayRow], + state: &ScrollState, + max_results: usize, + width: u16, +) -> u16 { + // Empty -> one line placeholder ("no matches"). + if rows_all.is_empty() { + return 1; + } + + // Prevent division by zero; treat zero-width as a single-column layout. + let content_width = width.max(1) as usize; + + let visible_rows = max_results.min(rows_all.len()); + + // Compute starting index like in render_rows to keep scroll behavior aligned. + let mut start_idx = state.scroll_top.min(rows_all.len().saturating_sub(1)); + if let Some(sel) = state.selected_idx { + if sel < start_idx { + start_idx = sel; + } else if visible_rows > 0 { + let bottom = start_idx + visible_rows - 1; + if sel > bottom { + start_idx = sel + 1 - visible_rows; + } + } + } + + // Sum the wrapped line count for the visible window. + let mut total_lines: usize = 0; + for row in rows_all + .iter() + .enumerate() + .skip(start_idx) + .take(visible_rows) + { + let (_i, GenericDisplayRow { name, description, .. }) = row; + + // Compute the display string width: name [+ two spaces + description]. + let mut line_width = UnicodeWidthStr::width(name.as_str()); + + if let Some(desc) = description.as_ref() { + // Two spaces between name and description, like in render_rows. + line_width += 2 + UnicodeWidthStr::width(desc.as_str()); + } + + // Wrapped height = ceil(line_width / content_width), minimum 1. + let wrapped = if line_width == 0 { 1 } else { (line_width + content_width - 1) / content_width }; + total_lines += wrapped.max(1); + } + + total_lines.min(u16::MAX as usize) as u16 +} diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap index 63f9b3a96487..f908fb6144cb 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap @@ -2,7 +2,7 @@ source: tui/src/bottom_pane/chat_composer.rs expression: terminal.backend() --- -"▌/mo " +"▌ /mo " "▌ " -"▌/model choose what model and reasoning effort to use " +"▌/model choose what model and reasoning effort to use " "▌/mention mention a file " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap new file mode 100644 index 000000000000..65606ed7d06c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/bottom_pane/list_selection_view.rs +expression: render_lines(&view) +--- +▌ Select Approval Mode +▌ Switch between Codex approval presets +▌ +▌> 1. Read Only (current) Codex can read files +▌ 2. Full Access Codex can edit files + +Press Enter to confirm or Esc to go back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap new file mode 100644 index 000000000000..b42a5f8c6b03 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap @@ -0,0 +1,10 @@ +--- +source: tui/src/bottom_pane/list_selection_view.rs +expression: render_lines(&view) +--- +▌ Select Approval Mode +▌ +▌> 1. Read Only (current) Codex can read files +▌ 2. Full Access Codex can edit files + +Press Enter to confirm or Esc to go back diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 86830609f2ef..64fde9c498e2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1605,7 +1605,7 @@ impl ChatWidget<'_> { let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); let app_event_tx_clone = app_event_tx.clone(); - // Create the Codex asynchronously so the UI loads as quickly as possible. + // Create the Code asynchronously so the UI loads as quickly as possible. let config_for_agent_loop = config.clone(); tokio::spawn(async move { // Use ConversationManager with an AuthManager (API key by default) @@ -1633,7 +1633,7 @@ impl ChatWidget<'_> { }), order: None, }; - app_event_tx_clone.send(AppEvent::CodexEvent(ev)); + app_event_tx_clone.send(AppEvent::CodeEvent(ev)); return; } }; @@ -1645,7 +1645,7 @@ impl ChatWidget<'_> { msg: EventMsg::SessionConfigured(new_conversation.session_configured), order: None, }; - app_event_tx_clone.send(AppEvent::CodexEvent(event)); + app_event_tx_clone.send(AppEvent::CodeEvent(event)); let conversation = new_conversation.conversation; let conversation_clone = conversation.clone(); @@ -1662,13 +1662,13 @@ impl ChatWidget<'_> { }), order: None, }; - app_event_tx_submit.send(AppEvent::CodexEvent(ev)); + app_event_tx_submit.send(AppEvent::CodeEvent(ev)); } } }); while let Ok(event) = conversation.next_event().await { - app_event_tx_clone.send(AppEvent::CodexEvent(event)); + app_event_tx_clone.send(AppEvent::CodeEvent(event)); } // (debug end notice removed) }); @@ -1848,7 +1848,7 @@ impl ChatWidget<'_> { msg: EventMsg::SessionConfigured(session_configured), order: None, }; - app_event_tx_clone.send(AppEvent::CodexEvent(event)); + app_event_tx_clone.send(AppEvent::CodeEvent(event)); let conversation_clone = conversation.clone(); tokio::spawn(async move { @@ -1861,7 +1861,7 @@ impl ChatWidget<'_> { }); while let Ok(event) = conversation.next_event().await { - app_event_tx_clone.send(AppEvent::CodexEvent(event)); + app_event_tx_clone.send(AppEvent::CodeEvent(event)); } }); @@ -2725,7 +2725,7 @@ impl ChatWidget<'_> { missing.display(), fallback_root.display() ); - let _ = self.app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = self.app_event_tx.send(AppEvent::CodeEvent(Event { id: "cwd-recover".to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg }), @@ -4046,6 +4046,12 @@ impl ChatWidget<'_> { // Request a redraw to update the display immediately self.app_event_tx.send(AppEvent::RequestRedraw); } + // Newer protocol variants we currently ignore in the TUI + EventMsg::UserMessage(_) => {} + EventMsg::TurnAborted(_) => {} + EventMsg::ConversationPath(_) => {} + EventMsg::EnteredReviewMode(_) => {} + EventMsg::ExitedReviewMode(_) => {} } } @@ -6192,7 +6198,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -6249,7 +6255,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -6304,7 +6310,7 @@ impl ChatWidget<'_> { BrowserScreenshotUpdateEvent, Event, EventMsg, }; let _ = app_event_tx_inner.send( - AppEvent::CodexEvent(Event { + AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate( @@ -6375,7 +6381,7 @@ impl ChatWidget<'_> { use codex_core::protocol::Event; use codex_core::protocol::EventMsg; let _ = - app_event_tx_bg.send(AppEvent::CodexEvent(Event { + app_event_tx_bg.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate( @@ -6478,7 +6484,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -6518,7 +6524,7 @@ impl ChatWidget<'_> { *latest = Some((first_path.clone(), url_inner.clone())); } use codex_core::protocol::{BrowserScreenshotUpdateEvent, Event, EventMsg}; - let _ = app_event_tx_inner.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx_inner.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate(BrowserScreenshotUpdateEvent { @@ -6582,7 +6588,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BrowserScreenshotUpdateEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx_bg.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx_bg.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate(BrowserScreenshotUpdateEvent { @@ -6619,7 +6625,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!( "❌ Failed to connect to Chrome after WS fallback: {} (original: {})", e2, err_msg @@ -6637,7 +6643,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!( "❌ CDP connect timed out after {}s during fallback. Ensure Chrome is running with --remote-debugging-port and /json/version is reachable", retry_deadline.as_secs() @@ -6656,7 +6662,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -6811,7 +6817,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -6839,7 +6845,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -6863,7 +6869,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -6973,7 +6979,7 @@ impl ChatWidget<'_> { BrowserScreenshotUpdateEvent, EventMsg, }; let _ = app_event_tx_inner.send( - AppEvent::CodexEvent(Event { + AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate( @@ -7034,7 +7040,7 @@ impl ChatWidget<'_> { // Send update event use codex_core::protocol::{BrowserScreenshotUpdateEvent, EventMsg}; - let _ = app_event_tx_inner.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate(BrowserScreenshotUpdateEvent { + let _ = app_event_tx_inner.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate(BrowserScreenshotUpdateEvent { screenshot_path: first_path.clone(), url: url_inner, }), order: None })); @@ -7061,7 +7067,7 @@ impl ChatWidget<'_> { // Send success message to chat use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -7091,7 +7097,7 @@ impl ChatWidget<'_> { // Send update event use codex_core::protocol::BrowserScreenshotUpdateEvent; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate( @@ -7648,7 +7654,7 @@ impl ChatWidget<'_> { // Explicitly (re)start the internal browser session now if let Err(e) = browser_manager.start().await { tracing::error!("Failed to start internal browser: {}", e); - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -7663,7 +7669,7 @@ impl ChatWidget<'_> { codex_browser::global::set_global_browser_manager(browser_manager.clone()).await; // Notify about successful switch/reconnect - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -7692,7 +7698,7 @@ impl ChatWidget<'_> { } use codex_core::protocol::BrowserScreenshotUpdateEvent; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate(BrowserScreenshotUpdateEvent { @@ -7783,7 +7789,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -7885,7 +7891,7 @@ impl ChatWidget<'_> { } use codex_core::protocol::BrowserScreenshotUpdateEvent; use codex_core::protocol::EventMsg; - let _ = app_event_tx.send(AppEvent::CodexEvent(Event { + let _ = app_event_tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BrowserScreenshotUpdate( @@ -8503,7 +8509,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -8529,7 +8535,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -8550,7 +8556,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -8636,7 +8642,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg }), @@ -8674,7 +8680,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -8699,7 +8705,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -8741,7 +8747,7 @@ impl ChatWidget<'_> { } else { stdout_s.trim().to_string() }; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -8763,7 +8769,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: "`/branch finalize` — failed to determine default branch (tried origin/HEAD, main, master)".to_string() }), order: None })); + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: "`/branch finalize` — failed to determine default branch (tried origin/HEAD, main, master)".to_string() }), order: None })); return; } }; @@ -8823,7 +8829,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -8967,7 +8973,7 @@ impl ChatWidget<'_> { if let Some(p) = hint { msg = format!("{} (checked out in worktree: {})", msg, p); } - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg }), @@ -8989,7 +8995,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { @@ -9023,7 +9029,7 @@ impl ChatWidget<'_> { use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { diff --git a/codex-rs/tui/src/chatwidget/agent.rs b/codex-rs/tui/src/chatwidget/agent.rs index 3d2de5c084c9..6157c659c170 100644 --- a/codex-rs/tui/src/chatwidget/agent.rs +++ b/codex-rs/tui/src/chatwidget/agent.rs @@ -41,7 +41,7 @@ pub(crate) fn spawn_agent( id: "".to_string(), msg: codex_core::protocol::EventMsg::SessionConfigured(session_configured), }; - app_event_tx_clone.send(AppEvent::CodexEvent(ev)); + app_event_tx_clone.send(AppEvent::CodeEvent(ev)); let conversation_clone = conversation.clone(); tokio::spawn(async move { @@ -54,7 +54,7 @@ pub(crate) fn spawn_agent( }); while let Ok(event) = conversation.next_event().await { - app_event_tx_clone.send(AppEvent::CodexEvent(event)); + app_event_tx_clone.send(AppEvent::CodeEvent(event)); } }); @@ -78,7 +78,7 @@ pub(crate) fn spawn_agent_from_existing( id: "".to_string(), msg: codex_core::protocol::EventMsg::SessionConfigured(session_configured), }; - app_event_tx_clone.send(AppEvent::CodexEvent(ev)); + app_event_tx_clone.send(AppEvent::CodeEvent(ev)); let conversation_clone = conversation.clone(); tokio::spawn(async move { @@ -91,7 +91,7 @@ pub(crate) fn spawn_agent_from_existing( }); while let Ok(event) = conversation.next_event().await { - app_event_tx_clone.send(AppEvent::CodexEvent(event)); + app_event_tx_clone.send(AppEvent::CodeEvent(event)); } }); diff --git a/codex-rs/tui/src/chatwidget/gh_actions.rs b/codex-rs/tui/src/chatwidget/gh_actions.rs index c1bfd9f427dd..32b70cf4c208 100644 --- a/codex-rs/tui/src/chatwidget/gh_actions.rs +++ b/codex-rs/tui/src/chatwidget/gh_actions.rs @@ -157,7 +157,7 @@ fn surface_failure(tx: &AppEventSender, owner: &str, repo: &str, branch: &str, s } else { format!("❌ GitHub Actions failed for {owner}/{repo}@{short} on {branch}: {conclusion} — {url}") }; - let _ = tx.send(AppEvent::CodexEvent(Event { + let _ = tx.send(AppEvent::CodeEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg }), diff --git a/codex-rs/tui/src/chatwidget/session_header.rs b/codex-rs/tui/src/chatwidget/session_header.rs new file mode 100644 index 000000000000..32e31b6682e5 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/session_header.rs @@ -0,0 +1,16 @@ +pub(crate) struct SessionHeader { + model: String, +} + +impl SessionHeader { + pub(crate) fn new(model: String) -> Self { + Self { model } + } + + /// Updates the header's model text. + pub(crate) fn set_model(&mut self, model: &str) { + if self.model != model { + self.model = model.to_string(); + } + } +} diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap index e3289220ad1e..b442bb198529 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap @@ -1,9 +1,8 @@ --- source: tui/src/chatwidget/tests.rs -assertion_line: 728 expression: terminal.backend() --- -"? Codex wants to run echo hello world " +"? Code wants to run echo hello world " " " "Model wants to run a command " " " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__disabled_slash_command_while_task_running_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__disabled_slash_command_while_task_running_snapshot.snap index 52b6807b4db2..e8f08a437acd 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__disabled_slash_command_while_task_running_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__disabled_slash_command_while_task_running_snapshot.snap @@ -2,4 +2,4 @@ source: tui/src/chatwidget/tests.rs expression: blob --- -🖐  '/model' is disabled while a task is in progress. +■ '/model' is disabled while a task is in progress. diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index b96c01762d7e..55110c404313 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -140,6 +140,17 @@ fn make_chatwidget_manual() -> ( (widget, rx, op_rx) } +pub(crate) fn make_chatwidget_manual_with_sender() -> ( + ChatWidget, + AppEventSender, + tokio::sync::mpsc::UnboundedReceiver, + tokio::sync::mpsc::UnboundedReceiver, +) { + let (widget, rx, op_rx) = make_chatwidget_manual(); + let app_event_tx = widget.app_event_tx.clone(); + (widget, app_event_tx, rx, op_rx) +} + fn drain_insert_history( rx: &std::sync::mpsc::Receiver, ) -> Vec>> { @@ -411,7 +422,7 @@ async fn binary_size_transcript_matches_ideal_fixture() { lines.pop(); } // Compare only after the last session banner marker, and start at the next 'thinking' line. - const MARKER_PREFIX: &str = ">_ You are using OpenAI Codex in "; + const MARKER_PREFIX: &str = ">_ You are using OpenAI Code in "; let last_marker_line_idx = lines .iter() .rposition(|l| l.starts_with(MARKER_PREFIX)) @@ -443,7 +454,7 @@ async fn binary_size_transcript_matches_ideal_fixture() { // // Snapshot test: command approval modal // -// Synthesizes a Codex ExecApprovalRequest event to trigger the approval modal +// Synthesizes a Code ExecApprovalRequest event to trigger the approval modal // and snapshots the visual output using the ratatui TestBackend. #[test] fn approval_modal_exec_snapshot() { @@ -622,7 +633,7 @@ fn status_widget_and_approval_modal_snapshot() { call_id: "call-approve-exec".into(), command: vec!["echo".into(), "hello world".into()], cwd: std::path::PathBuf::from("/tmp"), - reason: Some("Codex wants to run a command".into()), + reason: Some("Code wants to run a command".into()), }; chat.handle_codex_event(Event { id: "sub-approve-exec".into(), @@ -768,10 +779,10 @@ fn apply_patch_approval_sends_op_with_submission_id() { // Approve via key press 'y' chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); - // Expect a CodexOp with PatchApproval carrying the submission id, not call id + // Expect a CodeOp with PatchApproval carrying the submission id, not call id let mut found = false; while let Ok(app_ev) = rx.try_recv() { - if let AppEvent::CodexOp(Op::PatchApproval { id, decision }) = app_ev { + if let AppEvent::CodeOp(Op::PatchApproval { id, decision }) = app_ev { assert_eq!(id, "sub-123"); assert!(matches!( decision, @@ -804,16 +815,16 @@ fn apply_patch_full_flow_integration_like() { }), }); - // 2) User approves via 'y' and App receives a CodexOp + // 2) User approves via 'y' and App receives a CodeOp chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); let mut maybe_op: Option = None; while let Ok(app_ev) = rx.try_recv() { - if let AppEvent::CodexOp(op) = app_ev { + if let AppEvent::CodeOp(op) = app_ev { maybe_op = Some(op); break; } } - let op = maybe_op.expect("expected CodexOp after key press"); + let op = maybe_op.expect("expected CodeOp after key press"); // 3) App forwards to widget.submit_op, which pushes onto codex_op_tx chat.submit_op(op); diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index b137cb3c5ffd..1bf79787c8c0 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; #[command(version)] pub struct Cli { /// Optional user prompt to start the session. + #[arg(value_name = "PROMPT")] pub prompt: Option, /// Optional image(s) to attach to the initial prompt. diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index b5fc1d5a4d0f..1090bc41315f 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -30,6 +30,7 @@ use color_eyre::owo_colors::OwoColorize; mod app; mod app_event; mod app_event_sender; +mod backtrack_helpers; mod bottom_pane; mod chatwidget; mod citation_regex; @@ -69,6 +70,7 @@ mod util { } mod spinner; mod tui; +mod ui_consts; mod user_approval_widget; mod height_manager; mod transcript_app; @@ -97,7 +99,7 @@ pub async fn run_main( let (sandbox_mode, approval_policy) = if cli.full_auto { ( Some(SandboxMode::WorkspaceWrite), - Some(AskForApproval::OnFailure), + Some(AskForApproval::OnRequest), ) } else if cli.dangerously_bypass_approvals_and_sandbox { ( @@ -133,6 +135,7 @@ pub async fn run_main( let overrides = ConfigOverrides { model, + review_model: None, approval_policy, sandbox_mode, cwd, @@ -141,6 +144,8 @@ pub async fn run_main( codex_linux_sandbox_exe, base_instructions: None, include_plan_tool: Some(true), + include_apply_patch_tool: None, + include_view_image_tool: None, disable_response_storage: cli.oss.then_some(true), show_raw_agent_reasoning: cli.oss.then_some(true), debug: Some(cli.debug), @@ -208,7 +213,7 @@ pub async fn run_main( // Ensure the file is only readable and writable by the current user. // Doing the equivalent to `chmod 600` on Windows is quite a bit more code // and requires the Windows API crates, so we can reconsider that when - // Codex CLI is officially supported on Windows. + // Code CLI is officially supported on Windows. #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; @@ -513,3 +518,4 @@ fn determine_repo_trust_state( Ok(true) } } + diff --git a/codex-rs/tui/src/new_model_popup.rs b/codex-rs/tui/src/new_model_popup.rs index e89fb67ef54a..e149b361472e 100644 --- a/codex-rs/tui/src/new_model_popup.rs +++ b/codex-rs/tui/src/new_model_popup.rs @@ -1,10 +1,12 @@ use crate::tui::FrameRequester; use crate::tui::Tui; use crate::tui::TuiEvent; -use codex_core::config::GPT5_HIGH_MODEL; +use codex_core::config::SWIFTFOX_MODEL_DISPLAY_NAME; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; +use crossterm::event::KeyModifiers; +use rand::Rng as _; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Widget; @@ -14,8 +16,79 @@ use ratatui::widgets::Clear; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; +use std::time::Duration; use tokio_stream::StreamExt; +// Embed animation frames for each variant at compile time. +macro_rules! frames_for { + ($dir:literal) => { + [ + include_str!(concat!("../frames/", $dir, "/frame_1.txt")), + include_str!(concat!("../frames/", $dir, "/frame_2.txt")), + include_str!(concat!("../frames/", $dir, "/frame_3.txt")), + include_str!(concat!("../frames/", $dir, "/frame_4.txt")), + include_str!(concat!("../frames/", $dir, "/frame_5.txt")), + include_str!(concat!("../frames/", $dir, "/frame_6.txt")), + include_str!(concat!("../frames/", $dir, "/frame_7.txt")), + include_str!(concat!("../frames/", $dir, "/frame_8.txt")), + include_str!(concat!("../frames/", $dir, "/frame_9.txt")), + include_str!(concat!("../frames/", $dir, "/frame_10.txt")), + include_str!(concat!("../frames/", $dir, "/frame_11.txt")), + include_str!(concat!("../frames/", $dir, "/frame_12.txt")), + include_str!(concat!("../frames/", $dir, "/frame_13.txt")), + include_str!(concat!("../frames/", $dir, "/frame_14.txt")), + include_str!(concat!("../frames/", $dir, "/frame_15.txt")), + include_str!(concat!("../frames/", $dir, "/frame_16.txt")), + include_str!(concat!("../frames/", $dir, "/frame_17.txt")), + include_str!(concat!("../frames/", $dir, "/frame_18.txt")), + include_str!(concat!("../frames/", $dir, "/frame_19.txt")), + include_str!(concat!("../frames/", $dir, "/frame_20.txt")), + include_str!(concat!("../frames/", $dir, "/frame_21.txt")), + include_str!(concat!("../frames/", $dir, "/frame_22.txt")), + include_str!(concat!("../frames/", $dir, "/frame_23.txt")), + include_str!(concat!("../frames/", $dir, "/frame_24.txt")), + include_str!(concat!("../frames/", $dir, "/frame_25.txt")), + include_str!(concat!("../frames/", $dir, "/frame_26.txt")), + include_str!(concat!("../frames/", $dir, "/frame_27.txt")), + include_str!(concat!("../frames/", $dir, "/frame_28.txt")), + include_str!(concat!("../frames/", $dir, "/frame_29.txt")), + include_str!(concat!("../frames/", $dir, "/frame_30.txt")), + include_str!(concat!("../frames/", $dir, "/frame_31.txt")), + include_str!(concat!("../frames/", $dir, "/frame_32.txt")), + include_str!(concat!("../frames/", $dir, "/frame_33.txt")), + include_str!(concat!("../frames/", $dir, "/frame_34.txt")), + include_str!(concat!("../frames/", $dir, "/frame_35.txt")), + include_str!(concat!("../frames/", $dir, "/frame_36.txt")), + ] + }; +} + +const FRAMES_DEFAULT: [&str; 36] = frames_for!("default"); +const FRAMES_CODEX: [&str; 36] = frames_for!("codex"); +const FRAMES_OPENAI: [&str; 36] = frames_for!("openai"); +const FRAMES_BLOCKS: [&str; 36] = frames_for!("blocks"); +const FRAMES_DOTS: [&str; 36] = frames_for!("dots"); +const FRAMES_HASH: [&str; 36] = frames_for!("hash"); +const FRAMES_HBARS: [&str; 36] = frames_for!("hbars"); +const FRAMES_VBARS: [&str; 36] = frames_for!("vbars"); +const FRAMES_SHAPES: [&str; 36] = frames_for!("shapes"); +const FRAMES_SLUG: [&str; 36] = frames_for!("slug"); + +const VARIANTS: &[&[&str]] = &[ + &FRAMES_DEFAULT, + &FRAMES_CODEX, + &FRAMES_OPENAI, + &FRAMES_BLOCKS, + &FRAMES_DOTS, + &FRAMES_HASH, + &FRAMES_HBARS, + &FRAMES_VBARS, + &FRAMES_SHAPES, + &FRAMES_SLUG, +]; + +const FRAME_TICK: Duration = Duration::from_millis(60); + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum ModelUpgradeDecision { Switch, @@ -32,6 +105,8 @@ struct ModelUpgradePopup { highlighted: ModelUpgradeOption, decision: Option, request_frame: FrameRequester, + frame_idx: usize, + variant_idx: usize, } impl ModelUpgradePopup { @@ -40,6 +115,8 @@ impl ModelUpgradePopup { highlighted: ModelUpgradeOption::TryNewModel, decision: None, request_frame, + frame_idx: 0, + variant_idx: 0, } } @@ -51,6 +128,11 @@ impl ModelUpgradePopup { KeyCode::Char('2') => self.select(ModelUpgradeOption::KeepCurrent), KeyCode::Enter => self.select(self.highlighted), KeyCode::Esc => self.select(ModelUpgradeOption::KeepCurrent), + KeyCode::Char('.') => { + if key_event.modifiers.contains(KeyModifiers::CONTROL) { + self.pick_random_variant(); + } + } _ => {} } } @@ -66,6 +148,30 @@ impl ModelUpgradePopup { self.decision = Some(option.into()); self.request_frame.schedule_frame(); } + + fn advance_animation(&mut self) { + let len = self.frames().len(); + self.frame_idx = (self.frame_idx + 1) % len; + self.request_frame.schedule_frame_in(FRAME_TICK); + } + + fn frames(&self) -> &'static [&'static str] { + VARIANTS[self.variant_idx] + } + + fn pick_random_variant(&mut self) { + let total = VARIANTS.len(); + if total <= 1 { + return; + } + let mut rng = rand::rng(); + let mut next = self.variant_idx; + while next == self.variant_idx { + next = rng.random_range(0..total); + } + self.variant_idx = next; + self.request_frame.schedule_frame(); + } } impl From for ModelUpgradeDecision { @@ -81,15 +187,27 @@ impl WidgetRef for &ModelUpgradePopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { Clear.render(area, buf); - let mut lines: Vec = vec![ - Line::from(vec![ - "> ".into(), - format!("Try {GPT5_HIGH_MODEL} as your default model").bold(), - ]), - format!(" {GPT5_HIGH_MODEL} is our latest model tuned for coding workflows.").into(), - " Switch now or keep your current default – you can change models any time.".into(), - "".into(), - ]; + let mut lines: Vec = self.frames()[self.frame_idx] + .lines() + .map(|l| l.to_string().into()) + .collect(); + + // Spacer between animation and text content. + lines.push("".into()); + + lines.push( + format!( + " Code is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is" + ) + .into(), + ); + lines.push(Line::from(vec![ + " ".into(), + "faster, a better collaborator, ".bold(), + "and ".into(), + "more steerable.".bold(), + ])); + lines.push("".into()); let create_option = |index: usize, option: ModelUpgradeOption, text: &str| -> Line<'static> { @@ -106,7 +224,7 @@ impl WidgetRef for &ModelUpgradePopup { lines.push(create_option( 0, ModelUpgradeOption::TryNewModel, - &format!("Yes, switch me to {GPT5_HIGH_MODEL}"), + &format!("Yes, switch me to {SWIFTFOX_MODEL_DISPLAY_NAME}"), )); lines.push(create_option( 1, @@ -133,6 +251,8 @@ pub(crate) async fn run_model_upgrade_popup(tui: &mut Tui) -> Result Result popup.handle_key_event(key_event), TuiEvent::Draw => { + popup.advance_animation(); let _ = tui.draw(u16::MAX, |frame| { frame.render_widget_ref(&popup, frame.area()); }); diff --git a/codex-rs/tui/src/session_log.rs b/codex-rs/tui/src/session_log.rs index 94e30b99051c..94f1f80f94c4 100644 --- a/codex-rs/tui/src/session_log.rs +++ b/codex-rs/tui/src/session_log.rs @@ -129,7 +129,7 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) { } match event { - AppEvent::CodexEvent(ev) => { + AppEvent::CodeEvent(ev) => { write_record("to_tui", "codex_event", ev); } AppEvent::KeyEvent(k) => { diff --git a/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap index 8b8cc38a03ae..ef0ceabc48d9 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap @@ -2,7 +2,7 @@ source: tui/src/history_cell.rs expression: rendered --- -▌one two -▌three four -▌five six -▌seven +▌ one two +▌ three four +▌ five six +▌ seven diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap index 19dc5d313750..6aa3401770fb 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap @@ -2,5 +2,5 @@ source: tui/src/status_indicator_widget.rs expression: terminal.backend() --- -" Working (0s • Esc t" +" Working (0s • Esc " " " diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap index cb51754962d7..9d50b9636586 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap @@ -2,11 +2,11 @@ source: tui/src/status_indicator_widget.rs expression: terminal.backend() --- -" Working (0s • Esc to interrupt) " +" Working (0s • Esc to interrupt) " +" " " ↳ first " " ↳ second " " Alt+↑ edit " " " " " " " -" " diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap index fe9eebedd68f..debf2821180e 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap @@ -2,5 +2,5 @@ source: tui/src/status_indicator_widget.rs expression: terminal.backend() --- -" Working (0s • Esc to interrupt) " +" Working (0s • Esc to interrupt) " " " diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index b5814d0728ab..f6a99e18823d 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -47,10 +47,13 @@ impl StatusIndicatorWidget { } pub fn desired_height(&self, width: u16) -> u16 { - // Status line + wrapped queued messages (up to 3 lines per message) + // Status line + optional blank line + wrapped queued messages (up to 3 lines per message) // + optional ellipsis line per truncated message + 1 spacer line let inner_width = width.max(1) as usize; let mut total: u16 = 1; // status line + if !self.queued_messages.is_empty() { + total = total.saturating_add(1); // blank line between status and queued messages + } let text_width = inner_width.saturating_sub(3); // account for " ↳ " prefix if text_width > 0 { let opts = TwOptions::new(text_width) @@ -75,7 +78,7 @@ impl StatusIndicatorWidget { } pub(crate) fn interrupt(&self) { - self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); + self.app_event_tx.send(AppEvent::CodeOp(Op::Interrupt)); } /// Update the animated header label (left of the brackets). @@ -137,6 +140,9 @@ impl WidgetRef for StatusIndicatorWidget { // Build lines: status, then queued messages, then spacer. let mut lines: Vec> = Vec::new(); lines.push(Line::from(spans)); + if !self.queued_messages.is_empty() { + lines.push(Line::from("")); + } // Wrap queued messages using textwrap and show up to the first 3 lines per message. let text_width = area.width.saturating_sub(3); // " ↳ " prefix let opts = TwOptions::new(text_width as usize) @@ -181,6 +187,7 @@ mod tests { use ratatui::backend::TestBackend; use tokio::sync::mpsc::unbounded_channel; + // no extra tests added from upstream for elapsed formatting; our widget uses simple seconds #[test] fn renders_with_working_header() { let (tx_raw, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/ui_consts.rs b/codex-rs/tui/src/ui_consts.rs new file mode 100644 index 000000000000..f02f0991e020 --- /dev/null +++ b/codex-rs/tui/src/ui_consts.rs @@ -0,0 +1,10 @@ +//! Shared UI constants for layout and alignment within the TUI. + +/// Width (in terminal columns) reserved for the left gutter/prefix used by +/// live cells and aligned widgets. +/// +/// Semantics: +/// - Chat composer reserves this many columns for the left border + padding. +/// - Status indicator lines begin with this many spaces for alignment. +/// - User history lines account for this many columns (e.g., "▌ ") when wrapping. +pub(crate) const _LIVE_PREFIX_COLS: u16 = 2; diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index 46929b199f93..d31a50de786a 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -286,7 +286,7 @@ impl UserApprovalWidget<'_> { }, }; - self.app_event_tx.send(AppEvent::CodexOp(op)); + self.app_event_tx.send(AppEvent::CodeOp(op)); self.done = true; } @@ -392,7 +392,7 @@ mod tests { let events: Vec = rx.try_iter().collect(); assert!(events.iter().any(|e| matches!( e, - AppEvent::CodexOp(Op::ExecApproval { + AppEvent::CodeOp(Op::ExecApproval { decision: ReviewDecision::Approved, .. }) @@ -414,7 +414,7 @@ mod tests { let events: Vec = rx.try_iter().collect(); assert!(events.iter().any(|e| matches!( e, - AppEvent::CodexOp(Op::ExecApproval { + AppEvent::CodeOp(Op::ExecApproval { decision: ReviewDecision::Approved, .. }) diff --git a/docs/advanced.md b/docs/advanced.md index 26f735991f2b..c31d940e5938 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -12,6 +12,40 @@ Run Codex head-less in pipelines. Example GitHub Action step: codex exec --full-auto "update CHANGELOG for next release" ``` +### Resuming non-interactive sessions + +You can resume a previous headless run to continue the same conversation context and append to the same rollout file. + +Interactive TUI equivalent: + +```shell +codex resume # picker +codex resume --last # most recent +codex resume +``` + +Compatibility: + +- Latest source builds include `codex exec resume` (examples below). +- Current released CLI may not include this yet. If `codex exec --help` shows no `resume`, use the workaround in the next subsection. + +```shell +# Resume the most recent recorded session and run with a new prompt (source builds) +codex exec "ship a release draft changelog" resume --last + +# Alternatively, pass the prompt via stdin (source builds) +# Note: omit the trailing '-' to avoid it being parsed as a SESSION_ID +echo "ship a release draft changelog" | codex exec resume --last + +# Or resume a specific session by id (UUID) (source builds) +codex exec resume 7f9f9a2e-1b3c-4c7a-9b0e-123456789abc "continue the task" +``` + +Notes: + +- When using `--last`, Codex picks the newest recorded session; if none exist, it behaves like starting fresh. +- Resuming appends new events to the existing session file and maintains the same conversation id. + ## Tracing / verbose logging Because Codex is written in Rust, it honors the `RUST_LOG` environment variable to configure its logging behavior. @@ -38,5 +72,6 @@ args = ["-y", "mcp-server"] env = { "API_KEY" = "value" } ``` +## Using Codex as an MCP Server > [!TIP] -> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. \ No newline at end of file +> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. diff --git a/docs/config.md b/docs/config.md index 53ab7545f81e..3983844c5c9e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -380,6 +380,26 @@ Currently, customers whose accounts are set to use Zero Data Retention (ZDR) mus disable_response_storage = true ``` +### Managing MCP servers from CLI (experimental) + +You can also manage these entries from the CLI: + +```shell +# Add a server (env can be repeated; `--` separates the launcher command) +codex mcp add docs -- docs-server --port 4000 + +# List configured servers (pretty table or JSON) +codex mcp list +codex mcp list --json + +# Show one server (table or JSON) +codex mcp get docs +codex mcp get docs --json + +# Remove a server +codex mcp remove docs +``` + ## shell_environment_policy Codex spawns subprocesses (e.g. when executing a `local_shell` tool-call suggested by the assistant). By default it now passes **your full environment** to those subprocesses. You can tune this behavior via the **`shell_environment_policy`** block in `config.toml`: @@ -499,6 +519,9 @@ To have Codex use this script for notifications, you would configure it via `not notify = ["python3", "/Users/mbolin/.codex/notify.py"] ``` +> [!NOTE] +> Use `notify` for automation and integrations: Codex invokes your external program with a single JSON argument for each event, independent of the TUI. If you only want lightweight desktop notifications while using the TUI, prefer `tui.notifications`, which uses terminal escape codes and requires no external program. You can enable both; `tui.notifications` covers in‑TUI alerts (e.g., approval prompts), while `notify` is best for system‑level hooks or custom notifiers. Currently, `notify` emits only `agent-turn-complete`, whereas `tui.notifications` supports `agent-turn-complete` and `approval-requested` with optional filtering. + ## history By default, Codex CLI records messages sent to the model in `$CODEX_HOME/history.jsonl`. Note that on UNIX, the file permissions are set to `o600`, so it should only be readable and writable by the owner. @@ -571,9 +594,21 @@ Options that are specific to the TUI. ```toml [tui] -# More to come here +# Send desktop notifications when approvals are required or a turn completes. +# Defaults to false. +notifications = true + +# You can optionally filter to specific notification types. +# Available types are "agent-turn-complete" and "approval-requested". +notifications = [ "agent-turn-complete", "approval-requested" ] ``` +> [!NOTE] +> Codex emits desktop notifications using terminal escape codes. Not all terminals support these (notably, macOS Terminal.app and VS Code's terminal do not support custom notifications. iTerm2, Ghostty and WezTerm do support these notifications). + +> [!NOTE] +> `tui.notifications` is built‑in and limited to the TUI session. For programmatic or cross‑environment notifications—or to integrate with OS‑specific notifiers—use the top‑level `notify` option to run an external program that receives event JSON. The two settings are independent and can be used together. + ## Config reference | Key | Type / Values | Notes | @@ -611,7 +646,8 @@ Options that are specific to the TUI. | `history.persistence` | `save-all` \| `none` | History file persistence (default: `save-all`). | | `history.max_bytes` | number | Currently ignored (not enforced). | | `file_opener` | `vscode` \| `vscode-insiders` \| `windsurf` \| `cursor` \| `none` | URI scheme for clickable citations (default: `vscode`). | -| `tui` | table | TUI‑specific options (reserved). | +| `tui` | table | TUI‑specific options. | +| `tui.notifications` | boolean \| array | Enable desktop notifications in the tui (default: false). | | `hide_agent_reasoning` | boolean | Hide model reasoning events. | | `show_raw_agent_reasoning` | boolean | Show raw reasoning (when available). | | `model_reasoning_effort` | `minimal` \| `low` \| `medium` \| `high` | Responses API reasoning effort. |