feat(core): add nx console messaging to TUI (#31148)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
There is no communication between Nx CLI and Nx Console

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
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)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Jonathan Cammisuli 2025-05-14 15:58:30 -04:00 committed by GitHub
parent 47b9b51dd3
commit 81ecb22abe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1420 additions and 97 deletions

3
.gitignore vendored
View File

@ -72,3 +72,6 @@ storybook-static
# Ignore Gradle project-specific cache directory # Ignore Gradle project-specific cache directory
.gradle .gradle
.kotlin .kotlin
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md

546
Cargo.lock generated
View File

@ -188,6 +188,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3"
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -377,6 +383,12 @@ dependencies = [
"shlex", "shlex",
] ]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]] [[package]]
name = "cexpr" name = "cexpr"
version = "0.6.0" version = "0.6.0"
@ -469,6 +481,16 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "command-group" name = "command-group"
version = "5.0.1" version = "5.0.1"
@ -525,6 +547,16 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@ -720,6 +752,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]] [[package]]
name = "document-features" name = "document-features"
version = "0.2.11" version = "0.2.11"
@ -850,7 +888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
dependencies = [ dependencies = [
"libc", "libc",
"thiserror", "thiserror 1.0.58",
"winapi", "winapi",
] ]
@ -860,7 +898,7 @@ version = "0.8.3"
source = "git+https://github.com/cammisuli/wezterm?rev=b538ee29e1e89eeb4832fb35ae095564dce34c29#b538ee29e1e89eeb4832fb35ae095564dce34c29" source = "git+https://github.com/cammisuli/wezterm?rev=b538ee29e1e89eeb4832fb35ae095564dce34c29#b538ee29e1e89eeb4832fb35ae095564dce34c29"
dependencies = [ dependencies = [
"libc", "libc",
"thiserror", "thiserror 1.0.58",
"winapi", "winapi",
] ]
@ -1020,6 +1058,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.30" version = "0.3.30"
@ -1096,8 +1140,8 @@ dependencies = [
"btoi", "btoi",
"gix-date", "gix-date",
"itoa", "itoa",
"thiserror", "thiserror 1.0.58",
"winnow", "winnow 0.5.40",
] ]
[[package]] [[package]]
@ -1116,9 +1160,9 @@ dependencies = [
"memchr", "memchr",
"once_cell", "once_cell",
"smallvec", "smallvec",
"thiserror", "thiserror 1.0.58",
"unicode-bom", "unicode-bom",
"winnow", "winnow 0.5.40",
] ]
[[package]] [[package]]
@ -1131,7 +1175,7 @@ dependencies = [
"bstr", "bstr",
"gix-path", "gix-path",
"libc", "libc",
"thiserror", "thiserror 1.0.58",
] ]
[[package]] [[package]]
@ -1142,7 +1186,7 @@ checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c"
dependencies = [ dependencies = [
"bstr", "bstr",
"itoa", "itoa",
"thiserror", "thiserror 1.0.58",
"time", "time",
] ]
@ -1188,7 +1232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f8cf8c2266f63e582b7eb206799b63aa5fa68ee510ad349f637dfe2d0653de0" checksum = "1f8cf8c2266f63e582b7eb206799b63aa5fa68ee510ad349f637dfe2d0653de0"
dependencies = [ dependencies = [
"faster-hex", "faster-hex",
"thiserror", "thiserror 1.0.58",
] ]
[[package]] [[package]]
@ -1199,7 +1243,7 @@ checksum = "7e5c65e6a29830a435664891ced3f3c1af010f14900226019590ee0971a22f37"
dependencies = [ dependencies = [
"gix-tempfile", "gix-tempfile",
"gix-utils", "gix-utils",
"thiserror", "thiserror 1.0.58",
] ]
[[package]] [[package]]
@ -1217,8 +1261,8 @@ dependencies = [
"gix-validate", "gix-validate",
"itoa", "itoa",
"smallvec", "smallvec",
"thiserror", "thiserror 1.0.58",
"winnow", "winnow 0.5.40",
] ]
[[package]] [[package]]
@ -1231,7 +1275,7 @@ dependencies = [
"gix-trace", "gix-trace",
"home", "home",
"once_cell", "once_cell",
"thiserror", "thiserror 1.0.58",
] ]
[[package]] [[package]]
@ -1251,8 +1295,8 @@ dependencies = [
"gix-tempfile", "gix-tempfile",
"gix-validate", "gix-validate",
"memmap2", "memmap2",
"thiserror", "thiserror 1.0.58",
"winnow", "winnow 0.5.40",
] ]
[[package]] [[package]]
@ -1303,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545" checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545"
dependencies = [ dependencies = [
"bstr", "bstr",
"thiserror", "thiserror 1.0.58",
] ]
[[package]] [[package]]
@ -1336,6 +1380,25 @@ dependencies = [
"walkdir", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@ -1383,12 +1446,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.9" version = "0.5.9"
@ -1447,6 +1504,7 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"h2",
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
@ -1467,6 +1525,7 @@ dependencies = [
"http", "http",
"hyper", "hyper",
"hyper-util", "hyper-util",
"log",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
@ -1564,7 +1623,7 @@ dependencies = [
"miette", "miette",
"project-origins", "project-origins",
"radix_trie", "radix_trie",
"thiserror", "thiserror 1.0.58",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -1583,7 +1642,7 @@ dependencies = [
"normalize-path", "normalize-path",
"project-origins", "project-origins",
"radix_trie", "radix_trie",
"thiserror", "thiserror 1.0.58",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -1607,6 +1666,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 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]] [[package]]
name = "indoc" name = "indoc"
version = "2.0.6" version = "2.0.6"
@ -1635,14 +1704,12 @@ dependencies = [
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.42.2" version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [ dependencies = [
"console", "console",
"linked-hash-map",
"once_cell", "once_cell",
"pin-project",
"similar", "similar",
] ]
@ -1659,6 +1726,21 @@ dependencies = [
"syn 2.0.100", "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]] [[package]]
name = "ioctl-rs" name = "ioctl-rs"
version = "0.1.6" version = "0.1.6"
@ -1710,6 +1792,28 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 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]] [[package]]
name = "jpeg-decoder" name = "jpeg-decoder"
version = "0.3.1" version = "0.3.1"
@ -1726,6 +1830,92 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "kqueue" name = "kqueue"
version = "1.0.8" version = "1.0.8"
@ -1785,12 +1975,6 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.4.14"
@ -1887,7 +2071,7 @@ checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
dependencies = [ dependencies = [
"miette-derive", "miette-derive",
"once_cell", "once_cell",
"thiserror", "thiserror 1.0.58",
"unicode-width 0.1.11", "unicode-width 0.1.11",
] ]
@ -2159,16 +2343,6 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "num_threads" name = "num_threads"
version = "0.1.7" version = "0.1.7"
@ -2201,7 +2375,9 @@ dependencies = [
"ignore", "ignore",
"ignore-files 2.1.0", "ignore-files 2.1.0",
"insta", "insta",
"interprocess",
"itertools 0.10.5", "itertools 0.10.5",
"jsonrpsee",
"machine-uid", "machine-uid",
"mio 1.0.3", "mio 1.0.3",
"napi", "napi",
@ -2219,6 +2395,8 @@ dependencies = [
"reqwest", "reqwest",
"rkyv", "rkyv",
"rusqlite", "rusqlite",
"serde",
"serde_json",
"swc_common", "swc_common",
"swc_ecma_ast", "swc_ecma_ast",
"swc_ecma_dep_graph", "swc_ecma_dep_graph",
@ -2228,7 +2406,7 @@ dependencies = [
"tar", "tar",
"tempfile", "tempfile",
"terminal-colorsaurus", "terminal-colorsaurus",
"thiserror", "thiserror 1.0.58",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
@ -2236,6 +2414,7 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"tui-logger", "tui-logger",
"tui-term 0.2.0 (git+https://github.com/JamesHenry/tui-term?rev=88e3b61425c97220c528ef76c188df10032a75dd)", "tui-term 0.2.0 (git+https://github.com/JamesHenry/tui-term?rev=88e3b61425c97220c528ef76c188df10032a75dd)",
"uuid",
"vt100-ctt", "vt100-ctt",
"walkdir", "walkdir",
"watchexec", "watchexec",
@ -2332,6 +2511,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -2524,6 +2709,15 @@ dependencies = [
"syn 2.0.100", "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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.94" version = "1.0.94"
@ -2592,7 +2786,7 @@ dependencies = [
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"rustls", "rustls",
"socket2", "socket2",
"thiserror", "thiserror 1.0.58",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -2609,7 +2803,7 @@ dependencies = [
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"rustls", "rustls",
"slab", "slab",
"thiserror", "thiserror 1.0.58",
"tinyvec", "tinyvec",
"tracing", "tracing",
] ]
@ -2754,6 +2948,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@ -2962,10 +3162,11 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.25" version = "0.23.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
dependencies = [ dependencies = [
"log",
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -2974,6 +3175,18 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "2.2.0" version = "2.2.0"
@ -2990,10 +3203,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-platform-verifier"
version = "0.103.1" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -3021,6 +3261,15 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.1" version = "1.0.1"
@ -3039,6 +3288,29 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" 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]] [[package]]
name = "semver" name = "semver"
version = "1.0.22" version = "1.0.22"
@ -3047,18 +3319,18 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.197" version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3624,7 +3896,16 @@ version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [ 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]] [[package]]
@ -3638,6 +3919,17 @@ dependencies = [
"syn 2.0.100", "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]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.8" version = "1.1.8"
@ -3709,27 +4001,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"libc", "libc",
"mio 0.8.11", "mio 1.0.3",
"num_cpus",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.3.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3770,6 +4061,23 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@ -3816,7 +4124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"thiserror", "thiserror 1.0.58",
"time", "time",
"tracing-subscriber", "tracing-subscriber",
] ]
@ -4029,6 +4337,9 @@ name = "uuid"
version = "1.8.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [
"getrandom 0.2.12",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
@ -4205,7 +4516,7 @@ dependencies = [
"notify", "notify",
"once_cell", "once_cell",
"project-origins", "project-origins",
"thiserror", "thiserror 1.0.58",
"tokio", "tokio",
"tracing", "tracing",
"watchexec-events", "watchexec-events",
@ -4248,7 +4559,7 @@ checksum = "af0a778522cf0fc2fa8a8f1380e32893208cb2e7fd33e64de8bd81a00a2a7838"
dependencies = [ dependencies = [
"miette", "miette",
"nix 0.27.1", "nix 0.27.1",
"thiserror", "thiserror 1.0.58",
] ]
[[package]] [[package]]
@ -4276,6 +4587,24 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.26.8" version = "0.26.8"
@ -4303,6 +4632,12 @@ dependencies = [
"rustix 0.38.38", "rustix 0.38.38",
] ]
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -4422,6 +4757,15 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@ -4449,6 +4793,21 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.48.5" version = "0.48.5"
@ -4496,6 +4855,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.0", "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]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.48.5" version = "0.48.5"
@ -4514,6 +4879,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
@ -4532,6 +4903,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
@ -4562,6 +4939,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
@ -4580,6 +4963,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 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]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
@ -4598,6 +4987,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 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]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.48.5"
@ -4616,6 +5011,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
@ -4643,6 +5044,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winnow"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"

View File

@ -12,6 +12,13 @@ opt-level = "z"
strip = "none" strip = "none"
[dependencies] [dependencies]
tokio = { version = "1.44.0", features = [
"sync",
"macros",
"io-util",
"rt",
"time",
] }
anyhow = "1.0.71" anyhow = "1.0.71"
better-panic = "0.3.0" better-panic = "0.3.0"
colored = "2" colored = "2"
@ -51,7 +58,6 @@ terminal-colorsaurus = "0.4.0"
thiserror = "1.0.40" thiserror = "1.0.40"
tracing = "0.1.37" tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } 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" tokio-util = "0.7.9"
tracing-appender = "0.2" tracing-appender = "0.2"
tui-logger = { version = "0.17.2", features = ["tracing-support"] } 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' walkdir = '2.3.3'
xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] } xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] }
vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "b15dc3b0f7db94167a9c584f1d403899c0cc871d" } vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "b15dc3b0f7db94167a9c584f1d403899c0cc871d" }
serde = "1.0.219"
serde_json = "1.0.140"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["fileapi", "psapi", "shellapi"] } 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" ignore-files = "2.1.0"
fs4 = "0.12.0" fs4 = "0.12.0"
ratatui = { version = "0.29", features = ["scrolling-regions"] } 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"] } rusqlite = { version = "0.32.1", features = ["bundled", "array", "vtab"] }
watchexec = "3.0.1" watchexec = "3.0.1"
watchexec-events = "2.0.1" watchexec-events = "2.0.1"
watchexec-filterer-ignore = "3.0.0" watchexec-filterer-ignore = "3.0.0"
watchexec-signals = "2.1.0" watchexec-signals = "2.1.0"
machine-uid = "0.5.2" 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] [lib]
crate-type = ['cdylib'] crate-type = ['cdylib']
@ -94,3 +111,4 @@ insta = "1.42.2"
# This is only used for unit tests # This is only used for unit tests
swc_ecma_dep_graph = "0.109.1" swc_ecma_dep_graph = "0.109.1"
tempfile = "3.13.0" tempfile = "3.13.0"
uuid = { version = "1.0", features = ["v4"] }

