diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index a90712870..069fce06e 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -38,6 +38,26 @@ jobs: - name: Cache dependencies uses: Swatinem/rust-cache@v2 + - name: Install x86_64 OpenSSL + if: matrix.target == 'x86_64-apple-darwin' + shell: bash + env: + NONINTERACTIVE: 1 + run: | + sudo softwareupdate --install-rosetta --agree-to-license || true + /usr/bin/arch -x86_64 /bin/bash -lc ' + if [[ ! -x /usr/local/bin/brew ]]; then + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + fi + /usr/local/bin/brew update + /usr/local/bin/brew install openssl@3 + ' + OPENSSL_DIR=$(/usr/bin/arch -x86_64 /usr/local/bin/brew --prefix openssl@3) + echo "OPENSSL_DIR=$OPENSSL_DIR" >> "$GITHUB_ENV" + echo "OPENSSL_INCLUDE_DIR=$OPENSSL_DIR/include" >> "$GITHUB_ENV" + echo "OPENSSL_LIB_DIR=$OPENSSL_DIR/lib" >> "$GITHUB_ENV" + echo "PKG_CONFIG_PATH=$OPENSSL_DIR/lib/pkgconfig:${PKG_CONFIG_PATH:-}" >> "$GITHUB_ENV" + - name: Build release binary run: cargo build --release --target ${{ matrix.target }} diff --git a/.vtcode/tool-policy.json b/.vtcode/tool-policy.json index 0c51e968f..59af2fc5e 100644 --- a/.vtcode/tool-policy.json +++ b/.vtcode/tool-policy.json @@ -37,7 +37,7 @@ "list_pty_sessions": "allow", "read_pty_session": "allow", "resize_pty_session": "allow", - "send_pty_input": "prompt", + "send_pty_input": "allow", "git_diff": "allow", "grep_file": "allow", "create_file": "prompt", diff --git a/CHANGELOG.md b/CHANGELOG.md index 476e8a916..6c3192dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to vtcode will be documented in this file. ## [Unreleased] - Latest Improvements +# [Version 0.38.2] - 2025-11-02$' + +'### Features$' +' - feat: Add clear screen command to session and implement related functionality$' + +'### Documentation$' +' - docs: update changelog for v0.38.1 [skip ci] + - docs: update changelog for v0.38.0 [skip ci]$' + +'### Chores$' +' - chore: update npm package to v0.38.2 + - chore: update mcp-types integration and add tests for docs.rs compatibility + - chore: update dependencies for agent-client-protocol and related packages + - chore: add sudo to softwareupdate command for OpenSSL installation on macOS + - chore: enhance OpenSSL installation step for x86_64-apple-darwin target + - chore: release v0.38.1 + - chore: update npm package to v0.38.1 + - chore: update CI workflow to use stable Rust toolchain and add markdown linting filter + - chore: update dependabot configuration to monthly schedule and reduce open pull requests limit + - chore: release v0.38.0 + - chore: update npm package to v0.38.0$' + +' # [Version 0.38.1] - 2025-11-02$' '### Features$' diff --git a/Cargo.lock b/Cargo.lock index 651836ac1..395563ac5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,29 +17,32 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "agent-client-protocol" -version = "0.4.5" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d24bcd53c5ff2c9c75c2d378e8a4efd6519e79e62eebc7dedd36f356f79f48" +checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b" dependencies = [ + "agent-client-protocol-schema", "anyhow", "async-broadcast", "async-trait", + "derive_more", "futures", "log", "parking_lot", + "serde", + "serde_json", +] + +[[package]] +name = "agent-client-protocol-schema" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d08d095e8069115774caa50392e9c818e3fb1c482ef4f3153d26b4595482f2" +dependencies = [ + "anyhow", + "derive_more", "schemars 1.0.4", "serde", "serde_json", @@ -182,9 +185,6 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "arg_enum_proc_macro" @@ -403,16 +403,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "avt" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "156203bcce48a54533c6a718509c22825c1ebbb606e6b313a6f13e8c6097bd13" -dependencies = [ - "rgb", - "unicode-width 0.1.14", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -446,12 +436,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - [[package]] name = "bincode" version = "1.3.3" @@ -551,12 +535,6 @@ version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "byteorder-lite" version = "0.1.0" @@ -569,25 +547,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "cassowary" version = "0.3.0" @@ -711,16 +670,6 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clap" version = "4.5.48" @@ -838,48 +787,17 @@ dependencies = [ "encode_unicode", "libc", "once_cell", + "unicode-width 0.2.0", "windows-sys 0.61.1", ] [[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.22.0" +name = "convert_case" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" dependencies = [ - "cookie", - "document-features", - "idna", - "indexmap 2.11.4", - "log", - "serde", - "serde_derive", - "serde_json", - "time", - "url", + "unicode-segmentation", ] [[package]] @@ -917,21 +835,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -1018,6 +921,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.4", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.2", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1073,33 +994,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "darling" version = "0.14.4" @@ -1218,22 +1112,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "deflate64" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "zeroize", -] - [[package]] name = "deranged" version = "0.5.4" @@ -1255,17 +1133,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "derive_builder" version = "0.12.0" @@ -1297,6 +1164,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", +] + [[package]] name = "derive_setters" version = "0.1.8" @@ -1311,23 +1200,16 @@ dependencies = [ [[package]] name = "dialoguer" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" dependencies = [ - "console 0.15.11", + "console 0.16.1", "shell-words", "tempfile", - "thiserror 1.0.69", "zeroize", ] -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "difflib" version = "0.4.0" @@ -1342,7 +1224,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1384,7 +1265,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -1437,31 +1318,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "signature", - "subtle", - "zeroize", -] - [[package]] name = "either" version = "1.15.0" @@ -1483,6 +1339,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equator" version = "0.4.2" @@ -1516,7 +1378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -1528,17 +1390,6 @@ dependencies = [ "cc", ] -[[package]] -name = "etcetera" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.59.0", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -1632,12 +1483,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "filedescriptor" version = "0.8.3" @@ -1649,18 +1494,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "filetime" -version = "0.2.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.60.2", -] - [[package]] name = "find-msvc-tools" version = "0.1.2" @@ -1847,15 +1680,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width 0.2.0", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -2024,25 +1848,7 @@ dependencies = [ "serde", "serde_json", "thiserror 1.0.69", - "ureq 2.12.1", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", + "ureq", ] [[package]] @@ -2419,15 +2225,6 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "insta" version = "1.43.2" @@ -2498,7 +2295,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -2647,7 +2444,6 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", - "redox_syscall", ] [[package]] @@ -2740,27 +2536,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "macro_rules_attribute" version = "0.2.2" @@ -2799,8 +2574,6 @@ dependencies = [ [[package]] name = "mcp-types" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23244bcaabe34f681acd8cb0522144714a7d60a7fd043cb9122391f12387e9c1" dependencies = [ "prettyplease", "schemars 0.8.22", @@ -3277,16 +3050,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -3391,16 +3154,6 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -3415,7 +3168,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.11.4", - "quick-xml 0.38.3", + "quick-xml", "serde", "time", ] @@ -3553,16 +3306,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -3632,20 +3375,7 @@ checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ "bitflags 2.9.4", "memchr", - "pulldown-cmark-escape 0.10.1", - "unicase", -] - -[[package]] -name = "pulldown-cmark" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" -dependencies = [ - "bitflags 2.9.4", - "getopts", - "memchr", - "pulldown-cmark-escape 0.11.0", + "pulldown-cmark-escape", "unicase", ] @@ -3655,12 +3385,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - [[package]] name = "pxfm" version = "0.1.25" @@ -3685,15 +3409,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.38.3" @@ -3853,7 +3568,7 @@ dependencies = [ "bitflags 2.9.4", "cassowary", "compact_str", - "crossterm", + "crossterm 0.28.1", "indoc", "instability", "itertools 0.13.0", @@ -4119,11 +3834,8 @@ dependencies = [ [[package]] name = "rgb" version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" [[package]] name = "rig-core" @@ -4218,19 +3930,7 @@ checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" dependencies = [ "futures", "futures-timer", - "rstest_macros 0.23.0", - "rustc_version", -] - -[[package]] -name = "rstest" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" -dependencies = [ - "futures-timer", - "futures-util", - "rstest_macros 0.25.0", + "rstest_macros", "rustc_version", ] @@ -4252,24 +3952,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rstest_macros" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.106", - "unicode-ident", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -4320,7 +4002,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -4338,15 +4020,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -4505,38 +4178,6 @@ dependencies = [ "libc", ] -[[package]] -name = "self-replace" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" -dependencies = [ - "fastrand", - "tempfile", - "windows-sys 0.52.0", -] - -[[package]] -name = "self_update" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d832c086ece0dacc29fb2947bb4219b8f6e12fe9e40b7108f9e57c4224e47b5c" -dependencies = [ - "hyper", - "indicatif 0.17.11", - "log", - "quick-xml 0.37.5", - "regex", - "reqwest", - "self-replace", - "semver", - "serde_json", - "tempfile", - "urlencoding", - "zip", - "zipsign-api", -] - [[package]] name = "semver" version = "1.0.27" @@ -4813,16 +4454,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -4872,16 +4503,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - [[package]] name = "spm_precompiled" version = "0.1.4" @@ -5076,17 +4697,6 @@ dependencies = [ "version-compare", ] -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "target-lexicon" version = "0.12.16" @@ -5103,7 +4713,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.1.2", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -5677,22 +5287,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tui-markdown" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d10648c25931bfaaf5334ff4e7dc5f3d830e0c50d7b0119b1d5cfe771f540536" -dependencies = [ - "ansi-to-tui", - "itertools 0.14.0", - "pretty_assertions", - "pulldown-cmark 0.13.0", - "ratatui", - "rstest 0.25.0", - "syntect", - "tracing", -] - [[package]] name = "tui-popup" version = "0.6.0" @@ -5714,28 +5308,7 @@ dependencies = [ "itertools 0.13.0", "ratatui", "ratatui-macros", - "rstest 0.23.0", -] - -[[package]] -name = "tui-scrollview" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6e1d736488ba64c2e74637089a6b9ca666ccd2eaade3ab83854f415f1d260b" -dependencies = [ - "indoc", - "ratatui", - "rstest 0.23.0", -] - -[[package]] -name = "tui-tree-widget" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c14c4488e071617f5b5922222193cdf6725835e492c6229557af85d3c1a4e903" -dependencies = [ - "ratatui", - "unicode-width 0.2.0", + "rstest", ] [[package]] @@ -5847,6 +5420,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -5871,20 +5450,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "update-informer" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b27dcf766dc6ad64c2085201626e1a7955dc1983532bfc8406d552903ace2a" -dependencies = [ - "etcetera", - "reqwest", - "semver", - "serde", - "serde_json", - "ureq 3.1.2", -] - [[package]] name = "ureq" version = "2.12.1" @@ -5904,39 +5469,6 @@ dependencies = [ "webpki-roots 0.26.11", ] -[[package]] -name = "ureq" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" -dependencies = [ - "base64 0.22.1", - "cookie_store", - "flate2", - "log", - "percent-encoding", - "rustls", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "ureq-proto", - "utf-8", - "webpki-roots 1.0.2", -] - -[[package]] -name = "ureq-proto" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" -dependencies = [ - "base64 0.22.1", - "http", - "httparse", - "log", -] - [[package]] name = "url" version = "2.5.7" @@ -5949,18 +5481,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6020,9 +5540,21 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vt100" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de" +dependencies = [ + "itoa", + "log", + "unicode-width 0.1.14", + "vte", +] + [[package]] name = "vtcode" -version = "0.38.1" +version = "0.38.2" dependencies = [ "agent-client-protocol", "anstyle", @@ -6033,7 +5565,7 @@ dependencies = [ "clap", "colorchoice", "criterion", - "crossterm", + "crossterm 0.29.0", "dialoguer", "dotenvy", "futures", @@ -6045,13 +5577,11 @@ dependencies = [ "libc", "memchr", "once_cell", - "parking_lot", "path-clean", "percent-encoding", "rand 0.8.5", "ratatui", "regex", - "self_update", "serde", "serde_json", "shell-words", @@ -6063,7 +5593,6 @@ dependencies = [ "toml 0.9.8", "tracing", "tracing-subscriber", - "update-informer", "url", "uuid", "vtcode-acp-client", @@ -6076,14 +5605,14 @@ dependencies = [ [[package]] name = "vtcode-acp-client" -version = "0.38.1" +version = "0.38.2" dependencies = [ "agent-client-protocol", ] [[package]] name = "vtcode-bash-runner" -version = "0.38.1" +version = "0.38.2" dependencies = [ "anyhow", "path-clean", @@ -6096,14 +5625,14 @@ dependencies = [ [[package]] name = "vtcode-commons" -version = "0.38.1" +version = "0.38.2" dependencies = [ "anyhow", ] [[package]] name = "vtcode-config" -version = "0.38.1" +version = "0.38.2" dependencies = [ "anyhow", "dirs 6.0.0", @@ -6124,7 +5653,7 @@ dependencies = [ [[package]] name = "vtcode-core" -version = "0.38.1" +version = "0.38.2" dependencies = [ "ansi-to-tui", "anstream", @@ -6136,15 +5665,12 @@ dependencies = [ "async-process", "async-stream", "async-trait", - "avt", "base64 0.21.7", - "bytes", - "bzip2", "catppuccin", "chrono", "clap", "colorchoice-clap", - "crossterm", + "crossterm 0.29.0", "curl", "dashmap", "dialoguer", @@ -6154,11 +5680,9 @@ dependencies = [ "flate2", "futures", "futures-lite", - "futures-util", "glob", "humantime", "iana-time-zone", - "ignore", "indexmap 2.11.4", "is-terminal", "itertools 0.14.0", @@ -6172,7 +5696,7 @@ dependencies = [ "parking_lot", "perg", "portable-pty", - "pulldown-cmark 0.10.3", + "pulldown-cmark", "quick_cache", "rand 0.8.5", "ratatui", @@ -6181,15 +5705,12 @@ dependencies = [ "rig-core", "rmcp", "roff", - "self_update", - "semver", "serde", "serde_json", "serde_yaml", "sha2", "shell-words", "syntect", - "tar", "tempfile", "terminal_size", "thiserror 2.0.16", @@ -6208,15 +5729,12 @@ dependencies = [ "tree-sitter-rust", "tree-sitter-swift", "tree-sitter-typescript", - "tui-markdown", "tui-popup", "tui-prompts", - "tui-scrollview", - "tui-tree-widget", "unicode-segmentation", "unicode-width 0.2.0", - "update-informer", "url", + "vt100", "vtcode-commons", "vtcode-config", "vtcode-exec-events", @@ -6224,13 +5742,11 @@ dependencies = [ "vtcode-markdown-store", "vte", "walkdir", - "xz2", - "zip", ] [[package]] name = "vtcode-exec-events" -version = "0.38.1" +version = "0.38.2" dependencies = [ "log", "schemars 0.8.22", @@ -6241,7 +5757,7 @@ dependencies = [ [[package]] name = "vtcode-indexer" -version = "0.38.1" +version = "0.38.2" dependencies = [ "anyhow", "ignore", @@ -6252,7 +5768,7 @@ dependencies = [ [[package]] name = "vtcode-llm" -version = "0.38.1" +version = "0.38.2" dependencies = [ "anyhow", "async-trait", @@ -6264,7 +5780,7 @@ dependencies = [ [[package]] name = "vtcode-markdown-store" -version = "0.38.1" +version = "0.38.2" dependencies = [ "anyhow", "fs2", @@ -6278,7 +5794,7 @@ dependencies = [ [[package]] name = "vtcode-tools" -version = "0.38.1" +version = "0.38.2" dependencies = [ "anyhow", "async-trait", @@ -6509,15 +6025,13 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "which" -version = "5.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", - "windows-sys 0.48.0", + "env_home", + "rustix 1.1.2", + "winsafe", ] [[package]] @@ -6542,7 +6056,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -7025,6 +6539,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -7037,25 +6557,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.2", -] - -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "yaml-rust" version = "0.4.5" @@ -7065,12 +6566,6 @@ dependencies = [ "linked-hash-map", ] -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - [[package]] name = "yoke" version = "0.8.0" @@ -7141,20 +6636,6 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] [[package]] name = "zerotrie" @@ -7189,86 +6670,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zip" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "getrandom 0.3.3", - "hmac", - "indexmap 2.11.4", - "lzma-rs", - "memchr", - "pbkdf2", - "sha1", - "thiserror 2.0.16", - "time", - "xz2", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zipsign-api" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0" -dependencies = [ - "ed25519-dalek", - "thiserror 2.0.16", -] - -[[package]] -name = "zopfli" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 74aa477f2..16d92733a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers" @@ -43,6 +43,7 @@ vtcode-markdown-store = { path = "vtcode-markdown-store" } vtcode-indexer = { path = "vtcode-indexer" } vtcode-bash-runner = { path = "vtcode-bash-runner" } vtcode-exec-events = { path = "vtcode-exec-events" } +mcp-types = { path = "third-party/mcp-types" } [workspace.lints] rust = {} @@ -85,8 +86,8 @@ unwrap_used = "deny" [dependencies] -vtcode-acp-client = { path = "vtcode-acp-client", version = "0.38.1" } -vtcode-core = { path = "vtcode-core", version = "0.38.1" } +vtcode-acp-client = { path = "vtcode-acp-client", version = "0.38.2" } +vtcode-core = { path = "vtcode-core", version = "0.38.2" } anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } serde_json = "1.0" @@ -105,12 +106,6 @@ toml = "0.9.8" futures = "0.3" walkdir = "2.5" itertools = "0.14.0" -update-informer = { version = "1.3.0", features = ["github"] } -self_update = { version = "0.42.0", features = [ - "archive-zip", - "compression-zip-deflate", - "rustls", -] } memchr = "2.7" tempfile = "3.0" once_cell = "1.19" @@ -130,20 +125,19 @@ dotenvy = "0.15" anstyle = "1.0" sysinfo = "0.37.0" colorchoice = "1.0" -agent-client-protocol = "0.4.5" +agent-client-protocol = "0.7.0" percent-encoding = "2.3" url = "2.5" shell-words = "1.1" -dialoguer = "0.11" -crossterm = "0.28" +dialoguer = "0.12.0" +crossterm = "0.29.0" ratatui = { version = "0.29", default-features = false, features = [ "crossterm", ] } -which = "5.0" +which = "8.0.0" regex = "1.10" smallvec = "1.13" uuid = { version = "1.8.0", features = ["v4", "fast-rng"] } -parking_lot = { version = "0.12.1", features = ["send_guard"] } [features] default = ["tool-chat"] @@ -188,7 +182,7 @@ windows-sys = { version = "0.61", features = [ ] } [package.metadata.cargo-machete] -ignored = ["self_update", "update-informer", "criterion"] +ignored = ["criterion"] [package.metadata.docs.rs] all-features = true diff --git a/docs/guides/zed-acp.md b/docs/guides/zed-acp.md index 2ddab5712..c73be6d80 100644 --- a/docs/guides/zed-acp.md +++ b/docs/guides/zed-acp.md @@ -110,6 +110,98 @@ Edit `settings.json` (Command Palette → `zed: open settings`) and add a custom tool proxies to Zed's `fs.readTextFile` capability and streams results back into the turn, while `list_files` uses VT Code's workspace indexer for directory exploration. +## Package VT Code as a Zed Agent Server Extension + +When you are ready to distribute VT Code to other Zed users, wrap the ACP bridge inside an Agent +Server Extension. Extensions bundle both metadata and platform-specific binaries so users can install +VT Code from Zed's marketplace without touching `settings.json`. This repository ships a ready-to-edit +manifest at `zed-extension/extension.toml`; customize it for each release and publish the directory as +the Zed extension package. + +### Extension manifest layout + +Add the VT Code agent definition under the `[agent_servers]` table in `extension.toml`. The copy in +`zed-extension/extension.toml` uses the latest published macOS artifacts as a baseline: + +```toml +[agent_servers.vtcode] +name = "VT Code" +icon = "icons/vtcode.svg" # Optional, 16x16 monochrome SVG recommended + +[agent_servers.vtcode.env] +VT_ACP_ENABLED = "1" +VT_ACP_ZED_ENABLED = "1" + +[agent_servers.vtcode.targets.darwin-aarch64] +archive = "https://github.com/vtcode-org/vtcode/releases/download/v1.2.0/vtcode-darwin-aarch64.tar.gz" +cmd = "./vtcode" +args = ["acp"] +sha256 = "replace-with-real-sha256" + +[agent_servers.vtcode.targets.darwin-x86_64] +archive = "https://github.com/vtcode-org/vtcode/releases/download/v1.2.0/vtcode-darwin-x86_64.tar.gz" +cmd = "./vtcode" +args = ["acp"] +sha256 = "replace-with-real-sha256" + +[agent_servers.vtcode.targets.linux-x86_64] +archive = "https://github.com/vtcode-org/vtcode/releases/download/v1.2.0/vtcode-linux-x86_64.tar.gz" +cmd = "./vtcode" +args = ["acp"] +sha256 = "replace-with-real-sha256" + +[agent_servers.vtcode.targets.windows-x86_64] +archive = "https://github.com/vtcode-org/vtcode/releases/download/v1.2.0/vtcode-windows-x86_64.zip" +cmd = "./vtcode.exe" +args = ["acp"] +sha256 = "replace-with-real-sha256" +``` + +- `name` controls the label shown in Zed menus. +- Each `{os}-{arch}` target block supplies a download URL, the command to launch, and optional + arguments. The example above reuses the `acp` entry-point so the extension behaves like the manual + setup described earlier in this guide. +- The checked-in manifest currently declares macOS targets (`darwin-aarch64`, `darwin-x86_64`). + Add Linux or Windows target tables when you start publishing those builds. +- Set `sha256` to the checksum of the published archive to harden supply-chain trust. The release + script (`./scripts/release.sh`) regenerates these values automatically after the binaries are + built; you can also run `shasum -a 256 ` on macOS/Linux or + `certutil -hashfile SHA256` on Windows to verify them manually. +- Provide an optional `[agent_servers.vtcode.env]` section when you need to carry configuration such + as ACP toggles or provider credentials. Avoid hard-coding secrets; rely on Zed's environment + overlays or documented setup steps instead. + +### Building and publishing the archives + +1. Produce release builds for every platform you intend to support (see `scripts/` for cross-compiling + helpers). Bundle the artifacts as `.tar.gz` or `.zip` archives that include the `vtcode` binary at + the root, plus any support files (for example `vtcode.toml.example`). +2. Create a GitHub release and upload each archive. Copy the asset URLs into `zed-extension/extension.toml`. +3. Run `./scripts/release.sh` to execute the automated release flow. It rebuilds the binaries, + uploads release assets, and rewrites `zed-extension/extension.toml` with fresh SHA-256 checksums + for every archive that exists in `dist/`. +4. Confirm each target you ship is represented in the manifest; add new target tables as you + introduce additional builds. +5. Commit the extension assets alongside `extension.toml`. Keep the directory structure stable so + future updates can reuse the same icon and metadata. + +### Local testing workflow + +1. Use the Command Palette (`Cmd-Shift-P`) → `zed: install dev extension` to load the local + workspace as an extension. +2. Open the Agent panel, pick the **VT Code** entry, and confirm the download succeeds on your + platform. +3. Exercise ACP capabilities (tool calls, workspace prompts, cancellation) while watching Zed’s ACP + logs to ensure the packaged binary behaves the same as your development build. +4. Repeat on every supported platform (macOS, Linux, Windows) before publishing the extension to the + marketplace, verifying the correct archive is fetched and the shell wrapper behaves as expected. + +### Keep protocol alignment + +- Review the [ACP initialization contract](https://agentclientprotocol.com/protocol/initialization) when updating handshake fields so `agent_capabilities`, `agent_info`, and auth methods stay in sync with the spec. +- Cross-check `NewSession` behaviour with Zed’s expectations outlined in the [session setup flow](https://agentclientprotocol.com/workflows/session/new) before changing session lifecycle code. +- Tool routing (for example `fs.readTextFile`) should continue to follow the [tools guidance](https://agentclientprotocol.com/protocol/tools) so capability negotiation and permission prompts remain interoperable. + ## Runtime behaviour - **Session management** – Each prompt owns a dedicated ACP session with history maintained in VT diff --git a/docs/vtcode_docs_map.md b/docs/vtcode_docs_map.md index e4e1965c8..b068bbda7 100644 --- a/docs/vtcode_docs_map.md +++ b/docs/vtcode_docs_map.md @@ -155,6 +155,17 @@ This document serves as an index of all VT Code documentation. When users ask qu - **Topics**: VS Code extension, integration issues, setup problems - **User Questions**: "How do I use VT Code with my IDE?", "What IDE integrations exist?" +### Editor Integrations + +- **File**: `docs/guides/zed-acp.md` + - **Content**: Zed Agent Client Protocol setup, including Agent Server Extension packaging + - **Topics**: ACP bridge configuration, Zed-specific environment settings, extension manifest layout, release packaging, local testing + - **User Questions**: "How do I run VT Code inside Zed?", "Can I ship VT Code as a Zed extension?", "What ACP settings does VT Code require?" +- **Directory**: `zed-extension/` + - **Content**: Ready-to-publish Zed extension manifest and icon + - **Topics**: Agent Server Extension packaging, release asset wiring, checksum management + - **User Questions**: "Where is the Zed extension manifest?", "How do I update checksums for a new release?" + ### Implementation & Updates - **File**: `docs/IMPLEMENTATION_COMPLETE.md` @@ -343,4 +354,4 @@ When users ask questions about VT Code itself: --- -**Note**: This enhanced documentation map is designed for VT Code's self-documentation system. When users ask questions about VT Code itself, the system should fetch this document and use it to provide accurate, up-to-date information about VT Code's capabilities and features. The expanded trigger questions and response patterns ensure comprehensive coverage of user questions and consistent, helpful responses. \ No newline at end of file +**Note**: This enhanced documentation map is designed for VT Code's self-documentation system. When users ask questions about VT Code itself, the system should fetch this document and use it to provide accurate, up-to-date information about VT Code's capabilities and features. The expanded trigger questions and response patterns ensure comprehensive coverage of user questions and consistent, helpful responses. diff --git a/npm/package.json b/npm/package.json index 3b39c7697..4c7c94981 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "vtcode", - "version": "0.38.1", + "version": "0.38.2", "description": "A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers", "keywords": [ "ai", diff --git a/scripts/release.sh b/scripts/release.sh index 6533b552e..b9a42801d 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -580,6 +580,62 @@ build_and_upload_binaries() { print_success 'Binaries built and distributed successfully' } +update_zed_extension_checksums() { + local version=$1 + local manifest="zed-extension/extension.toml" + local dist_dir="dist" + + if [[ ! -f "$manifest" ]]; then + print_warning "Zed extension manifest not found at $manifest; skipping checksum update" + return 0 + fi + + if [[ ! -d "$dist_dir" ]]; then + print_warning "Distribution directory $dist_dir missing; skipping checksum update" + return 0 + fi + + print_distribution "Updating Zed extension checksums from $dist_dir" + + python3 <<'PYTHON' "$version" "$manifest" "$dist_dir" +import re +import subprocess +import sys +from pathlib import Path + +version, manifest_path, dist_dir = sys.argv[1], Path(sys.argv[2]), Path(sys.argv[3]) + +targets = { + "darwin-aarch64": f"vtcode-v{version}-aarch64-apple-darwin.tar.gz", + "darwin-x86_64": f"vtcode-v{version}-x86_64-apple-darwin.tar.gz", +} + +text = manifest_path.read_text() +updated = False + +for target, filename in targets.items(): + archive = dist_dir / filename + if not archive.exists(): + print(f"WARNING: Archive {archive} not found; leaving sha256 unchanged for {target}", file=sys.stderr) + continue + + sha = subprocess.check_output(["shasum", "-a", "256", str(archive)], text=True).split()[0] + pattern = re.compile(rf"(\[agent_servers\.vtcode\.targets\.{re.escape(target)}\][^\[]*?sha256 = \")([^\"]*)(\")", re.DOTALL) + new_text, count = pattern.subn(rf"\1{sha}\3", text, count=1) + if count == 0: + print(f"WARNING: sha256 entry not found for target {target}", file=sys.stderr) + else: + text = new_text + updated = True + print(f"INFO: Updated {target} checksum to {sha}") + +if updated: + manifest_path.write_text(text) +else: + print("WARNING: No sha256 fields updated in Zed extension manifest", file=sys.stderr) +PYTHON +} + run_release() { local release_argument=$1 local dry_run_flag=$2 @@ -897,10 +953,11 @@ main() { fi build_and_upload_binaries "$released_version" "$skip_homebrew" + update_zed_extension_checksums "$released_version" print_success 'Release process finished' print_info "GitHub Release should now contain changelog notes generated by cargo-release" print_info "All commits, tags, and releases have been pushed to the remote repository" } -main "$@" \ No newline at end of file +main "$@" diff --git a/src/acp/zed.rs b/src/acp/zed.rs index 7486b408e..06dabf3a2 100644 --- a/src/acp/zed.rs +++ b/src/acp/zed.rs @@ -97,6 +97,21 @@ const WORKSPACE_TRUST_ALREADY_SATISFIED_LOG: &str = "ACP workspace trust level a const WORKSPACE_TRUST_DOWNGRADE_SKIPPED_LOG: &str = "ACP workspace trust downgrade skipped because workspace is fully trusted"; +fn text_chunk(text: impl Into) -> acp::ContentChunk { + acp::ContentChunk { + content: acp::ContentBlock::from(text.into()), + meta: None, + } +} + +fn agent_implementation_info() -> acp::Implementation { + acp::Implementation { + name: "vtcode".to_string(), + title: Some("VTCode".to_string()), + version: env!("CARGO_PKG_VERSION").to_string(), + } +} + enum ToolRuntime<'a> { Enabled, Disabled(ToolDisableReason<'a>), @@ -739,9 +754,7 @@ impl ZedAgent { self.send_update( session_id, - acp::SessionUpdate::AgentThoughtChunk { - content: combined.into(), - }, + acp::SessionUpdate::AgentThoughtChunk(text_chunk(combined)), ) .await } @@ -1662,6 +1675,7 @@ impl acp::Agent for ZedAgent { protocol_version: acp::V1, agent_capabilities: capabilities, auth_methods: Vec::new(), + agent_info: Some(agent_implementation_info()), meta: None, }) } @@ -1795,22 +1809,20 @@ impl acp::Agent for ZedAgent { LLMStreamEvent::Token { delta } => { if !delta.is_empty() { assistant_message.push_str(&delta); + let chunk = text_chunk(delta); self.send_update( &args.session_id, - acp::SessionUpdate::AgentMessageChunk { - content: delta.into(), - }, + acp::SessionUpdate::AgentMessageChunk(chunk), ) .await?; } } LLMStreamEvent::Reasoning { delta } => { if !delta.is_empty() { + let chunk = text_chunk(delta); self.send_update( &args.session_id, - acp::SessionUpdate::AgentThoughtChunk { - content: delta.into(), - }, + acp::SessionUpdate::AgentThoughtChunk(chunk), ) .await?; } @@ -1820,11 +1832,10 @@ impl acp::Agent for ZedAgent { && let Some(content) = response.content { if !content.is_empty() { + let chunk = text_chunk(content.clone()); self.send_update( &args.session_id, - acp::SessionUpdate::AgentMessageChunk { - content: content.clone().into(), - }, + acp::SessionUpdate::AgentMessageChunk(chunk), ) .await?; } @@ -1834,11 +1845,10 @@ impl acp::Agent for ZedAgent { if let Some(reasoning) = response.reasoning.filter(|reasoning| !reasoning.is_empty()) { + let chunk = text_chunk(reasoning); self.send_update( &args.session_id, - acp::SessionUpdate::AgentThoughtChunk { - content: reasoning.into(), - }, + acp::SessionUpdate::AgentThoughtChunk(chunk), ) .await?; } @@ -1930,11 +1940,10 @@ impl acp::Agent for ZedAgent { stop_reason = acp::StopReason::Cancelled; break; } + let chunk = text_chunk(content.clone()); self.send_update( &args.session_id, - acp::SessionUpdate::AgentMessageChunk { - content: content.clone().into(), - }, + acp::SessionUpdate::AgentMessageChunk(chunk), ) .await?; } @@ -1948,11 +1957,10 @@ impl acp::Agent for ZedAgent { stop_reason = acp::StopReason::Cancelled; break; } + let chunk = text_chunk(reasoning); self.send_update( &args.session_id, - acp::SessionUpdate::AgentThoughtChunk { - content: reasoning.into(), - }, + acp::SessionUpdate::AgentThoughtChunk(chunk), ) .await?; } diff --git a/src/agent/runloop/slash_commands.rs b/src/agent/runloop/slash_commands.rs index 15f24d274..6ce7abe71 100644 --- a/src/agent/runloop/slash_commands.rs +++ b/src/agent/runloop/slash_commands.rs @@ -59,21 +59,11 @@ pub enum SlashCommandOutcome { ManageSandbox { action: SandboxAction, }, - CheckForUpdates { - action: UpdateAction, - }, SubmitPrompt { prompt: String, }, } -#[derive(Clone, Debug)] -pub enum UpdateAction { - Check, - Install, - Status, -} - #[derive(Clone, Debug)] pub enum McpCommandAction { Overview, @@ -565,23 +555,6 @@ pub async fn handle_slash_command( } Ok(SlashCommandOutcome::Handled) } - "update" => { - let action = if args.is_empty() { - UpdateAction::Check - } else { - match args.trim().to_ascii_lowercase().as_str() { - "check" => UpdateAction::Check, - "install" => UpdateAction::Install, - "status" => UpdateAction::Status, - _ => { - renderer - .line(MessageStyle::Error, "Usage: /update [check|install|status]")?; - return Ok(SlashCommandOutcome::Handled); - } - } - }; - Ok(SlashCommandOutcome::CheckForUpdates { action }) - } "new" => { if !args.is_empty() { renderer.line(MessageStyle::Error, "Usage: /new")?; diff --git a/src/agent/runloop/tool_output/files.rs b/src/agent/runloop/tool_output/files.rs index 1db771785..af938e371 100644 --- a/src/agent/runloop/tool_output/files.rs +++ b/src/agent/runloop/tool_output/files.rs @@ -134,7 +134,7 @@ pub(crate) fn render_write_file_preview( pub(crate) fn render_list_dir_output( renderer: &mut AnsiRenderer, val: &Value, - ls_styles: &LsStyles, + _ls_styles: &LsStyles, ) -> Result<()> { if let Some(path) = val.get("path").and_then(|v| v.as_str()) { renderer.line(MessageStyle::Info, &format!(" {}", path))?; @@ -170,11 +170,7 @@ pub(crate) fn render_list_dir_output( format!(" {}", display_name) }; - if let Some(style) = ls_styles.style_for_line(&display_name) { - renderer.line_with_style(style, &display)?; - } else { - renderer.line(MessageStyle::Response, &display)?; - } + renderer.line(MessageStyle::Response, &display)?; } } } diff --git a/src/agent/runloop/unified/turn.rs b/src/agent/runloop/unified/turn.rs index 901af08de..bfec7a38e 100644 --- a/src/agent/runloop/unified/turn.rs +++ b/src/agent/runloop/unified/turn.rs @@ -1362,11 +1362,6 @@ pub(crate) async fn run_single_agent_loop_unified( renderer.line_if_not_empty(MessageStyle::Output)?; continue; } - SlashCommandOutcome::CheckForUpdates { action } => { - handle_update_command_async(&mut renderer, action).await?; - renderer.line_if_not_empty(MessageStyle::Output)?; - continue; - } SlashCommandOutcome::NewSession => { renderer.line(MessageStyle::Info, "Starting new session...")?; session_end_reason = SessionEndReason::NewSession; @@ -2949,138 +2944,3 @@ mod tests { assert_eq!(result, "Text with partial tags"); } } - -/// Handle the /update slash command -async fn handle_update_command_async( - renderer: &mut AnsiRenderer, - action: crate::agent::runloop::slash_commands::UpdateAction, -) -> Result<()> { - use crate::agent::runloop::slash_commands::UpdateAction; - use vtcode_core::update::{UpdateConfig, UpdateManager}; - - match action { - UpdateAction::Check => { - renderer.line(MessageStyle::Info, "Checking for updates...")?; - - let config = match UpdateConfig::from_env() { - Ok(cfg) => cfg, - Err(e) => { - renderer.line( - MessageStyle::Error, - &format!("Failed to load update configuration: {}", e), - )?; - return Ok(()); - } - }; - - let manager = match UpdateManager::new(config) { - Ok(mgr) => mgr, - Err(e) => { - renderer.line( - MessageStyle::Error, - &format!("Failed to create update manager: {}", e), - )?; - return Ok(()); - } - }; - - match manager.check_for_updates().await { - Ok(status) => { - renderer.line( - MessageStyle::Info, - &format!("Current version: {}", status.current_version), - )?; - - if let Some(latest) = &status.latest_version { - renderer - .line(MessageStyle::Info, &format!("Latest version: {}", latest))?; - } - - if status.update_available { - renderer.line(MessageStyle::Status, "An update is available!")?; - - if let Some(notes) = &status.release_notes { - let lines: Vec<&str> = notes.lines().take(5).collect(); - if !lines.is_empty() { - renderer.line(MessageStyle::Info, "Release highlights:")?; - for line in lines { - if !line.trim().is_empty() { - renderer.line( - MessageStyle::Info, - &format!(" {}", line.trim()), - )?; - } - } - } - } - - renderer.line( - MessageStyle::Info, - "Run '/update install' or 'vtcode update install' to install the update.", - )?; - } else { - renderer - .line(MessageStyle::Status, "You are running the latest version.")?; - } - } - Err(e) => { - renderer.line( - MessageStyle::Error, - &format!("Failed to check for updates: {}", e), - )?; - } - } - } - UpdateAction::Install => { - renderer.line( - MessageStyle::Info, - "Installing updates from within a session is not recommended.", - )?; - renderer.line( - MessageStyle::Info, - "Please exit and run 'vtcode update install' from your terminal.", - )?; - renderer.line( - MessageStyle::Info, - "This ensures a clean update process and proper restart.", - )?; - } - UpdateAction::Status => { - renderer.line(MessageStyle::Info, "Update system status:")?; - - let config = match UpdateConfig::from_env() { - Ok(cfg) => cfg, - Err(e) => { - renderer.line( - MessageStyle::Error, - &format!("Failed to load configuration: {}", e), - )?; - return Ok(()); - } - }; - - renderer.line( - MessageStyle::Info, - &format!(" Enabled: {}", config.enabled), - )?; - renderer.line( - MessageStyle::Info, - &format!(" Channel: {}", config.channel), - )?; - renderer.line( - MessageStyle::Info, - &format!(" Frequency: {:?}", config.frequency), - )?; - renderer.line( - MessageStyle::Info, - &format!(" Auto-download: {}", config.auto_download), - )?; - renderer.line( - MessageStyle::Info, - &format!(" Auto-install: {}", config.auto_install), - )?; - } - } - - Ok(()) -} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 61f5ddcba..6da038af5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -15,7 +15,6 @@ pub mod init; pub mod init_project; pub mod man; pub mod mcp; -pub mod performance; pub mod revert; pub mod sessions; pub mod snapshots; @@ -37,7 +36,6 @@ pub use init::handle_init_command; pub use init_project::handle_init_project_command; pub use man::handle_man_command; pub use mcp::handle_mcp_command; -pub use performance::handle_performance_command; pub use revert::handle_revert_command; pub use sessions::handle_resume_session_command; pub use snapshots::{handle_cleanup_snapshots_command, handle_snapshots_command}; diff --git a/src/cli/performance.rs b/src/cli/performance.rs deleted file mode 100644 index eeef064d0..000000000 --- a/src/cli/performance.rs +++ /dev/null @@ -1,45 +0,0 @@ -use anyhow::Result; -use sysinfo::System; -use vtcode_core::utils::colors::style; - -/// Handle the performance command -pub async fn handle_performance_command() -> Result<()> { - println!("{}", style("Performance Metrics").blue().bold()); - - let mut sys = System::new_all(); - sys.refresh_all(); - - println!("\nSystem:"); - println!(" OS: {}", std::env::consts::OS); - println!(" Arch: {}", std::env::consts::ARCH); - println!(" Family: {}", std::env::consts::FAMILY); - - println!("\nMemory:"); - println!( - " Total: {:.2} GB", - sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0 - ); - println!( - " Used: {:.2} GB", - sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0 - ); - println!( - " Available: {:.2} GB", - sys.available_memory() as f64 / 1024.0 / 1024.0 / 1024.0 - ); - - println!("\nCPU:"); - println!(" Cores: {}", sys.cpus().len()); - println!(" Usage: {:.1}%", sys.global_cpu_usage()); - - println!("\nProcesses: {}", sys.processes().len()); - if let Some(process) = sys.process(sysinfo::Pid::from_u32(std::process::id())) { - println!( - " Self RSS: {:.2} MB", - process.memory() as f64 / 1024.0 / 1024.0 - ); - println!(" Self CPU: {:.1}%", process.cpu_usage()); - } - - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index b757f976e..f85d8bbf9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,14 +54,6 @@ async fn main() -> Result<()> { let skip_confirmations = startup.skip_confirmations; let full_auto_requested = startup.full_auto_requested; - // Check for updates on startup (only for interactive commands) - if vtcode::startup::update_check::should_check_for_updates() && args.command.is_none() { - // Run update check before starting interactive session - if let Err(e) = vtcode::startup::update_check::check_for_updates_on_startup().await { - tracing::debug!("Update check failed: {}", e); - } - } - if let Some(print_value) = print_mode { let prompt = build_print_prompt(print_value)?; cli::handle_ask_single_command(core_cfg, &prompt, cli::AskCommandOptions::default()) @@ -125,9 +117,6 @@ async fn main() -> Result<()> { Some(Commands::Analyze) => { cli::handle_analyze_command(core_cfg).await?; } - Some(Commands::Performance) => { - cli::handle_performance_command().await?; - } Some(Commands::Trajectory { file, top }) => { cli::handle_trajectory_logs_command(core_cfg, file.clone(), *top).await?; } @@ -176,9 +165,6 @@ async fn main() -> Result<()> { Some(Commands::Man { command, output }) => { cli::handle_man_command(command.clone(), output.clone()).await?; } - Some(Commands::Update { command }) => { - vtcode_core::cli::handle_update_command(command.clone()).await?; - } _ => { // Default to chat cli::handle_chat_command(core_cfg, skip_confirmations, full_auto_requested).await?; diff --git a/src/main_modular.rs b/src/main_modular.rs index 8ccb3d2a0..cf9389b7d 100644 --- a/src/main_modular.rs +++ b/src/main_modular.rs @@ -77,8 +77,6 @@ pub enum Commands { #[arg(long)] output: Option, }, - /// Show performance metrics - Performance, } #[tokio::main] @@ -201,13 +199,6 @@ async fn main() -> Result<()> { Commands::Config { output } => { handle_config_command(output.as_deref()).await?; } - Commands::Performance => { - println!( - "{}", - style("Performance metrics mode selected").blue().bold() - ); - handle_performance_command().await?; - } } if args.verbose { diff --git a/src/startup/mod.rs b/src/startup/mod.rs index dace5c1c8..96f110a7e 100644 --- a/src/startup/mod.rs +++ b/src/startup/mod.rs @@ -6,7 +6,6 @@ use toml::Value as TomlValue; use toml::value::Table as TomlTable; mod first_run; -pub mod update_check; use first_run::maybe_run_first_run_setup; use vtcode_core::cli::args::Cli; diff --git a/src/startup/update_check.rs b/src/startup/update_check.rs deleted file mode 100644 index ea770a537..000000000 --- a/src/startup/update_check.rs +++ /dev/null @@ -1,355 +0,0 @@ -//! Startup update check functionality -//! -//! This module handles checking for updates on application startup and -//! prompting users to update when new versions are available. - -use anyhow::Result; -use vtcode_core::update::{UpdateConfig, UpdateManager, UpdateStatus}; -use vtcode_core::utils::colors::style; - -/// Check for updates on startup and prompt user if available -pub async fn check_for_updates_on_startup() -> Result<()> { - // Check if update checks are disabled via environment variable - if std::env::var("VT_UPDATE_CHECK") - .unwrap_or_else(|_| "true".to_string()) - .to_lowercase() - == "false" - { - return Ok(()); - } - - // Load update configuration - let config = match UpdateConfig::from_env() { - Ok(cfg) => cfg, - Err(_) => { - // Silently fail if config can't be loaded - return Ok(()); - } - }; - - // Skip if updates are disabled - if !config.enabled { - return Ok(()); - } - - // Create update manager - let manager = match UpdateManager::new(config) { - Ok(mgr) => mgr, - Err(_) => { - // Silently fail if manager can't be created - return Ok(()); - } - }; - - // Check for updates (non-blocking, with timeout) - let status = match tokio::time::timeout( - std::time::Duration::from_secs(5), - manager.check_for_updates(), - ) - .await - { - Ok(Ok(status)) => status, - _ => { - // Silently fail on timeout or error - return Ok(()); - } - }; - - // Display update notification if available - if status.update_available { - display_update_notification(&status)?; - prompt_for_update(manager, &status).await?; - } - - Ok(()) -} - -/// Display a prominent update notification -fn display_update_notification(status: &UpdateStatus) -> Result<()> { - let current = &status.current_version; - let latest = status.latest_version.as_deref().unwrap_or("unknown"); - - // Print prominent header - println!(); - println!("{}", style("═".repeat(80)).cyan().bold()); - println!("{}", style(" UPDATE AVAILABLE").cyan().bold().on_black()); - println!("{}", style("═".repeat(80)).cyan().bold()); - println!(); - println!( - " {} {}", - style("Current version:").dim(), - style(current).yellow() - ); - println!( - " {} {}", - style("Latest version: ").dim(), - style(latest).green().bold() - ); - println!(); - - // Show release notes if available - if let Some(notes) = &status.release_notes { - let lines: Vec<&str> = notes.lines().take(5).collect(); - if !lines.is_empty() { - println!(" {}", style("Release highlights:").dim()); - for line in lines { - if !line.trim().is_empty() { - println!(" {}", style(line.trim()).dim()); - } - } - println!(); - } - } - - println!( - " {} {}", - style("→").cyan(), - style("Run 'vtcode update install' to update").dim() - ); - println!("{}", style("═".repeat(80)).cyan().bold()); - println!(); - - Ok(()) -} - -/// Prompt user to install update -async fn prompt_for_update(manager: UpdateManager, status: &UpdateStatus) -> Result<()> { - // Check if we're in an interactive terminal - if !is_terminal::is_terminal(&std::io::stdin()) { - return Ok(()); - } - - // Check if auto-install is enabled - if manager.config().auto_install { - println!( - " {} Auto-install is enabled. Installing update...", - style("→").cyan() - ); - return perform_update(manager, status).await; - } - - // Prompt user - use dialoguer::Confirm; - - let prompt = Confirm::new() - .with_prompt("Would you like to install this update now?") - .default(false) - .interact_opt()?; - - match prompt { - Some(true) => { - perform_update(manager, status).await?; - } - Some(false) => { - println!( - " {} Update skipped. Run 'vtcode update install' later to update.", - style("ℹ").blue() - ); - } - None => { - // User cancelled (Ctrl+C) - return Ok(()); - } - } - - Ok(()) -} - -/// Perform the update installation -async fn perform_update(mut manager: UpdateManager, _status: &UpdateStatus) -> Result<()> { - println!(); - println!(" {} Downloading update...", style("→").cyan()); - - // Show progress indicator - let spinner = indicatif::ProgressBar::new_spinner(); - spinner.set_message("Downloading and verifying update..."); - spinner.enable_steady_tick(std::time::Duration::from_millis(100)); - - // Perform update - let result = manager.perform_update().await; - - spinner.finish_and_clear(); - - match result { - Ok(update_result) => { - if update_result.success { - println!(); - println!( - " {} Update installed successfully!", - style("✓").green().bold() - ); - println!( - " {} Updated from {} to {}", - style("→").cyan(), - style(&update_result.old_version).yellow(), - style(&update_result.new_version).green().bold() - ); - - if let Some(backup) = update_result.backup_path { - println!( - " {} Backup created at: {}", - style("ℹ").blue(), - style(backup.display()).dim() - ); - } - - if update_result.requires_restart { - println!(); - println!( - " {} Please restart vtcode to use the new version.", - style("⚠").yellow() - ); - println!( - " {} Run 'vtcode --version' to verify the update.", - style("→").cyan() - ); - } - - println!(); - } else { - println!(" {} Update installation failed.", style("✗").red().bold()); - } - } - Err(e) => { - println!( - " {} Update failed: {}", - style("✗").red().bold(), - style(e).red() - ); - println!( - " {} Your previous version has been restored.", - style("ℹ").blue() - ); - println!( - " {} Try again later with 'vtcode update install'", - style("→").cyan() - ); - } - } - - Ok(()) -} - -/// Check if we should show the update prompt based on frequency -pub fn should_check_for_updates() -> bool { - // Check environment variable - if std::env::var("VT_UPDATE_CHECK") - .unwrap_or_else(|_| "true".to_string()) - .to_lowercase() - == "false" - { - return false; - } - - // Check if we're in a CI/CD environment - if is_ci_environment() { - return false; - } - - // Check if we're in a non-interactive environment - if !is_terminal::is_terminal(&std::io::stdin()) { - return false; - } - - true -} - -/// Detect if we're running in a CI/CD environment -fn is_ci_environment() -> bool { - std::env::var("CI").is_ok() - || std::env::var("CONTINUOUS_INTEGRATION").is_ok() - || std::env::var("GITHUB_ACTIONS").is_ok() - || std::env::var("GITLAB_CI").is_ok() - || std::env::var("CIRCLECI").is_ok() - || std::env::var("TRAVIS").is_ok() - || std::env::var("JENKINS_URL").is_ok() -} - -/// Display a minimal update notification (non-interactive) -pub fn display_minimal_update_notification(status: &UpdateStatus) { - if !status.update_available { - return; - } - - let current = &status.current_version; - let latest = status.latest_version.as_deref().unwrap_or("unknown"); - - eprintln!( - "{} Update available: {} → {} (run 'vtcode update install')", - style("ℹ").blue(), - style(current).yellow(), - style(latest).green() - ); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_should_check_for_updates() { - // Should return a boolean - let result = should_check_for_updates(); - assert!(result || !result); // Always true - } - - #[test] - fn test_is_ci_environment() { - // Save current CI environment variables - let ci_vars = [ - "CI", - "CONTINUOUS_INTEGRATION", - "GITHUB_ACTIONS", - "GITLAB_CI", - "CIRCLECI", - "TRAVIS", - "JENKINS_URL", - ]; - let saved_values: Vec<_> = ci_vars - .iter() - .map(|var| (*var, std::env::var(var).ok())) - .collect(); - - // Test CI detection when no CI vars are set - unsafe { - // SAFETY: Tests run single-threaded here and clean up the mutation immediately. - for var in &ci_vars { - std::env::remove_var(var); - } - } - assert!(!is_ci_environment()); - - // Test CI detection when CI var is set - unsafe { - // SAFETY: The test owns this temporary CI value and restores it right after use. - std::env::set_var("CI", "true"); - } - assert!(is_ci_environment()); - - // Restore original environment - unsafe { - // SAFETY: Restores the environment to its previous state for subsequent tests. - for (var, value) in saved_values { - match value { - Some(v) => std::env::set_var(var, v), - None => std::env::remove_var(var), - } - } - } - } - - #[test] - fn test_display_minimal_notification() { - let status = UpdateStatus { - current_version: "0.33.1".to_string(), - latest_version: Some("0.34.0".to_string()), - update_available: true, - download_url: None, - release_notes: None, - last_checked: None, - }; - - // Should not panic - display_minimal_update_notification(&status); - } -} diff --git a/test_docs_rs_fix.sh b/test_docs_rs_fix.sh new file mode 100755 index 000000000..7fbc32276 --- /dev/null +++ b/test_docs_rs_fix.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Test script to verify the docs.rs fix for mcp-types build script +echo "Testing docs.rs fix for mcp-types build script..." + +# Test 1: Normal build (should generate files if not exists or in normal mode) +echo "Test 1: Running normal build..." +cd third-party/mcp-types +cargo build --release +if [ $? -eq 0 ]; then + echo "✓ Normal build succeeded" +else + echo "✗ Normal build failed" + exit 1 +fi + +# Test 2: Simulate docs.rs environment (should skip generation) +echo "Test 2: Simulating docs.rs build..." +DOCS_RS=1 cargo build --release +if [ $? -eq 0 ]; then + echo "✓ docs.rs simulation succeeded (build script skipped file generation)" +else + echo "✗ docs.rs simulation failed" + exit 1 +fi + +echo "All tests passed! The docs.rs fix is working correctly." \ No newline at end of file diff --git a/third-party/mcp-types/build.rs b/third-party/mcp-types/build.rs index 880b57326..cac1487b3 100644 --- a/third-party/mcp-types/build.rs +++ b/third-party/mcp-types/build.rs @@ -1,4 +1,4 @@ -use std::{env, fs, io, path::Path}; +use std::{env, fs, path::Path}; use typify::{TypeSpace, TypeSpaceSettings}; fn main() { @@ -6,6 +6,15 @@ fn main() { } fn generate(version: &str) { + let is_docsrs = env::var_os("DOCS_RS").is_some(); + + // Don't regenerate schema when building on docs.rs + if is_docsrs { + println!("cargo:warning=docs.rs build detected, skipping schema regeneration"); + println!("cargo:warning=Using existing generated sources"); + return; + } + let schema_path = format!("./spec/{}-schema.json", version); let content = std::fs::read_to_string(schema_path).unwrap(); let schema = serde_json::from_str::(&content).unwrap(); @@ -24,15 +33,5 @@ fn generate(version: &str) { let mut out_file = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).to_path_buf(); out_file.push(file_name); - if let Err(err) = fs::write(&out_file, contents) { - let is_docsrs = env::var_os("DOCS_RS").is_some(); - let is_read_only = err.kind() == io::ErrorKind::PermissionDenied - || err.raw_os_error() == Some(30); - if is_docsrs || is_read_only { - println!("cargo:warning=skipping schema regeneration: {err}"); - println!("cargo:warning=Using existing generated sources at {}", out_file.display()); - } else { - panic!("failed to write generated bindings to {}: {err}", out_file.display()); - } - } + fs::write(&out_file, contents).unwrap(); } diff --git a/vtcode-acp-client/Cargo.toml b/vtcode-acp-client/Cargo.toml index 0cfeae8a4..dab18449d 100644 --- a/vtcode-acp-client/Cargo.toml +++ b/vtcode-acp-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-acp-client" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "Shared utilities for managing Agent Client Protocol client connections" @@ -10,4 +10,4 @@ homepage = "https://github.com/vinhnx/vtcode" documentation = "https://docs.rs/vtcode-acp-client" [dependencies] -agent-client-protocol = "0.4.5" +agent-client-protocol = "0.7.0" diff --git a/vtcode-bash-runner/Cargo.toml b/vtcode-bash-runner/Cargo.toml index e8c530223..83c6d6aa0 100644 --- a/vtcode-bash-runner/Cargo.toml +++ b/vtcode-bash-runner/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-bash-runner" -version = "0.38.1" +version = "0.38.2" edition = "2024" description = "Cross-platform shell execution helpers extracted from VTCode" license = "MIT" @@ -20,9 +20,9 @@ exec-events = ["dep:vtcode-exec-events"] anyhow = "1.0" path-clean = "1.0" shell-escape = "0.1" -vtcode-commons = { path = "../vtcode-commons", version = "0.38.1" } +vtcode-commons = { path = "../vtcode-commons", version = "0.38.2" } serde = { version = "1.0", features = ["derive"], optional = true } -vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.38.1", optional = true } +vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.38.2", optional = true } [lints] workspace = true diff --git a/vtcode-commons/Cargo.toml b/vtcode-commons/Cargo.toml index 2b1cbbe0c..52a93462e 100644 --- a/vtcode-commons/Cargo.toml +++ b/vtcode-commons/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-commons" -version = "0.38.1" +version = "0.38.2" edition = "2021" authors = ["vinhnx "] description = "Shared traits for paths, telemetry, and error reporting reused across VTCode component extractions" diff --git a/vtcode-config/Cargo.toml b/vtcode-config/Cargo.toml index 6680abc26..b588efc96 100644 --- a/vtcode-config/Cargo.toml +++ b/vtcode-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-config" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "Config loader components shared across VTCode and downstream adopters" @@ -24,7 +24,7 @@ serde_json = "1.0" toml = "0.9.8" toml_edit = "0.22" tracing = "0.1" -vtcode-commons = { path = "../vtcode-commons", version = "0.38.1" } +vtcode-commons = { path = "../vtcode-commons", version = "0.38.2" } dotenvy = "0.15" schemars = { version = "0.8", optional = true, features = ["indexmap"] } diff --git a/vtcode-config/src/constants.rs b/vtcode-config/src/constants.rs index 3c0720471..7727fb99d 100644 --- a/vtcode-config/src/constants.rs +++ b/vtcode-config/src/constants.rs @@ -344,9 +344,6 @@ pub mod model_helpers { /// Environment variable names shared across the application. pub mod env { - /// Toggle automatic update checks in the onboarding banner. - pub const UPDATE_CHECK: &str = "VT_UPDATE_CHECK"; - /// Agent Client Protocol specific environment keys pub mod acp { #[derive(Debug, Clone, Copy)] diff --git a/vtcode-config/src/root.rs b/vtcode-config/src/root.rs index 906ca11cc..340eb471c 100644 --- a/vtcode-config/src/root.rs +++ b/vtcode-config/src/root.rs @@ -116,6 +116,10 @@ pub struct PtyConfig { /// Maximum number of scrollback lines retained per PTY session #[serde(default = "default_scrollback_lines")] pub scrollback_lines: usize, + + /// Preferred shell program for PTY sessions (falls back to environment when unset) + #[serde(default)] + pub preferred_shell: Option, } impl Default for PtyConfig { @@ -128,6 +132,7 @@ impl Default for PtyConfig { command_timeout_seconds: default_pty_timeout(), stdout_tail_lines: default_stdout_tail_lines(), scrollback_lines: default_scrollback_lines(), + preferred_shell: None, } } } diff --git a/vtcode-core/Cargo.toml b/vtcode-core/Cargo.toml index f86c9de67..a6b42c22e 100644 --- a/vtcode-core/Cargo.toml +++ b/vtcode-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-core" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "Core library for VTCode - a Rust-based terminal coding agent" @@ -43,14 +43,12 @@ tokio-util = { version = "0.7", features = ["codec"] } async-process = "2.2" futures = "0.3" futures-lite = "2.3" -futures-util = "0.3" async-stream = "0.3" -bytes = "1.0" base64 = "0.21" walkdir = "2.5" glob = "0.3" thiserror = "2.0" -dialoguer = "0.11" +dialoguer = "0.12.0" regex = "1.10" shell-words = "1.1" tree-sitter = "0.25" @@ -97,20 +95,14 @@ roff = "0.2" syntect = "5.2" unicode-segmentation = "1.11" unicode-width = "0.2.0" -crossterm = "0.28" +crossterm = "0.29.0" ratatui = { version = "0.29", default-features = false, features = [ "crossterm", "unstable-rendered-line-info", "unstable-widget-ref", ] } -tui-markdown = { version = "0.3.5", default-features = false, features = [ - "highlight-code", -] } -tui-scrollview = "0.5.1" -tui-tree-widget = "0.23" tui-popup = "0.6" tui-prompts = "0.5" -ignore = "0.4" perg = "0.6.1" nucleo-matcher = "0.3" line-clipping = "0.3" @@ -122,35 +114,18 @@ dissimilar = "1.0" rig = { package = "rig-core", version = "0.21", default-features = false, features = [ "reqwest-rustls", ] } -avt = "0.16.0" +vt100 = "0.15.2" portable-pty = "0.9.0" ansi-to-tui = "7.0.0" -vtcode-commons = { path = "../vtcode-commons", version = "0.38.1" } -vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.38.1" } -vtcode-config = { path = "../vtcode-config", version = "0.38.1" } -vtcode-markdown-store = { path = "../vtcode-markdown-store", version = "0.38.1" } -vtcode-indexer = { path = "../vtcode-indexer", version = "0.38.1" } - -# Self-update dependencies -zip = "2.2" -tar = "0.4" -bzip2 = "0.5" -xz2 = "0.1" -self_update = { version = "0.42.0", features = [ - "archive-zip", - "compression-zip-deflate", - "rustls", -] } +vtcode-commons = { path = "../vtcode-commons", version = "0.38.2" } +vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.38.2" } +vtcode-config = { path = "../vtcode-config", version = "0.38.2" } +vtcode-markdown-store = { path = "../vtcode-markdown-store", version = "0.38.2" } +vtcode-indexer = { path = "../vtcode-indexer", version = "0.38.2" } # Token counting for attention budget management tokenizers = { version = "0.15", features = ["http"] } -# Version parsing -semver = "1.0" - -# Update notifier -update-informer = { version = "1.3.0", features = ["github"] } - # MCP (Model Context Protocol) support rmcp = { version = "0.7.0", features = [ "client", @@ -162,7 +137,7 @@ openai-harmony = "0.0.3" url = "2.5" [build-dependencies] -vtcode-config = { path = "../vtcode-config", version = "0.38.1" } +vtcode-config = { path = "../vtcode-config", version = "0.38.2" } [target.'cfg(unix)'.dependencies] @@ -187,7 +162,7 @@ version = "0.7.1" optional = true [package.metadata.cargo-machete] -ignored = ["dotenvy", "itertools", "toml_edit", "dissimilar"] +ignored = ["dotenvy", "itertools", "toml_edit"] [package.metadata.docs.rs] all-features = true diff --git a/vtcode-core/embedded_assets_source/docs/vtcode_docs_map.md b/vtcode-core/embedded_assets_source/docs/vtcode_docs_map.md index e4e1965c8..b068bbda7 100644 --- a/vtcode-core/embedded_assets_source/docs/vtcode_docs_map.md +++ b/vtcode-core/embedded_assets_source/docs/vtcode_docs_map.md @@ -155,6 +155,17 @@ This document serves as an index of all VT Code documentation. When users ask qu - **Topics**: VS Code extension, integration issues, setup problems - **User Questions**: "How do I use VT Code with my IDE?", "What IDE integrations exist?" +### Editor Integrations + +- **File**: `docs/guides/zed-acp.md` + - **Content**: Zed Agent Client Protocol setup, including Agent Server Extension packaging + - **Topics**: ACP bridge configuration, Zed-specific environment settings, extension manifest layout, release packaging, local testing + - **User Questions**: "How do I run VT Code inside Zed?", "Can I ship VT Code as a Zed extension?", "What ACP settings does VT Code require?" +- **Directory**: `zed-extension/` + - **Content**: Ready-to-publish Zed extension manifest and icon + - **Topics**: Agent Server Extension packaging, release asset wiring, checksum management + - **User Questions**: "Where is the Zed extension manifest?", "How do I update checksums for a new release?" + ### Implementation & Updates - **File**: `docs/IMPLEMENTATION_COMPLETE.md` @@ -343,4 +354,4 @@ When users ask questions about VT Code itself: --- -**Note**: This enhanced documentation map is designed for VT Code's self-documentation system. When users ask questions about VT Code itself, the system should fetch this document and use it to provide accurate, up-to-date information about VT Code's capabilities and features. The expanded trigger questions and response patterns ensure comprehensive coverage of user questions and consistent, helpful responses. \ No newline at end of file +**Note**: This enhanced documentation map is designed for VT Code's self-documentation system. When users ask questions about VT Code itself, the system should fetch this document and use it to provide accurate, up-to-date information about VT Code's capabilities and features. The expanded trigger questions and response patterns ensure comprehensive coverage of user questions and consistent, helpful responses. diff --git a/vtcode-core/src/cli/args.rs b/vtcode-core/src/cli/args.rs index d176adc0e..80c14a0cb 100644 --- a/vtcode-core/src/cli/args.rs +++ b/vtcode-core/src/cli/args.rs @@ -99,16 +99,6 @@ pub struct Cli { #[arg(long, global = true)] pub enable_tree_sitter: bool, - /// **Enable performance monitoring** - /// - /// Tracks: - /// • Token usage and API costs - /// • Response times and latency - /// • Tool execution metrics - /// • Memory usage patterns - #[arg(long, global = true)] - pub performance_monitoring: bool, - /// **Enable research-preview features** /// /// Includes: @@ -326,7 +316,6 @@ pub enum Commands { #[arg(long = "output-format", value_enum, value_name = "FORMAT")] output_format: Option, }, - /// **Headless execution mode** mirroring Codex exec semantics /// /// Features: @@ -374,9 +363,6 @@ pub enum Commands { /// Usage: vtcode analyze Analyze, - /// **Display performance metrics** and system status\n\n**Shows:**\n• Token usage and API costs\n• Response times and latency\n• Tool execution statistics\n• Memory usage patterns\n\n**Usage:** vtcode performance - Performance, - /// Pretty-print trajectory logs and show basic analytics /// /// Sources: @@ -623,12 +609,6 @@ pub enum Commands { #[arg(short, long)] output: Option, }, - - /// **Self-update management** - check for and install updates\n\n**Features:**\n• Automatic version checking from GitHub releases\n• Secure download with checksum verification\n• Automatic backup before updates\n• Rollback support for failed updates\n• Cross-platform support (Linux, macOS, Windows)\n\n**Examples:**\n vtcode update check\n vtcode update install\n vtcode update config --channel beta\n vtcode update rollback - Update { - #[command(subcommand)] - command: crate::cli::update_commands::UpdateCommands, - }, } /// Supported Agent Client Protocol clients @@ -778,7 +758,6 @@ impl Default for Cli { api_key_env: "GEMINI_API_KEY".to_string(), workspace: None, enable_tree_sitter: false, - performance_monitoring: false, research_preview: false, security_level: "moderate".to_string(), show_file_diffs: false, @@ -990,10 +969,6 @@ impl Cli { } /// Check if performance monitoring is enabled - pub fn is_performance_monitoring_enabled(&self) -> bool { - self.performance_monitoring - } - /// Check if research-preview features are enabled pub fn is_research_preview_enabled(&self) -> bool { self.research_preview diff --git a/vtcode-core/src/cli/mod.rs b/vtcode-core/src/cli/mod.rs index 2dede0a5e..ac827a473 100644 --- a/vtcode-core/src/cli/mod.rs +++ b/vtcode-core/src/cli/mod.rs @@ -9,7 +9,6 @@ pub mod mcp_commands; pub mod models_commands; pub mod rate_limiter; pub mod tool_policy_commands; -pub mod update_commands; pub use args::*; pub use commands::*; @@ -18,4 +17,3 @@ pub use mcp_commands::*; pub use models_commands::*; pub use rate_limiter::*; pub use tool_policy_commands::*; -pub use update_commands::*; diff --git a/vtcode-core/src/cli/update_commands.rs b/vtcode-core/src/cli/update_commands.rs deleted file mode 100644 index f9203804d..000000000 --- a/vtcode-core/src/cli/update_commands.rs +++ /dev/null @@ -1,369 +0,0 @@ -//! CLI commands for self-update functionality - -use crate::update::{UpdateChannel, UpdateConfig, UpdateFrequency, UpdateManager}; -use anyhow::{Context, Result}; -use clap::Subcommand; - -#[derive(Debug, Clone, Subcommand)] -pub enum UpdateCommands { - /// Check for available updates - Check { - /// Show detailed information - #[arg(short, long)] - verbose: bool, - }, - - /// Install available updates - Install { - /// Skip confirmation prompt - #[arg(short = 'y', long)] - yes: bool, - - /// Force reinstall even if no update is available - #[arg(short, long)] - force: bool, - }, - - /// Configure update settings - Config { - /// Enable or disable automatic updates - #[arg(long)] - enabled: Option, - - /// Set update channel (stable, beta, nightly) - #[arg(long)] - channel: Option, - - /// Set update frequency (always, daily, weekly, never) - #[arg(long)] - frequency: Option, - - /// Enable or disable automatic downloads - #[arg(long)] - auto_download: Option, - - /// Enable or disable automatic installation - #[arg(long)] - auto_install: Option, - }, - - /// List available backups - Backups, - - /// Rollback to a previous version - Rollback { - /// Backup file to rollback to - backup: Option, - }, - - /// Clean up old backups - Cleanup, -} - -/// Handle update-related commands -pub async fn handle_update_command(command: UpdateCommands) -> Result<()> { - match command { - UpdateCommands::Check { verbose } => handle_check_command(verbose).await, - UpdateCommands::Install { yes, force } => handle_install_command(yes, force).await, - UpdateCommands::Config { - enabled, - channel, - frequency, - auto_download, - auto_install, - } => handle_config_command(enabled, channel, frequency, auto_download, auto_install).await, - UpdateCommands::Backups => handle_backups_command().await, - UpdateCommands::Rollback { backup } => handle_rollback_command(backup).await, - UpdateCommands::Cleanup => handle_cleanup_command().await, - } -} - -/// Check for available updates -async fn handle_check_command(verbose: bool) -> Result<()> { - let config = UpdateConfig::from_env()?; - let manager = UpdateManager::new(config)?; - - println!("Checking for updates..."); - - let status = manager - .check_for_updates() - .await - .context("Failed to check for updates")?; - - println!("\nCurrent version: {}", status.current_version); - - if let Some(latest) = &status.latest_version { - println!("Latest version: {}", latest); - } - - if status.update_available { - println!("\nAn update is available!"); - - if verbose { - if let Some(notes) = &status.release_notes { - println!("\nRelease notes:"); - println!("{}", notes); - } - - if let Some(url) = &status.download_url { - println!("\nDownload URL: {}", url); - } - } - - println!("\nRun 'vtcode update install' to install the update."); - } else { - println!("\nYou are running the latest version."); - } - - Ok(()) -} - -/// Install available updates -async fn handle_install_command(yes: bool, force: bool) -> Result<()> { - let config = UpdateConfig::from_env()?; - let mut manager = UpdateManager::new(config)?; - - // Check for updates first - let status = manager - .check_for_updates() - .await - .context("Failed to check for updates")?; - - if !status.update_available && !force { - println!("No update available. You are running the latest version."); - return Ok(()); - } - - if !yes { - println!( - "This will update vtcode from {} to {}.", - status.current_version, - status.latest_version.as_deref().unwrap_or("unknown") - ); - println!("A backup will be created before updating."); - print!("Continue? [y/N] "); - - use std::io::{self, Write}; - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - if !input.trim().eq_ignore_ascii_case("y") { - println!("Update cancelled."); - return Ok(()); - } - } - - println!("Downloading and installing update..."); - - let result = manager - .perform_update() - .await - .context("Failed to perform update")?; - - if result.success { - println!("\nUpdate installed successfully!"); - println!( - "Updated from {} to {}", - result.old_version, result.new_version - ); - - if let Some(backup) = result.backup_path { - println!("Backup created at: {:?}", backup); - } - - if result.requires_restart { - println!("\nPlease restart vtcode to use the new version."); - } - } else { - anyhow::bail!("Update installation failed"); - } - - Ok(()) -} - -/// Configure update settings -async fn handle_config_command( - enabled: Option, - channel: Option, - frequency: Option, - auto_download: Option, - auto_install: Option, -) -> Result<()> { - let mut config = UpdateConfig::from_env()?; - - let mut changed = false; - - if let Some(val) = enabled { - config.enabled = val; - changed = true; - println!( - "Automatic updates: {}", - if val { "enabled" } else { "disabled" } - ); - } - - if let Some(val) = channel { - config.channel = match val.to_lowercase().as_str() { - "stable" => UpdateChannel::Stable, - "beta" => UpdateChannel::Beta, - "nightly" => UpdateChannel::Nightly, - _ => anyhow::bail!( - "Invalid channel: {}. Use 'stable', 'beta', or 'nightly'.", - val - ), - }; - changed = true; - println!("Update channel: {}", config.channel); - } - - if let Some(val) = frequency { - config.frequency = match val.to_lowercase().as_str() { - "always" => UpdateFrequency::Always, - "daily" => UpdateFrequency::Daily, - "weekly" => UpdateFrequency::Weekly, - "never" => UpdateFrequency::Never, - _ => anyhow::bail!( - "Invalid frequency: {}. Use 'always', 'daily', 'weekly', or 'never'.", - val - ), - }; - changed = true; - println!("Update frequency: {:?}", config.frequency); - } - - if let Some(val) = auto_download { - config.auto_download = val; - changed = true; - println!( - "Automatic downloads: {}", - if val { "enabled" } else { "disabled" } - ); - } - - if let Some(val) = auto_install { - config.auto_install = val; - changed = true; - println!( - "Automatic installation: {}", - if val { "enabled" } else { "disabled" } - ); - } - - if !changed { - println!("Current update configuration:"); - println!(" Enabled: {}", config.enabled); - println!(" Channel: {}", config.channel); - println!(" Frequency: {:?}", config.frequency); - println!(" Auto-download: {}", config.auto_download); - println!(" Auto-install: {}", config.auto_install); - } - - Ok(()) -} - -/// List available backups -async fn handle_backups_command() -> Result<()> { - let config = UpdateConfig::from_env()?; - let manager = UpdateManager::new(config)?; - - let backups = manager - .config() - .backup_dir - .read_dir() - .context("Failed to read backup directory")? - .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .file_name() - .to_string_lossy() - .starts_with("vtcode_backup_") - }) - .collect::>(); - - if backups.is_empty() { - println!("No backups found."); - return Ok(()); - } - - println!("Available backups:"); - for backup in backups { - let path = backup.path(); - let metadata = std::fs::metadata(&path)?; - let size = metadata.len(); - let modified = metadata.modified()?; - - println!( - " {} ({} bytes, modified: {:?})", - path.display(), - size, - modified - ); - } - - Ok(()) -} - -/// Rollback to a previous version -async fn handle_rollback_command(backup: Option) -> Result<()> { - let config = UpdateConfig::from_env()?; - let manager = UpdateManager::new(config)?; - - let backup_path = if let Some(path) = backup { - std::path::PathBuf::from(path) - } else { - // Find the most recent backup - let backups = manager - .config() - .backup_dir - .read_dir() - .context("Failed to read backup directory")? - .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .file_name() - .to_string_lossy() - .starts_with("vtcode_backup_") - }) - .max_by_key(|entry| { - entry - .metadata() - .and_then(|m| m.modified()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH) - }); - - if let Some(backup) = backups { - backup.path() - } else { - anyhow::bail!("No backups found"); - } - }; - - println!("Rolling back to: {:?}", backup_path); - - manager - .rollback_to_backup(&backup_path) - .context("Failed to rollback")?; - - println!("Rollback completed successfully!"); - println!("Please restart vtcode to use the restored version."); - - Ok(()) -} - -/// Clean up old backups -async fn handle_cleanup_command() -> Result<()> { - let config = UpdateConfig::from_env()?; - let manager = UpdateManager::new(config)?; - - println!("Cleaning up old backups..."); - - manager - .cleanup_old_backups() - .context("Failed to cleanup backups")?; - - println!("Cleanup completed successfully!"); - - Ok(()) -} diff --git a/vtcode-core/src/core/agent/mod.rs b/vtcode-core/src/core/agent/mod.rs index 61f6a1cdc..693dacfca 100644 --- a/vtcode-core/src/core/agent/mod.rs +++ b/vtcode-core/src/core/agent/mod.rs @@ -10,7 +10,6 @@ pub mod engine; pub mod events; pub mod examples; pub mod intelligence; -pub mod performance; pub mod runner; pub mod semantic; pub mod snapshots; diff --git a/vtcode-core/src/core/agent/performance.rs b/vtcode-core/src/core/agent/performance.rs deleted file mode 100644 index 5cc2cffd9..000000000 --- a/vtcode-core/src/core/agent/performance.rs +++ /dev/null @@ -1,563 +0,0 @@ -//! Performance optimization and caching systems for the coding agent -//! -//! This module implements Research-preview performance features including: -//! - Intelligent caching with LRU eviction -//! - Parallel processing for large codebases -//! - Memory-efficient data structures -//! - Response time optimization - -use crate::tools::tree_sitter::CodeAnalysis; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, VecDeque}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::RwLock; - -/// Intelligent caching system with LRU eviction -pub struct IntelligentCache { - cache: Arc>>>, - access_order: Arc>>, - max_size: usize, - ttl: Duration, -} - -#[derive(Debug, Clone)] -struct CacheEntry { - data: T, - timestamp: Instant, - access_count: usize, - size_estimate: usize, -} - -impl IntelligentCache { - pub fn new(max_size: usize, ttl: Duration) -> Self { - Self { - cache: Arc::new(RwLock::new(HashMap::new())), - access_order: Arc::new(RwLock::new(VecDeque::new())), - max_size, - ttl, - } - } - - pub async fn get(&self, key: &str) -> Option - where - T: Clone, - { - let mut cache = self.cache.write().await; - let mut access_order = self.access_order.write().await; - - if let Some(entry) = cache.get_mut(key) { - // Check if entry has expired - if entry.timestamp.elapsed() > self.ttl { - cache.remove(key); - access_order.retain(|k| k != key); - return None; - } - - // Update access statistics - entry.access_count += 1; - - // Move to front of access order - access_order.retain(|k| k != key); - access_order.push_front(key.to_string()); - - Some(entry.data.clone()) - } else { - None - } - } - - pub async fn put(&self, key: String, value: T, size_estimate: usize) { - let mut cache = self.cache.write().await; - let mut access_order = self.access_order.write().await; - - // Remove existing entry if present - if cache.contains_key(&key) { - cache.remove(&key); - access_order.retain(|k| k != &key); - } - - // Evict entries if cache is full - while cache.len() >= self.max_size { - if let Some(evict_key) = access_order.pop_back() { - cache.remove(&evict_key); - } - } - - // Add new entry - let entry = CacheEntry { - data: value, - timestamp: Instant::now(), - access_count: 1, - size_estimate, - }; - - cache.insert(key.clone(), entry); - access_order.push_front(key); - } - - pub async fn clear(&self) { - let mut cache = self.cache.write().await; - let mut access_order = self.access_order.write().await; - cache.clear(); - access_order.clear(); - } - - pub async fn stats(&self) -> CacheStats { - let cache = self.cache.read().await; - let _access_order = self.access_order.read().await; - - let total_entries = cache.len(); - let total_accesses: usize = cache.values().map(|e| e.access_count).sum(); - let total_size: usize = cache.values().map(|e| e.size_estimate).sum(); - let avg_access_count = if total_entries > 0 { - total_accesses as f64 / total_entries as f64 - } else { - 0.0 - }; - - CacheStats { - total_entries, - total_accesses, - total_size_bytes: total_size, - avg_access_count, - hit_rate: 0.0, // Would need to track hits/misses separately - } - } -} - -/// Cache statistics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CacheStats { - pub total_entries: usize, - pub total_accesses: usize, - pub total_size_bytes: usize, - pub avg_access_count: f64, - pub hit_rate: f64, -} - -/// Parallel processing engine for large codebases -pub struct ParallelProcessor { - max_concurrent_tasks: usize, -} - -impl ParallelProcessor { - pub fn new(max_concurrent_tasks: usize) -> Self { - Self { - max_concurrent_tasks, - } - } - - /// Process multiple files in parallel - pub async fn process_files( - &self, - files: Vec, - processor: F, - ) -> Result> - where - F: Fn(PathBuf) -> Fut + Send + Sync + Clone, - Fut: std::future::Future> + Send, - T: Send, - { - use futures::stream::{self, StreamExt}; - - let results: Vec> = stream::iter(files) - .map(|file| { - let processor = processor.clone(); - async move { processor(file).await } - }) - .buffer_unordered(self.max_concurrent_tasks) - .collect() - .await; - - // Collect successful results, propagating first error - let mut successful_results = Vec::new(); - for result in results { - successful_results.push(result?); - } - - Ok(successful_results) - } - - /// Process files with priority-based scheduling - pub async fn process_with_priority( - &self, - files_with_priority: Vec<(PathBuf, P)>, - processor: F, - ) -> Result> - where - F: Fn(PathBuf) -> Fut + Send + Sync + Clone, - Fut: std::future::Future> + Send, - T: Send, - P: Ord + Send, - { - // Sort by priority (highest first) - let mut sorted_files: Vec<_> = files_with_priority; - sorted_files.sort_by(|a, b| b.1.cmp(&a.1)); - - let files: Vec = sorted_files.into_iter().map(|(file, _)| file).collect(); - - self.process_files(files, processor).await - } -} - -/// Memory-efficient code analysis storage -pub struct MemoryEfficientStorage { - analyses: Arc>>, - max_memory_mb: usize, - current_memory_usage: Arc>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CompressedAnalysis { - symbols: Vec, // Compressed symbol data - dependencies: Vec, // Compressed dependency data - metrics: Vec, // Compressed metrics data - original_size: usize, - compressed_size: usize, -} - -impl MemoryEfficientStorage { - pub fn new(max_memory_mb: usize) -> Self { - Self { - analyses: Arc::new(RwLock::new(HashMap::new())), - max_memory_mb: max_memory_mb * 1024 * 1024, // Convert to bytes - current_memory_usage: Arc::new(RwLock::new(0)), - } - } - - pub async fn store_analysis(&self, file_path: &Path, analysis: CodeAnalysis) -> Result<()> { - let compressed = self.compress_analysis(analysis).await?; - let key = file_path.to_string_lossy().to_string(); - - let mut analyses = self.analyses.write().await; - let mut memory_usage = self.current_memory_usage.write().await; - - // Remove old entry if exists - if let Some(old_entry) = analyses.remove(&key) { - *memory_usage = memory_usage.saturating_sub(old_entry.compressed_size); - } - - // Evict entries if needed - while *memory_usage + compressed.compressed_size > self.max_memory_mb { - if let Some((_, entry)) = analyses.iter().next() { - let entry_size = entry.compressed_size; - analyses.retain(|_, e| e.compressed_size != entry_size); - *memory_usage = memory_usage.saturating_sub(entry_size); - break; - } - } - - analyses.insert(key, compressed.clone()); - *memory_usage += compressed.compressed_size; - - Ok(()) - } - - pub async fn get_analysis(&self, file_path: &Path) -> Result> { - let analyses = self.analyses.read().await; - let key = file_path.to_string_lossy().to_string(); - - if let Some(compressed) = analyses.get(&key) { - let analysis = self.decompress_analysis(compressed).await?; - Ok(Some(analysis)) - } else { - Ok(None) - } - } - - async fn compress_analysis(&self, analysis: CodeAnalysis) -> Result { - use flate2::{Compression, write::GzEncoder}; - use std::io::Write; - - let serialize_and_compress = |data: &serde_json::Value| -> Result> { - let json = serde_json::to_vec(data)?; - let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(&json)?; - Ok(encoder.finish()?) - }; - - let symbols = serialize_and_compress(&serde_json::to_value(&analysis.symbols)?)?; - let dependencies = serialize_and_compress(&serde_json::to_value(&analysis.dependencies)?)?; - let metrics = serialize_and_compress(&serde_json::to_value(&analysis.metrics)?)?; - - let original_size = analysis.symbols.len() - + analysis.dependencies.len() - + std::mem::size_of_val(&analysis.metrics); - let compressed_size = symbols.len() + dependencies.len() + metrics.len(); - - Ok(CompressedAnalysis { - symbols, - dependencies, - metrics, - original_size, - compressed_size, - }) - } - - async fn decompress_analysis(&self, compressed: &CompressedAnalysis) -> Result { - use flate2::read::GzDecoder; - use std::io::Read; - - let decompress_and_deserialize = |data: &[u8]| -> Result { - let mut decoder = GzDecoder::new(data); - let mut decompressed = Vec::new(); - decoder.read_to_end(&mut decompressed)?; - Ok(serde_json::from_slice(&decompressed)?) - }; - - let symbols: Vec = - decompress_and_deserialize(&compressed.symbols)? - .as_array() - .unwrap_or(&vec![]) - .iter() - .filter_map(|v| serde_json::from_value(v.clone()).ok()) - .collect(); - - let _dependencies: Vec = decompress_and_deserialize(&compressed.dependencies)? - .as_array() - .unwrap_or(&vec![]) - .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(); - - let metrics: crate::tools::tree_sitter::CodeMetrics = - decompress_and_deserialize(&compressed.metrics)? - .as_object() - .and_then(|obj| serde_json::from_value(serde_json::Value::Object(obj.clone())).ok()) - .unwrap_or_default(); - - Ok(CodeAnalysis { - file_path: String::new(), // Would need to be stored separately - language: crate::tools::tree_sitter::analyzer::LanguageSupport::Rust, // Default to Rust - symbols, - dependencies: Vec::new(), // Would need proper deserialization - metrics, - issues: Vec::new(), - complexity: Default::default(), - structure: Default::default(), - }) - } -} - -/// Response time optimizer -pub struct ResponseOptimizer { - response_times: Arc>>>, - optimization_strategies: Arc>>, -} - -#[derive(Debug, Clone)] -pub enum OptimizationStrategy { - CacheFrequentlyAccessed, - PrecomputeResults, - ParallelProcessing, - ReducePayloadSize, - StreamResponse, -} - -impl ResponseOptimizer { - pub fn new() -> Self { - Self { - response_times: Arc::new(RwLock::new(HashMap::new())), - optimization_strategies: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Record response time for a specific operation - pub async fn record_response_time(&self, operation: &str, duration: Duration) { - let mut response_times = self.response_times.write().await; - - let times = response_times - .entry(operation.to_string()) - .or_insert_with(Vec::new); - times.push(duration); - - // Keep only last 100 measurements - if times.len() > 100 { - times.remove(0); - } - - // Analyze and update optimization strategies - self.analyze_and_optimize(operation, times).await; - } - - /// Get optimized response strategy for an operation - pub async fn get_optimization_strategy(&self, operation: &str) -> OptimizationStrategy { - let strategies = self.optimization_strategies.read().await; - - strategies - .get(operation) - .cloned() - .unwrap_or(OptimizationStrategy::CacheFrequentlyAccessed) - } - - async fn analyze_and_optimize(&self, operation: &str, times: &[Duration]) { - if times.len() < 10 { - return; // Need more data - } - - let avg_time: Duration = times.iter().sum::() / times.len() as u32; - let recent_avg: Duration = times.iter().rev().take(5).sum::() / 5; - - let mut strategies = self.optimization_strategies.write().await; - - let strategy = if avg_time > Duration::from_millis(1000) { - // Slow operation - use parallel processing - OptimizationStrategy::ParallelProcessing - } else if recent_avg > avg_time * 2 { - // Performance degrading - precompute results - OptimizationStrategy::PrecomputeResults - } else if times.len() > 50 { - // Frequently called - cache results - OptimizationStrategy::CacheFrequentlyAccessed - } else { - // Default strategy - OptimizationStrategy::CacheFrequentlyAccessed - }; - - strategies.insert(operation.to_string(), strategy); - } - - /// Get performance statistics - pub async fn get_performance_stats(&self) -> HashMap { - let response_times = self.response_times.read().await; - let mut stats = HashMap::new(); - - for (operation, times) in response_times.iter() { - if times.is_empty() { - continue; - } - - let avg_time = times.iter().sum::() / times.len() as u32; - let min_time = times.iter().min().unwrap(); - let max_time = times.iter().max().unwrap(); - - stats.insert( - operation.clone(), - PerformanceStats { - operation: operation.clone(), - avg_response_time: avg_time, - min_response_time: *min_time, - max_response_time: *max_time, - total_calls: times.len(), - p95_response_time: self.calculate_percentile(times, 95), - }, - ); - } - - stats - } - - fn calculate_percentile(&self, times: &[Duration], percentile: u8) -> Duration { - if times.is_empty() { - return Duration::from_millis(0); - } - - let mut sorted_times = times.to_vec(); - sorted_times.sort(); - - let index = (percentile as f64 / 100.0 * (sorted_times.len() - 1) as f64) as usize; - sorted_times[index] - } -} - -/// Performance statistics for operations -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PerformanceStats { - pub operation: String, - pub avg_response_time: Duration, - pub min_response_time: Duration, - pub max_response_time: Duration, - pub total_calls: usize, - pub p95_response_time: Duration, -} - -/// Performance monitoring system -pub struct PerformanceMonitor { - start_time: Instant, - operation_counts: Arc>>, - error_counts: Arc>>, - memory_usage: Arc>>, -} - -impl PerformanceMonitor { - pub fn new() -> Self { - Self { - start_time: Instant::now(), - operation_counts: Arc::new(RwLock::new(HashMap::new())), - error_counts: Arc::new(RwLock::new(HashMap::new())), - memory_usage: Arc::new(RwLock::new(Vec::new())), - } - } - - /// Record operation execution - pub async fn record_operation(&self, operation: &str) { - let mut counts = self.operation_counts.write().await; - *counts.entry(operation.to_string()).or_insert(0) += 1; - } - - /// Record error occurrence - pub async fn record_error(&self, operation: &str) { - let mut counts = self.error_counts.write().await; - *counts.entry(operation.to_string()).or_insert(0) += 1; - } - - /// Record memory usage - pub async fn record_memory_usage(&self, usage_bytes: usize) { - let mut memory_usage = self.memory_usage.write().await; - memory_usage.push((Instant::now(), usage_bytes)); - - // Keep only last 1000 measurements - if memory_usage.len() > 1000 { - memory_usage.remove(0); - } - } - - /// Generate comprehensive performance report - pub async fn generate_report(&self) -> PerformanceReport { - let operation_counts = self.operation_counts.read().await; - let error_counts = self.error_counts.read().await; - let memory_usage = self.memory_usage.read().await; - - let total_operations: usize = operation_counts.values().sum(); - let total_errors: usize = error_counts.values().sum(); - - let avg_memory_usage = if !memory_usage.is_empty() { - memory_usage.iter().map(|(_, usage)| usage).sum::() / memory_usage.len() - } else { - 0 - }; - - let uptime = self.start_time.elapsed(); - - PerformanceReport { - uptime, - total_operations, - total_errors, - error_rate: if total_operations > 0 { - total_errors as f64 / total_operations as f64 - } else { - 0.0 - }, - avg_memory_usage, - operations_per_second: total_operations as f64 / uptime.as_secs_f64(), - operation_breakdown: operation_counts.clone(), - error_breakdown: error_counts.clone(), - } - } -} - -/// Comprehensive performance report -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PerformanceReport { - pub uptime: Duration, - pub total_operations: usize, - pub total_errors: usize, - pub error_rate: f64, - pub avg_memory_usage: usize, - pub operations_per_second: f64, - pub operation_breakdown: HashMap, - pub error_breakdown: HashMap, -} diff --git a/vtcode-core/src/core/mod.rs b/vtcode-core/src/core/mod.rs index 5988ba4b2..6c7106f5f 100644 --- a/vtcode-core/src/core/mod.rs +++ b/vtcode-core/src/core/mod.rs @@ -1,7 +1,7 @@ //! # Core Agent Architecture //! //! This module contains the core components of the VTCode agent system, -//! implementing the main agent loop, context management, and performance monitoring. +//! implementing the main agent loop, context management, and supporting infrastructure. //! //! ## Architecture Overview //! @@ -9,7 +9,6 @@ //! //! - **Agent**: Main agent implementation with conversation management //! - **Context Compression**: Intelligent context management and summarization -//! - **Performance Monitoring**: Real-time metrics and benchmarking //! - **Prompt Caching**: Strategic caching for improved response times //! - **Decision Tracking**: Audit trail of agent decisions and actions //! - **Error Recovery**: Intelligent error handling with context preservation @@ -40,16 +39,6 @@ //! let compressed = compressor.compress(&conversation_history)?; //! ``` //! -//! ### Performance Monitoring -//! ```rust,no_run -//! use vtcode_core::core::performance_profiler::PerformanceProfiler; -//! -//! let profiler = PerformanceProfiler::new(); -//! profiler.start_operation("tool_execution"); -//! // ... execute tool ... -//! let metrics = profiler.end_operation("tool_execution"); -//! ``` - pub mod agent; pub mod context_compression; pub mod context_curator; @@ -58,8 +47,6 @@ pub mod decision_tracker; pub mod error_recovery; pub mod interfaces; pub mod orchestrator_retry; -pub mod performance_monitor; -pub mod performance_profiler; pub mod prompt_caching; pub mod router; pub mod timeout_detector; diff --git a/vtcode-core/src/core/performance_monitor.rs b/vtcode-core/src/core/performance_monitor.rs deleted file mode 100644 index 1471c34c9..000000000 --- a/vtcode-core/src/core/performance_monitor.rs +++ /dev/null @@ -1,236 +0,0 @@ -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::RwLock; - -/// Performance metrics for the vtcode system -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PerformanceMetrics { - pub response_times: Vec, - pub cache_hit_rate: f64, - pub memory_usage: usize, - pub error_rate: f64, - pub throughput: usize, // requests per second - pub context_accuracy: f64, -} - -/// Performance monitor for tracking system metrics -pub struct PerformanceMonitor { - metrics: Arc>, - operation_start_times: Arc>>, - total_requests: Arc>, - successful_requests: Arc>, - context_predictions: Arc>, - context_correct: Arc>, -} - -impl PerformanceMonitor { - pub fn new() -> Self { - Self { - metrics: Arc::new(RwLock::new(PerformanceMetrics { - response_times: Vec::new(), - cache_hit_rate: 0.0, - memory_usage: 0, - error_rate: 0.0, - throughput: 0, - context_accuracy: 0.0, - })), - operation_start_times: Arc::new(RwLock::new(HashMap::new())), - total_requests: Arc::new(RwLock::new(0)), - successful_requests: Arc::new(RwLock::new(0)), - context_predictions: Arc::new(RwLock::new(0)), - context_correct: Arc::new(RwLock::new(0)), - } - } - - /// Start tracking an operation - pub async fn start_operation(&self, operation_id: String) { - let mut start_times = self.operation_start_times.write().await; - start_times.insert(operation_id, Instant::now()); - - let mut total = self.total_requests.write().await; - *total += 1; - } - - /// End tracking an operation and record response time - pub async fn end_operation(&self, operation_id: String, success: bool) { - let start_time = { - let mut start_times = self.operation_start_times.write().await; - start_times.remove(&operation_id) - }; - - if let Some(start) = start_time { - let duration = start.elapsed(); - - let mut metrics = self.metrics.write().await; - metrics.response_times.push(duration); - - // Keep only last 1000 measurements - if metrics.response_times.len() > 1000 { - metrics.response_times.remove(0); - } - - // Update success rate - if success { - let mut successful = self.successful_requests.write().await; - *successful += 1; - } - - // Update error rate - let total = *self.total_requests.read().await; - let successful = *self.successful_requests.read().await; - metrics.error_rate = if total > 0 { - (total - successful) as f64 / total as f64 - } else { - 0.0 - }; - } - } - - /// Record context prediction result - pub async fn record_context_prediction(&self, correct: bool) { - let mut predictions = self.context_predictions.write().await; - *predictions += 1; - - if correct { - let mut correct_predictions = self.context_correct.write().await; - *correct_predictions += 1; - } - - // Update context accuracy - let predictions = *self.context_predictions.read().await; - let correct = *self.context_correct.read().await; - - let mut metrics = self.metrics.write().await; - metrics.context_accuracy = if predictions > 0 { - correct as f64 / predictions as f64 - } else { - 0.0 - }; - } - - /// Update cache hit rate - pub async fn update_cache_hit_rate(&self, hit_rate: f64) { - let mut metrics = self.metrics.write().await; - metrics.cache_hit_rate = hit_rate; - } - - /// Update memory usage - pub async fn update_memory_usage(&self, memory_mb: usize) { - let mut metrics = self.metrics.write().await; - metrics.memory_usage = memory_mb; - } - - /// Get current performance metrics - pub async fn get_metrics(&self) -> PerformanceMetrics { - let metrics = self.metrics.read().await; - metrics.clone() - } - - /// Calculate average response time - pub async fn average_response_time(&self) -> Duration { - let metrics = self.metrics.read().await; - if metrics.response_times.is_empty() { - Duration::from_millis(0) - } else { - let total: Duration = metrics.response_times.iter().sum(); - total / metrics.response_times.len() as u32 - } - } - - /// Calculate 95th percentile response time - pub async fn percentile_95_response_time(&self) -> Duration { - let metrics = self.metrics.read().await; - if metrics.response_times.is_empty() { - Duration::from_millis(0) - } else { - let mut times = metrics.response_times.clone(); - times.sort(); - let index = (times.len() as f64 * 0.95) as usize; - let safe_index = index.min(times.len() - 1); - times[safe_index] - } - } - - /// Check if Phase 1 performance targets are met - pub async fn check_phase1_targets(&self) -> Phase1Status { - let avg_response = self.average_response_time().await; - let p95_response = self.percentile_95_response_time().await; - let metrics = self.get_metrics().await; - - Phase1Status { - response_time_target: avg_response < Duration::from_millis(500), - p95_response_time: p95_response, - memory_target: metrics.memory_usage < 100, - cache_target: metrics.cache_hit_rate >= 0.6, - error_recovery_target: metrics.error_rate <= 0.1, - context_target: metrics.context_accuracy >= 0.8, - } - } - - /// Generate performance report - pub async fn generate_report(&self) -> String { - let avg_response = self.average_response_time().await; - let p95_response = self.percentile_95_response_time().await; - let metrics = self.get_metrics().await; - - format!( - "Performance Report - Phase 1 Targets\n\n\ - Response Times:\n\ - • Average: {:.2}ms (Target: <500ms)\n\ - • 95th percentile: {:.2}ms\n\n\ - 💾 Resource Usage:\n\ - • Memory: {}MB (Target: <100MB)\n\ - • Cache Hit Rate: {:.1}% (Target: ≥60%)\n\n\ - System Health:\n\ - • Error Rate: {:.1}% (Target: ≤10%)\n\ - • Context Accuracy: {:.1}% (Target: ≥80%)\n\n\ - Throughput: {} req/sec", - avg_response.as_millis(), - p95_response.as_millis(), - metrics.memory_usage, - metrics.cache_hit_rate * 100.0, - metrics.error_rate * 100.0, - metrics.context_accuracy * 100.0, - metrics.throughput - ) - } -} - -/// Phase 1 target status -#[derive(Debug, Clone)] -pub struct Phase1Status { - pub response_time_target: bool, - pub p95_response_time: Duration, - pub memory_target: bool, - pub cache_target: bool, - pub error_recovery_target: bool, - pub context_target: bool, -} - -impl Phase1Status { - pub fn all_targets_met(&self) -> bool { - self.response_time_target - && self.memory_target - && self.cache_target - && self.error_recovery_target - && self.context_target - } - - pub fn completion_percentage(&self) -> f64 { - let targets = [ - self.response_time_target, - self.memory_target, - self.cache_target, - self.error_recovery_target, - self.context_target, - ]; - - let met = targets.iter().filter(|&&t| t).count(); - met as f64 / targets.len() as f64 - } -} - -/// Global performance monitor instance -pub static PERFORMANCE_MONITOR: Lazy = Lazy::new(PerformanceMonitor::new); diff --git a/vtcode-core/src/core/performance_profiler.rs b/vtcode-core/src/core/performance_profiler.rs deleted file mode 100644 index b529df410..000000000 --- a/vtcode-core/src/core/performance_profiler.rs +++ /dev/null @@ -1,395 +0,0 @@ -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -/// Performance metrics for different operations -#[derive(Debug, Clone)] -pub struct PerformanceMetrics { - pub operation_count: u64, - pub total_duration: Duration, - pub min_duration: Duration, - pub max_duration: Duration, - pub avg_duration: Duration, - pub p50_duration: Duration, - pub p95_duration: Duration, - pub p99_duration: Duration, - pub error_count: u64, - pub cache_hit_rate: f64, - pub memory_usage_mb: f64, -} - -/// Global performance profiler instance -pub static PROFILER: Lazy> = - Lazy::new(|| Arc::new(PerformanceProfiler::new())); - -/// Performance profiler for tracking system metrics -pub struct PerformanceProfiler { - metrics: Arc>, - active_operations: Arc>, -} - -impl PerformanceProfiler { - pub fn new() -> Self { - Self { - metrics: Arc::new(dashmap::DashMap::new()), - active_operations: Arc::new(dashmap::DashMap::new()), - } - } - - /// Start tracking an operation - pub fn start_operation(&self, operation: &str) -> OperationTimer { - let start_time = Instant::now(); - self.active_operations - .insert(operation.to_string(), start_time); - OperationTimer { - operation: operation.to_string(), - start_time, - profiler: Arc::clone(&self.metrics), - } - } - - /// Record a completed operation - pub fn record_operation(&self, operation: &str, duration: Duration, success: bool) { - let mut entry = self - .metrics - .entry(operation.to_string()) - .or_insert_with(|| PerformanceMetrics { - operation_count: 0, - total_duration: Duration::ZERO, - min_duration: Duration::MAX, - max_duration: Duration::ZERO, - avg_duration: Duration::ZERO, - p50_duration: Duration::ZERO, - p95_duration: Duration::ZERO, - p99_duration: Duration::ZERO, - error_count: 0, - cache_hit_rate: 0.0, - memory_usage_mb: 0.0, - }); - - entry.operation_count += 1; - entry.total_duration += duration; - entry.min_duration = entry.min_duration.min(duration); - entry.max_duration = entry.max_duration.max(duration); - - if !success { - entry.error_count += 1; - } - - entry.avg_duration = entry.total_duration / entry.operation_count as u32; - - // Update memory usage - entry.memory_usage_mb = self.get_current_memory_mb(); - } - - /// Get current memory usage in MB - pub fn get_current_memory_mb(&self) -> f64 { - // Simple heuristic - in a real system, you'd use system APIs - let _base_memory = 50.0; // Base memory usage - let _operation_count = self.metrics.len() as f64; - let _active_count = self.active_operations.len() as f64; - // Enhanced memory calculation with more accurate tracking - let base_memory = 35.0; // Reduced base memory usage - let operation_count = self.metrics.len() as f64; - let active_count = self.active_operations.len() as f64; - - // More accurate memory calculation - let metrics_memory = operation_count * 0.3; // Memory per stored metric - let active_memory = active_count * 0.1; // Memory per active operation - let cache_memory = self.estimate_cache_memory(); // Cache memory usage - - let total = base_memory + metrics_memory + active_memory + cache_memory; - - // Cap at reasonable maximum to prevent unrealistic estimates - total.min(150.0) - } - - /// Estimate cache memory usage - fn estimate_cache_memory(&self) -> f64 { - // Estimate based on typical cache sizes - let cache_entries = self.metrics.len() as f64; - let avg_entry_size_kb = 5.0; // Estimated KB per cache entry - (cache_entries * avg_entry_size_kb) / 1024.0 // Convert to MB - } - - /// Get memory optimization recommendations - pub fn get_memory_recommendations(&self) -> Vec { - let mut recommendations = Vec::new(); - let current_memory = self.get_current_memory_mb(); - - if current_memory > 100.0 { - recommendations.push("Memory usage exceeds 100MB target".to_string()); - recommendations.push(" Consider using ClientConfig::low_memory()".to_string()); - } - - if current_memory > 50.0 { - recommendations.push("Memory usage > 50MB, monitor closely".to_string()); - } - - let active_ops = self.active_operations.len(); - if active_ops > 10 { - recommendations.push(format!( - "{} active operations may impact memory", - active_ops - )); - } - - let total_ops = self - .metrics - .iter() - .map(|entry| entry.value().operation_count) - .sum::(); - if total_ops > 1000 { - recommendations.push("🗂️ Consider clearing old metrics to reduce memory".to_string()); - } - - if recommendations.is_empty() { - recommendations.push("Memory usage is within acceptable limits".to_string()); - } - - recommendations - } - - /// Get metrics for an operation - pub fn get_metrics(&self, operation: &str) -> Option { - self.metrics.get(operation).map(|m| m.clone()) - } - - /// Get all metrics - pub fn get_all_metrics(&self) -> HashMap { - self.metrics - .iter() - .map(|entry| (entry.key().clone(), entry.value().clone())) - .collect() - } - - /// Check if operation meets performance targets - pub fn check_performance_targets(&self, operation: &str) -> PerformanceStatus { - let metrics = match self.get_metrics(operation) { - Some(m) => m, - None => return PerformanceStatus::Unknown, - }; - - // Target: < 500ms for common operations - let target_duration = Duration::from_millis(500); - let error_rate = metrics.error_count as f64 / metrics.operation_count as f64; - - if metrics.avg_duration > target_duration { - PerformanceStatus::Slow(metrics.avg_duration) - } else if error_rate > 0.1 { - // 10% error rate threshold - PerformanceStatus::HighErrorRate(error_rate) - } else if metrics.memory_usage_mb > 100.0 { - PerformanceStatus::HighMemoryUsage(metrics.memory_usage_mb) - } else { - PerformanceStatus::Good - } - } - - /// Generate performance report - pub fn generate_report(&self) -> String { - let mut report = String::new(); - report.push_str("Performance Report\n"); - report.push_str("====================\n\n"); - - for entry in self.metrics.iter() { - let operation = entry.key(); - let metrics = entry.value(); - let status = self.check_performance_targets(operation); - let status_icon = match status { - PerformanceStatus::Good => "", - PerformanceStatus::Slow(_) => "🐌", - PerformanceStatus::HighErrorRate(_) => "", - PerformanceStatus::HighMemoryUsage(_) => "[MEM]", - PerformanceStatus::Unknown => "❓", - }; - - report.push_str(&format!( - "{} {}: {:.1}ms avg ({} ops, {:.1}% errors)\n", - status_icon, - operation, - metrics.avg_duration.as_millis(), - metrics.operation_count, - (metrics.error_count as f64 / metrics.operation_count as f64) * 100.0 - )); - - if matches!( - status, - PerformanceStatus::Slow(_) - | PerformanceStatus::HighErrorRate(_) - | PerformanceStatus::HighMemoryUsage(_) - ) { - report.push_str(&format!(" Needs optimization\n")); - } - } - - report.push_str(&format!( - "\nMemory Usage: {:.1}MB\n", - self.get_current_memory_mb() - )); - report - } -} - -/// Performance status of an operation -#[derive(Debug, Clone)] -pub enum PerformanceStatus { - Good, - Slow(Duration), - HighErrorRate(f64), - HighMemoryUsage(f64), - Unknown, -} - -/// Timer for measuring operation duration -pub struct OperationTimer { - operation: String, - start_time: Instant, - profiler: Arc>, -} - -impl OperationTimer { - /// Complete the operation with success - pub fn complete(self, success: bool) { - let duration = self.start_time.elapsed(); - - if let Some(mut metrics) = self.profiler.get_mut(&self.operation) { - metrics.operation_count += 1; - metrics.total_duration += duration; - metrics.min_duration = metrics.min_duration.min(duration); - metrics.max_duration = metrics.max_duration.max(duration); - - if !success { - metrics.error_count += 1; - } - - metrics.avg_duration = metrics.total_duration / metrics.operation_count as u32; - } - } -} - -impl Drop for OperationTimer { - fn drop(&mut self) { - // Auto-complete with success if not manually completed - let duration = self.start_time.elapsed(); - - if let Some(mut metrics) = self.profiler.get_mut(&self.operation) { - metrics.operation_count += 1; - metrics.total_duration += duration; - metrics.min_duration = metrics.min_duration.min(duration); - metrics.max_duration = metrics.max_duration.max(duration); - metrics.avg_duration = metrics.total_duration / metrics.operation_count as u32; - } - } -} - -/// Performance targets checker -pub struct PerformanceTargets { - pub response_time_target_ms: u64, - pub context_accuracy_target: f64, - pub completion_acceptance_target: f64, - pub memory_target_mb: f64, - pub cache_hit_target: f64, - pub error_recovery_target: f64, -} - -impl Default for PerformanceTargets { - fn default() -> Self { - Self { - response_time_target_ms: 500, - context_accuracy_target: 0.8, // 80% - completion_acceptance_target: 0.7, // 70% - memory_target_mb: 100.0, - cache_hit_target: 0.6, // 60% - error_recovery_target: 0.9, // 90% - } - } -} - -impl PerformanceTargets { - /// Check if current metrics meet targets - pub fn check_targets(&self, profiler: &PerformanceProfiler) -> TargetsStatus { - let mut status = TargetsStatus::default(); - let all_metrics = profiler.get_all_metrics(); - - // Check response times - for (operation, metrics) in &all_metrics { - if operation.contains("api") || operation.contains("tool") { - if metrics.avg_duration > Duration::from_millis(self.response_time_target_ms) { - status.response_time_met = false; - } - } - } - - // Check memory usage - if profiler.get_current_memory_mb() > self.memory_target_mb { - status.memory_target_met = false; - } - - // Check error rates - for metrics in all_metrics.values() { - let error_rate = metrics.error_count as f64 / metrics.operation_count as f64; - if error_rate > (1.0 - self.error_recovery_target) { - status.error_recovery_met = false; - break; - } - } - - status - } -} - -/// Status of performance targets -#[derive(Debug, Default)] -pub struct TargetsStatus { - pub response_time_met: bool, - pub context_accuracy_met: bool, - pub completion_acceptance_met: bool, - pub memory_target_met: bool, - pub cache_hit_met: bool, - pub error_recovery_met: bool, -} - -impl TargetsStatus { - pub fn all_met(&self) -> bool { - self.response_time_met - && self.context_accuracy_met - && self.completion_acceptance_met - && self.memory_target_met - && self.cache_hit_met - && self.error_recovery_met - } - - pub fn generate_report(&self) -> String { - let mut report = String::new(); - report.push_str(" Performance Targets Status\n"); - report.push_str("==============================\n"); - - let targets = [ - ("Response Time < 500ms", self.response_time_met), - ("Context Accuracy > 80%", self.context_accuracy_met), - ( - "Completion Acceptance > 70%", - self.completion_acceptance_met, - ), - ("Memory Usage < 100MB", self.memory_target_met), - ("Cache Hit Rate > 60%", self.cache_hit_met), - ("Error Recovery > 90%", self.error_recovery_met), - ]; - - for (target, met) in &targets { - let icon = if *met { "" } else { "" }; - report.push_str(&format!("{} {}\n", icon, target)); - } - - let overall_status = if self.all_met() { - "All targets met!" - } else { - " Some targets need improvement" - }; - report.push_str(&format!("\n{}", overall_status)); - - report - } -} diff --git a/vtcode-core/src/lib.rs b/vtcode-core/src/lib.rs index a2330e4fb..1295b6ed1 100644 --- a/vtcode-core/src/lib.rs +++ b/vtcode-core/src/lib.rs @@ -147,7 +147,6 @@ pub mod tool_policy; pub mod tools; pub mod types; pub mod ui; -pub mod update; pub mod utils; // New MCP enhancement modules @@ -174,7 +173,6 @@ pub use core::context_compression::{ CompressedContext, ContextCompressionConfig, ContextCompressor, }; pub use core::conversation_summarizer::ConversationSummarizer; -pub use core::performance_profiler::PerformanceProfiler; pub use core::prompt_caching::{CacheStats, PromptCache, PromptCacheConfig, PromptOptimizer}; pub use core::timeout_detector::TimeoutDetector; pub use exec::events::{ @@ -199,9 +197,6 @@ pub use tools::{ build_function_declarations_for_level, build_function_declarations_with_mode, }; pub use ui::diff_renderer::DiffRenderer; -pub use update::{ - UpdateChannel, UpdateConfig, UpdateFrequency, UpdateManager, UpdateResult, UpdateStatus, -}; pub use utils::dot_config::{ CacheConfig, DotConfig, DotManager, ProviderConfigs, UiConfig, UserPreferences, WorkspaceTrustRecord, WorkspaceTrustStore, initialize_dot_folder, load_user_config, diff --git a/vtcode-core/src/tools/pty.rs b/vtcode-core/src/tools/pty.rs index 699878a06..eb36fb7ef 100644 --- a/vtcode-core/src/tools/pty.rs +++ b/vtcode-core/src/tools/pty.rs @@ -1,16 +1,17 @@ use std::collections::{HashMap, VecDeque}; use std::io::{Read, Write}; use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; use std::sync::mpsc; -use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; use anyhow::{Context, Result, anyhow}; -use avt::Vt; +use parking_lot::Mutex; use portable_pty::{Child, CommandBuilder, MasterPty, PtySize, native_pty_system}; use shell_words::join; use tracing::{debug, warn}; +use vt100::Parser; use crate::config::PtyConfig; use crate::sandbox::SandboxProfile; @@ -48,8 +49,7 @@ impl PtyScrollback { } } - fn push(&mut self, chunk: &[u8]) { - let text = String::from_utf8_lossy(chunk); + fn push_text(&mut self, text: &str) { for part in text.split_inclusive('\n') { self.partial.push_str(part); self.pending_partial.push_str(part); @@ -68,6 +68,45 @@ impl PtyScrollback { } } + fn push_utf8(&mut self, buffer: &mut Vec, eof: bool) { + loop { + match std::str::from_utf8(buffer) { + Ok(valid) => { + if !valid.is_empty() { + self.push_text(valid); + } + buffer.clear(); + break; + } + Err(error) => { + let valid_up_to = error.valid_up_to(); + if valid_up_to > 0 { + if let Ok(valid) = std::str::from_utf8(&buffer[..valid_up_to]) { + if !valid.is_empty() { + self.push_text(valid); + } + } + buffer.drain(..valid_up_to); + continue; + } + + if let Some(error_len) = error.error_len() { + self.push_text("\u{FFFD}"); + buffer.drain(..error_len); + continue; + } + + if eof && !buffer.is_empty() { + self.push_text("\u{FFFD}"); + buffer.clear(); + } + + break; + } + } + } + } + fn snapshot(&self) -> String { let mut output = String::new(); for line in &self.lines { @@ -103,7 +142,7 @@ struct PtySessionHandle { master: Mutex>, child: Mutex>, writer: Mutex>>, - terminal: Arc>, + terminal: Arc>, scrollback: Arc>, reader_thread: Mutex>>, metadata: VTCodePtySession, @@ -112,17 +151,20 @@ struct PtySessionHandle { impl PtySessionHandle { fn snapshot_metadata(&self) -> VTCodePtySession { let mut metadata = self.metadata.clone(); - if let Ok(master) = self.master.lock() { + { + let master = self.master.lock(); if let Ok(size) = master.get_size() { metadata.rows = size.rows; metadata.cols = size.cols; } } - if let Ok(terminal) = self.terminal.lock() { - let contents = terminal.text().join("\n"); + { + let parser = self.terminal.lock(); + let contents = parser.screen().contents(); metadata.screen_contents = Some(contents); } - if let Ok(scrollback) = self.scrollback.lock() { + { + let scrollback = self.scrollback.lock(); let contents = scrollback.snapshot(); if !contents.is_empty() { metadata.scrollback = Some(contents); @@ -132,7 +174,7 @@ impl PtySessionHandle { } fn read_output(&self, drain: bool) -> Option { - let mut scrollback = self.scrollback.lock().ok()?; + let mut scrollback = self.scrollback.lock(); let text = if drain { scrollback.take_pending() } else { @@ -175,9 +217,8 @@ impl PtyManager { } pub fn set_sandbox_profile(&self, profile: Option) { - if let Ok(mut slot) = self.sandbox_profile.lock() { - *slot = profile; - } + let mut slot = self.sandbox_profile.lock(); + *slot = profile; } pub fn sandbox_profile(&self) -> Option { @@ -185,10 +226,7 @@ impl PtyManager { } fn current_sandbox_profile(&self) -> Option { - self.sandbox_profile - .lock() - .ok() - .and_then(|value| value.clone()) + self.sandbox_profile.lock().clone() } pub fn describe_working_dir(&self, path: &Path) -> String { @@ -403,11 +441,7 @@ impl PtyManager { return Err(anyhow!("PTY session command cannot be empty")); } - let mut sessions = self - .inner - .sessions - .lock() - .expect("PTY session mutex poisoned"); + let mut sessions = self.inner.sessions.lock(); if sessions.contains_key(&session_id) { return Err(anyhow!("PTY session '{}' already exists", session_id)); } @@ -465,12 +499,13 @@ impl PtyManager { .context("failed to clone PTY reader")?; let writer = master.take_writer().context("failed to take PTY writer")?; - let vt = Arc::new(Mutex::new(Vt::new( - usize::from(size.cols), - usize::from(size.rows), + let parser = Arc::new(Mutex::new(Parser::new( + size.rows, + size.cols, + self.config.scrollback_lines, ))); let scrollback = Arc::new(Mutex::new(PtyScrollback::new(self.config.scrollback_lines))); - let vt_clone = Arc::clone(&vt); + let parser_clone = Arc::clone(&parser); let scrollback_clone = Arc::clone(&scrollback); let session_name = session_id.clone(); let reader_thread = thread::Builder::new() @@ -481,47 +516,24 @@ impl PtyManager { loop { match reader.read(&mut buffer) { Ok(0) => { + if !utf8_buffer.is_empty() { + let mut scrollback = scrollback_clone.lock(); + scrollback.push_utf8(&mut utf8_buffer, true); + } debug!("PTY session '{}' reader reached EOF", session_name); break; } Ok(bytes_read) => { let chunk = &buffer[..bytes_read]; - utf8_buffer.extend_from_slice(chunk); - if let Ok(mut terminal) = vt_clone.lock() { - loop { - match std::str::from_utf8(&utf8_buffer) { - Ok(valid) => { - if !valid.is_empty() { - let _ = terminal.feed_str(valid); - } - utf8_buffer.clear(); - break; - } - Err(error) => { - let valid_up_to = error.valid_up_to(); - if valid_up_to > 0 { - if let Ok(valid) = - std::str::from_utf8(&utf8_buffer[..valid_up_to]) - { - let _ = terminal.feed_str(valid); - } - utf8_buffer.drain(..valid_up_to); - continue; - } - - if let Some(error_len) = error.error_len() { - let _ = terminal.feed_str("\u{FFFD}"); - utf8_buffer.drain(..error_len); - continue; - } - - break; - } - } - } + { + let mut parser = parser_clone.lock(); + parser.process(chunk); } - if let Ok(mut scrollback) = scrollback_clone.lock() { - scrollback.push(chunk); + + utf8_buffer.extend_from_slice(chunk); + { + let mut scrollback = scrollback_clone.lock(); + scrollback.push_utf8(&mut utf8_buffer, false); } } Err(error) => { @@ -550,7 +562,7 @@ impl PtyManager { master: Mutex::new(master), child: Mutex::new(child), writer: Mutex::new(Some(writer)), - terminal: vt, + terminal: parser, scrollback, reader_thread: Mutex::new(Some(reader_thread)), metadata: metadata.clone(), @@ -561,11 +573,7 @@ impl PtyManager { } pub fn list_sessions(&self) -> Vec { - let sessions = self - .inner - .sessions - .lock() - .expect("PTY session mutex poisoned"); + let sessions = self.inner.sessions.lock(); sessions .values() .map(|handle| handle.snapshot_metadata()) @@ -589,7 +597,7 @@ impl PtyManager { append_newline: bool, ) -> Result { let handle = self.session_handle(session_id)?; - let mut writer_guard = handle.writer.lock().expect("PTY writer mutex poisoned"); + let mut writer_guard = handle.writer.lock(); let writer = writer_guard .as_mut() .ok_or_else(|| anyhow!("PTY session '{}' is no longer writable", session_id))?; @@ -614,17 +622,19 @@ impl PtyManager { pub fn resize_session(&self, session_id: &str, size: PtySize) -> Result { let handle = self.session_handle(session_id)?; { - let master = handle.master.lock().expect("PTY master mutex poisoned"); + let master = handle.master.lock(); master .resize(size) .context("failed to resize PTY session")?; } + let mut parser = handle.terminal.lock(); + parser.set_size(size.rows, size.cols); Ok(handle.snapshot_metadata()) } pub fn is_session_completed(&self, session_id: &str) -> Result> { let handle = self.session_handle(session_id)?; - let mut child = handle.child.lock().expect("PTY child mutex poisoned"); + let mut child = handle.child.lock(); Ok( if let Some(status) = child .try_wait() @@ -639,24 +649,21 @@ impl PtyManager { pub fn close_session(&self, session_id: &str) -> Result { let handle = { - let mut sessions = self - .inner - .sessions - .lock() - .expect("PTY session mutex poisoned"); + let mut sessions = self.inner.sessions.lock(); sessions .remove(session_id) .ok_or_else(|| anyhow!("PTY session '{}' not found", session_id))? }; - if let Ok(mut writer_guard) = handle.writer.lock() { + { + let mut writer_guard = handle.writer.lock(); if let Some(mut writer) = writer_guard.take() { let _ = writer.write_all(b"exit\n"); let _ = writer.flush(); } } - let mut child = handle.child.lock().expect("PTY child mutex poisoned"); + let mut child = handle.child.lock(); if child .try_wait() .context("failed to poll PTY session status")? @@ -666,7 +673,8 @@ impl PtyManager { let _ = child.wait(); } - if let Ok(mut thread_guard) = handle.reader_thread.lock() { + { + let mut thread_guard = handle.reader_thread.lock(); if let Some(reader_thread) = thread_guard.take() { if let Err(panic) = reader_thread.join() { warn!( @@ -689,11 +697,7 @@ impl PtyManager { } fn session_handle(&self, session_id: &str) -> Result> { - let sessions = self - .inner - .sessions - .lock() - .expect("PTY session mutex poisoned"); + let sessions = self.inner.sessions.lock(); sessions .get(session_id) .cloned() @@ -756,6 +760,12 @@ fn set_command_environment( builder.env("LINES", size.rows.to_string()); builder.env("WORKSPACE_DIR", workspace_root.as_os_str()); + // Disable automatic color output from ls and other commands + builder.env("CLICOLOR", "0"); + builder.env("CLICOLOR_FORCE", "0"); + builder.env("LS_COLORS", ""); + builder.env("NO_COLOR", "1"); + if let Some(profile) = sandbox_profile { builder.env("VT_SANDBOX_RUNTIME", profile.runtime_kind().as_str()); builder.env("VT_SANDBOX_SETTINGS", profile.settings().as_os_str()); diff --git a/vtcode-core/src/tools/registry/executors.rs b/vtcode-core/src/tools/registry/executors.rs index 0f88e7e76..d7254c117 100644 --- a/vtcode-core/src/tools/registry/executors.rs +++ b/vtcode-core/src/tools/registry/executors.rs @@ -5,15 +5,17 @@ use futures::future::BoxFuture; use portable_pty::PtySize; use serde::Deserialize; use serde_json::{Map, Value, json}; -use shell_words::split; +use shell_words::{join, split}; use std::{ borrow::Cow, + env, path::{Path, PathBuf}, time::{Duration, Instant}, }; use tokio::time::sleep; use vte::{Parser, Perform}; +use crate::config::PtyConfig; use crate::tools::apply_patch::Patch; use crate::tools::grep_file::GrepSearchInput; use crate::tools::traits::Tool; @@ -429,12 +431,57 @@ impl ToolRegistry { &self, payload: &Map, ) -> Result { - let command = parse_command_parts( + let mut command = parse_command_parts( payload, "run_pty_cmd requires a 'command' value", "PTY command cannot be empty", )?; + let raw_command = payload + .get("raw_command") + .and_then(|value| value.as_str()) + .map(|value| value.to_string()); + let shell = resolve_shell_preference( + payload.get("shell").and_then(|value| value.as_str()), + self.pty_config(), + ); + let login_shell = payload + .get("login") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + + if let Some(shell_program) = shell { + let normalized_shell = normalized_shell_name(&shell_program); + let existing_shell = command + .first() + .map(|existing| normalized_shell_name(existing)); + if existing_shell != Some(normalized_shell.clone()) { + let command_string = + build_shell_command_string(raw_command.as_deref(), &command, &shell_program); + + let mut shell_invocation = Vec::with_capacity(4); + shell_invocation.push(shell_program.clone()); + + if login_shell && !should_use_windows_command_tokenizer(Some(&shell_program)) { + shell_invocation.push("-l".to_string()); + } + + let command_flag = if should_use_windows_command_tokenizer(Some(&shell_program)) { + match normalized_shell.as_str() { + "cmd" | "cmd.exe" => "/C".to_string(), + "powershell" | "powershell.exe" | "pwsh" => "-Command".to_string(), + _ => "-c".to_string(), + } + } else { + "-c".to_string() + }; + + shell_invocation.push(command_flag); + shell_invocation.push(command_string); + command = shell_invocation; + } + } + let timeout_secs = parse_timeout_secs( payload.get("timeout_secs"), self.pty_config().command_timeout_seconds, @@ -541,12 +588,36 @@ impl ToolRegistry { let session_id = parse_session_id(payload, "create_pty_session requires a 'session_id' string")?; - let command_parts = parse_command_parts( + let mut command_parts = parse_command_parts( payload, "create_pty_session requires a 'command' value", "PTY session command cannot be empty", )?; + let login_shell = payload + .get("login") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + + if let Some(shell_program) = resolve_shell_preference( + payload.get("shell").and_then(|value| value.as_str()), + self.pty_config(), + ) { + let should_replace = payload.get("shell").is_some() + || (command_parts.len() == 1 && is_default_shell_placeholder(&command_parts[0])); + if should_replace { + command_parts = vec![shell_program]; + } + } + + if login_shell + && !command_parts.is_empty() + && !should_use_windows_command_tokenizer(Some(&command_parts[0])) + && !command_parts.iter().skip(1).any(|arg| arg == "-l") + { + command_parts.push("-l".to_string()); + } + // Check if this is a development toolchain command in sandbox mode if !command_parts.is_empty() { let program = &command_parts[0]; @@ -991,6 +1062,15 @@ fn build_pty_args_from_terminal(args: &Value, command_vec: &[String]) -> Map, + parts: &[String], + shell_hint: &str, +) -> String { + if let Some(raw) = raw_command { + return raw.to_string(); + } + + if should_use_windows_command_tokenizer(Some(shell_hint)) { + return join_windows_command(parts); + } + + join(parts.iter().map(|part| part.as_str())) +} + +fn join_windows_command(parts: &[String]) -> String { + parts + .iter() + .map(|part| quote_windows_argument(part)) + .collect::>() + .join(" ") +} + +fn quote_windows_argument(arg: &str) -> String { + if arg.is_empty() { + return "\"\"".to_string(); + } + + let requires_quotes = arg + .chars() + .any(|c| c.is_whitespace() || c == '"' || c == '\t'); + if !requires_quotes { + return arg.to_string(); + } + + let mut result = String::with_capacity(arg.len() + 2); + result.push('"'); + + let mut backslashes = 0; + for ch in arg.chars() { + match ch { + '\\' => { + backslashes += 1; + } + '"' => { + result.extend(std::iter::repeat('\\').take(backslashes * 2 + 1)); + result.push('"'); + backslashes = 0; + } + _ => { + if backslashes > 0 { + result.extend(std::iter::repeat('\\').take(backslashes)); + backslashes = 0; + } + result.push(ch); + } + } + } + + if backslashes > 0 { + result.extend(std::iter::repeat('\\').take(backslashes * 2)); + } + + result.push('"'); + result +} + fn sanitize_command_string(command: &str) -> Cow<'_, str> { let trimmed = command.trim_end_matches(char::is_whitespace); @@ -1745,6 +1929,59 @@ fn tokenize_windows_command(command: &str) -> Result> { Ok(tokens) } +fn resolve_shell_preference(explicit: Option<&str>, config: &PtyConfig) -> Option { + explicit + .and_then(sanitize_shell_candidate) + .or_else(|| { + config + .preferred_shell + .as_deref() + .and_then(sanitize_shell_candidate) + }) + .or_else(|| { + env::var("SHELL") + .ok() + .and_then(|value| sanitize_shell_candidate(&value)) + }) + .or_else(detect_posix_shell_candidate) +} + +fn sanitize_shell_candidate(shell: &str) -> Option { + let trimmed = shell.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } +} + +fn detect_posix_shell_candidate() -> Option { + if cfg!(windows) { + return None; + } + + const CANDIDATES: [&str; 6] = [ + "/bin/zsh", + "/usr/bin/zsh", + "/bin/bash", + "/usr/bin/bash", + "/bin/sh", + "/usr/bin/sh", + ]; + + for candidate in CANDIDATES { + if Path::new(candidate).exists() { + return Some(candidate.to_string()); + } + } + + None +} + +fn is_default_shell_placeholder(program: &str) -> bool { + matches!(normalized_shell_name(program).as_str(), "bash" | "sh") +} + fn is_windows_shell(shell: &str) -> bool { matches!( normalized_shell_name(shell).as_str(), diff --git a/vtcode-core/src/ui/tui/session.rs b/vtcode-core/src/ui/tui/session.rs index e53f778d2..a3fa474f6 100644 --- a/vtcode-core/src/ui/tui/session.rs +++ b/vtcode-core/src/ui/tui/session.rs @@ -1,4 +1,4 @@ -use std::{cmp::min, fmt::Write, mem, path::Path, time::Instant}; +use std::{cmp::min, fmt::Write, mem, time::Instant}; use ansi_to_tui::IntoText; use anstyle::{AnsiColor, Color as AnsiColorEnum, RgbColor}; @@ -10,7 +10,7 @@ use line_clipping::cohen_sutherland::clip_line; use line_clipping::{LineSegment, Point, Window}; use ratatui::{ Frame, - layout::{Constraint, Layout, Position, Rect}, + layout::{Constraint, Layout, Rect}, style::{Color, Modifier, Style}, symbols::border, text::{Line, Span, Text}, @@ -18,7 +18,6 @@ use ratatui::{ }; use tokio::sync::mpsc::UnboundedSender; use tui_popup::{Popup, PopupState, SizedWrapper}; -use tui_scrollview::ScrollViewState; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -30,7 +29,6 @@ use super::types::{ use crate::config::constants::{prompts, ui}; mod file_palette; -mod file_tree; mod input; mod message; mod modal; @@ -132,7 +130,7 @@ pub struct Session { scroll_offset: usize, transcript_rows: u16, transcript_width: u16, - transcript_scroll: ScrollViewState, + transcript_view_top: usize, cached_max_scroll_offset: usize, scroll_metrics_dirty: bool, transcript_cache: Option, @@ -198,7 +196,7 @@ impl Session { scroll_offset: 0, transcript_rows: initial_transcript_rows, transcript_width: 0, - transcript_scroll: ScrollViewState::default(), + transcript_view_top: 0, cached_max_scroll_offset: 0, scroll_metrics_dirty: true, transcript_cache: None, @@ -1343,9 +1341,7 @@ impl Session { self.apply_transcript_rows(inner.height); - let available_padding = - ui::INLINE_SCROLLBAR_EDGE_PADDING.min(inner.width.saturating_sub(1)); - let content_width = inner.width.saturating_sub(available_padding); + let content_width = inner.width; if content_width == 0 { return; } @@ -1355,13 +1351,10 @@ impl Session { let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING); let effective_padding = padding.min(viewport_rows.saturating_sub(1)); let total_rows = self.total_transcript_rows(content_width) + effective_padding; - let (top_offset, _total_rows) = self.prepare_transcript_scroll(total_rows, viewport_rows); + let (top_offset, _clamped_total_rows) = + self.prepare_transcript_scroll(total_rows, viewport_rows); let vertical_offset = top_offset.min(self.cached_max_scroll_offset); - let clamped_offset = vertical_offset.min(u16::MAX as usize) as u16; - self.transcript_scroll.set_offset(Position { - x: 0, - y: clamped_offset, - }); + self.transcript_view_top = vertical_offset; let visible_start = vertical_offset; let scroll_area = Rect::new(inner.x, inner.y, content_width, inner.height); @@ -1369,27 +1362,15 @@ impl Session { self.collect_transcript_window(content_width, visible_start, viewport_rows); let fill_count = viewport_rows.saturating_sub(visible_lines.len()); if fill_count > 0 { - visible_lines - .extend((0..fill_count).map(|_| self.blank_transcript_line(content_width))); + let target_len = visible_lines.len() + fill_count; + visible_lines.resize_with(target_len, Line::default); } self.overlay_queue_lines(&mut visible_lines, content_width); - self.pad_lines_to_width(&mut visible_lines, content_width); let paragraph = Paragraph::new(visible_lines) .style(self.default_style()) - .wrap(Wrap { trim: false }); + .wrap(Wrap { trim: true }); frame.render_widget(Clear, scroll_area); frame.render_widget(paragraph, scroll_area); - - if inner.width > content_width { - let padding_width = inner.width - content_width; - let padding_area = Rect::new( - scroll_area.x + content_width, - scroll_area.y, - padding_width, - inner.height, - ); - frame.render_widget(Clear, padding_area); - } } fn set_plan(&mut self, plan: TaskPlan) { @@ -1397,37 +1378,6 @@ impl Session { self.mark_dirty(); } - fn blank_transcript_line(&self, width: u16) -> Line<'static> { - if width == 0 { - return Line::default(); - } - - let spaces = " ".repeat(width as usize); - Line::from(vec![Span::raw(spaces)]) - } - - fn pad_lines_to_width(&self, lines: &mut [Line<'static>], width: u16) { - let target = width as usize; - if target == 0 { - return; - } - - for line in lines.iter_mut() { - let current = line.spans.iter().map(|span| span.width()).sum::(); - if current >= target { - continue; - } - - if current == 0 { - line.spans - .push(Span::styled(" ".repeat(target), Style::default())); - } else { - line.spans - .push(Span::styled(" ".repeat(target - current), Style::default())); - } - } - } - fn render_slash_palette(&mut self, frame: &mut Frame<'_>, viewport: Rect) { if viewport.height == 0 || viewport.width == 0 || self.modal.is_some() { self.slash_palette.clear_visible_rows(); @@ -1551,14 +1501,11 @@ impl Session { let area = compute_modal_area(viewport, width_hint, modal_height, 0, 0, true); frame.render_widget(Clear, area); - let title = match palette.display_mode() { - file_palette::DisplayMode::List => format!( - "File Browser (Page {}/{})", - palette.current_page_number(), - palette.total_pages() - ), - file_palette::DisplayMode::Tree => "File Browser (Tree View)".to_string(), - }; + let title = format!( + "File Browser (Page {}/{})", + palette.current_page_number(), + palette.total_pages() + ); let block = Block::default() .title(title) .borders(Borders::ALL) @@ -1578,106 +1525,52 @@ impl Session { frame.render_widget(paragraph, text_area); } - // Check display mode and render accordingly - match palette.display_mode() { - file_palette::DisplayMode::List => { - let mut list_items: Vec = items - .iter() - .map(|(_, entry, is_selected)| { - let base_style = if *is_selected { - self.modal_list_highlight_style() - } else { - self.default_style() - }; - - // Add visual distinction for directories - let style = if entry.is_dir { - base_style.add_modifier(Modifier::BOLD) - } else { - base_style - }; - - // Add icon prefix - let prefix = if entry.is_dir { - "↳ " // Folder indicator - } else { - " · " // Indent files - }; - - let display_text = format!("{}{}", prefix, entry.display_name); - - ListItem::new(Line::from(Span::styled(display_text, style))) - }) - .collect(); - - // Add continuation indicator if there are more items - if palette.has_more_items() { - let continuation_text = format!( - " ... ({} more items)", - palette.total_items() - (palette.current_page_number() * 20) - ); - let continuation_style = self - .default_style() - .add_modifier(Modifier::DIM | Modifier::ITALIC); - list_items.push(ListItem::new(Line::from(Span::styled( - continuation_text, - continuation_style, - )))); - } - - let list = List::new(list_items).style(self.default_style()); - frame.render_widget(list, layout.list_area); - } - file_palette::DisplayMode::Tree => { - // Render tree view (no need to pass items, tree uses all filtered files) - self.render_file_tree(frame, layout.list_area); - } - } - } - - fn render_file_tree(&mut self, frame: &mut Frame<'_>, area: Rect) { - use tui_tree_widget::Tree; + let mut list_items: Vec = items + .iter() + .map(|(_, entry, is_selected)| { + let base_style = if *is_selected { + self.modal_list_highlight_style() + } else { + self.default_style() + }; - // Get styles first (before any mutable borrows) - let default_style = self.default_style(); - let highlight_style = self.modal_list_highlight_style(); + // Add visual distinction for directories + let style = if entry.is_dir { + base_style.add_modifier(Modifier::BOLD) + } else { + base_style + }; - let Some(palette) = self.file_palette.as_mut() else { - return; - }; + // Add icon prefix + let prefix = if entry.is_dir { + "↳ " // Folder indicator + } else { + " · " // Indent files + }; - if !palette.has_files() { - return; - } + let display_text = format!("{}{}", prefix, entry.display_name); - // Get cached tree items (clone to avoid borrow conflicts) - let tree_items: Vec<_> = palette.get_tree_items().to_vec(); + ListItem::new(Line::from(Span::styled(display_text, style))) + }) + .collect(); - if tree_items.is_empty() { - let dim_style = default_style.add_modifier(Modifier::DIM); - let paragraph = Paragraph::new("No files to display").style(dim_style); - frame.render_widget(paragraph, area); - return; + // Add continuation indicator if there are more items + if palette.has_more_items() { + let continuation_text = format!( + " ... ({} more items)", + palette.total_items() - (palette.current_page_number() * 20) + ); + let continuation_style = self + .default_style() + .add_modifier(Modifier::DIM | Modifier::ITALIC); + list_items.push(ListItem::new(Line::from(Span::styled( + continuation_text, + continuation_style, + )))); } - // Tree::new returns a Result, handle it - match Tree::new(&tree_items) { - Ok(tree) => { - let styled_tree = tree - .style(default_style) - .highlight_style(highlight_style) - .highlight_symbol(""); - - // Render tree widget with state for expand/collapse - frame.render_stateful_widget(styled_tree, area, palette.tree_state_mut()); - } - Err(_) => { - // Fallback to empty display if tree creation fails - let dim_style = default_style.add_modifier(Modifier::DIM); - let paragraph = Paragraph::new("Error displaying tree view").style(dim_style); - frame.render_widget(paragraph, area); - } - } + let list = List::new(list_items).style(self.default_style()); + frame.render_widget(list, layout.list_area); } fn render_file_palette_loading(&self, frame: &mut Frame<'_>, viewport: Rect) { @@ -1721,33 +1614,16 @@ impl Session { format!("{} files", total) }; - let mode_text = match palette.display_mode() { - file_palette::DisplayMode::List => "List", - file_palette::DisplayMode::Tree => "Tree", - }; - - let nav_text = match palette.display_mode() { - file_palette::DisplayMode::List => { - "↑↓ Navigate · PgUp/PgDn Page · Tab/Enter Select" - } - file_palette::DisplayMode::Tree => { - "↑↓ Navigate · ←→ Expand · Tab Select (Enter=expand)" - } - }; + let nav_text = "↑↓ Navigate · PgUp/PgDn Page · Tab/Enter Select"; lines.push(Line::from(vec![Span::styled( - format!("{} · Ctrl+t Toggle View · Esc Close", nav_text), + format!("{} · Esc Close", nav_text), self.default_style(), )])); - let note = match palette.display_mode() { - file_palette::DisplayMode::List => "", - file_palette::DisplayMode::Tree => " • Tree: read-only, use List for selection", - }; - lines.push(Line::from(vec![ Span::styled( - format!("Showing {} ({} view){}", count_text, mode_text, note), + format!("Showing {}", count_text), self.default_style().add_modifier(Modifier::DIM), ), Span::styled( @@ -3083,83 +2959,23 @@ impl Session { true } KeyCode::Tab => { - // Get selected file based on current display mode - let file_path = if matches!(palette.display_mode(), file_palette::DisplayMode::Tree) - { - // In tree mode, get selection from tree state - palette.get_tree_selected().and_then(|path| { - // Convert absolute path to relative path - let workspace = palette.workspace_root(); - Path::new(&path) - .strip_prefix(workspace) - .ok() - .map(|p| p.to_string_lossy().to_string()) - }) - } else { - // In list mode, get selection from list - palette - .get_selected() - .map(|entry| entry.relative_path.clone()) - }; - - if let Some(path) = file_path { + if let Some(entry) = palette.get_selected() { + let path = entry.relative_path.clone(); self.insert_file_reference(&path); self.close_file_palette(); self.mark_dirty(); } true } - KeyCode::Left => { - if matches!(palette.display_mode(), file_palette::DisplayMode::Tree) { - palette.tree_state_mut().key_left(); - self.mark_dirty(); - } - true - } - KeyCode::Right => { - if matches!(palette.display_mode(), file_palette::DisplayMode::Tree) { - palette.tree_state_mut().key_right(); - self.mark_dirty(); - } - true - } KeyCode::Enter => { - match palette.display_mode() { - file_palette::DisplayMode::Tree => { - // In tree mode: select files, toggle folders - if let Some((is_file, relative_path)) = palette.get_tree_selection_info() { - if is_file { - // It's a file - insert reference and close - self.insert_file_reference(&relative_path); - self.close_file_palette(); - } else { - // It's a directory - toggle expand/collapse - palette.tree_state_mut().toggle_selected(); - } - self.mark_dirty(); - } else { - // No selection or can't determine - just toggle - palette.tree_state_mut().toggle_selected(); - self.mark_dirty(); - } - } - file_palette::DisplayMode::List => { - // In list mode, insert file reference and close modal - if let Some(entry) = palette.get_selected() { - let path = entry.relative_path.clone(); - self.insert_file_reference(&path); - self.close_file_palette(); - self.mark_dirty(); - } - } + if let Some(entry) = palette.get_selected() { + let path = entry.relative_path.clone(); + self.insert_file_reference(&path); + self.close_file_palette(); + self.mark_dirty(); } true } - KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => { - palette.toggle_display_mode(); - self.mark_dirty(); - true - } _ => false, } } @@ -5227,7 +5043,7 @@ mod tests { let width = session.transcript_width; let viewport = session.viewport_height(); - let offset = usize::from(session.transcript_scroll.offset().y); + let offset = session.transcript_view_top; let lines = session.reflow_transcript_lines(width); let start = offset.min(lines.len()); diff --git a/vtcode-core/src/ui/tui/session/file_palette.rs b/vtcode-core/src/ui/tui/session/file_palette.rs index 5fc25394a..1b452b609 100644 --- a/vtcode-core/src/ui/tui/session/file_palette.rs +++ b/vtcode-core/src/ui/tui/session/file_palette.rs @@ -1,7 +1,6 @@ use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern}; use nucleo_matcher::{Config, Matcher, Utf32Str}; use std::path::{Path, PathBuf}; -use tui_tree_widget::TreeState; const PAGE_SIZE: usize = 20; @@ -13,12 +12,6 @@ pub struct FileEntry { pub is_dir: bool, } -#[derive(Debug, Clone, Copy)] -pub enum DisplayMode { - List, - Tree, -} - pub struct FilePalette { all_files: Vec, filtered_files: Vec, @@ -27,23 +20,10 @@ pub struct FilePalette { filter_query: String, workspace_root: PathBuf, filter_cache: std::collections::HashMap>, - display_mode: DisplayMode, - tree_state: TreeState, - cached_tree_items: Option>>, } impl FilePalette { pub fn new(workspace_root: PathBuf) -> Self { - Self::with_display_mode(workspace_root, None) - } - - pub fn with_display_mode(workspace_root: PathBuf, default_view: Option<&str>) -> Self { - let display_mode = match default_view { - Some("list") => DisplayMode::List, - Some("tree") => DisplayMode::Tree, - _ => DisplayMode::List, // Default to list view (Enter works reliably) - }; - Self { all_files: Vec::new(), filtered_files: Vec::new(), @@ -52,76 +32,24 @@ impl FilePalette { filter_query: String::new(), workspace_root, filter_cache: std::collections::HashMap::new(), - display_mode, - tree_state: TreeState::default(), - cached_tree_items: None, } } - pub fn toggle_display_mode(&mut self) { - self.display_mode = match self.display_mode { - DisplayMode::List => DisplayMode::Tree, - DisplayMode::Tree => DisplayMode::List, - }; - // Invalidate tree cache when switching modes - self.cached_tree_items = None; - } - - pub fn display_mode(&self) -> &DisplayMode { - &self.display_mode - } - /// Reset selection and filter (call when opening file browser) pub fn reset(&mut self) { self.selected_index = 0; self.current_page = 0; self.filter_query.clear(); - self.tree_state.select_first(); - self.cached_tree_items = None; self.apply_filter(); // Refresh filtered_files to show all } /// Clean up resources to free memory (call when closing file browser) pub fn cleanup(&mut self) { self.filter_cache.clear(); - self.cached_tree_items = None; self.filtered_files.clear(); self.filtered_files.shrink_to_fit(); } - pub fn tree_state_mut(&mut self) -> &mut TreeState { - &mut self.tree_state - } - - pub fn workspace_root(&self) -> &PathBuf { - &self.workspace_root - } - - /// Get or build tree items for current filtered files - pub fn get_tree_items(&mut self) -> &[tui_tree_widget::TreeItem<'static, String>] { - use crate::ui::tui::session::file_tree::FileTreeNode; - - if self.cached_tree_items.is_none() { - // Build tree from filtered files - let file_paths: Vec = - self.filtered_files.iter().map(|f| f.path.clone()).collect(); - let tree_root = FileTreeNode::build_tree(file_paths, &self.workspace_root); - self.cached_tree_items = Some(tree_root.to_tree_items()); - - // Ensure tree has a selection when first built - if self.tree_state.selected().is_empty() - && !self.cached_tree_items.as_ref().unwrap().is_empty() - { - self.tree_state.select_first(); - } - } - - self.cached_tree_items - .as_ref() - .map(|v| v.as_slice()) - .unwrap_or(&[]) - } - /// SECURITY: Check if a file should be excluded from the file browser /// Always excludes .env files, .git directories, and other sensitive data fn should_exclude_file(path: &Path) -> bool { @@ -222,9 +150,6 @@ impl FilePalette { } fn apply_filter(&mut self) { - // Invalidate tree cache when filter changes - self.cached_tree_items = None; - if self.filter_query.is_empty() { // Avoid cloning when no filter - just reference all files self.filtered_files = self.all_files.clone(); @@ -361,101 +286,52 @@ impl FilePalette { if self.filtered_files.is_empty() { return; } - match self.display_mode { - DisplayMode::List => { - if self.selected_index > 0 { - self.selected_index -= 1; - } else { - self.selected_index = self.filtered_files.len().saturating_sub(1); - } - self.update_page_from_selection(); - } - DisplayMode::Tree => { - self.tree_state.key_up(); - } + if self.selected_index > 0 { + self.selected_index -= 1; + } else { + self.selected_index = self.filtered_files.len().saturating_sub(1); } + self.update_page_from_selection(); } pub fn move_selection_down(&mut self) { if self.filtered_files.is_empty() { return; } - match self.display_mode { - DisplayMode::List => { - if self.selected_index + 1 < self.filtered_files.len() { - self.selected_index += 1; - } else { - self.selected_index = 0; - } - self.update_page_from_selection(); - } - DisplayMode::Tree => { - self.tree_state.key_down(); - } + if self.selected_index + 1 < self.filtered_files.len() { + self.selected_index += 1; + } else { + self.selected_index = 0; } + self.update_page_from_selection(); } pub fn move_to_first(&mut self) { if !self.filtered_files.is_empty() { - match self.display_mode { - DisplayMode::List => { - self.selected_index = 0; - self.current_page = 0; - } - DisplayMode::Tree => { - self.tree_state.select_first(); - } - } + self.selected_index = 0; + self.current_page = 0; } } pub fn move_to_last(&mut self) { if !self.filtered_files.is_empty() { - match self.display_mode { - DisplayMode::List => { - self.selected_index = self.filtered_files.len().saturating_sub(1); - self.update_page_from_selection(); - } - DisplayMode::Tree => { - // Tree doesn't have select_last, use key_down repeatedly - // This is a limitation of tui-tree-widget - } - } + self.selected_index = self.filtered_files.len().saturating_sub(1); + self.update_page_from_selection(); } } pub fn page_up(&mut self) { - match self.display_mode { - DisplayMode::List => { - if self.current_page > 0 { - self.current_page -= 1; - self.selected_index = self.current_page * PAGE_SIZE; - } - } - DisplayMode::Tree => { - // In tree mode, PageUp = collapse all (close selected node and its parents) - if let Some(selected) = self.tree_state.selected().first() { - self.tree_state.close(&[selected.clone()]); - } - } + if self.current_page > 0 { + self.current_page -= 1; + self.selected_index = self.current_page * PAGE_SIZE; } } pub fn page_down(&mut self) { - match self.display_mode { - DisplayMode::List => { - let total_pages = self.total_pages(); - if self.current_page + 1 < total_pages { - self.current_page += 1; - self.selected_index = self.current_page * PAGE_SIZE; - } - } - DisplayMode::Tree => { - // In tree mode, PageDown = expand current node - if let Some(selected) = self.tree_state.selected().first() { - self.tree_state.open(vec![selected.clone()]); - } - } + let total_pages = self.total_pages(); + if self.current_page + 1 < total_pages { + self.current_page += 1; + self.selected_index = self.current_page * PAGE_SIZE; } } @@ -467,49 +343,6 @@ impl FilePalette { self.filtered_files.get(self.selected_index) } - /// Get the selected file path from tree state (for tree mode) - pub fn get_tree_selected(&self) -> Option { - self.tree_state.selected().first().cloned() - } - - /// Check if the tree selection is a file (not a directory) - /// Returns (is_file, relative_path) if something is selected - pub fn get_tree_selection_info(&self) -> Option<(bool, String)> { - let selected = self.tree_state.selected().first()?.clone(); - let selected_path = Path::new(&selected); - - // Robustly compute relative path (handle both absolute and relative IDs) - let relative_path = if selected_path.is_absolute() { - // Try strip_prefix first - match selected_path.strip_prefix(&self.workspace_root) { - Ok(p) => p.to_string_lossy().to_string(), - Err(_) => { - // Fallback: look up by absolute path in all_files - self.all_files - .iter() - .find(|e| e.path == selected) - .map(|e| e.relative_path.clone())? - } - } - } else { - // Already relative - selected.clone() - }; - - // Prefer model data to determine file vs directory - let is_dir = self - .all_files - .iter() - .find(|e| e.relative_path == relative_path || e.path == selected) - .map(|e| e.is_dir) - .unwrap_or_else(|| { - // Fallback: files have extensions, directories don't - Path::new(&relative_path).extension().is_none() - }); - - Some((!is_dir, relative_path)) - } - pub fn current_page_items(&self) -> Vec<(usize, &FileEntry, bool)> { let start = self.current_page * PAGE_SIZE; let end = (start + PAGE_SIZE).min(self.filtered_files.len()); @@ -641,9 +474,6 @@ mod tests { #[test] fn test_pagination() { let mut palette = FilePalette::new(PathBuf::from("/workspace")); - // Force list mode for pagination test - palette.display_mode = DisplayMode::List; - let files: Vec = (0..50).map(|i| format!("file{}.rs", i)).collect(); palette.load_files(files); @@ -714,9 +544,6 @@ mod tests { #[test] fn test_circular_navigation() { let mut palette = FilePalette::new(PathBuf::from("/workspace")); - // Force list mode for navigation test - palette.display_mode = DisplayMode::List; - let files = vec!["a.rs".to_string(), "b.rs".to_string(), "c.rs".to_string()]; palette.load_files(files); @@ -896,36 +723,6 @@ mod tests { assert_eq!(items[5].1.relative_path, "zebra.txt"); } - #[test] - fn test_tree_selection_info() { - let mut palette = FilePalette::new(PathBuf::from("/workspace")); - - // Add some actual test files - palette.all_files = vec![ - FileEntry { - path: "/workspace/src/main.rs".to_string(), - display_name: "src/main.rs".to_string(), - relative_path: "src/main.rs".to_string(), - is_dir: false, - }, - FileEntry { - path: "/workspace/src".to_string(), - display_name: "src/".to_string(), - relative_path: "src".to_string(), - is_dir: true, - }, - ]; - palette.filtered_files = palette.all_files.clone(); - - // Simulate tree selection by manually setting state - // Note: In real usage, tree_state is managed by widget - - // Test that extension detection works - let info1 = FilePalette::new(PathBuf::from("/workspace")).get_tree_selection_info(); - // Without selection, should return None or handle gracefully - assert!(info1.is_none() || info1.is_some()); - } - #[test] fn test_simple_fuzzy_match() { // Test basic fuzzy matching diff --git a/vtcode-core/src/ui/tui/session/file_tree.rs b/vtcode-core/src/ui/tui/session/file_tree.rs index 1d5b3ee3e..9c91161cb 100644 --- a/vtcode-core/src/ui/tui/session/file_tree.rs +++ b/vtcode-core/src/ui/tui/session/file_tree.rs @@ -1,195 +1,3 @@ -use std::path::Path; -use tui_tree_widget::TreeItem; - -#[derive(Debug, Clone)] -pub struct FileTreeNode { - pub name: String, - pub path: String, - pub is_dir: bool, - pub children: Vec, -} - -impl FileTreeNode { - /// Build a tree structure from a flat list of file paths - pub fn build_tree(files: Vec, workspace: &Path) -> Self { - let mut root = FileTreeNode { - name: ".".to_string(), - path: workspace.to_string_lossy().to_string(), - is_dir: true, - children: Vec::new(), - }; - - for file_path in files { - root.insert_file(&file_path, workspace); - } - - root.sort_children_recursive(); - root - } - - /// Insert a file into the tree - fn insert_file(&mut self, file_path: &str, workspace: &Path) { - let path = Path::new(file_path); - let relative = path.strip_prefix(workspace).unwrap_or(path); - - let components: Vec<&str> = relative - .components() - .filter_map(|c| c.as_os_str().to_str()) - .collect(); - - if !components.is_empty() { - self.insert_components(&components, file_path, workspace); - } - } - - /// Recursively insert path components - fn insert_components(&mut self, components: &[&str], full_path: &str, workspace: &Path) { - if components.is_empty() { - return; - } - - let name = components[0]; - let is_last = components.len() == 1; - - // Find or create child node - let child = self.children.iter_mut().find(|c| c.name == name); - - if let Some(child) = child { - if !is_last { - child.insert_components(&components[1..], full_path, workspace); - } - } else { - // Construct unique path for this node (needed for TreeItem identifier) - let node_path = if is_last { - full_path.to_string() - } else { - // For directories, construct the path from parent path + name - let parent_path = &self.path; - if parent_path.is_empty() || parent_path == &workspace.to_string_lossy().to_string() - { - workspace.join(name).to_string_lossy().to_string() - } else { - Path::new(parent_path) - .join(name) - .to_string_lossy() - .to_string() - } - }; - - // Create new node - let mut new_node = FileTreeNode { - name: name.to_string(), - path: node_path, - is_dir: !is_last, - children: Vec::new(), - }; - - if !is_last { - new_node.insert_components(&components[1..], full_path, workspace); - } - - self.children.push(new_node); - } - } - - /// Sort children recursively (directories first, then alphabetically) - fn sort_children_recursive(&mut self) { - self.children.sort_by(|a, b| match (a.is_dir, b.is_dir) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), - }); - - for child in &mut self.children { - child.sort_children_recursive(); - } - } - - /// Convert to TreeItem for tui-tree-widget - pub fn to_tree_items(&self) -> Vec> { - self.children - .iter() - .map(|child| child.to_tree_item()) - .collect() - } - - fn to_tree_item(&self) -> TreeItem<'static, String> { - let display_text = if self.is_dir { - // Tree widget adds its own arrow, just add folder indicator - format!("{}/", self.name) - } else { - self.name.clone() - }; - - if self.is_dir && !self.children.is_empty() { - let children: Vec> = self - .children - .iter() - .map(|child| child.to_tree_item()) - .collect(); - TreeItem::new(self.path.clone(), display_text, children) - .expect("Failed to create tree item") - } else { - TreeItem::new_leaf(self.path.clone(), display_text) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - - #[test] - fn test_tree_building() { - let workspace = PathBuf::from("/workspace"); - let files = vec![ - "/workspace/src/main.rs".to_string(), - "/workspace/src/lib.rs".to_string(), - "/workspace/tests/test.rs".to_string(), - "/workspace/README.md".to_string(), - ]; - - let tree = FileTreeNode::build_tree(files, &workspace); - - assert_eq!(tree.children.len(), 3); // src/, tests/, README.md - assert!(tree.children[0].is_dir); // src/ - assert!(tree.children[1].is_dir); // tests/ - assert!(!tree.children[2].is_dir); // README.md - } - - #[test] - fn test_sorting() { - let workspace = PathBuf::from("/workspace"); - let files = vec![ - "/workspace/file.txt".to_string(), - "/workspace/src/main.rs".to_string(), - "/workspace/another.txt".to_string(), - ]; - - let tree = FileTreeNode::build_tree(files, &workspace); - - // Directories should come first - assert!(tree.children[0].is_dir); - assert_eq!(tree.children[0].name, "src"); - - // Then files alphabetically - assert_eq!(tree.children[1].name, "another.txt"); - assert_eq!(tree.children[2].name, "file.txt"); - } - - #[test] - fn test_nested_directories() { - let workspace = PathBuf::from("/workspace"); - let files = vec![ - "/workspace/src/agent/mod.rs".to_string(), - "/workspace/src/agent/runloop.rs".to_string(), - ]; - - let tree = FileTreeNode::build_tree(files, &workspace); - - assert_eq!(tree.children.len(), 1); // src/ - assert_eq!(tree.children[0].children.len(), 1); // agent/ - assert_eq!(tree.children[0].children[0].children.len(), 2); // mod.rs, runloop.rs - } -} +//! Legacy file tree module intentionally left empty after removing the +//! tui-tree-widget dependency. Retained only so older build artifacts that +//! reference the module path continue to compile cleanly. diff --git a/vtcode-core/src/update/README.md b/vtcode-core/src/update/README.md deleted file mode 100644 index ae089cc62..000000000 --- a/vtcode-core/src/update/README.md +++ /dev/null @@ -1,250 +0,0 @@ -# Self-Update Module - -This module provides a robust self-update mechanism for vtcode, allowing users to easily keep their installation up to date with the latest features and bug fixes. - -## Architecture - -The update system is composed of several key components: - -### Core Components - -1. **UpdateManager** (`mod.rs`) - - Main coordinator for all update operations - - Orchestrates checking, downloading, installing, and rolling back updates - - Provides a high-level API for update operations - -2. **UpdateChecker** (`checker.rs`) - - Checks for available updates from GitHub releases - - Compares versions and determines if an update is available - - Handles update frequency and caching - - Finds appropriate binary for the current platform - -3. **UpdateDownloader** (`downloader.rs`) - - Downloads update files from GitHub releases - - Supports streaming downloads with progress tracking - - Downloads checksums and signatures for verification - -4. **UpdateVerifier** (`verifier.rs`) - - Verifies downloaded binaries using SHA256 checksums - - Validates signatures (when available) - - Ensures executable permissions on Unix systems - -5. **UpdateInstaller** (`installer.rs`) - - Installs downloaded updates - - Handles archive extraction (ZIP, TAR, etc.) - - Replaces the current executable - - Platform-specific installation logic - -6. **RollbackManager** (`rollback.rs`) - - Creates backups before updates - - Manages backup retention - - Provides rollback functionality - - Cleans up old backups - -7. **UpdateConfig** (`config.rs`) - - Configuration for the update system - - Update channels (stable, beta, nightly) - - Update frequency settings - - Directory management - -## Features - -### Version Checking - -- Automatic version checking from GitHub releases -- Configurable check frequency (always, daily, weekly, never) -- Caching of check results to avoid rate limiting -- Support for pre-releases and beta channels - -### Secure Downloads - -- HTTPS downloads from GitHub releases -- SHA256 checksum verification -- Signature verification (when available) -- Streaming downloads with progress tracking -- Automatic retry on failure - -### Safe Installation - -- Automatic backup before installation -- Atomic replacement of executable -- Platform-specific installation logic -- Rollback on installation failure -- Verification of installed binary - -### Backup Management - -- Automatic backup creation -- Configurable backup retention -- Backup metadata tracking -- Easy rollback to previous versions - -### Cross-Platform Support - -- Linux (x86_64, aarch64) -- macOS (x86_64, aarch64) -- Windows (x86_64, aarch64) -- Automatic platform detection -- Platform-specific binary selection - -## Usage - -### Basic Usage - -```rust -use vtcode_core::update::{UpdateConfig, UpdateManager}; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // Create update manager with default configuration - let config = UpdateConfig::from_env()?; - let mut manager = UpdateManager::new(config)?; - - // Check for updates - let status = manager.check_for_updates().await?; - - if status.update_available { - println!("Update available: {}", status.latest_version.unwrap()); - - // Perform update - let result = manager.perform_update().await?; - - if result.success { - println!("Update successful!"); - } - } - - Ok(()) -} -``` - -### Configuration - -```rust -use vtcode_core::update::{UpdateChannel, UpdateConfig, UpdateFrequency}; - -let mut config = UpdateConfig::default(); -config.channel = UpdateChannel::Beta; -config.frequency = UpdateFrequency::Weekly; -config.auto_download = true; -config.max_backups = 5; -``` - -### Environment Variables - -```bash -export VTCODE_UPDATE_ENABLED=true -export VTCODE_UPDATE_CHANNEL=stable -export VTCODE_UPDATE_FREQUENCY=daily -export VTCODE_UPDATE_AUTO_DOWNLOAD=false -export VTCODE_UPDATE_MAX_BACKUPS=3 -``` - -## Security - -### Checksum Verification - -All downloads are verified using SHA256 checksums: - -1. Download the binary -2. Download the `.sha256` file -3. Calculate the SHA256 hash of the binary -4. Compare with the expected hash -5. Fail if hashes don't match - -### Signature Verification - -Binary signatures are verified when available: - -1. Download the binary -2. Download the `.sig` file -3. Verify the signature using a public key -4. Fail if signature is invalid - -### HTTPS Downloads - -All downloads use HTTPS to prevent man-in-the-middle attacks. - -### Automatic Backups - -A backup is automatically created before each update, allowing rollback if needed. - -## Error Handling - -The update system implements comprehensive error handling: - -- Network errors: Retry with exponential backoff -- Verification errors: Fail and cleanup -- Installation errors: Automatic rollback -- Rollback errors: Manual recovery instructions - -## Testing - -The module includes comprehensive tests: - -- Unit tests for each component -- Integration tests for the full update workflow -- Platform-specific tests -- Error handling tests - -Run tests with: - -```bash -cargo test --package vtcode-core --lib update -``` - -## CLI Integration - -The update system is integrated into the vtcode CLI: - -```bash -# Check for updates -vtcode update check - -# Install updates -vtcode update install - -# Configure updates -vtcode update config --channel beta - -# List backups -vtcode update backups - -# Rollback -vtcode update rollback - -# Cleanup old backups -vtcode update cleanup -``` - -## Future Enhancements - -Potential future improvements: - -1. Delta updates for faster downloads -2. Peer-to-peer distribution -3. Automatic update scheduling -4. Update notifications -5. Rollback to specific versions -6. Update history tracking -7. Bandwidth throttling -8. Resume interrupted downloads -9. Multi-source downloads -10. Update verification using multiple checksums - -## Contributing - -When contributing to the update module: - -1. Follow the existing code style -2. Add tests for new functionality -3. Update documentation -4. Test on all supported platforms -5. Consider security implications -6. Handle errors gracefully - -## See Also - -- [Self-Update Guide](../../../docs/guides/self-update.md) -- [Security Guide](../../../docs/guides/security.md) -- [CLI Documentation](../cli/README.md) diff --git a/vtcode-core/src/update/checker.rs b/vtcode-core/src/update/checker.rs deleted file mode 100644 index 0b292f1bc..000000000 --- a/vtcode-core/src/update/checker.rs +++ /dev/null @@ -1,418 +0,0 @@ -//! Version checking and update detection - -use super::{ - CURRENT_VERSION, GITHUB_REPO_NAME, GITHUB_REPO_OWNER, UpdateStatus, config::UpdateConfig, -}; -use anyhow::{Context, Result}; -use serde::Deserialize; -use std::path::PathBuf; -use tracing::debug; -use update_informer::{Check, registry}; - -/// GitHub release information -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -struct GitHubRelease { - tag_name: String, - name: String, - body: Option, - prerelease: bool, - draft: bool, - assets: Vec, - published_at: String, - #[serde(default)] - tarball_url: Option, - #[serde(default)] - zipball_url: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -struct GitHubAsset { - name: String, - browser_download_url: String, - size: u64, -} - -/// Handles checking for available updates -pub struct UpdateChecker { - config: UpdateConfig, - // Keep reqwest client for other operations like downloading release notes - client: reqwest::Client, - last_check_file: PathBuf, -} - -impl UpdateChecker { - pub fn new(config: UpdateConfig) -> Result { - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .user_agent(format!("vtcode/{}", CURRENT_VERSION)) - .build()?; - - let last_check_file = config.update_dir.join("last_check.json"); - - Ok(Self { - config, - client, - last_check_file, - }) - } - - /// Check if an update is available - pub async fn check_for_updates(&self) -> Result { - // Check if we should skip based on frequency - if !self.should_check()? { - return self.load_cached_status(); - } - - // Use update-informer to check for the latest version from GitHub - let update_informer = update_informer::new( - registry::GitHub, - format!("{}/{}", GITHUB_REPO_OWNER, GITHUB_REPO_NAME), - CURRENT_VERSION, - ) - .interval(std::time::Duration::from_secs(60 * 60 * 24)); - - let mut fallback_release: Option = None; - - let informer_result = update_informer - .check_version() - .map_err(|err| err.to_string()); - - let latest_version = match informer_result { - Ok(Some(latest_version)) => latest_version.to_string(), - Ok(None) => { - // No newer version available - let status = UpdateStatus { - current_version: CURRENT_VERSION.to_string(), - latest_version: Some(CURRENT_VERSION.to_string()), - update_available: false, - download_url: None, - release_notes: None, - last_checked: Some(chrono::Utc::now()), - }; - - self.save_status(&status)?; - return Ok(status); - } - Err(err) => { - debug!( - "update-informer check failed; falling back to GitHub API: {}", - err - ); - - let release = self.fetch_latest_release().await?; - let tag_name = release.tag_name.clone(); - fallback_release = Some(release); - tag_name - } - }; - - let current_version = semver::Version::parse(CURRENT_VERSION.trim_start_matches('v')) - .map_err(|e| anyhow::anyhow!("Failed to parse current version: {}", e))?; - - let latest_version_parsed = semver::Version::parse(latest_version.trim_start_matches('v')) - .map_err(|e| anyhow::anyhow!("Failed to parse latest version: {}", e))?; - - let update_available = latest_version_parsed > current_version; - - let (download_url, release_notes) = if update_available { - let release = match fallback_release { - Some(release) => release, - None => self.fetch_latest_release_by_tag(&latest_version).await?, - }; - - let download_url = if release.assets.is_empty() { - tracing::warn!( - "No binary assets found for release {}, only source distribution available", - release.tag_name - ); - None - } else { - self.find_platform_asset(&release.assets)? - }; - - let release_notes = release.body; - (download_url, release_notes) - } else { - (None, None) - }; - - let status = UpdateStatus { - current_version: CURRENT_VERSION.to_string(), - latest_version: Some(latest_version), - update_available, - download_url, - release_notes, - last_checked: Some(chrono::Utc::now()), - }; - - // Cache the status - self.save_status(&status)?; - - Ok(status) - } - - /// Fetch a specific release by tag from GitHub - async fn fetch_latest_release_by_tag(&self, tag: &str) -> Result { - let url = format!( - "{}/repos/{}/{}/releases/tags/{}", - self.config.github_api_base(), - GITHUB_REPO_OWNER, - GITHUB_REPO_NAME, - tag - ); - - let mut request = self.client.get(&url); - - // Add authentication if token is available - if let Some(token) = &self.config.github_token { - request = request.header("Authorization", format!("Bearer {}", token)); - } - - let response = request - .send() - .await - .context("Failed to fetch release information")?; - - if !response.status().is_success() { - anyhow::bail!("GitHub API returned error: {}", response.status()); - } - - let release: GitHubRelease = response - .json() - .await - .context("Failed to parse release information")?; - - Ok(release) - } - - /// Fetch the latest release from GitHub (fallback method) - async fn fetch_latest_release(&self) -> Result { - let url = format!( - "{}/repos/{}/{}/releases/latest", - self.config.github_api_base(), - GITHUB_REPO_OWNER, - GITHUB_REPO_NAME - ); - - let mut request = self.client.get(&url); - - // Add authentication if token is available - if let Some(token) = &self.config.github_token { - request = request.header("Authorization", format!("Bearer {}", token)); - } - - let response = request - .send() - .await - .context("Failed to fetch release information")?; - - if !response.status().is_success() { - anyhow::bail!("GitHub API returned error: {}", response.status()); - } - - let release: GitHubRelease = response - .json() - .await - .context("Failed to parse release information")?; - - // Filter based on channel - if release.draft { - anyhow::bail!("Latest release is a draft"); - } - - if release.prerelease && self.config.channel == super::config::UpdateChannel::Stable { - anyhow::bail!("Latest release is a pre-release but stable channel is configured"); - } - - Ok(release) - } - - /// Find the appropriate asset for the current platform - fn find_platform_asset(&self, assets: &[GitHubAsset]) -> Result> { - let target = self.get_target_triple(); - - // Try exact target match first - for asset in assets { - if asset.name.contains(&target) { - tracing::debug!("Found matching asset for target {}: {}", target, asset.name); - return Ok(Some(asset.browser_download_url.clone())); - } - } - - // Fallback: try to find by OS and architecture - let (os, arch) = self.get_os_arch(); - for asset in assets { - let name_lower = asset.name.to_lowercase(); - if name_lower.contains(os) && name_lower.contains(arch) { - tracing::debug!("Found matching asset for {}-{}: {}", os, arch, asset.name); - return Ok(Some(asset.browser_download_url.clone())); - } - } - - tracing::warn!( - "No matching asset found for platform {} ({}-{}). Available assets: {:?}", - target, - os, - arch, - assets.iter().map(|a| &a.name).collect::>() - ); - - Ok(None) - } - - /// Get the target triple for the current platform - fn get_target_triple(&self) -> String { - #[cfg(all(target_os = "linux", target_arch = "x86_64"))] - return "x86_64-unknown-linux-gnu".to_string(); - - #[cfg(all(target_os = "linux", target_arch = "aarch64"))] - return "aarch64-unknown-linux-gnu".to_string(); - - #[cfg(all(target_os = "macos", target_arch = "x86_64"))] - return "x86_64-apple-darwin".to_string(); - - #[cfg(all(target_os = "macos", target_arch = "aarch64"))] - return "aarch64-apple-darwin".to_string(); - - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] - return "x86_64-pc-windows-msvc".to_string(); - - #[cfg(all(target_os = "windows", target_arch = "aarch64"))] - return "aarch64-pc-windows-msvc".to_string(); - - #[allow(unreachable_code)] - "unknown".to_string() - } - - /// Get OS and architecture as separate strings - fn get_os_arch(&self) -> (&'static str, &'static str) { - let os = if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "macos") { - "macos" - } else if cfg!(target_os = "windows") { - "windows" - } else { - "unknown" - }; - - let arch = if cfg!(target_arch = "x86_64") { - "x86_64" - } else if cfg!(target_arch = "aarch64") { - "aarch64" - } else { - "unknown" - }; - - (os, arch) - } - - /// Parse a version string into a comparable tuple - #[cfg_attr(not(test), allow(dead_code))] - fn parse_version(&self, version: &str) -> Result<(u32, u32, u32)> { - let version = version.trim_start_matches('v'); - let parts: Vec<&str> = version.split('.').collect(); - - if parts.len() < 3 { - anyhow::bail!("Invalid version format: {}", version); - } - - let major = parts[0].parse::().context("Invalid major version")?; - let minor = parts[1].parse::().context("Invalid minor version")?; - let patch = parts[2].parse::().context("Invalid patch version")?; - - Ok((major, minor, patch)) - } - - /// Check if we should perform an update check based on frequency - fn should_check(&self) -> Result { - use super::config::UpdateFrequency; - - match self.config.frequency { - UpdateFrequency::Never => return Ok(false), - UpdateFrequency::Always => return Ok(true), - _ => {} - } - - // Load last check time - let last_check = match self.load_last_check_time() { - Ok(time) => time, - Err(_) => return Ok(true), // No previous check, so check now - }; - - let now = chrono::Utc::now(); - let duration = now.signed_duration_since(last_check); - - let should_check = match self.config.frequency { - UpdateFrequency::Daily => duration.num_hours() >= 24, - UpdateFrequency::Weekly => duration.num_days() >= 7, - _ => true, - }; - - Ok(should_check) - } - - /// Load the last check time from cache - fn load_last_check_time(&self) -> Result> { - let status = self.load_cached_status()?; - status - .last_checked - .context("No last check time in cached status") - } - - /// Load cached update status - fn load_cached_status(&self) -> Result { - let content = std::fs::read_to_string(&self.last_check_file) - .context("Failed to read cached status")?; - let status: UpdateStatus = - serde_json::from_str(&content).context("Failed to parse cached status")?; - Ok(status) - } - - /// Save update status to cache - fn save_status(&self, status: &UpdateStatus) -> Result<()> { - self.config.ensure_directories()?; - let content = serde_json::to_string_pretty(status)?; - std::fs::write(&self.last_check_file, content)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_version() { - let config = UpdateConfig::default(); - let checker = UpdateChecker::new(config).unwrap(); - - assert_eq!(checker.parse_version("1.2.3").unwrap(), (1, 2, 3)); - assert_eq!(checker.parse_version("v1.2.3").unwrap(), (1, 2, 3)); - assert_eq!(checker.parse_version("0.33.1").unwrap(), (0, 33, 1)); - } - - #[test] - fn test_version_comparison() { - let config = UpdateConfig::default(); - let checker = UpdateChecker::new(config).unwrap(); - - let v1 = checker.parse_version("0.33.1").unwrap(); - let v2 = checker.parse_version("0.34.0").unwrap(); - assert!(v2 > v1); - - let v3 = checker.parse_version("1.0.0").unwrap(); - assert!(v3 > v2); - } - - #[test] - fn test_get_target_triple() { - let config = UpdateConfig::default(); - let checker = UpdateChecker::new(config).unwrap(); - let target = checker.get_target_triple(); - assert!(!target.is_empty()); - assert_ne!(target, "unknown"); - } -} diff --git a/vtcode-core/src/update/config.rs b/vtcode-core/src/update/config.rs deleted file mode 100644 index 5de3f6269..000000000 --- a/vtcode-core/src/update/config.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! Configuration for the self-update mechanism - -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -/// Update channel selection -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum UpdateChannel { - /// Stable releases only - Stable, - /// Beta releases (pre-releases) - Beta, - /// Development builds (nightly) - Nightly, -} - -impl Default for UpdateChannel { - fn default() -> Self { - Self::Stable - } -} - -impl std::fmt::Display for UpdateChannel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Stable => write!(f, "stable"), - Self::Beta => write!(f, "beta"), - Self::Nightly => write!(f, "nightly"), - } - } -} - -/// Update frequency configuration -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum UpdateFrequency { - /// Check on every launch - Always, - /// Check once per day - Daily, - /// Check once per week - Weekly, - /// Never check automatically - Never, -} - -impl Default for UpdateFrequency { - fn default() -> Self { - Self::Daily - } -} - -/// Configuration for the update system -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateConfig { - /// Whether automatic updates are enabled - pub enabled: bool, - - /// Update channel to use - pub channel: UpdateChannel, - - /// How often to check for updates - pub frequency: UpdateFrequency, - - /// Whether to automatically download updates - pub auto_download: bool, - - /// Whether to automatically install updates - pub auto_install: bool, - - /// Directory for storing update files - pub update_dir: PathBuf, - - /// Directory for storing backups - pub backup_dir: PathBuf, - - /// Maximum number of backups to keep - pub max_backups: usize, - - /// Timeout for download operations (in seconds) - pub download_timeout_secs: u64, - - /// Whether to verify signatures - pub verify_signatures: bool, - - /// Whether to verify checksums - pub verify_checksums: bool, - - /// GitHub API token for authenticated requests (optional) - pub github_token: Option, - - /// Custom GitHub API base URL (for enterprise) - pub github_api_base: Option, -} - -impl Default for UpdateConfig { - fn default() -> Self { - let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); - let vtcode_dir = home_dir.join(".vtcode"); - - Self { - enabled: false, - channel: UpdateChannel::default(), - frequency: UpdateFrequency::default(), - auto_download: false, - auto_install: false, - update_dir: vtcode_dir.join("updates"), - backup_dir: vtcode_dir.join("backups"), - max_backups: 3, - download_timeout_secs: 300, - verify_signatures: true, - verify_checksums: true, - github_token: None, - github_api_base: None, - } - } -} - -impl UpdateConfig { - /// Load configuration from environment variables and defaults - pub fn from_env() -> Result { - let mut config = Self::default(); - - // Check environment variables - if let Ok(val) = std::env::var("VTCODE_UPDATE_ENABLED") { - config.enabled = val.parse().unwrap_or(false); - } - - if let Ok(val) = std::env::var("VTCODE_UPDATE_CHANNEL") { - config.channel = match val.to_lowercase().as_str() { - "beta" => UpdateChannel::Beta, - "nightly" => UpdateChannel::Nightly, - _ => UpdateChannel::Stable, - }; - } - - if let Ok(val) = std::env::var("VTCODE_UPDATE_FREQUENCY") { - config.frequency = match val.to_lowercase().as_str() { - "always" => UpdateFrequency::Always, - "weekly" => UpdateFrequency::Weekly, - "never" => UpdateFrequency::Never, - _ => UpdateFrequency::Daily, - }; - } - - if let Ok(val) = std::env::var("VTCODE_UPDATE_AUTO_DOWNLOAD") { - config.auto_download = val.parse().unwrap_or(false); - } - - if let Ok(val) = std::env::var("VTCODE_UPDATE_AUTO_INSTALL") { - config.auto_install = val.parse().unwrap_or(false); - } - - if let Ok(val) = std::env::var("VTCODE_UPDATE_DIR") { - config.update_dir = PathBuf::from(val); - } - - if let Ok(val) = std::env::var("VTCODE_UPDATE_BACKUP_DIR") { - config.backup_dir = PathBuf::from(val); - } - - if let Ok(val) = std::env::var("VTCODE_UPDATE_MAX_BACKUPS") { - config.max_backups = val.parse().unwrap_or(3); - } - - if let Ok(val) = std::env::var("GITHUB_TOKEN") { - config.github_token = Some(val); - } - - if let Ok(val) = std::env::var("GITHUB_API_BASE") { - config.github_api_base = Some(val); - } - - Ok(config) - } - - /// Ensure required directories exist - pub fn ensure_directories(&self) -> Result<()> { - std::fs::create_dir_all(&self.update_dir)?; - std::fs::create_dir_all(&self.backup_dir)?; - Ok(()) - } - - /// Get the GitHub API base URL - pub fn github_api_base(&self) -> &str { - self.github_api_base - .as_deref() - .unwrap_or("https://api.github.com") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_update_channel_display() { - assert_eq!(UpdateChannel::Stable.to_string(), "stable"); - assert_eq!(UpdateChannel::Beta.to_string(), "beta"); - assert_eq!(UpdateChannel::Nightly.to_string(), "nightly"); - } - - #[test] - fn test_default_config() { - let config = UpdateConfig::default(); - assert!(!config.enabled); - assert_eq!(config.channel, UpdateChannel::Stable); - assert_eq!(config.frequency, UpdateFrequency::Daily); - assert!(!config.auto_download); - assert!(!config.auto_install); - assert_eq!(config.max_backups, 3); - } -} diff --git a/vtcode-core/src/update/downloader.rs b/vtcode-core/src/update/downloader.rs deleted file mode 100644 index 76d7d9acf..000000000 --- a/vtcode-core/src/update/downloader.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Download manager for update files - -use super::config::UpdateConfig; -use anyhow::{Context, Result}; -use std::path::PathBuf; -use tokio::io::AsyncWriteExt; - -/// Handles downloading update files -pub struct UpdateDownloader { - config: UpdateConfig, - client: reqwest::Client, -} - -impl UpdateDownloader { - pub fn new(config: UpdateConfig) -> Result { - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(config.download_timeout_secs)) - .user_agent(format!("vtcode/{}", super::CURRENT_VERSION)) - .build()?; - - Ok(Self { config, client }) - } - - /// Download an update from the given URL - pub async fn download(&self, url: &str) -> Result { - self.config.ensure_directories()?; - - // Extract filename from URL - let filename = url.split('/').last().context("Invalid download URL")?; - - let download_path = self.config.update_dir.join(filename); - - // Download the file - tracing::info!("Downloading update from: {}", url); - - let response = self - .client - .get(url) - .send() - .await - .context("Failed to initiate download")?; - - if !response.status().is_success() { - anyhow::bail!("Download failed with status: {}", response.status()); - } - - let total_size = response.content_length().unwrap_or(0); - tracing::info!("Download size: {} bytes", total_size); - - // Stream the response to a file - let mut file = tokio::fs::File::create(&download_path) - .await - .context("Failed to create download file")?; - - let mut downloaded: u64 = 0; - let mut stream = response.bytes_stream(); - - use futures::StreamExt; - while let Some(chunk) = stream.next().await { - let chunk = chunk.context("Failed to read download chunk")?; - file.write_all(&chunk) - .await - .context("Failed to write download chunk")?; - - downloaded += chunk.len() as u64; - - if total_size > 0 { - let progress = (downloaded as f64 / total_size as f64) * 100.0; - tracing::debug!("Download progress: {:.1}%", progress); - } - } - - file.flush() - .await - .context("Failed to flush download file")?; - - tracing::info!("Download completed: {:?}", download_path); - - // Download checksum file if available - if self.config.verify_checksums { - let checksum_url = format!("{}.sha256", url); - if let Ok(checksum_path) = self.download_checksum(&checksum_url).await { - tracing::info!("Downloaded checksum file: {:?}", checksum_path); - } - } - - // Download signature file if available - if self.config.verify_signatures { - let signature_url = format!("{}.sig", url); - if let Ok(signature_path) = self.download_signature(&signature_url).await { - tracing::info!("Downloaded signature file: {:?}", signature_path); - } - } - - Ok(download_path) - } - - /// Download checksum file - async fn download_checksum(&self, url: &str) -> Result { - let filename = url.split('/').last().context("Invalid checksum URL")?; - - let checksum_path = self.config.update_dir.join(filename); - - let response = self - .client - .get(url) - .send() - .await - .context("Failed to download checksum")?; - - if !response.status().is_success() { - anyhow::bail!("Checksum download failed"); - } - - let content = response - .bytes() - .await - .context("Failed to read checksum content")?; - - tokio::fs::write(&checksum_path, content) - .await - .context("Failed to write checksum file")?; - - Ok(checksum_path) - } - - /// Download signature file - async fn download_signature(&self, url: &str) -> Result { - let filename = url.split('/').last().context("Invalid signature URL")?; - - let signature_path = self.config.update_dir.join(filename); - - let response = self - .client - .get(url) - .send() - .await - .context("Failed to download signature")?; - - if !response.status().is_success() { - anyhow::bail!("Signature download failed"); - } - - let content = response - .bytes() - .await - .context("Failed to read signature content")?; - - tokio::fs::write(&signature_path, content) - .await - .context("Failed to write signature file")?; - - Ok(signature_path) - } - - /// Clean up downloaded files - pub fn cleanup(&self, path: &PathBuf) -> Result<()> { - if path.exists() { - std::fs::remove_file(path).context("Failed to remove download file")?; - } - - // Also remove checksum and signature files - let checksum_path = path.with_extension("sha256"); - if checksum_path.exists() { - std::fs::remove_file(checksum_path).ok(); - } - - let signature_path = path.with_extension("sig"); - if signature_path.exists() { - std::fs::remove_file(signature_path).ok(); - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_downloader_creation() { - let config = UpdateConfig::default(); - let downloader = UpdateDownloader::new(config); - assert!(downloader.is_ok()); - } -} diff --git a/vtcode-core/src/update/installer.rs b/vtcode-core/src/update/installer.rs deleted file mode 100644 index df416191e..000000000 --- a/vtcode-core/src/update/installer.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! Installation manager for updates - -use super::config::UpdateConfig; -use anyhow::{Context, Result}; -use std::path::Path; - -/// Handles installation of downloaded updates -pub struct UpdateInstaller { - config: UpdateConfig, -} - -impl UpdateInstaller { - pub fn new(config: UpdateConfig) -> Result { - Ok(Self { config }) - } - - /// Install an update from the given path - pub async fn install(&self, update_path: &Path) -> Result<()> { - tracing::info!("Installing update from: {:?}", update_path); - - // Always handle as a file path since the downloader has already downloaded the file - // Use self_update's binary replacement functionality for cross-platform compatibility - match self_update::self_replace::self_replace(update_path) { - Ok(_) => { - tracing::info!("Successfully replaced binary with new version"); - } - Err(e) => { - tracing::warn!( - "Failed to use self_replace, falling back to manual replacement: {}", - e - ); - // Fallback to original file-based installation - self.install_from_file(update_path).await?; - } - } - - tracing::info!("Update installation completed successfully"); - - Ok(()) - } - - /// Internal method to install from a local file path (original logic) - async fn install_from_file(&self, update_path: &Path) -> Result<()> { - // Get the current executable path - let current_exe = - std::env::current_exe().context("Failed to get current executable path")?; - - tracing::info!("Current executable: {:?}", current_exe); - - // Extract the update if it's an archive - let binary_path = if self.is_archive(update_path) { - self.extract_archive(update_path).await? - } else { - update_path.to_path_buf() - }; - - // Set executable permissions on Unix - #[cfg(unix)] - self.set_executable_permissions(&binary_path)?; - - // Replace the current executable - self.replace_executable(&binary_path, ¤t_exe) - .await - .context("Failed to replace executable")?; - - Ok(()) - } - - /// Check if the file is an archive - fn is_archive(&self, path: &Path) -> bool { - if let Some(ext) = path.extension() { - let ext = ext.to_string_lossy().to_lowercase(); - matches!(ext.as_str(), "tar" | "gz" | "tgz" | "zip" | "bz2" | "xz") - } else { - false - } - } - - /// Extract an archive and return the path to the binary - async fn extract_archive(&self, archive_path: &Path) -> Result { - let extract_dir = self.config.update_dir.join("extracted"); - tokio::fs::create_dir_all(&extract_dir) - .await - .context("Failed to create extraction directory")?; - - tracing::info!("Extracting archive to: {:?}", extract_dir); - - // Determine archive type and extract - if let Some(ext) = archive_path.extension() { - let ext = ext.to_string_lossy().to_lowercase(); - - match ext.as_str() { - "zip" => self.extract_zip(archive_path, &extract_dir).await?, - "tar" | "tgz" | "gz" | "bz2" | "xz" => { - self.extract_tar(archive_path, &extract_dir).await? - } - _ => anyhow::bail!("Unsupported archive format: {}", ext), - } - } else { - anyhow::bail!("Archive has no extension"); - } - - // Find the binary in the extracted files - self.find_binary_in_dir(&extract_dir) - .await - .context("Failed to find binary in extracted archive") - } - - /// Extract a ZIP archive - async fn extract_zip(&self, archive_path: &Path, extract_dir: &Path) -> Result<()> { - let file = std::fs::File::open(archive_path).context("Failed to open ZIP archive")?; - let mut archive = zip::ZipArchive::new(file).context("Failed to read ZIP archive")?; - - for i in 0..archive.len() { - let mut file = archive.by_index(i).context("Failed to read ZIP entry")?; - let outpath = extract_dir.join(file.name()); - - if file.is_dir() { - tokio::fs::create_dir_all(&outpath) - .await - .context("Failed to create directory")?; - } else { - if let Some(parent) = outpath.parent() { - tokio::fs::create_dir_all(parent) - .await - .context("Failed to create parent directory")?; - } - - let mut outfile = - std::fs::File::create(&outpath).context("Failed to create output file")?; - std::io::copy(&mut file, &mut outfile).context("Failed to extract file")?; - } - - // Set permissions on Unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Some(mode) = file.unix_mode() { - std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode)).ok(); - } - } - } - - Ok(()) - } - - /// Extract a TAR archive (including compressed variants) - async fn extract_tar(&self, archive_path: &Path, extract_dir: &Path) -> Result<()> { - let file = std::fs::File::open(archive_path).context("Failed to open TAR archive")?; - - // Determine if the archive is compressed - let ext = archive_path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); - - let decoder: Box = match ext { - "gz" | "tgz" => Box::new(flate2::read::GzDecoder::new(file)), - "bz2" => Box::new(bzip2::read::BzDecoder::new(file)), - "xz" => Box::new(xz2::read::XzDecoder::new(file)), - _ => Box::new(file), - }; - - let mut archive = tar::Archive::new(decoder); - archive - .unpack(extract_dir) - .context("Failed to extract TAR archive")?; - - Ok(()) - } - - /// Find the binary executable in a directory - async fn find_binary_in_dir(&self, dir: &Path) -> Result { - Box::pin(self.find_binary_in_dir_impl(dir)).await - } - - /// Implementation of find_binary_in_dir with proper boxing for recursion - fn find_binary_in_dir_impl<'a>( - &'a self, - dir: &'a Path, - ) -> std::pin::Pin> + 'a>> { - Box::pin(async move { - let mut entries = tokio::fs::read_dir(dir) - .await - .context("Failed to read extraction directory")?; - - while let Some(entry) = entries - .next_entry() - .await - .context("Failed to read directory entry")? - { - let path = entry.path(); - - if path.is_file() { - // Check if it's an executable - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let metadata = tokio::fs::metadata(&path).await?; - if metadata.permissions().mode() & 0o111 != 0 { - // Check if filename matches "vtcode" - if let Some(name) = path.file_name() { - if name.to_string_lossy().starts_with("vtcode") { - return Ok(path); - } - } - } - } - - #[cfg(windows)] - { - if let Some(name) = path.file_name() { - let name = name.to_string_lossy(); - if name.starts_with("vtcode") && name.ends_with(".exe") { - return Ok(path); - } - } - } - } else if path.is_dir() { - // Recursively search subdirectories - if let Ok(binary) = self.find_binary_in_dir_impl(&path).await { - return Ok(binary); - } - } - } - - anyhow::bail!("Binary not found in extracted archive") - }) - } - - /// Set executable permissions (Unix only) - #[cfg(unix)] - fn set_executable_permissions(&self, path: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - - let metadata = std::fs::metadata(path)?; - let mut permissions = metadata.permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(path, permissions)?; - - tracing::info!("Set executable permissions: {:?}", path); - - Ok(()) - } - - /// Replace the current executable with the new one - async fn replace_executable(&self, new_binary: &Path, current_exe: &Path) -> Result<()> { - // On Windows, we can't replace a running executable directly - // We need to use a different approach - #[cfg(windows)] - { - self.replace_executable_windows(new_binary, current_exe) - .await - } - - // On Unix, we can replace the executable directly - #[cfg(unix)] - { - self.replace_executable_unix(new_binary, current_exe).await - } - } - - /// Replace executable on Unix systems - #[cfg(unix)] - async fn replace_executable_unix(&self, new_binary: &Path, current_exe: &Path) -> Result<()> { - // Copy the new binary over the current one - tokio::fs::copy(new_binary, current_exe) - .await - .context("Failed to copy new binary")?; - - tracing::info!("Replaced executable: {:?}", current_exe); - - Ok(()) - } - - /// Replace executable on Windows systems - #[cfg(windows)] - async fn replace_executable_windows( - &self, - new_binary: &Path, - current_exe: &Path, - ) -> Result<()> { - // On Windows, we need to rename the current executable and then copy the new one - let backup_path = current_exe.with_extension("exe.old"); - - // Remove old backup if it exists - if backup_path.exists() { - tokio::fs::remove_file(&backup_path).await.ok(); - } - - // Rename current executable - tokio::fs::rename(current_exe, &backup_path) - .await - .context("Failed to rename current executable")?; - - // Copy new binary - match tokio::fs::copy(new_binary, current_exe).await { - Ok(_) => { - tracing::info!("Replaced executable: {:?}", current_exe); - Ok(()) - } - Err(e) => { - // Restore backup on failure - tokio::fs::rename(&backup_path, current_exe).await.ok(); - Err(e).context("Failed to copy new binary") - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_archive() { - let config = UpdateConfig::default(); - let installer = UpdateInstaller::new(config).unwrap(); - - assert!(installer.is_archive(Path::new("file.tar.gz"))); - assert!(installer.is_archive(Path::new("file.zip"))); - assert!(installer.is_archive(Path::new("file.tgz"))); - assert!(!installer.is_archive(Path::new("file.bin"))); - assert!(!installer.is_archive(Path::new("file"))); - } - - #[test] - fn test_installer_creation() { - let config = UpdateConfig::default(); - let installer = UpdateInstaller::new(config); - assert!(installer.is_ok()); - } -} diff --git a/vtcode-core/src/update/mod.rs b/vtcode-core/src/update/mod.rs deleted file mode 100644 index 5f404d7e0..000000000 --- a/vtcode-core/src/update/mod.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Self-update mechanism for vtcode -//! -//! This module provides automatic version checking, downloading updates from GitHub releases, -//! verifying binary integrity, managing backups and rollbacks, and handling cross-platform -//! compatibility. - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -mod checker; -mod config; -mod downloader; -mod installer; -mod rollback; -mod verifier; - -pub use checker::UpdateChecker; -pub use config::{UpdateChannel, UpdateConfig, UpdateFrequency}; -pub use downloader::UpdateDownloader; -pub use installer::UpdateInstaller; -pub use rollback::RollbackManager; -pub use verifier::UpdateVerifier; - -/// Current version of vtcode -pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// GitHub repository for releases -pub const GITHUB_REPO_OWNER: &str = "vinhnx"; -pub const GITHUB_REPO_NAME: &str = "vtcode"; - -/// Update status information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateStatus { - pub current_version: String, - pub latest_version: Option, - pub update_available: bool, - pub download_url: Option, - pub release_notes: Option, - pub last_checked: Option>, -} - -/// Update result after installation -#[derive(Debug, Clone)] -pub struct UpdateResult { - pub success: bool, - pub old_version: String, - pub new_version: String, - pub backup_path: Option, - pub requires_restart: bool, -} - -/// Main update manager coordinating all update operations -pub struct UpdateManager { - config: UpdateConfig, - checker: UpdateChecker, - downloader: UpdateDownloader, - installer: UpdateInstaller, - verifier: UpdateVerifier, - rollback: RollbackManager, -} - -impl UpdateManager { - /// Create a new update manager with the given configuration - pub fn new(config: UpdateConfig) -> Result { - let checker = UpdateChecker::new(config.clone())?; - let downloader = UpdateDownloader::new(config.clone())?; - let installer = UpdateInstaller::new(config.clone())?; - let verifier = UpdateVerifier::new(config.clone())?; - let rollback = RollbackManager::new(config.clone())?; - - Ok(Self { - config, - checker, - downloader, - installer, - verifier, - rollback, - }) - } - - /// Check if an update is available - pub async fn check_for_updates(&self) -> Result { - self.checker.check_for_updates().await - } - - /// Download and install an available update - pub async fn perform_update(&mut self) -> Result { - // Check for updates - let status = self.check_for_updates().await?; - - if !status.update_available { - anyhow::bail!("No update available"); - } - - // Some GitHub Release payloads may omit a top-level download_url. - // Try a graceful fallback by inspecting release assets for a downloadable URL. - let download_url = status - .download_url - .clone() - .with_context(|| { - // Provide a more helpful error message - if status.latest_version.is_some() { - let latest_version = status.latest_version.as_ref().unwrap(); - format!( - "No download URL available for version {}. This may be because:\n\ - 1. Pre-compiled binaries are not attached to this GitHub release\n\ - 2. No matching binary asset was found for your platform\n\ - \n\ - You can:\n\ - - Check for binaries manually at: https://github.com/vinhnx/vtcode/releases/tag/{}\n\ - - Build from source: cargo install vtcode\n\ - - Download source code and compile manually", - latest_version, latest_version - ) - } else { - "No download URL available".to_string() - } - })?; - let new_version = status - .latest_version - .context("No version information available")?; - - // Create backup before updating - let backup_path = self - .rollback - .create_backup() - .context("Failed to create backup")?; - - // Download the update - let download_path = self - .downloader - .download(&download_url) - .await - .context("Failed to download update")?; - - // Verify the downloaded binary - self.verifier - .verify(&download_path) - .await - .context("Failed to verify update")?; - - // Install the update - match self.installer.install(&download_path).await { - Ok(_) => Ok(UpdateResult { - success: true, - old_version: CURRENT_VERSION.to_string(), - new_version, - backup_path: Some(backup_path), - requires_restart: true, - }), - Err(e) => { - // Rollback on failure - self.rollback - .rollback(&backup_path) - .context("Failed to rollback after installation failure")?; - Err(e).context("Update installation failed and was rolled back") - } - } - } - - /// Rollback to a previous version - pub fn rollback_to_backup(&self, backup_path: &PathBuf) -> Result<()> { - self.rollback.rollback(backup_path) - } - - /// Clean up old backups - pub fn cleanup_old_backups(&self) -> Result<()> { - self.rollback.cleanup_old_backups() - } - - /// Get the current configuration - pub fn config(&self) -> &UpdateConfig { - &self.config - } - - /// Update the configuration - pub fn set_config(&mut self, config: UpdateConfig) -> Result<()> { - self.config = config.clone(); - self.checker = UpdateChecker::new(config.clone())?; - self.downloader = UpdateDownloader::new(config.clone())?; - self.installer = UpdateInstaller::new(config.clone())?; - self.verifier = UpdateVerifier::new(config.clone())?; - self.rollback = RollbackManager::new(config)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_current_version() { - assert!(!CURRENT_VERSION.is_empty()); - assert!(CURRENT_VERSION.contains('.')); - } - - #[test] - fn test_github_repo_constants() { - assert_eq!(GITHUB_REPO_OWNER, "vinhnx"); - assert_eq!(GITHUB_REPO_NAME, "vtcode"); - } -} diff --git a/vtcode-core/src/update/rollback.rs b/vtcode-core/src/update/rollback.rs deleted file mode 100644 index 3cec9da44..000000000 --- a/vtcode-core/src/update/rollback.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! Backup and rollback management - -use super::config::UpdateConfig; -use anyhow::{Context, Result}; -use std::path::PathBuf; - -/// Manages backups and rollback operations -pub struct RollbackManager { - config: UpdateConfig, -} - -impl RollbackManager { - pub fn new(config: UpdateConfig) -> Result { - Ok(Self { config }) - } - - /// Create a backup of the current executable - pub fn create_backup(&self) -> Result { - self.config.ensure_directories()?; - - let current_exe = std::env::current_exe().context("Failed to get current executable")?; - - // Generate backup filename with timestamp - let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); - let backup_filename = format!("vtcode_backup_{}", timestamp); - - #[cfg(windows)] - let backup_filename = format!("{}.exe", backup_filename); - - let backup_path = self.config.backup_dir.join(backup_filename); - - tracing::info!("Creating backup: {:?}", backup_path); - - // Copy current executable to backup location - std::fs::copy(¤t_exe, &backup_path).context("Failed to create backup")?; - - // Set executable permissions on Unix - #[cfg(unix)] - { - let metadata = std::fs::metadata(¤t_exe)?; - let permissions = metadata.permissions(); - std::fs::set_permissions(&backup_path, permissions)?; - } - - tracing::info!("Backup created successfully"); - - // Clean up old backups - self.cleanup_old_backups()?; - - Ok(backup_path) - } - - /// Rollback to a previous backup - pub fn rollback(&self, backup_path: &PathBuf) -> Result<()> { - if !backup_path.exists() { - anyhow::bail!("Backup file does not exist: {:?}", backup_path); - } - - let current_exe = std::env::current_exe().context("Failed to get current executable")?; - - tracing::info!("Rolling back to: {:?}", backup_path); - - // On Windows, we need special handling - #[cfg(windows)] - { - self.rollback_windows(backup_path, ¤t_exe)?; - } - - // On Unix, we can directly replace - #[cfg(unix)] - { - self.rollback_unix(backup_path, ¤t_exe)?; - } - - tracing::info!("Rollback completed successfully"); - - Ok(()) - } - - /// Rollback on Unix systems - #[cfg(unix)] - fn rollback_unix(&self, backup_path: &PathBuf, current_exe: &PathBuf) -> Result<()> { - std::fs::copy(backup_path, current_exe).context("Failed to restore backup")?; - - // Restore executable permissions - let metadata = std::fs::metadata(backup_path)?; - let permissions = metadata.permissions(); - std::fs::set_permissions(current_exe, permissions)?; - - Ok(()) - } - - /// Rollback on Windows systems - #[cfg(windows)] - fn rollback_windows(&self, backup_path: &PathBuf, current_exe: &PathBuf) -> Result<()> { - let temp_path = current_exe.with_extension("exe.tmp"); - - // Remove temp file if it exists - if temp_path.exists() { - std::fs::remove_file(&temp_path).ok(); - } - - // Rename current executable to temp - std::fs::rename(current_exe, &temp_path).context("Failed to rename current executable")?; - - // Copy backup to current location - match std::fs::copy(backup_path, current_exe) { - Ok(_) => { - // Remove temp file - std::fs::remove_file(&temp_path).ok(); - Ok(()) - } - Err(e) => { - // Restore from temp on failure - std::fs::rename(&temp_path, current_exe).ok(); - Err(e).context("Failed to restore backup") - } - } - } - - /// Clean up old backups, keeping only the most recent ones - pub fn cleanup_old_backups(&self) -> Result<()> { - let backup_dir = &self.config.backup_dir; - - if !backup_dir.exists() { - return Ok(()); - } - - // Get all backup files - let mut backups: Vec<_> = std::fs::read_dir(backup_dir) - .context("Failed to read backup directory")? - .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .file_name() - .to_string_lossy() - .starts_with("vtcode_backup_") - }) - .collect(); - - // Sort by modification time (newest first) - backups.sort_by_key(|entry| { - entry - .metadata() - .and_then(|m| m.modified()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH) - }); - backups.reverse(); - - // Remove old backups beyond max_backups - if backups.len() > self.config.max_backups { - for backup in backups.iter().skip(self.config.max_backups) { - let path = backup.path(); - tracing::info!("Removing old backup: {:?}", path); - std::fs::remove_file(&path).ok(); - } - } - - Ok(()) - } - - /// List all available backups - pub fn list_backups(&self) -> Result> { - let backup_dir = &self.config.backup_dir; - - if !backup_dir.exists() { - return Ok(Vec::new()); - } - - let mut backups: Vec<_> = std::fs::read_dir(backup_dir) - .context("Failed to read backup directory")? - .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .file_name() - .to_string_lossy() - .starts_with("vtcode_backup_") - }) - .map(|entry| entry.path()) - .collect(); - - // Sort by modification time (newest first) - backups.sort_by_key(|path| { - std::fs::metadata(path) - .and_then(|m| m.modified()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH) - }); - backups.reverse(); - - Ok(backups) - } - - /// Get information about a backup - pub fn get_backup_info(&self, backup_path: &PathBuf) -> Result { - let metadata = std::fs::metadata(backup_path).context("Failed to read backup metadata")?; - - let size = metadata.len(); - let modified = metadata - .modified() - .context("Failed to get modification time")?; - - let filename = backup_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - .to_string(); - - Ok(BackupInfo { - path: backup_path.clone(), - filename, - size, - modified, - }) - } -} - -/// Information about a backup -#[derive(Debug, Clone)] -pub struct BackupInfo { - pub path: PathBuf, - pub filename: String, - pub size: u64, - pub modified: std::time::SystemTime, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rollback_manager_creation() { - let config = UpdateConfig::default(); - let manager = RollbackManager::new(config); - assert!(manager.is_ok()); - } - - #[test] - fn test_list_backups_empty() { - let config = UpdateConfig::default(); - let manager = RollbackManager::new(config).unwrap(); - let backups = manager.list_backups().unwrap(); - assert!(backups.is_empty() || !backups.is_empty()); // May have existing backups - } -} diff --git a/vtcode-core/src/update/verifier.rs b/vtcode-core/src/update/verifier.rs deleted file mode 100644 index a503a7247..000000000 --- a/vtcode-core/src/update/verifier.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Binary verification for downloaded updates - -use super::config::UpdateConfig; -use anyhow::{Context, Result}; -use sha2::{Digest, Sha256}; -use std::path::Path; - -/// Handles verification of downloaded update files -pub struct UpdateVerifier { - config: UpdateConfig, -} - -impl UpdateVerifier { - pub fn new(config: UpdateConfig) -> Result { - Ok(Self { config }) - } - - /// Verify the integrity of a downloaded update - pub async fn verify(&self, path: &Path) -> Result<()> { - tracing::info!("Verifying update file: {:?}", path); - - // Check if file exists - if !path.exists() { - anyhow::bail!("Update file does not exist: {:?}", path); - } - - // Verify checksum if enabled - if self.config.verify_checksums { - self.verify_checksum(path) - .await - .context("Checksum verification failed")?; - } - - // Verify signature if enabled - if self.config.verify_signatures { - self.verify_signature(path) - .await - .context("Signature verification failed")?; - } - - // Verify the binary is executable (on Unix) - #[cfg(unix)] - self.verify_executable(path)?; - - tracing::info!("Update file verification successful"); - - Ok(()) - } - - /// Verify the checksum of the downloaded file - async fn verify_checksum(&self, path: &Path) -> Result<()> { - let checksum_path = path.with_extension("sha256"); - - if !checksum_path.exists() { - tracing::warn!("Checksum file not found, skipping verification"); - return Ok(()); - } - - // Read expected checksum - let expected_checksum = tokio::fs::read_to_string(&checksum_path) - .await - .context("Failed to read checksum file")?; - - let expected_checksum = expected_checksum - .split_whitespace() - .next() - .context("Invalid checksum format")? - .trim(); - - // Calculate actual checksum - let actual_checksum = self.calculate_sha256(path).await?; - - if actual_checksum.to_lowercase() != expected_checksum.to_lowercase() { - anyhow::bail!( - "Checksum mismatch: expected {}, got {}", - expected_checksum, - actual_checksum - ); - } - - tracing::info!("Checksum verification passed"); - - Ok(()) - } - - /// Calculate SHA256 checksum of a file - async fn calculate_sha256(&self, path: &Path) -> Result { - let content = tokio::fs::read(path) - .await - .context("Failed to read file for checksum")?; - - let mut hasher = Sha256::new(); - hasher.update(&content); - let result = hasher.finalize(); - - Ok(format!("{:x}", result)) - } - - /// Verify the signature of the downloaded file - async fn verify_signature(&self, path: &Path) -> Result<()> { - let signature_path = path.with_extension("sig"); - - if !signature_path.exists() { - tracing::warn!("Signature file not found, skipping verification"); - return Ok(()); - } - - // For now, we just check that the signature file exists - // In a production implementation, you would use a proper signature verification library - // such as `ed25519-dalek` or `rsa` to verify the signature against a public key - - tracing::warn!( - "Signature verification not fully implemented - signature file exists but not verified" - ); - - Ok(()) - } - - /// Verify the binary is executable (Unix only) - #[cfg(unix)] - fn verify_executable(&self, path: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - - let metadata = std::fs::metadata(path).context("Failed to read file metadata")?; - let permissions = metadata.permissions(); - - // Check if the file has execute permissions - if permissions.mode() & 0o111 == 0 { - tracing::warn!("Binary is not executable, attempting to set permissions"); - self.make_executable(path)?; - } - - Ok(()) - } - - /// Make a file executable (Unix only) - #[cfg(unix)] - fn make_executable(&self, path: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - - let metadata = std::fs::metadata(path)?; - let mut permissions = metadata.permissions(); - permissions.set_mode(permissions.mode() | 0o111); - std::fs::set_permissions(path, permissions)?; - - tracing::info!("Set executable permissions on binary"); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - - #[tokio::test] - async fn test_calculate_sha256() { - let config = UpdateConfig::default(); - let verifier = UpdateVerifier::new(config).unwrap(); - - // Create a temporary file - let temp_dir = tempfile::tempdir().unwrap(); - let file_path = temp_dir.path().join("test.txt"); - let mut file = std::fs::File::create(&file_path).unwrap(); - file.write_all(b"test content").unwrap(); - - // Calculate checksum - let checksum = verifier.calculate_sha256(&file_path).await.unwrap(); - - // Verify it's a valid SHA256 hash (64 hex characters) - assert_eq!(checksum.len(), 64); - assert!(checksum.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn test_verifier_creation() { - let config = UpdateConfig::default(); - let verifier = UpdateVerifier::new(config); - assert!(verifier.is_ok()); - } -} diff --git a/vtcode-core/src/utils/ansi.rs b/vtcode-core/src/utils/ansi.rs index 7deaa608c..37a938ba6 100644 --- a/vtcode-core/src/utils/ansi.rs +++ b/vtcode-core/src/utils/ansi.rs @@ -15,7 +15,6 @@ use anstyle_query::{clicolor, clicolor_force, no_color, term_supports_color}; use anyhow::{Result, anyhow}; use ratatui::style::{Color as RatColor, Modifier as RatModifier, Style as RatatuiStyle}; use std::io::{self, Write}; -use tui_markdown::from_str as parse_markdown_text; /// Styles available for rendering messages #[derive(Clone, Copy)] @@ -557,106 +556,66 @@ impl InlineSink { base_style: Style, ) -> (Vec>, Vec, bool) { let fallback = self.resolve_fallback_style(base_style); - let parsed = parse_markdown_text(text); - let mut prepared = Vec::new(); - let mut plain = Vec::new(); + let theme_styles = theme::active_styles(); + let mut rendered = render_markdown_to_lines(text, base_style, &theme_styles, None); - for line in parsed.lines.into_iter() { + if rendered.is_empty() { + rendered.push(MarkdownLine::default()); + } + + let mut prepared = Vec::with_capacity(rendered.len()); + let mut plain = Vec::with_capacity(rendered.len()); + + for line in rendered { let mut segments = Vec::new(); let mut plain_line = String::new(); - let line_style = RatatuiStyle::default() - .patch(parsed.style) - .patch(line.style); - for span in line.spans.into_iter() { - let content = span.content.into_owned(); - if content.is_empty() { - continue; - } - let span_style = line_style.patch(span.style); - let inline_style = self.inline_style_from_ratatui(span_style, &fallback); - plain_line.push_str(&content); + let has_content = line + .segments + .iter() + .any(|segment| !segment.text.trim().is_empty()); + + if !indent.is_empty() && has_content { segments.push(InlineSegment { - text: content, - style: inline_style, + text: indent.to_string(), + style: fallback.clone(), }); + plain_line.push_str(indent); } - if !indent.is_empty() && !plain_line.is_empty() { - segments.insert( - 0, - InlineSegment { - text: indent.to_string(), - style: fallback.clone(), - }, - ); - plain_line.insert_str(0, indent); - } - - prepared.push(segments); - plain.push(plain_line); - } - - let mut filtered_prepared = Vec::new(); - let mut filtered_plain = Vec::new(); - let mut in_code_fence = false; - - for (segments, plain_line) in prepared.into_iter().zip(plain.into_iter()) { - let trimmed = plain_line.trim(); - - if trimmed.starts_with("```") { - let inline_candidate = trimmed.ends_with("```") && trimmed.len() > 6; - if inline_candidate { - let inner = trimmed - .trim_start_matches("```") - .trim_end_matches("```") - .trim(); - - if !inner.is_empty() { - let mut code_style = fallback.clone(); - code_style.bold = true; - filtered_prepared.push(vec![InlineSegment { - text: inner.to_string(), - style: code_style.clone(), - }]); - filtered_plain.push(inner.to_string()); - } else { - filtered_prepared.push(Vec::new()); - filtered_plain.push(String::new()); - } + for segment in line.segments { + if segment.text.is_empty() { continue; } - - if in_code_fence { - in_code_fence = false; - continue; - } else { - in_code_fence = true; - continue; + let converted = convert_to_inline_style(segment.style); + let mut inline_style = fallback.clone(); + if let Some(color) = converted.color { + inline_style.color = Some(color); } + inline_style.bold = converted.bold; + inline_style.italic = converted.italic; + plain_line.push_str(&segment.text); + segments.push(InlineSegment { + text: segment.text, + style: inline_style, + }); } - if in_code_fence { - filtered_prepared.push(segments); - filtered_plain.push(plain_line); - continue; - } - - filtered_prepared.push(segments); - filtered_plain.push(plain_line); + prepared.push(segments); + plain.push(plain_line); } - if filtered_prepared.is_empty() { - filtered_prepared.push(Vec::new()); - filtered_plain.push(String::new()); + if prepared.is_empty() { + prepared.push(Vec::new()); + plain.push(String::new()); } - let last_empty = filtered_plain + let last_empty = plain .last() .map(|line| line.trim().is_empty()) .unwrap_or(true); - (filtered_prepared, filtered_plain, last_empty) + (prepared, plain, last_empty) } fn write_markdown( diff --git a/vtcode-core/src/utils/dot_config.rs b/vtcode-core/src/utils/dot_config.rs index 0319139eb..a9eec984a 100644 --- a/vtcode-core/src/utils/dot_config.rs +++ b/vtcode-core/src/utils/dot_config.rs @@ -82,12 +82,6 @@ pub struct UiConfig { pub syntax_highlighting: bool, pub auto_complete: bool, pub history_size: usize, - #[serde(default = "default_file_browser_view")] - pub file_browser_default_view: String, // "tree" or "list" -} - -fn default_file_browser_view() -> String { - "tree".to_string() } impl Default for DotConfig { @@ -141,7 +135,6 @@ impl Default for UiConfig { syntax_highlighting: true, auto_complete: true, history_size: 1000, - file_browser_default_view: "list".to_string(), } } } diff --git a/vtcode-core/tests/pty_test.rs b/vtcode-core/tests/pty_test.rs index 8d02b8898..1f02ca83d 100644 --- a/vtcode-core/tests/pty_test.rs +++ b/vtcode-core/tests/pty_test.rs @@ -5,14 +5,16 @@ use vtcode_core::tools::ToolRegistry; #[tokio::test] async fn test_pty_functionality() { let mut registry = ToolRegistry::new(PathBuf::from(".")).await; + registry.allow_all_tools().await.ok(); - // Test a simple echo command + // Run an allow-listed command and verify output is captured let result = registry .execute_tool( "run_pty_cmd", json!({ - "command": "echo", - "args": ["hello world"] + "mode": "pty", + "command": "ls", + "args": ["Cargo.toml"], }), ) .await; @@ -21,21 +23,23 @@ async fn test_pty_functionality() { let response = result.unwrap(); assert_eq!(response["success"], true); - let output = response["output"].as_str().unwrap(); - assert!(output.contains("hello world")); + let output = response["output"].as_str().unwrap_or_default(); + assert!(output.contains("Cargo.toml")); } #[tokio::test] async fn test_pty_functionality_with_exit_code() { let mut registry = ToolRegistry::new(PathBuf::from(".")).await; + registry.allow_all_tools().await.ok(); - // Test a command that exits with code 1 + // Run an allow-listed command that exits with a non-zero code let result = registry .execute_tool( "run_pty_cmd", json!({ - "command": "sh", - "args": ["-c", "exit 1"] + "mode": "pty", + "command": "ls", + "args": ["this_file_does_not_exist"], }), ) .await; @@ -46,4 +50,99 @@ async fn test_pty_functionality_with_exit_code() { // The command should execute successfully (no error in execution) // but the exit code should be 1 assert_eq!(response["success"], true); + assert_eq!(response["code"].as_i64(), Some(1)); +} + +#[cfg(unix)] +#[tokio::test] +async fn test_pty_shell_option_runs_through_requested_shell() { + let mut registry = ToolRegistry::new(PathBuf::from(".")).await; + registry.allow_all_tools().await.ok(); + + let result = registry + .execute_tool( + "run_pty_cmd", + json!({ + "mode": "pty", + "shell": "sh", + "command": "echo shell-check", + }), + ) + .await + .expect("shell run result"); + + assert_eq!(result["success"], true); + let output = result["output"].as_str().unwrap_or_default(); + assert!(output.contains("shell-check")); +} + +#[cfg(unix)] +#[tokio::test] +async fn test_create_pty_session_uses_requested_shell() { + let mut registry = ToolRegistry::new(PathBuf::from(".")).await; + registry.allow_all_tools().await.ok(); + + let create_result = registry + .execute_tool( + "create_pty_session", + json!({ + "session_id": "shell-session", + "command": "bash", + "shell": "/bin/sh" + }), + ) + .await + .expect("create session result"); + + assert_eq!(create_result["success"], true); + assert_eq!(create_result["session_id"], "shell-session"); + let command = create_result["command"].as_str().unwrap_or_default(); + assert!(command.contains("sh")); + + registry + .execute_tool( + "close_pty_session", + json!({ + "session_id": "shell-session" + }), + ) + .await + .expect("close session result"); +} + +#[tokio::test] +async fn test_pty_output_has_no_ansi_codes() { + let mut registry = ToolRegistry::new(PathBuf::from(".")).await; + registry.allow_all_tools().await.ok(); + + let result = registry + .execute_tool( + "run_pty_cmd", + json!({ + "mode": "pty", + "command": "ls", + "args": ["-a"], + }), + ) + .await + .expect("ls result"); + + assert_eq!(result["success"], true); + let output = result["output"].as_str().unwrap_or_default(); + + // Check that output doesn't contain ANSI escape sequences + assert!( + !output.contains("\x1b["), + "Output should not contain ANSI escape codes" + ); + assert!( + !output.contains("\u{001b}["), + "Output should not contain ANSI escape codes" + ); + + // Verify we got actual file names + assert!( + output.contains("Cargo.toml") || output.contains("cargo") || output.len() > 10, + "Output should contain actual filenames, not just escape codes" + ); } diff --git a/vtcode-core/tests/update_tests.rs b/vtcode-core/tests/update_tests.rs deleted file mode 100644 index 2bc924a81..000000000 --- a/vtcode-core/tests/update_tests.rs +++ /dev/null @@ -1,297 +0,0 @@ -//! Integration tests for the self-update functionality - -use anyhow::Result; -use vtcode_core::update::{UpdateChannel, UpdateConfig, UpdateFrequency, UpdateManager}; - -fn set_env_var(key: &str, value: &str) { - // SAFETY: tests run in isolation and only touch process-local env vars. - unsafe { - std::env::set_var(key, value); - } -} - -fn remove_env_var(key: &str) { - // SAFETY: reverting env changes made in this test scope. - unsafe { - std::env::remove_var(key); - } -} - -#[test] -fn test_update_config_default() { - let config = UpdateConfig::default(); - - assert!(!config.enabled); - assert_eq!(config.channel, UpdateChannel::Stable); - assert_eq!(config.frequency, UpdateFrequency::Daily); - assert!(!config.auto_download); - assert!(!config.auto_install); - assert_eq!(config.max_backups, 3); - assert!(config.verify_signatures); - assert!(config.verify_checksums); -} - -#[test] -fn test_update_config_from_env() { - // Set environment variables - unsafe { - // SAFETY: The test controls these variables and resets them before finishing. - std::env::set_var("VTCODE_UPDATE_ENABLED", "false"); - std::env::set_var("VTCODE_UPDATE_CHANNEL", "beta"); - std::env::set_var("VTCODE_UPDATE_FREQUENCY", "weekly"); - std::env::set_var("VTCODE_UPDATE_AUTO_DOWNLOAD", "true"); - std::env::set_var("VTCODE_UPDATE_MAX_BACKUPS", "5"); - } - - let config = UpdateConfig::from_env().unwrap(); - - assert!(!config.enabled); - assert_eq!(config.channel, UpdateChannel::Beta); - assert_eq!(config.frequency, UpdateFrequency::Weekly); - assert!(config.auto_download); - assert_eq!(config.max_backups, 5); - - // Clean up - unsafe { - // SAFETY: Restores the environment to the state it had before the test mutated it. - std::env::remove_var("VTCODE_UPDATE_ENABLED"); - std::env::remove_var("VTCODE_UPDATE_CHANNEL"); - std::env::remove_var("VTCODE_UPDATE_FREQUENCY"); - std::env::remove_var("VTCODE_UPDATE_AUTO_DOWNLOAD"); - std::env::remove_var("VTCODE_UPDATE_MAX_BACKUPS"); - } -} - -#[test] -fn test_update_channel_display() { - assert_eq!(UpdateChannel::Stable.to_string(), "stable"); - assert_eq!(UpdateChannel::Beta.to_string(), "beta"); - assert_eq!(UpdateChannel::Nightly.to_string(), "nightly"); -} - -#[test] -fn test_update_manager_creation() { - let config = UpdateConfig::default(); - let manager = UpdateManager::new(config); - assert!(manager.is_ok()); -} - -#[tokio::test] -async fn test_update_checker_version_parsing() { - use vtcode_core::update::UpdateChecker; - - let config = UpdateConfig::default(); - let checker = UpdateChecker::new(config).unwrap(); - - // Test version parsing through reflection (if methods are public) - // This is a simplified test - in practice you'd need to expose the parse_version method - // or test it indirectly through the public API -} - -#[test] -fn test_update_config_ensure_directories() { - let temp_dir = tempfile::tempdir().unwrap(); - let mut config = UpdateConfig::default(); - config.update_dir = temp_dir.path().join("updates"); - config.backup_dir = temp_dir.path().join("backups"); - - assert!(config.ensure_directories().is_ok()); - assert!(config.update_dir.exists()); - assert!(config.backup_dir.exists()); -} - -#[test] -fn test_update_frequency_variants() { - let frequencies = vec![ - UpdateFrequency::Always, - UpdateFrequency::Daily, - UpdateFrequency::Weekly, - UpdateFrequency::Never, - ]; - - for freq in frequencies { - // Just ensure they can be created and compared - assert_eq!(freq, freq); - } -} - -#[test] -fn test_update_channel_variants() { - let channels = vec![ - UpdateChannel::Stable, - UpdateChannel::Beta, - UpdateChannel::Nightly, - ]; - - for channel in channels { - // Just ensure they can be created and compared - assert_eq!(channel, channel); - } -} - -#[test] -fn test_github_api_base_url() { - let config = UpdateConfig::default(); - assert_eq!(config.github_api_base(), "https://api.github.com"); - - let mut config_custom = UpdateConfig::default(); - config_custom.github_api_base = Some("https://github.company.com/api/v3".to_string()); - assert_eq!( - config_custom.github_api_base(), - "https://github.company.com/api/v3" - ); -} - -#[tokio::test] -async fn test_rollback_manager_creation() { - use vtcode_core::update::RollbackManager; - - let config = UpdateConfig::default(); - let manager = RollbackManager::new(config); - assert!(manager.is_ok()); -} - -#[tokio::test] -async fn test_rollback_manager_list_backups() { - use vtcode_core::update::RollbackManager; - - let temp_dir = tempfile::tempdir().unwrap(); - let mut config = UpdateConfig::default(); - config.backup_dir = temp_dir.path().join("backups"); - config.ensure_directories().unwrap(); - - let manager = RollbackManager::new(config).unwrap(); - let backups = manager.list_backups().unwrap(); - - // Should be empty initially - assert!(backups.is_empty()); -} - -#[test] -fn test_update_config_serialization() { - let config = UpdateConfig::default(); - - // Test that config can be serialized - let json = serde_json::to_string(&config); - assert!(json.is_ok()); -} - -#[test] -fn test_update_status_serialization() { - use vtcode_core::update::UpdateStatus; - - let status = UpdateStatus { - current_version: "0.33.1".to_string(), - latest_version: Some("0.34.0".to_string()), - update_available: true, - download_url: Some("https://example.com/download".to_string()), - release_notes: Some("New features".to_string()), - last_checked: Some(chrono::Utc::now()), - }; - - // Test serialization - let json = serde_json::to_string(&status); - assert!(json.is_ok()); - - // Test deserialization - let json_str = json.unwrap(); - let deserialized: Result = serde_json::from_str(&json_str); - assert!(deserialized.is_ok()); -} - -#[tokio::test] -async fn test_update_verifier_creation() { - use vtcode_core::update::UpdateVerifier; - - let config = UpdateConfig::default(); - let verifier = UpdateVerifier::new(config); - assert!(verifier.is_ok()); -} - -#[tokio::test] -async fn test_update_downloader_creation() { - use vtcode_core::update::UpdateDownloader; - - let config = UpdateConfig::default(); - let downloader = UpdateDownloader::new(config); - assert!(downloader.is_ok()); -} - -#[tokio::test] -async fn test_update_installer_creation() { - use vtcode_core::update::UpdateInstaller; - - let config = UpdateConfig::default(); - let installer = UpdateInstaller::new(config); - assert!(installer.is_ok()); -} - -#[test] -fn test_current_version_constant() { - use vtcode_core::update::CURRENT_VERSION; - - assert!(!CURRENT_VERSION.is_empty()); - assert!(CURRENT_VERSION.contains('.')); - - // Should be a valid semver - let parts: Vec<&str> = CURRENT_VERSION.split('.').collect(); - assert!(parts.len() >= 3); -} - -#[test] -fn test_github_repo_constants() { - use vtcode_core::update::{GITHUB_REPO_NAME, GITHUB_REPO_OWNER}; - - assert_eq!(GITHUB_REPO_OWNER, "vinhnx"); - assert_eq!(GITHUB_REPO_NAME, "vtcode"); -} - -#[test] -fn test_update_config_modification() { - let mut config = UpdateConfig::default(); - - // Test modifying configuration - config.enabled = false; - config.channel = UpdateChannel::Beta; - config.frequency = UpdateFrequency::Weekly; - config.auto_download = true; - config.auto_install = true; - config.max_backups = 10; - - assert!(!config.enabled); - assert_eq!(config.channel, UpdateChannel::Beta); - assert_eq!(config.frequency, UpdateFrequency::Weekly); - assert!(config.auto_download); - assert!(config.auto_install); - assert_eq!(config.max_backups, 10); -} - -#[tokio::test] -async fn test_update_manager_config_access() { - let config = UpdateConfig::default(); - let manager = UpdateManager::new(config.clone()).unwrap(); - - // Test that we can access the configuration - let manager_config = manager.config(); - assert_eq!(manager_config.enabled, config.enabled); - assert_eq!(manager_config.channel, config.channel); -} - -#[tokio::test] -async fn test_update_manager_config_update() { - let config = UpdateConfig::default(); - let mut manager = UpdateManager::new(config).unwrap(); - - // Create a new configuration - let mut new_config = UpdateConfig::default(); - new_config.channel = UpdateChannel::Beta; - new_config.frequency = UpdateFrequency::Weekly; - - // Update the manager's configuration - let result = manager.set_config(new_config.clone()); - assert!(result.is_ok()); - - // Verify the configuration was updated - assert_eq!(manager.config().channel, UpdateChannel::Beta); - assert_eq!(manager.config().frequency, UpdateFrequency::Weekly); -} diff --git a/vtcode-exec-events/Cargo.toml b/vtcode-exec-events/Cargo.toml index 9e07b3044..1dfec0171 100644 --- a/vtcode-exec-events/Cargo.toml +++ b/vtcode-exec-events/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-exec-events" -version = "0.38.1" +version = "0.38.2" edition = "2024" description = "Structured execution telemetry event schema used across VTCode crates." license = "MIT OR Apache-2.0" diff --git a/vtcode-indexer/Cargo.toml b/vtcode-indexer/Cargo.toml index 10867b01a..2bb95301a 100644 --- a/vtcode-indexer/Cargo.toml +++ b/vtcode-indexer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-indexer" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "Workspace-friendly code indexer extracted from VTCode" diff --git a/vtcode-llm/Cargo.toml b/vtcode-llm/Cargo.toml index 9ff0ccb7d..9f99aa564 100644 --- a/vtcode-llm/Cargo.toml +++ b/vtcode-llm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-llm" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "Prototype extraction of VTCode's unified LLM client layer" @@ -37,7 +37,7 @@ mock = ["dep:async-trait"] [dependencies] anyhow = "1.0" async-trait = { version = "0.1", optional = true } -vtcode-commons = { path = "../vtcode-commons", version = "0.38.1" } +vtcode-commons = { path = "../vtcode-commons", version = "0.38.2" } vtcode-core = { path = "../vtcode-core" } [dev-dependencies] diff --git a/vtcode-markdown-store/Cargo.toml b/vtcode-markdown-store/Cargo.toml index 3d35b24ac..33848164d 100644 --- a/vtcode-markdown-store/Cargo.toml +++ b/vtcode-markdown-store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-markdown-store" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "Markdown-backed storage utilities extracted from VTCode" diff --git a/vtcode-tools/Cargo.toml b/vtcode-tools/Cargo.toml index b49d9e401..d0e64d912 100644 --- a/vtcode-tools/Cargo.toml +++ b/vtcode-tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-tools" -version = "0.38.1" +version = "0.38.2" edition = "2024" authors = ["vinhnx "] description = "Prototype extraction of VTCode's modular tool registry" @@ -18,7 +18,7 @@ policies = [] examples = [] [dependencies] -vtcode-commons = { path = "../vtcode-commons", version = "0.38.1" } +vtcode-commons = { path = "../vtcode-commons", version = "0.38.2" } vtcode-core = { path = "../vtcode-core" } [dev-dependencies] diff --git a/vtcode.toml b/vtcode.toml index 90bb46358..4f51ea10f 100644 --- a/vtcode.toml +++ b/vtcode.toml @@ -5,13 +5,13 @@ # Core agent behavior; see docs/config/CONFIGURATION_PRECEDENCE.md. [agent] # Primary LLM provider to use (e.g., "openai", "gemini", "anthropic", "openrouter") -provider = "openai" +provider = "ollama" # Environment variable containing the API key for the provider -api_key_env = "OPENAI_API_KEY" +api_key_env = "OLLAMA_API_KEY" # Default model to use when no specific model is specified -default_model = "gpt-5-nano" +default_model = "gpt-oss:120b-cloud" # Visual theme for the terminal interface theme = "ciapre-dark" @@ -279,6 +279,9 @@ stdout_tail_lines = 20 # Total lines to keep in PTY scrollback buffer scrollback_lines = 400 +# Preferred shell program for PTY sessions (defaults to $SHELL or auto-detect) +# preferred_shell = "/bin/zsh" + # Context management configuration - Controls conversation memory [context] # Maximum number of tokens to keep in context (affects model cost and performance) @@ -362,15 +365,15 @@ llm_router_model = "" # Model mapping for different task types [router.models] # Model for simple queries -simple = "gpt-5-nano" +simple = "gpt-oss:120b-cloud" # Model for standard tasks -standard = "gpt-5-nano" +standard = "gpt-oss:120b-cloud" # Model for complex tasks -complex = "gpt-5-nano" +complex = "gpt-oss:120b-cloud" # Model for code generation heavy tasks -codegen_heavy = "gpt-5-nano" +codegen_heavy = "gpt-oss:120b-cloud" # Model for information retrieval heavy tasks -retrieval_heavy = "gpt-5-nano" +retrieval_heavy = "gpt-oss:120b-cloud" # Router budget settings (if applicable) [router.budgets] @@ -604,12 +607,24 @@ workspace_trust = "full_auto" [mcp.allowlist.default] +[mcp.security] +auth_enabled = false + +[mcp.security.rate_limit] +requests_per_minute = 100 +concurrent_requests = 10 + [acp.zed.tools] read_file = true list_files = true [mcp.allowlist.providers] +[mcp.security.validation] +schema_validation_enabled = true +path_traversal_protection = true +max_argument_size = 1048576 + [hooks.lifecycle] session_start = [] session_end = [] diff --git a/vtcode.toml.example b/vtcode.toml.example index 95414d0b8..9f4704f78 100644 --- a/vtcode.toml.example +++ b/vtcode.toml.example @@ -242,11 +242,6 @@ inline_viewport_rows = 16 # Show timeline navigation panel (displays plan when available, timeline otherwise) show_timeline_pane = true -# File browser default view mode -# "list" - Show files in flat list (default, reliable Enter selection) -# "tree" - Show files in tree structure (toggle with 't' key) -file_browser_default_view = "list" - # Status line configuration [ui.status_line] # Status line mode ("auto", "command", "hidden") @@ -281,6 +276,9 @@ stdout_tail_lines = 20 # Total lines to keep in PTY scrollback buffer scrollback_lines = 400 +# Preferred shell program for PTY sessions (defaults to $SHELL or auto-detect) +# preferred_shell = "/bin/zsh" + # Context management configuration - Controls conversation memory [context] # Maximum number of tokens to keep in context (affects model cost and performance) diff --git a/zed-extension/README.md b/zed-extension/README.md new file mode 100644 index 000000000..5fc0cbb4e --- /dev/null +++ b/zed-extension/README.md @@ -0,0 +1,29 @@ +# VT Code Zed Agent Server Extension + +This directory packages VT Code as a Zed Agent Server Extension so users can install the binary directly from Zed's marketplace or as a local dev extension. + +## Contents + +- `extension.toml` – Manifest that registers the VT Code agent server with Zed. +- `icons/vtcode.svg` – Monochrome icon displayed in Zed's menus. + +## Updating for a New Release + +1. Build and upload platform archives via `./scripts/release.sh` (or manually produce the `dist/` artifacts). +2. Update the `version` field in `extension.toml` to match the new tag. +3. Replace the `archive` URLs so they point at the freshly published GitHub release assets. +4. Run `./scripts/release.sh` to execute the automated release workflow. It rebuilds binaries, uploads release assets, and rewrites `extension.toml` with fresh SHA-256 checksums for every available target. +5. Commit the updated files and include them in the release PR. + +## Local Testing + +1. From Zed, run the Command Palette command `zed: install dev extension` and select this directory. +2. Choose **VT Code** from the Agent panel and confirm the download succeeds. +3. Exercise ACP features (tool calls, cancellations) to verify the packaged binary works as expected. + +After verification, push the manifest changes and publish the release so the extension can be listed publicly. + +## Next Steps + +- When you add Linux or Windows builds, extend `extension.toml` with the appropriate target tables and rerun the release script so their checksums are captured automatically. +- Re-run `zed: install dev extension` after each release to confirm download, checksum validation, and ACP negotiation succeed with the updated manifest. diff --git a/zed-extension/extension.toml b/zed-extension/extension.toml new file mode 100644 index 000000000..a41d7275c --- /dev/null +++ b/zed-extension/extension.toml @@ -0,0 +1,30 @@ +[package] +name = "vtcode-agent-server" +version = "0.37.1" +description = "VT Code packaged as a Zed Agent Server Extension." +repository = "https://github.com/vinhnx/vtcode" + +[package.metadata] +readme = "README.md" + +[agent_servers.vtcode] +name = "VT Code" +icon = "icons/vtcode.svg" + +[agent_servers.vtcode.env] +VT_ACP_ENABLED = "1" +VT_ACP_ZED_ENABLED = "1" +VT_ACP_ZED_TOOLS_READ_FILE_ENABLED = "1" +VT_ACP_ZED_TOOLS_LIST_FILES_ENABLED = "1" + +[agent_servers.vtcode.targets.darwin-aarch64] +archive = "https://github.com/vinhnx/vtcode/releases/download/v0.37.1/vtcode-v0.37.1-aarch64-apple-darwin.tar.gz" +cmd = "./vtcode" +args = ["acp"] +sha256 = "b8132619b0dbbaa62326b52ecf7bdacf6b589b33f82ca85a82cbcae0bb7c3125" + +[agent_servers.vtcode.targets.darwin-x86_64] +archive = "https://github.com/vinhnx/vtcode/releases/download/v0.37.1/vtcode-v0.37.1-x86_64-apple-darwin.tar.gz" +cmd = "./vtcode" +args = ["acp"] +sha256 = "cdf59c45c13da137cb5783eb5d78ae35b982e87af647d4779b9fd2543f017a3c" diff --git a/zed-extension/icons/vtcode.svg b/zed-extension/icons/vtcode.svg new file mode 100644 index 000000000..409bc070d --- /dev/null +++ b/zed-extension/icons/vtcode.svg @@ -0,0 +1,3 @@ + + +