From 81ecb22abe3e81dcdc77951a5c54fd5112903df8 Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli <4332460+Cammisuli@users.noreply.github.com> Date: Wed, 14 May 2025 15:58:30 -0400 Subject: [PATCH] feat(core): add nx console messaging to TUI (#31148) ## Current Behavior There is no communication between Nx CLI and Nx Console ## Expected Behavior This enables a connection between the Nx TUI app and Nx Console so that we can send messages to console. This is used to update MCP tools on Nx Console to assist with LLMs ## Related Issue(s) Fixes # --- .gitignore | 3 + Cargo.lock | 546 +++++++++++++++--- packages/nx/Cargo.toml | 22 +- packages/nx/src/native/index.d.ts | 2 +- packages/nx/src/native/index.js | 11 +- packages/nx/src/native/tui/action.rs | 2 + packages/nx/src/native/tui/app.rs | 55 +- .../src/native/tui/components/help_popup.rs | 41 +- .../src/native/tui/components/tasks_list.rs | 5 +- .../native/tui/components/terminal_pane.rs | 31 +- packages/nx/src/native/tui/lifecycle.rs | 20 +- packages/nx/src/native/tui/mod.rs | 1 + packages/nx/src/native/tui/nx_console.rs | 195 +++++++ .../native/tui/nx_console/ipc_transport.rs | 320 ++++++++++ .../nx/src/native/tui/nx_console/messaging.rs | 164 ++++++ packages/nx/src/native/utils/mod.rs | 1 + packages/nx/src/native/utils/socket_path.rs | 95 +++ packages/nx/src/tasks-runner/run-command.ts | 3 +- 18 files changed, 1420 insertions(+), 97 deletions(-) create mode 100644 packages/nx/src/native/tui/nx_console.rs create mode 100644 packages/nx/src/native/tui/nx_console/ipc_transport.rs create mode 100644 packages/nx/src/native/tui/nx_console/messaging.rs create mode 100644 packages/nx/src/native/utils/socket_path.rs diff --git a/.gitignore b/.gitignore index afff128d8d..9981a021a4 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ storybook-static # Ignore Gradle project-specific cache directory .gradle .kotlin + +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 87d6cbc720..8d05599d64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.1.0" @@ -377,6 +383,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -469,6 +481,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "command-group" version = "5.0.1" @@ -525,6 +547,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -720,6 +752,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "document-features" version = "0.2.11" @@ -850,7 +888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.58", "winapi", ] @@ -860,7 +898,7 @@ version = "0.8.3" source = "git+https://github.com/cammisuli/wezterm?rev=b538ee29e1e89eeb4832fb35ae095564dce34c29#b538ee29e1e89eeb4832fb35ae095564dce34c29" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.58", "winapi", ] @@ -1020,6 +1058,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" @@ -1096,8 +1140,8 @@ dependencies = [ "btoi", "gix-date", "itoa", - "thiserror", - "winnow", + "thiserror 1.0.58", + "winnow 0.5.40", ] [[package]] @@ -1116,9 +1160,9 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.58", "unicode-bom", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -1131,7 +1175,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1142,7 +1186,7 @@ checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c" dependencies = [ "bstr", "itoa", - "thiserror", + "thiserror 1.0.58", "time", ] @@ -1188,7 +1232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f8cf8c2266f63e582b7eb206799b63aa5fa68ee510ad349f637dfe2d0653de0" dependencies = [ "faster-hex", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1199,7 +1243,7 @@ checksum = "7e5c65e6a29830a435664891ced3f3c1af010f14900226019590ee0971a22f37" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1217,8 +1261,8 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror", - "winnow", + "thiserror 1.0.58", + "winnow 0.5.40", ] [[package]] @@ -1231,7 +1275,7 @@ dependencies = [ "gix-trace", "home", "once_cell", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1251,8 +1295,8 @@ dependencies = [ "gix-tempfile", "gix-validate", "memmap2", - "thiserror", - "winnow", + "thiserror 1.0.58", + "winnow 0.5.40", ] [[package]] @@ -1303,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545" dependencies = [ "bstr", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1336,6 +1380,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1383,12 +1446,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "home" version = "0.5.9" @@ -1447,6 +1504,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1467,6 +1525,7 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", "rustls-pki-types", "tokio", @@ -1564,7 +1623,7 @@ dependencies = [ "miette", "project-origins", "radix_trie", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", ] @@ -1583,7 +1642,7 @@ dependencies = [ "normalize-path", "project-origins", "radix_trie", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", ] @@ -1607,6 +1666,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + [[package]] name = "indoc" version = "2.0.6" @@ -1635,14 +1704,12 @@ dependencies = [ [[package]] name = "insta" -version = "1.42.2" +version = "1.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" dependencies = [ "console", - "linked-hash-map", "once_cell", - "pin-project", "similar", ] @@ -1659,6 +1726,21 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "ioctl-rs" version = "0.1.6" @@ -1710,6 +1792,28 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.58", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -1726,6 +1830,92 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpsee" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-types", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" +dependencies = [ + "async-trait", + "bytes", + "futures-timer", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "pin-project", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791" +dependencies = [ + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -1785,12 +1975,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1887,7 +2071,7 @@ checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ "miette-derive", "once_cell", - "thiserror", + "thiserror 1.0.58", "unicode-width 0.1.11", ] @@ -2159,16 +2343,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -2201,7 +2375,9 @@ dependencies = [ "ignore", "ignore-files 2.1.0", "insta", + "interprocess", "itertools 0.10.5", + "jsonrpsee", "machine-uid", "mio 1.0.3", "napi", @@ -2219,6 +2395,8 @@ dependencies = [ "reqwest", "rkyv", "rusqlite", + "serde", + "serde_json", "swc_common", "swc_ecma_ast", "swc_ecma_dep_graph", @@ -2228,7 +2406,7 @@ dependencies = [ "tar", "tempfile", "terminal-colorsaurus", - "thiserror", + "thiserror 1.0.58", "tokio", "tokio-util", "tracing", @@ -2236,6 +2414,7 @@ dependencies = [ "tracing-subscriber", "tui-logger", "tui-term 0.2.0 (git+https://github.com/JamesHenry/tui-term?rev=88e3b61425c97220c528ef76c188df10032a75dd)", + "uuid", "vt100-ctt", "walkdir", "watchexec", @@ -2332,6 +2511,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "overload" version = "0.1.1" @@ -2524,6 +2709,15 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -2592,7 +2786,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", ] @@ -2609,7 +2803,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "slab", - "thiserror", + "thiserror 1.0.58", "tinyvec", "tracing", ] @@ -2754,6 +2948,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2962,10 +3162,11 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -2974,6 +3175,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -2990,10 +3203,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] -name = "rustls-webpki" -version = "0.103.1" +name = "rustls-platform-verifier" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -3021,6 +3261,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3039,6 +3288,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.22" @@ -3047,18 +3319,18 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -3624,7 +3896,16 @@ version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.58", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -3638,6 +3919,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3709,27 +4001,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", "libc", - "mio 0.8.11", - "num_cpus", + "mio 1.0.3", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -3770,6 +4061,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.7.10", +] + [[package]] name = "tower" version = "0.5.2" @@ -3816,7 +4124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.58", "time", "tracing-subscriber", ] @@ -4029,6 +4337,9 @@ name = "uuid" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom 0.2.12", +] [[package]] name = "valuable" @@ -4205,7 +4516,7 @@ dependencies = [ "notify", "once_cell", "project-origins", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", "watchexec-events", @@ -4248,7 +4559,7 @@ checksum = "af0a778522cf0fc2fa8a8f1380e32893208cb2e7fd33e64de8bd81a00a2a7838" dependencies = [ "miette", "nix 0.27.1", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4276,6 +4587,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.8" @@ -4303,6 +4632,12 @@ dependencies = [ "rustix 0.38.38", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -4422,6 +4757,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4449,6 +4793,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4496,6 +4855,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4514,6 +4879,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4532,6 +4903,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4562,6 +4939,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4580,6 +4963,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4598,6 +4987,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4616,6 +5011,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4643,6 +5044,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/packages/nx/Cargo.toml b/packages/nx/Cargo.toml index a3461b8200..86d76b9fb7 100644 --- a/packages/nx/Cargo.toml +++ b/packages/nx/Cargo.toml @@ -12,6 +12,13 @@ opt-level = "z" strip = "none" [dependencies] +tokio = { version = "1.44.0", features = [ + "sync", + "macros", + "io-util", + "rt", + "time", +] } anyhow = "1.0.71" better-panic = "0.3.0" colored = "2" @@ -51,7 +58,6 @@ terminal-colorsaurus = "0.4.0" thiserror = "1.0.40" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -tokio = { version = "1.32.0", features = ['sync','macros','io-util','rt','time'] } tokio-util = "0.7.9" tracing-appender = "0.2" tui-logger = { version = "0.17.2", features = ["tracing-support"] } @@ -59,6 +65,8 @@ tui-term = { git = "https://github.com/JamesHenry/tui-term", rev = "88e3b61425c9 walkdir = '2.3.3' xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] } vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "b15dc3b0f7db94167a9c584f1d403899c0cc871d" } +serde = "1.0.219" +serde_json = "1.0.140" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["fileapi", "psapi", "shellapi"] } @@ -74,13 +82,22 @@ portable-pty = { git = "https://github.com/cammisuli/wezterm", rev = "b538ee29e1 ignore-files = "2.1.0" fs4 = "0.12.0" ratatui = { version = "0.29", features = ["scrolling-regions"] } -reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12.15", default-features = false, features = [ + "rustls-tls", +] } rusqlite = { version = "0.32.1", features = ["bundled", "array", "vtab"] } watchexec = "3.0.1" watchexec-events = "2.0.1" watchexec-filterer-ignore = "3.0.0" watchexec-signals = "2.1.0" machine-uid = "0.5.2" +interprocess = { version = "2.2.3", features = ["tokio"] } +jsonrpsee = { version = "0.25.1", features = [ + "client-core", + "async-client", + "macros", + "http-client", +] } [lib] crate-type = ['cdylib'] @@ -94,3 +111,4 @@ insta = "1.42.2" # This is only used for unit tests swc_ecma_dep_graph = "0.109.1" tempfile = "3.13.0" +uuid = { version = "1.0", features = ["v4"] } diff --git a/packages/nx/src/native/index.d.ts b/packages/nx/src/native/index.d.ts index f4f7888a66..94bb030766 100644 --- a/packages/nx/src/native/index.d.ts +++ b/packages/nx/src/native/index.d.ts @@ -8,7 +8,7 @@ export declare class ExternalObject { } } export declare class AppLifeCycle { - constructor(tasks: Array, initiatingTasks: Array, runMode: RunMode, pinnedTasks: Array, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string) + constructor(tasks: Array, initiatingTasks: Array, runMode: RunMode, pinnedTasks: Array, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string, workspaceRoot: string) startCommand(threadCount?: number | undefined | null): void scheduleTask(task: Task): void startTasks(tasks: Array, metadata: object): void diff --git a/packages/nx/src/native/index.js b/packages/nx/src/native/index.js index b0cdba7ce6..257054c6dc 100644 --- a/packages/nx/src/native/index.js +++ b/packages/nx/src/native/index.js @@ -62,10 +62,15 @@ const originalLoad = Module._load; // Will only be called once because the require cache takes over afterwards. Module._load = function (request, parent, isMain) { const modulePath = request; - if ( + // Check if we should use the native file cache (enabled by default) + const useNativeFileCache = process.env.NX_SKIP_NATIVE_FILE_CACHE !== 'true'; + // Check if this is an Nx native module (either from npm or local file) + const isNxNativeModule = nxPackages.has(modulePath) || - localNodeFiles.some((f) => modulePath.endsWith(f)) - ) { + localNodeFiles.some((file) => modulePath.endsWith(file)); + + // Only use the file cache for Nx native modules when caching is enabled + if (useNativeFileCache && isNxNativeModule) { const nativeLocation = require.resolve(modulePath); const fileName = basename(nativeLocation); diff --git a/packages/nx/src/native/tui/action.rs b/packages/nx/src/native/tui/action.rs index 34a2ce4ae4..12598a2207 100644 --- a/packages/nx/src/native/tui/action.rs +++ b/packages/nx/src/native/tui/action.rs @@ -35,4 +35,6 @@ pub enum Action { StartTasks(Vec), EndTasks(Vec), ToggleDebugMode, + SendConsoleMessage(String), + ConsoleMessengerAvailable(bool), } diff --git a/packages/nx/src/native/tui/app.rs b/packages/nx/src/native/tui/app.rs index 1e8b0f9104..afde4c64ca 100644 --- a/packages/nx/src/native/tui/app.rs +++ b/packages/nx/src/native/tui/app.rs @@ -23,7 +23,6 @@ use crate::native::{ tasks::types::{Task, TaskResult}, }; -use super::action::Action; use super::components::Component; use super::components::countdown_popup::CountdownPopup; use super::components::help_popup::HelpPopup; @@ -39,6 +38,7 @@ use super::pty::PtyInstance; use super::theme::THEME; use super::tui; use super::utils::normalize_newlines; +use super::{action::Action, nx_console::messaging::NxConsoleMessageConnection}; pub struct App { pub components: Vec>, @@ -68,6 +68,7 @@ pub struct App { tasks: Vec, debug_mode: bool, debug_state: TuiWidgetState, + console_messenger: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -134,6 +135,7 @@ impl App { tasks, debug_mode: false, debug_state: TuiWidgetState::default().set_default_display_level(LevelFilter::Debug), + console_messenger: None, }) } @@ -209,6 +211,10 @@ impl App { // Show countdown popup for the configured duration (making sure the help popup is not open first) pub fn end_command(&mut self) { + self.console_messenger + .as_ref() + .and_then(|c| c.end_running_tasks()); + // If the user has interacted with the app, or auto-exit is disabled, do nothing if self.user_has_interacted || !self.tui_config.auto_exit.should_exit_automatically() { return; @@ -321,6 +327,10 @@ impl App { && !(matches!(self.focus, Focus::MultipleOutput(_)) && self.is_interactive_mode()) { + self.console_messenger + .as_ref() + .and_then(|c| c.end_running_tasks()); + self.is_forced_shutdown = true; // Quit immediately self.quit_at = Some(std::time::Instant::now()); @@ -769,8 +779,25 @@ impl App { trace!("{action:?}"); } match &action { + Action::StartCommand(_) => { + self.console_messenger + .as_ref() + .and_then(|c| c.start_running_tasks()); + } + Action::Tick => { + self.console_messenger.as_ref().and_then(|messenger| { + self.components + .iter() + .find_map(|c| c.as_any().downcast_ref::()) + .and_then(|tasks_list| { + messenger.update_running_tasks(&tasks_list.tasks, &self.pty_instances) + }) + }); + } // Quit immediately - Action::Quit => self.quit_at = Some(std::time::Instant::now()), + Action::Quit => { + self.quit_at = Some(std::time::Instant::now()); + } // Cancel quitting Action::CancelQuit => { self.quit_at = None; @@ -981,6 +1008,7 @@ impl App { is_focused, has_pty, is_next_tab_target, + self.console_messenger.is_some(), ); let terminal_pane = TerminalPane::new() @@ -1008,6 +1036,13 @@ impl App { }) .ok(); } + Action::SendConsoleMessage(msg) => { + if let Some(connection) = &self.console_messenger { + connection.send_terminal_string(msg); + } else { + trace!("No console connection available"); + } + } _ => {} } @@ -1358,7 +1393,10 @@ impl App { fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { if let Focus::MultipleOutput(pane_idx) = self.focus { let terminal_pane_data = &mut self.terminal_pane_data[pane_idx]; - terminal_pane_data.handle_key_event(key) + if let Some(action) = terminal_pane_data.handle_key_event(key)? { + self.dispatch_action(action); + } + Ok(()) } else { Ok(()) } @@ -1540,4 +1578,15 @@ impl App { self.debug_state.transition(event); } } + + pub fn set_console_messenger(&mut self, messenger: NxConsoleMessageConnection) { + self.console_messenger = Some(messenger); + if self + .console_messenger + .as_ref() + .is_some_and(|c| c.is_connected()) + { + self.dispatch_action(Action::ConsoleMessengerAvailable(true)); + } + } } diff --git a/packages/nx/src/native/tui/components/help_popup.rs b/packages/nx/src/native/tui/components/help_popup.rs index acdecc07d8..439a1ed6ad 100644 --- a/packages/nx/src/native/tui/components/help_popup.rs +++ b/packages/nx/src/native/tui/components/help_popup.rs @@ -12,9 +12,11 @@ use std::any::Any; use tokio::sync::mpsc::UnboundedSender; use super::{Component, Frame}; -use crate::native::tui::action::Action; +use crate::native::tui::{action::Action, nx_console}; + use crate::native::tui::theme::THEME; +#[derive(Default)] pub struct HelpPopup { scroll_offset: usize, scrollbar_state: ScrollbarState, @@ -22,6 +24,7 @@ pub struct HelpPopup { viewport_height: usize, visible: bool, action_tx: Option>, + console_available: bool, } impl HelpPopup { @@ -33,6 +36,7 @@ impl HelpPopup { viewport_height: 0, visible: false, action_tx: None, + console_available: false, } } @@ -40,6 +44,10 @@ impl HelpPopup { self.visible = visible; } + pub fn set_console_available(&mut self, available: bool) { + self.console_available = available; + } + // Ensure the scroll state is reset to avoid recalc issues pub fn handle_resize(&mut self, _width: u16, _height: u16) { self.scroll_offset = 0; @@ -110,7 +118,7 @@ impl HelpPopup { ]) .split(popup_layout[1])[1]; - let keybindings = vec![ + let mut keybindings = vec![ // Misc ("?", "Toggle this popup"), ("q or +c", "Quit the TUI"), @@ -149,6 +157,25 @@ impl HelpPopup { ("+z", "Stop interacting with a continuous task"), ]; + if self.console_available { + // add Copilot specific keybindings for AI assistance + + keybindings.extend([ + ("", ""), + ( + "+a", + match nx_console::get_current_editor() { + nx_console::SupportedEditor::VSCode => { + "Send terminal output to Copilot so that it can assist with any issues" + } + _ => { + "Send terminal output to your AI assistant so that it can assist with any issues" + } + }, + ), + ]); + } + let mut content: Vec = vec![ // Welcome text Line::from(vec![ @@ -357,8 +384,14 @@ impl Component for HelpPopup { } fn update(&mut self, action: Action) -> Result> { - if let Action::Resize(w, h) = action { - self.handle_resize(w, h); + match action { + Action::Resize(w, h) => { + self.handle_resize(w, h); + } + Action::ConsoleMessengerAvailable(available) => { + self.set_console_available(available); + } + _ => {} } Ok(None) } diff --git a/packages/nx/src/native/tui/components/tasks_list.rs b/packages/nx/src/native/tui/components/tasks_list.rs index 4cc94fc39b..04517e4a22 100644 --- a/packages/nx/src/native/tui/components/tasks_list.rs +++ b/packages/nx/src/native/tui/components/tasks_list.rs @@ -7,6 +7,7 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Cell, Paragraph, Row, Table}, }; +use serde::{Deserialize, Serialize}; use std::{ any::Any, sync::{Arc, Mutex}, @@ -52,7 +53,7 @@ pub struct TaskItem { cache_status: String, // Public to aid with sorting utility and testing pub status: TaskStatus, - terminal_output: String, + pub terminal_output: String, pub continuous: bool, start_time: Option, // Public to aid with sorting utility and testing @@ -115,7 +116,7 @@ impl TaskItem { } #[napi] -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TaskStatus { // Explicit statuses that can come from the task runner Success, diff --git a/packages/nx/src/native/tui/components/terminal_pane.rs b/packages/nx/src/native/tui/components/terminal_pane.rs index ca7b10c7bb..fdfd742f3d 100644 --- a/packages/nx/src/native/tui/components/terminal_pane.rs +++ b/packages/nx/src/native/tui/components/terminal_pane.rs @@ -13,9 +13,9 @@ use ratatui::{ use std::{io, sync::Arc}; use tui_term::widget::PseudoTerminal; -use super::tasks_list::TaskStatus; -use crate::native::tui::pty::PtyInstance; +use crate::native::tui::components::tasks_list::TaskStatus; use crate::native::tui::theme::THEME; +use crate::native::tui::{action::Action, pty::PtyInstance}; pub struct TerminalPaneData { pub pty: Option>, @@ -34,7 +34,7 @@ impl TerminalPaneData { } } - pub fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { + pub fn handle_key_event(&mut self, key: KeyEvent) -> io::Result> { if let Some(pty) = &mut self.pty { let mut pty_mut = pty.as_ref().clone(); match key.code { @@ -42,11 +42,11 @@ impl TerminalPaneData { // If interactive, the event falls through to be forwarded to the PTY so that we can support things like interactive prompts within tasks. KeyCode::Up | KeyCode::Char('k') if !self.is_interactive => { pty_mut.scroll_up(); - return Ok(()); + return Ok(None); } KeyCode::Down | KeyCode::Char('j') if !self.is_interactive => { pty_mut.scroll_down(); - return Ok(()); + return Ok(None); } // Handle ctrl+u and ctrl+d for scrolling when not in interactive mode KeyCode::Char('u') @@ -56,7 +56,7 @@ impl TerminalPaneData { for _ in 0..12 { pty_mut.scroll_up(); } - return Ok(()); + return Ok(None); } KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) && !self.is_interactive => @@ -65,7 +65,7 @@ impl TerminalPaneData { for _ in 0..12 { pty_mut.scroll_down(); } - return Ok(()); + return Ok(None); } // Handle 'c' for copying when not in interactive mode KeyCode::Char('c') if !self.is_interactive => { @@ -81,12 +81,20 @@ impl TerminalPaneData { } } } - return Ok(()); + return Ok(None); } // Handle 'i' to enter interactive mode for in progress tasks KeyCode::Char('i') if self.can_be_interactive && !self.is_interactive => { self.set_interactive(true); - return Ok(()); + return Ok(None); + } + KeyCode::Char('a') + if key.modifiers.contains(KeyModifiers::CONTROL) && !self.is_interactive => + { + let Some(screen) = pty.get_screen() else { + return Ok(None); + }; + return Ok(Some(Action::SendConsoleMessage(screen.all_contents()))); } // Only send input to PTY if we're in interactive mode _ if self.is_interactive => match key.code { @@ -114,7 +122,7 @@ impl TerminalPaneData { _ => {} } } - Ok(()) + Ok(None) } pub fn handle_mouse_event(&mut self, event: MouseEvent) -> io::Result<()> { @@ -161,6 +169,7 @@ pub struct TerminalPaneState { pub scrollbar_state: ScrollbarState, pub has_pty: bool, pub is_next_tab_target: bool, + pub console_available: bool, } impl TerminalPaneState { @@ -171,6 +180,7 @@ impl TerminalPaneState { is_focused: bool, has_pty: bool, is_next_tab_target: bool, + console_available: bool, ) -> Self { Self { task_name, @@ -181,6 +191,7 @@ impl TerminalPaneState { scrollbar_state: ScrollbarState::default(), has_pty, is_next_tab_target, + console_available, } } } diff --git a/packages/nx/src/native/tui/lifecycle.rs b/packages/nx/src/native/tui/lifecycle.rs index 5a5d53ff7f..346d043b11 100644 --- a/packages/nx/src/native/tui/lifecycle.rs +++ b/packages/nx/src/native/tui/lifecycle.rs @@ -5,13 +5,17 @@ use parking_lot::Mutex; use std::sync::Arc; use tracing::debug; +use crate::native::logger::enable_logger; +use crate::native::tasks::types::{Task, TaskResult}; +use crate::native::{ + pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc}, + tui::nx_console::messaging::NxConsoleMessageConnection, +}; + use super::app::App; use super::components::tasks_list::TaskStatus; use super::config::{AutoExit, TuiCliArgs as RustTuiCliArgs, TuiConfig as RustTuiConfig}; use super::tui::Tui; -use crate::native::logger::enable_logger; -use crate::native::pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc}; -use crate::native::tasks::types::{Task, TaskResult}; #[napi(object)] #[derive(Clone)] @@ -63,6 +67,7 @@ pub enum RunMode { #[derive(Clone)] pub struct AppLifeCycle { app: Arc>, + workspace_root: Arc, } #[napi] @@ -76,6 +81,7 @@ impl AppLifeCycle { tui_cli_args: TuiCliArgs, tui_config: TuiConfig, title_text: String, + workspace_root: String, ) -> Self { // Get the target names from nx_args.targets let rust_tui_cli_args = tui_cli_args.into(); @@ -97,6 +103,7 @@ impl AppLifeCycle { ) .unwrap(), )), + workspace_root: Arc::new(workspace_root), } } @@ -207,7 +214,14 @@ impl AppLifeCycle { debug!("Initialized Components"); + let workspace_root = self.workspace_root.clone(); napi::tokio::spawn(async move { + { + // set up Console Messenger in a async context + let connection = NxConsoleMessageConnection::new(&workspace_root).await; + app_mutex.lock().set_console_messenger(connection); + } + loop { // Handle events using our Tui abstraction if let Some(event) = tui.next().await { diff --git a/packages/nx/src/native/tui/mod.rs b/packages/nx/src/native/tui/mod.rs index 876bd28441..1e3917c062 100644 --- a/packages/nx/src/native/tui/mod.rs +++ b/packages/nx/src/native/tui/mod.rs @@ -3,6 +3,7 @@ pub mod app; pub mod components; pub mod config; pub mod lifecycle; +pub mod nx_console; pub mod pty; pub mod theme; #[allow(clippy::module_inception)] diff --git a/packages/nx/src/native/tui/nx_console.rs b/packages/nx/src/native/tui/nx_console.rs new file mode 100644 index 0000000000..615232fe67 --- /dev/null +++ b/packages/nx/src/native/tui/nx_console.rs @@ -0,0 +1,195 @@ +use std::collections::HashMap; +use std::sync::OnceLock; + +mod ipc_transport; +pub mod messaging; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SupportedEditor { + VSCode, + Cursor, + Windsurf, + JetBrains, + Unknown, +} + +static CURRENT_EDITOR: OnceLock = OnceLock::new(); + +pub fn get_current_editor() -> &'static SupportedEditor { + CURRENT_EDITOR.get_or_init(|| detect_editor(HashMap::new())) +} + +fn detect_editor(mut env_map: HashMap) -> SupportedEditor { + let term_editor = if let Some(term) = get_env_var("TERM_PROGRAM", &mut env_map) { + let term_lower = term.to_lowercase(); + match term_lower.as_str() { + "vscode" => SupportedEditor::VSCode, + "cursor" => SupportedEditor::Cursor, + "windsurf" => SupportedEditor::Windsurf, + "jetbrains" => SupportedEditor::JetBrains, + _ => SupportedEditor::Unknown, + } + } else { + SupportedEditor::Unknown + }; + + // For JetBrains, we don't need any additional checks + if matches!(term_editor, SupportedEditor::JetBrains) { + return term_editor; + } + + if matches!(term_editor, SupportedEditor::VSCode) { + if let Some(vscode_git_var) = get_env_var("VSCODE_GIT_ASKPASS_NODE", &mut env_map) { + let vscode_git_var_lowercase = vscode_git_var.to_lowercase(); + if vscode_git_var_lowercase.contains("cursor") { + return SupportedEditor::Cursor; + } else if vscode_git_var_lowercase.contains("windsurf") { + return SupportedEditor::Windsurf; + } else { + return SupportedEditor::VSCode; + } + } else { + return term_editor; + } + } + + SupportedEditor::Unknown +} + +fn get_env_var<'a>(name: &str, env_map: &'a mut HashMap) -> Option<&'a str> { + if env_map.contains_key(name) { + return env_map.get(name).map(|s| s.as_str()); + } + + match std::env::var(name) { + Ok(val) => { + env_map.insert(name.to_string(), val); + env_map.get(name).map(|s| s.as_str()) + } + Err(_) => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn test_detect_vscode() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/vscode/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_detect_cursor() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/cursor/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Cursor); + } + + #[test] + fn test_detect_windsurf() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/windsurf/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Windsurf); + } + + #[test] + fn test_detect_jetbrains() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "jetbrains".to_string()); + assert_eq!(detect_editor(test_env), SupportedEditor::JetBrains); + } + + #[test] + fn test_term_program_unknown() { + let mut test_env = HashMap::new(); + test_env.insert( + "TERM_PROGRAM".to_string(), + "some-unknown-editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_vscode_without_askpass_confirmation() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + // No VSCODE_GIT_ASKPASS_NODE set or doesn't contain "vscode" + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_vscode_with_wrong_askpass() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/no/matching/editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_case_insensitivity() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "VSCode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/VSCODE/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_cursor_without_askpass_confirmation() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "cursor".to_string()); + // No VSCODE_GIT_ASKPASS_NODE set + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_cursor_with_wrong_askpass() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "cursor".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/no/matching/editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_windsurf_without_askpass_confirmation() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "windsurf".to_string()); + // No VSCODE_GIT_ASKPASS_NODE set + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_windsurf_with_wrong_askpass() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "windsurf".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/no/matching/editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } +} diff --git a/packages/nx/src/native/tui/nx_console/ipc_transport.rs b/packages/nx/src/native/tui/nx_console/ipc_transport.rs new file mode 100644 index 0000000000..c4e187484c --- /dev/null +++ b/packages/nx/src/native/tui/nx_console/ipc_transport.rs @@ -0,0 +1,320 @@ +use std::path::Path; +use std::sync::Arc; + +use anyhow::anyhow; +use interprocess::{ + bound_util::{RefTokioAsyncRead, RefTokioAsyncWrite}, + local_socket::{ + GenericFilePath, ToFsName, + tokio::{Stream, prelude::*}, + }, +}; +use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT}; +use thiserror::Error; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +pub struct IpcTransport { + pub reader: IpcTransportReceiver, + pub writer: IpcTransportSender, +} +impl IpcTransport { + pub async fn new(socket_path: &Path) -> Result { + let socket_path = socket_path.to_fs_name::()?; + let conn = Stream::connect(socket_path).await?; + let stream = Arc::new(conn); + let writer = IpcTransportSender(Arc::clone(&stream)); + let reader = IpcTransportReceiver(Arc::clone(&stream)); + Ok(Self { reader, writer }) + } +} + +pub struct IpcTransportSender(Arc); +pub struct IpcTransportReceiver(Arc); + +const NEW_LINE: &str = "\r\n"; + +#[derive(Debug, Error)] +#[error(transparent)] +pub enum IpcError { + GenericError(#[from] anyhow::Error), + IoError(#[from] std::io::Error), +} + +impl TransportSenderT for IpcTransportSender { + type Error = IpcError; + + async fn send(&mut self, msg: String) -> Result<(), Self::Error> { + let mut stream = self.0.as_tokio_async_write(); + let headers = format!("content-length: {}{}{}", msg.len(), NEW_LINE, NEW_LINE); + stream.write_all(headers.as_bytes()).await?; + stream.flush().await?; + let mut msg = msg; + msg.push_str(NEW_LINE); + msg.push_str(NEW_LINE); + stream.write_all(msg.as_bytes()).await?; + stream.flush().await?; + Ok(()) + } +} + +impl TransportReceiverT for IpcTransportReceiver { + type Error = IpcError; + + async fn receive(&mut self) -> Result { + let mut stream = self.0.as_tokio_async_read(); + let mut response_data = Vec::new(); + let mut buffer = [0u8; 1024]; + + loop { + let bytes_read = stream.read(buffer.as_mut()).await?; + if bytes_read == 0 { + break; + } + response_data.extend_from_slice(&buffer[..bytes_read]); + + if let Ok(response_str) = String::from_utf8(response_data.clone()) { + if response_str.contains('\n') { + let parts: Vec<&str> = response_str.split('\n').collect(); + if let Some(response_part) = parts.first() { + return Ok(ReceivedMessage::Text(response_part.to_string())); + } + } + } + } + + Err(anyhow!("Failed to read from IPC stream").into()) + } +} + +#[cfg(test)] +mod test_utils { + use super::*; + use std::time::Duration; + use std::{future::Future, path::PathBuf}; + use tokio::task; + + // Define trait for platform-specific test setup + pub trait IpcTestSetup { + type ServerHandle: Sized; + type ServerSocket: AsyncReadExt + AsyncWriteExt + Unpin; + type AcceptFuture: Future + Send; + type ConnectFuture: Future> + Send; + + // Setup connection paths + fn create_connection_path() -> (PathBuf, PathBuf); + + // Create server + fn create_server(path: PathBuf) -> Self::ServerHandle; + + // Accept client connection + fn accept_connection(handle: &mut Self::ServerHandle) -> Self::AcceptFuture; + + // Connect client + fn connect_client(path: PathBuf) -> Self::ConnectFuture; + } + + // Common test implementations + pub async fn test_ipc_transport_connection() { + let (server_path, client_path) = T::create_connection_path(); + + // Create a mock server + let mut server = T::create_server(server_path); + + // Connect in a separate task + let client_task = task::spawn(async move { + // Small delay to ensure server is ready + tokio::time::sleep(Duration::from_millis(100)).await; + T::connect_client(client_path).await + }); + + // Accept the connection + T::accept_connection(&mut server).await; + + let result = client_task.await.unwrap(); + assert!(result.is_ok()); + } + + pub async fn test_transport_sender_send() { + let (server_path, client_path) = T::create_connection_path(); + + // Create a mock server + let mut server = T::create_server(server_path); + + // Start client in background + let client_task = task::spawn(async move { + let mut transport = T::connect_client(client_path).await.unwrap(); + transport.writer.send("test message".to_string()).await + }); + + // Accept the connection and read the message + let mut socket = T::accept_connection(&mut server).await; + let mut buf = [0u8; 1024]; + let n = socket.read(&mut buf).await.unwrap(); + let received = String::from_utf8_lossy(&buf[0..n]); + + assert!(received.contains("content-length: 12")); + + let result = client_task.await.unwrap(); + assert!(result.is_ok()); + } + + pub async fn test_transport_receiver_receive() { + let (server_path, client_path) = T::create_connection_path(); + + // Create a mock server + let mut server = T::create_server(server_path); + + // Start client in background + let client_task = task::spawn(async move { + let mut transport = T::connect_client(client_path).await.unwrap(); + transport.reader.receive().await + }); + + // Accept connection and send a message + let mut socket = T::accept_connection(&mut server).await; + let message = "test\nresponse"; + socket.write_all(message.as_bytes()).await.unwrap(); + socket.flush().await.unwrap(); + + let result = client_task.await.unwrap(); + assert!(result.is_ok()); + if let Ok(ReceivedMessage::Text(text)) = result { + assert_eq!(text, "test"); + } else { + panic!("Expected Text message"); + } + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::test_utils::*; + use super::*; + use std::pin::Pin; + use std::{future::Future, path::PathBuf}; + use tempfile::NamedTempFile; + use tokio::net::UnixListener; + + struct UnixIpcTestSetup; + + // Define concrete future types + type AcceptConnectionFuture = Pin + Send>>; + type ConnectClientFuture = + Pin> + Send>>; + + impl IpcTestSetup for UnixIpcTestSetup { + type ServerHandle = UnixListener; + type ServerSocket = tokio::net::UnixStream; + type AcceptFuture = AcceptConnectionFuture; + type ConnectFuture = ConnectClientFuture; + + fn create_connection_path() -> (PathBuf, PathBuf) { + let temp_file = NamedTempFile::new().unwrap(); + let socket_path = temp_file.path().to_path_buf(); + std::fs::remove_file(&socket_path).unwrap_or(()); + (socket_path.clone(), socket_path) + } + + fn create_server(path: PathBuf) -> Self::ServerHandle { + UnixListener::bind(&path).unwrap() + } + + fn accept_connection(handle: &mut Self::ServerHandle) -> Self::AcceptFuture { + // Take ownership of the listener and create a new one for subsequent calls + let path = std::env::temp_dir().join(format!("socket-{}", uuid::Uuid::new_v4())); + std::fs::remove_file(&path).unwrap_or(()); + let old_listener = std::mem::replace(handle, UnixListener::bind(&path).unwrap()); + + Box::pin(async move { + let (socket, _) = old_listener.accept().await.unwrap(); + socket + }) + } + + fn connect_client(path: PathBuf) -> Self::ConnectFuture { + Box::pin(async move { IpcTransport::new(&path).await }) + } + } + + #[tokio::test] + async fn test_ipc_transport_connection() { + test_utils::test_ipc_transport_connection::().await; + } + + #[tokio::test] + async fn test_transport_sender_send() { + test_utils::test_transport_sender_send::().await; + } + + #[tokio::test] + async fn test_transport_receiver_receive() { + test_utils::test_transport_receiver_receive::().await; + } +} + +#[cfg(all(test, windows))] +mod tests_windows { + use super::test_utils::*; + use super::*; + use std::pin::Pin; + use std::{future::Future, path::PathBuf}; + use tokio::net::windows::named_pipe::ServerOptions; + use uuid::Uuid; + + struct WindowsIpcTestSetup; + + // Define concrete future types + type AcceptConnectionFuture = + Pin + Send>>; + type ConnectClientFuture = + Pin> + Send>>; + + impl IpcTestSetup for WindowsIpcTestSetup { + type ServerHandle = (ServerOptions, PathBuf); + type ServerSocket = tokio::net::windows::named_pipe::NamedPipeServer; + type AcceptFuture = AcceptConnectionFuture; + type ConnectFuture = ConnectClientFuture; + + fn create_connection_path() -> (PathBuf, PathBuf) { + let pipe_name = format!(r"\\.\pipe\test-{}", Uuid::new_v4()); + (pipe_name.clone().into(), pipe_name.into()) + } + + fn create_server(path: PathBuf) -> Self::ServerHandle { + let mut options = ServerOptions::new(); + options.first_pipe_instance(true); + (options, path) + } + + fn accept_connection(handle: &mut Self::ServerHandle) -> Self::AcceptFuture { + let (options, path) = handle; + let options = options.clone(); + let path = path.clone(); + Box::pin(async move { + let path_str = path.to_str().unwrap(); + let server = options.create(path_str).unwrap(); + server.connect().await.unwrap(); + server + }) + } + + fn connect_client(path: PathBuf) -> Self::ConnectFuture { + Box::pin(async move { IpcTransport::new(&path).await }) + } + } + + #[tokio::test] + async fn test_ipc_transport_connection() { + test_utils::test_ipc_transport_connection::().await; + } + + #[tokio::test] + async fn test_transport_sender_send() { + test_utils::test_transport_sender_send::().await; + } + + #[tokio::test] + async fn test_transport_receiver_receive() { + test_utils::test_transport_receiver_receive::().await; + } +} diff --git a/packages/nx/src/native/tui/nx_console/messaging.rs b/packages/nx/src/native/tui/nx_console/messaging.rs new file mode 100644 index 0000000000..da10cffc62 --- /dev/null +++ b/packages/nx/src/native/tui/nx_console/messaging.rs @@ -0,0 +1,164 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use tokio::time::Instant; +use tracing::trace; + +use jsonrpsee::{ + async_client::{Client, ClientBuilder}, + proc_macros::rpc, +}; + +use crate::native::{ + tui::{ + components::tasks_list::{TaskItem, TaskStatus}, + nx_console::ipc_transport::IpcTransport, + pty::PtyInstance, + }, + utils::socket_path::get_full_nx_console_socket_path, +}; + +#[derive(Serialize, Deserialize)] +pub struct UpdatedRunningTask { + pub name: String, + pub status: TaskStatus, + pub output: String, + pub continuous: bool, +} + +#[rpc(client, namespace = "nx", namespace_separator = "/")] +pub trait ConsoleRpc { + #[method(name = "terminalMessage")] + fn terminal_message(&self, text: String); + + #[method(name = "updateRunningTasks")] + fn update_running_tasks(&self, process_id: u32, updates: Vec); + #[method(name = "startedRunningTasks")] + fn start_running_tasks(&self, process_id: u32); + #[method(name = "endedRunningTasks")] + fn end_running_tasks(&self, process_id: u32); +} + +pub struct NxConsoleMessageConnection { + client: Option>, +} + +static LAST_UPDATES: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); +const THROTTLE_DURATION: Duration = Duration::from_secs(2); + +/// Utility function to check if an operation should be throttled. +/// Returns true if the operation should be throttled (skipped), false if it should proceed. +fn throttled(operation_key: &'static str, throttle_duration: Duration) -> bool { + let mut last_updates = LAST_UPDATES.lock(); + let now = Instant::now(); + + match last_updates.get(operation_key) { + Some(last) if now.duration_since(*last) < throttle_duration => true, // Should throttle + _ => { + // Update the last update time + last_updates.insert(operation_key, now); + false // Should NOT throttle, operation should proceed + } + } +} + +impl NxConsoleMessageConnection { + pub async fn new(workspace_root: &str) -> Self { + let socket_path = get_full_nx_console_socket_path(workspace_root); + let client = IpcTransport::new(&socket_path) + .await + .map(|transport| { + ClientBuilder::new().build_with_tokio(transport.writer, transport.reader) + }) + .inspect_err(|e| { + trace!(?socket_path, "Could not connect to Nx Console: {}", e); + }) + .ok() + .map(Arc::new); + + Self { client } + } + + pub fn is_connected(&self) -> bool { + self.client.is_some() + } + + pub fn send_terminal_string(&self, message: impl Into) -> Option<()> { + self.client.as_ref().map(|client| { + let message = message.into(); + let client = client.clone(); + tokio::spawn(async move { + if let Err(e) = client.terminal_message(message).await { + trace!("Failed to send terminal message: {}", e); + } + }); + }) + } + + pub fn update_running_tasks( + &self, + task_statuses: &[TaskItem], + ptys: &HashMap>, + ) -> Option<()> { + if throttled("update_running_tasks", THROTTLE_DURATION) { + return None; + } + + self.client.as_ref().map(|client| { + let client = client.clone(); + + let task_statuses: Vec = task_statuses + .iter() + .map(|task| { + let output = ptys + .get(&task.name) + .and_then(|pty| pty.get_screen()) + .map(|screen| screen.all_contents()) + .unwrap_or_default(); + UpdatedRunningTask { + name: task.name.clone(), + status: task.status, + output, + continuous: task.continuous, + } + }) + .collect(); + + tokio::spawn(async move { + if let Err(e) = client + .update_running_tasks(std::process::id(), task_statuses) + .await + { + trace!("Failed to send task statuses: {}", e); + } + }); + }) + } + + pub fn start_running_tasks(&self) -> Option<()> { + self.client.as_ref().map(|client| { + let client = client.clone(); + let process_id = std::process::id(); + tokio::spawn(async move { + if let Err(e) = client.start_running_tasks(process_id).await { + trace!("Failed to send start running tasks: {}", e); + } + }); + }) + } + + pub fn end_running_tasks(&self) -> Option<()> { + self.client.as_ref().map(|client| { + let client = client.clone(); + let process_id = std::process::id(); + tokio::spawn(async move { + if let Err(e) = client.end_running_tasks(process_id).await { + trace!("Failed to send end running tasks: {}", e); + } + }); + }) + } +} diff --git a/packages/nx/src/native/utils/mod.rs b/packages/nx/src/native/utils/mod.rs index ddd46fee2f..01d97e4713 100644 --- a/packages/nx/src/native/utils/mod.rs +++ b/packages/nx/src/native/utils/mod.rs @@ -2,6 +2,7 @@ mod find_matching_projects; mod get_mod_time; mod normalize_trait; pub mod path; +pub mod socket_path; pub use find_matching_projects::*; pub use get_mod_time::*; diff --git a/packages/nx/src/native/utils/socket_path.rs b/packages/nx/src/native/utils/socket_path.rs new file mode 100644 index 0000000000..480816a297 --- /dev/null +++ b/packages/nx/src/native/utils/socket_path.rs @@ -0,0 +1,95 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use crate::native::hasher::hash; + +const DAEMON_DIR_FOR_CURRENT_WORKSPACE: &str = "./nx/workspace-data/d"; + +fn socket_dir_name(workspace_root: &str, unique_name: Option<&'static str>) -> PathBuf { + let mut hashing_string = workspace_root.to_lowercase(); + if let Some(name) = unique_name { + hashing_string.push(','); + hashing_string.push_str(name); + } + let result = hash(hashing_string.as_bytes()); + let temp_dir = std::env::temp_dir(); + temp_dir.join(result) +} + +fn get_socket_dir(workspace_root: &str, unique_name: Option<&'static str>) -> PathBuf { + let dir_path = env::var("NX_SOCKET_DIR") + .or_else(|_| env::var("NX_DAEMON_SOCKET_DIR")) + .map(PathBuf::from) + .unwrap_or_else(|_| socket_dir_name(workspace_root, unique_name)); + + let path = if cfg!(target_os = "windows") { + dir_path + } else { + match fs::create_dir_all(&dir_path) { + Ok(_) => dir_path, + Err(_) => PathBuf::from(workspace_root).join(DAEMON_DIR_FOR_CURRENT_WORKSPACE), + } + }; + + if cfg!(target_os = "windows") { + let path_str = path.to_string_lossy(); + PathBuf::from(format!(r"\\.\pipe\nx\{}", path_str)) + } else { + path + } +} + +pub fn get_full_os_socket_path(workspace_root: &str) -> PathBuf { + get_socket_dir(workspace_root, None) +} + +pub fn get_full_nx_console_socket_path(workspace_root: &str) -> PathBuf { + get_socket_dir(workspace_root, Some("nx-console")).join("nx-console.sock") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + #[test] + fn test_socket_dir_name_basic() { + let root = "/tmp/test_workspace"; + let dir = socket_dir_name(root, None); + assert_eq!(dir.to_string_lossy(), "/tmp/17684150229889955837"); + assert!(dir.is_absolute()); + } + + #[test] + fn test_socket_dir_name_with_unique_name() { + let root = "/tmp/test_workspace"; + let dir = socket_dir_name(root, Some("unique")); + assert_eq!(dir.to_string_lossy(), "/tmp/10757852796479033769"); + assert!(dir.is_absolute()); + } + + #[test] + fn test_get_socket_dir_env_var() { + let root = "/tmp/test_workspace"; + let temp_dir = std::env::temp_dir().join("nx_test_socket_dir"); + unsafe { env::set_var("NX_SOCKET_DIR", &temp_dir) }; + let dir = get_socket_dir(root, None); + assert_eq!(dir.to_string_lossy(), "/tmp/nx_test_socket_dir"); + unsafe { env::remove_var("NX_SOCKET_DIR") }; + } + + #[test] + fn test_get_full_os_socket_path() { + let root = "/tmp/test_workspace"; + let path = get_full_os_socket_path(root); + assert!(path.is_absolute() || path.starts_with("./nx/workspace-data/d")); + } + + #[test] + fn test_get_full_nx_console_socket_path() { + let root = "/tmp/test_workspace"; + let path = get_full_nx_console_socket_path(root); + assert!(path.to_string_lossy().contains("nx-console.sock")); + } +} diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index b259fbddcf..6cbbe250df 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -199,7 +199,8 @@ async function getTerminalOutputLifeCycle( pinnedTasks, nxArgs ?? {}, nxJson.tui ?? {}, - titleText + titleText, + workspaceRoot ); lifeCycles.unshift(appLifeCycle);