View File

@ -8,7 +8,7 @@ export declare class ExternalObject<T> {
} }
} }
export declare class AppLifeCycle { export declare class AppLifeCycle {
constructor(tasks: Array<Task>, initiatingTasks: Array<string>, runMode: RunMode, pinnedTasks: Array<string>, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string) constructor(tasks: Array<Task>, initiatingTasks: Array<string>, runMode: RunMode, pinnedTasks: Array<string>, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string, workspaceRoot: string)
startCommand(threadCount?: number | undefined | null): void startCommand(threadCount?: number | undefined | null): void
scheduleTask(task: Task): void scheduleTask(task: Task): void
startTasks(tasks: Array<Task>, metadata: object): void startTasks(tasks: Array<Task>, metadata: object): void

View File

@ -62,10 +62,15 @@ const originalLoad = Module._load;
// Will only be called once because the require cache takes over afterwards. // Will only be called once because the require cache takes over afterwards.
Module._load = function (request, parent, isMain) { Module._load = function (request, parent, isMain) {
const modulePath = request; 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) || 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 nativeLocation = require.resolve(modulePath);
const fileName = basename(nativeLocation); const fileName = basename(nativeLocation);

View File

@ -35,4 +35,6 @@ pub enum Action {
StartTasks(Vec<Task>), StartTasks(Vec<Task>),
EndTasks(Vec<TaskResult>), EndTasks(Vec<TaskResult>),
ToggleDebugMode, ToggleDebugMode,
SendConsoleMessage(String),
ConsoleMessengerAvailable(bool),
} }

View File

@ -23,7 +23,6 @@ use crate::native::{
tasks::types::{Task, TaskResult}, tasks::types::{Task, TaskResult},
}; };
use super::action::Action;
use super::components::Component; use super::components::Component;
use super::components::countdown_popup::CountdownPopup; use super::components::countdown_popup::CountdownPopup;
use super::components::help_popup::HelpPopup; use super::components::help_popup::HelpPopup;
@ -39,6 +38,7 @@ use super::pty::PtyInstance;
use super::theme::THEME; use super::theme::THEME;
use super::tui; use super::tui;
use super::utils::normalize_newlines; use super::utils::normalize_newlines;
use super::{action::Action, nx_console::messaging::NxConsoleMessageConnection};
pub struct App { pub struct App {
pub components: Vec<Box<dyn Component>>, pub components: Vec<Box<dyn Component>>,
@ -68,6 +68,7 @@ pub struct App {
tasks: Vec<Task>, tasks: Vec<Task>,
debug_mode: bool, debug_mode: bool,
debug_state: TuiWidgetState, debug_state: TuiWidgetState,
console_messenger: Option<NxConsoleMessageConnection>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -134,6 +135,7 @@ impl App {
tasks, tasks,
debug_mode: false, debug_mode: false,
debug_state: TuiWidgetState::default().set_default_display_level(LevelFilter::Debug), 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) // Show countdown popup for the configured duration (making sure the help popup is not open first)
pub fn end_command(&mut self) { 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 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() { if self.user_has_interacted || !self.tui_config.auto_exit.should_exit_automatically() {
return; return;
@ -321,6 +327,10 @@ impl App {
&& !(matches!(self.focus, Focus::MultipleOutput(_)) && !(matches!(self.focus, Focus::MultipleOutput(_))
&& self.is_interactive_mode()) && self.is_interactive_mode())
{ {
self.console_messenger
.as_ref()
.and_then(|c| c.end_running_tasks());
self.is_forced_shutdown = true; self.is_forced_shutdown = true;
// Quit immediately // Quit immediately
self.quit_at = Some(std::time::Instant::now()); self.quit_at = Some(std::time::Instant::now());
@ -769,8 +779,25 @@ impl App {
trace!("{action:?}"); trace!("{action:?}");
} }
match &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::<TasksList>())
.and_then(|tasks_list| {
messenger.update_running_tasks(&tasks_list.tasks, &self.pty_instances)
})
});
}
// Quit immediately // Quit immediately
Action::Quit => self.quit_at = Some(std::time::Instant::now()), Action::Quit => {
self.quit_at = Some(std::time::Instant::now());
}
// Cancel quitting // Cancel quitting
Action::CancelQuit => { Action::CancelQuit => {
self.quit_at = None; self.quit_at = None;
@ -981,6 +1008,7 @@ impl App {
is_focused, is_focused,
has_pty, has_pty,
is_next_tab_target, is_next_tab_target,
self.console_messenger.is_some(),
); );
let terminal_pane = TerminalPane::new() let terminal_pane = TerminalPane::new()
@ -1008,6 +1036,13 @@ impl App {
}) })
.ok(); .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<()> { fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> {
if let Focus::MultipleOutput(pane_idx) = self.focus { if let Focus::MultipleOutput(pane_idx) = self.focus {
let terminal_pane_data = &mut self.terminal_pane_data[pane_idx]; 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 { } else {
Ok(()) Ok(())
} }
@ -1540,4 +1578,15 @@ impl App {
self.debug_state.transition(event); 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));
}
}
} }

View File

@ -12,9 +12,11 @@ use std::any::Any;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use super::{Component, Frame}; use super::{Component, Frame};
use crate::native::tui::action::Action; use crate::native::tui::{action::Action, nx_console};
use crate::native::tui::theme::THEME; use crate::native::tui::theme::THEME;
#[derive(Default)]
pub struct HelpPopup { pub struct HelpPopup {
scroll_offset: usize, scroll_offset: usize,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
@ -22,6 +24,7 @@ pub struct HelpPopup {
viewport_height: usize, viewport_height: usize,
visible: bool, visible: bool,
action_tx: Option<UnboundedSender<Action>>, action_tx: Option<UnboundedSender<Action>>,
console_available: bool,
} }
impl HelpPopup { impl HelpPopup {
@ -33,6 +36,7 @@ impl HelpPopup {
viewport_height: 0, viewport_height: 0,
visible: false, visible: false,
action_tx: None, action_tx: None,
console_available: false,
} }
} }
@ -40,6 +44,10 @@ impl HelpPopup {
self.visible = visible; 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 // Ensure the scroll state is reset to avoid recalc issues
pub fn handle_resize(&mut self, _width: u16, _height: u16) { pub fn handle_resize(&mut self, _width: u16, _height: u16) {
self.scroll_offset = 0; self.scroll_offset = 0;
@ -110,7 +118,7 @@ impl HelpPopup {
]) ])
.split(popup_layout[1])[1]; .split(popup_layout[1])[1];
let keybindings = vec![ let mut keybindings = vec![
// Misc // Misc
("?", "Toggle this popup"), ("?", "Toggle this popup"),
("q or <ctrl>+c", "Quit the TUI"), ("q or <ctrl>+c", "Quit the TUI"),
@ -149,6 +157,25 @@ impl HelpPopup {
("<ctrl>+z", "Stop interacting with a continuous task"), ("<ctrl>+z", "Stop interacting with a continuous task"),
]; ];
if self.console_available {
// add Copilot specific keybindings for AI assistance
keybindings.extend([
("", ""),
(
"<ctrl>+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<Line> = vec![ let mut content: Vec<Line> = vec![
// Welcome text // Welcome text
Line::from(vec![ Line::from(vec![
@ -357,8 +384,14 @@ impl Component for HelpPopup {
} }
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
if let Action::Resize(w, h) = action { match action {
self.handle_resize(w, h); Action::Resize(w, h) => {
self.handle_resize(w, h);
}
Action::ConsoleMessengerAvailable(available) => {
self.set_console_available(available);
}
_ => {}
} }
Ok(None) Ok(None)
} }

View File

@ -7,6 +7,7 @@ use ratatui::{
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Cell, Paragraph, Row, Table}, widgets::{Block, Cell, Paragraph, Row, Table},
}; };
use serde::{Deserialize, Serialize};
use std::{ use std::{
any::Any, any::Any,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@ -52,7 +53,7 @@ pub struct TaskItem {
cache_status: String, cache_status: String,
// Public to aid with sorting utility and testing // Public to aid with sorting utility and testing
pub status: TaskStatus, pub status: TaskStatus,
terminal_output: String, pub terminal_output: String,
pub continuous: bool, pub continuous: bool,
start_time: Option<i64>, start_time: Option<i64>,
// Public to aid with sorting utility and testing // Public to aid with sorting utility and testing
@ -115,7 +116,7 @@ impl TaskItem {
} }
#[napi] #[napi]
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskStatus { pub enum TaskStatus {
// Explicit statuses that can come from the task runner // Explicit statuses that can come from the task runner
Success, Success,

View File

@ -13,9 +13,9 @@ use ratatui::{
use std::{io, sync::Arc}; use std::{io, sync::Arc};
use tui_term::widget::PseudoTerminal; use tui_term::widget::PseudoTerminal;
use super::tasks_list::TaskStatus; use crate::native::tui::components::tasks_list::TaskStatus;
use crate::native::tui::pty::PtyInstance;
use crate::native::tui::theme::THEME; use crate::native::tui::theme::THEME;
use crate::native::tui::{action::Action, pty::PtyInstance};
pub struct TerminalPaneData { pub struct TerminalPaneData {
pub pty: Option<Arc<PtyInstance>>, pub pty: Option<Arc<PtyInstance>>,
@ -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<Option<Action>> {
if let Some(pty) = &mut self.pty { if let Some(pty) = &mut self.pty {
let mut pty_mut = pty.as_ref().clone(); let mut pty_mut = pty.as_ref().clone();
match key.code { 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. // 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 => { KeyCode::Up | KeyCode::Char('k') if !self.is_interactive => {
pty_mut.scroll_up(); pty_mut.scroll_up();
return Ok(()); return Ok(None);
} }
KeyCode::Down | KeyCode::Char('j') if !self.is_interactive => { KeyCode::Down | KeyCode::Char('j') if !self.is_interactive => {
pty_mut.scroll_down(); pty_mut.scroll_down();
return Ok(()); return Ok(None);
} }
// Handle ctrl+u and ctrl+d for scrolling when not in interactive mode // Handle ctrl+u and ctrl+d for scrolling when not in interactive mode
KeyCode::Char('u') KeyCode::Char('u')
@ -56,7 +56,7 @@ impl TerminalPaneData {
for _ in 0..12 { for _ in 0..12 {
pty_mut.scroll_up(); pty_mut.scroll_up();
} }
return Ok(()); return Ok(None);
} }
KeyCode::Char('d') KeyCode::Char('d')
if key.modifiers.contains(KeyModifiers::CONTROL) && !self.is_interactive => if key.modifiers.contains(KeyModifiers::CONTROL) && !self.is_interactive =>
@ -65,7 +65,7 @@ impl TerminalPaneData {
for _ in 0..12 { for _ in 0..12 {
pty_mut.scroll_down(); pty_mut.scroll_down();
} }
return Ok(()); return Ok(None);
} }
// Handle 'c' for copying when not in interactive mode // Handle 'c' for copying when not in interactive mode
KeyCode::Char('c') if !self.is_interactive => { 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 // Handle 'i' to enter interactive mode for in progress tasks
KeyCode::Char('i') if self.can_be_interactive && !self.is_interactive => { KeyCode::Char('i') if self.can_be_interactive && !self.is_interactive => {
self.set_interactive(true); 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 // Only send input to PTY if we're in interactive mode
_ if self.is_interactive => match key.code { _ 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<()> { pub fn handle_mouse_event(&mut self, event: MouseEvent) -> io::Result<()> {
@ -161,6 +169,7 @@ pub struct TerminalPaneState {
pub scrollbar_state: ScrollbarState, pub scrollbar_state: ScrollbarState,
pub has_pty: bool, pub has_pty: bool,
pub is_next_tab_target: bool, pub is_next_tab_target: bool,
pub console_available: bool,
} }
impl TerminalPaneState { impl TerminalPaneState {
@ -171,6 +180,7 @@ impl TerminalPaneState {
is_focused: bool, is_focused: bool,
has_pty: bool, has_pty: bool,
is_next_tab_target: bool, is_next_tab_target: bool,
console_available: bool,
) -> Self { ) -> Self {
Self { Self {
task_name, task_name,
@ -181,6 +191,7 @@ impl TerminalPaneState {
scrollbar_state: ScrollbarState::default(), scrollbar_state: ScrollbarState::default(),
has_pty, has_pty,
is_next_tab_target, is_next_tab_target,
console_available,
} }
} }
} }

View File

@ -5,13 +5,17 @@ use parking_lot::Mutex;
use std::sync::Arc; use std::sync::Arc;
use tracing::debug; 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::app::App;
use super::components::tasks_list::TaskStatus; use super::components::tasks_list::TaskStatus;
use super::config::{AutoExit, TuiCliArgs as RustTuiCliArgs, TuiConfig as RustTuiConfig}; use super::config::{AutoExit, TuiCliArgs as RustTuiCliArgs, TuiConfig as RustTuiConfig};
use super::tui::Tui; 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)] #[napi(object)]
#[derive(Clone)] #[derive(Clone)]
@ -63,6 +67,7 @@ pub enum RunMode {
#[derive(Clone)] #[derive(Clone)]
pub struct AppLifeCycle { pub struct AppLifeCycle {
app: Arc<Mutex<App>>, app: Arc<Mutex<App>>,
workspace_root: Arc<String>,
} }
#[napi] #[napi]
@ -76,6 +81,7 @@ impl AppLifeCycle {
tui_cli_args: TuiCliArgs, tui_cli_args: TuiCliArgs,
tui_config: TuiConfig, tui_config: TuiConfig,
title_text: String, title_text: String,
workspace_root: String,
) -> Self { ) -> Self {
// Get the target names from nx_args.targets // Get the target names from nx_args.targets
let rust_tui_cli_args = tui_cli_args.into(); let rust_tui_cli_args = tui_cli_args.into();
@ -97,6 +103,7 @@ impl AppLifeCycle {
) )
.unwrap(), .unwrap(),
)), )),
workspace_root: Arc::new(workspace_root),
} }
} }
@ -207,7 +214,14 @@ impl AppLifeCycle {
debug!("Initialized Components"); debug!("Initialized Components");
let workspace_root = self.workspace_root.clone();
napi::tokio::spawn(async move { 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 { loop {
// Handle events using our Tui abstraction // Handle events using our Tui abstraction
if let Some(event) = tui.next().await { if let Some(event) = tui.next().await {

View File

@ -3,6 +3,7 @@ pub mod app;
pub mod components; pub mod components;
pub mod config; pub mod config;
pub mod lifecycle; pub mod lifecycle;
pub mod nx_console;
pub mod pty; pub mod pty;
pub mod theme; pub mod theme;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]

View File

@ -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<SupportedEditor> = 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<String, String>) -> 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<String, String>) -> 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);
}
}

View File

@ -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<Self, anyhow::Error> {
let socket_path = socket_path.to_fs_name::<GenericFilePath>()?;
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<Stream>);
pub struct IpcTransportReceiver(Arc<Stream>);
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<ReceivedMessage, Self::Error> {
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<Output = Self::ServerSocket> + Send;
type ConnectFuture: Future<Output = Result<IpcTransport, anyhow::Error>> + 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<T: IpcTestSetup>() {
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<T: IpcTestSetup>() {
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<T: IpcTestSetup>() {
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<Box<dyn Future<Output = tokio::net::UnixStream> + Send>>;
type ConnectClientFuture =
Pin<Box<dyn Future<Output = Result<IpcTransport, anyhow::Error>> + 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::<UnixIpcTestSetup>().await;
}
#[tokio::test]
async fn test_transport_sender_send() {
test_utils::test_transport_sender_send::<UnixIpcTestSetup>().await;
}
#[tokio::test]
async fn test_transport_receiver_receive() {
test_utils::test_transport_receiver_receive::<UnixIpcTestSetup>().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<Box<dyn Future<Output = tokio::net::windows::named_pipe::NamedPipeServer> + Send>>;
type ConnectClientFuture =
Pin<Box<dyn Future<Output = Result<IpcTransport, anyhow::Error>> + 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::<WindowsIpcTestSetup>().await;
}
#[tokio::test]
async fn test_transport_sender_send() {
test_utils::test_transport_sender_send::<WindowsIpcTestSetup>().await;
}
#[tokio::test]
async fn test_transport_receiver_receive() {
test_utils::test_transport_receiver_receive::<WindowsIpcTestSetup>().await;
}
}

View File

@ -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<UpdatedRunningTask>);
#[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<Arc<Client>>,
}
static LAST_UPDATES: Lazy<Mutex<HashMap<&'static str, Instant>>> =
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<String>) -> 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<String, Arc<PtyInstance>>,
) -> Option<()> {
if throttled("update_running_tasks", THROTTLE_DURATION) {
return None;
}
self.client.as_ref().map(|client| {
let client = client.clone();
let task_statuses: Vec<UpdatedRunningTask> = 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);
}
});
})
}
}

View File

@ -2,6 +2,7 @@ mod find_matching_projects;
mod get_mod_time; mod get_mod_time;
mod normalize_trait; mod normalize_trait;
pub mod path; pub mod path;
pub mod socket_path;
pub use find_matching_projects::*; pub use find_matching_projects::*;
pub use get_mod_time::*; pub use get_mod_time::*;

View File

@ -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"));
}
}

View File

@ -199,7 +199,8 @@ async function getTerminalOutputLifeCycle(
pinnedTasks, pinnedTasks,
nxArgs ?? {}, nxArgs ?? {},
nxJson.tui ?? {}, nxJson.tui ?? {},
titleText titleText,
workspaceRoot
); );
lifeCycles.unshift(appLifeCycle); lifeCycles.unshift(appLifeCycle);