feat(code): wire TUI into real code path, /exit, color swap
- user input: white text, dim > prompt - sol responses: warm yellow - /exit slash command quits cleanly - TUI replaces stdin loop in sunbeam code start - hidden demo mode for testing (sunbeam code demo)
This commit is contained in:
576
Cargo.lock
generated
576
Cargo.lock
generated
@@ -274,6 +274,49 @@ dependencies = [
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"sync_wrapper",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backon"
|
||||
version = "1.6.0"
|
||||
@@ -369,6 +412,21 @@ version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -502,6 +560,20 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -587,6 +659,31 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
@@ -658,8 +755,18 @@ version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.20.11",
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -676,13 +783,37 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.20.11",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
@@ -1011,6 +1142,12 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -1651,6 +1788,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -1661,6 +1807,19 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@@ -1683,6 +1842,24 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -1832,7 +2009,7 @@ version = "0.99.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c562f58dc9f7ca5feac8a6ee5850ca221edd6f04ce0dd2ee873202a88cd494c9"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
@@ -1936,6 +2113,12 @@ dependencies = [
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -1963,6 +2146,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
@@ -1978,6 +2170,12 @@ dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
@@ -2019,10 +2217,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -2269,6 +2474,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -2347,6 +2558,17 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.11"
|
||||
@@ -2498,6 +2720,59 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
"petgraph",
|
||||
"prettyplease",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"pulldown-cmark",
|
||||
"pulldown-cmark-to-cmark",
|
||||
"regex",
|
||||
"syn",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
|
||||
dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.30"
|
||||
@@ -2508,6 +2783,26 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-to-cmark"
|
||||
version = "22.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90"
|
||||
dependencies = [
|
||||
"pulldown-cmark",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@@ -2649,6 +2944,27 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.14.7"
|
||||
@@ -2968,6 +3284,19 @@ dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
@@ -2977,7 +3306,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -3259,6 +3588,15 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -3321,6 +3659,27 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -3455,12 +3814,40 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -3471,15 +3858,35 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
name = "sunbeam"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sunbeam-proto",
|
||||
"sunbeam-sdk",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"toml",
|
||||
"tonic",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sunbeam-proto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prost",
|
||||
"tonic",
|
||||
"tonic-build",
|
||||
"tonic-prost",
|
||||
"tonic-prost-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sunbeam-sdk"
|
||||
version = "1.0.1"
|
||||
@@ -3567,7 +3974,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -3761,6 +4168,115 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64",
|
||||
"bytes",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-timeout",
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"socket2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-build"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-prost"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-prost-build"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"tonic-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
@@ -3769,7 +4285,9 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -3908,12 +4426,47 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
@@ -4515,6 +5068,15 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wiremock"
|
||||
version = "0.6.5"
|
||||
@@ -4657,7 +5219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
"rustix 1.1.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod client;
|
||||
pub mod config;
|
||||
pub mod project;
|
||||
pub mod tools;
|
||||
pub mod tui;
|
||||
|
||||
use clap::Subcommand;
|
||||
use tracing::info;
|
||||
@@ -17,6 +18,9 @@ pub enum CodeCommand {
|
||||
#[arg(long)]
|
||||
endpoint: Option<String>,
|
||||
},
|
||||
/// Demo the TUI with sample data (no Sol connection needed)
|
||||
#[command(hide = true)]
|
||||
Demo,
|
||||
}
|
||||
|
||||
pub async fn cmd_code(cmd: Option<CodeCommand>) -> sunbeam_sdk::error::Result<()> {
|
||||
@@ -30,6 +34,9 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
});
|
||||
|
||||
match cmd {
|
||||
CodeCommand::Demo => {
|
||||
return run_demo().await;
|
||||
}
|
||||
CodeCommand::Start { model, endpoint } => {
|
||||
let endpoint = endpoint.unwrap_or_else(|| "http://127.0.0.1:50051".into());
|
||||
|
||||
@@ -59,39 +66,187 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
"Connected to Sol"
|
||||
);
|
||||
|
||||
// For now, simple stdin loop (ratatui TUI in Phase 4)
|
||||
println!("sunbeam code · {} · {}", project.name, model);
|
||||
println!("connected to Sol (session: {})", &session.session_id[..8]);
|
||||
println!("type a message, /quit to exit\n");
|
||||
// TUI event loop
|
||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let reader = tokio::io::BufReader::new(stdin);
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
let mut lines = reader.lines();
|
||||
let mut terminal = tui::setup_terminal()?;
|
||||
let branch = project.git_branch.as_deref().unwrap_or("?");
|
||||
let mut app = tui::App::new(&project.name, branch, &model);
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let line = line.trim().to_string();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
let result = loop {
|
||||
terminal.draw(|frame| tui::draw(frame, &app))?;
|
||||
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break Ok(()),
|
||||
KeyCode::Char(c) => {
|
||||
app.input.insert(app.cursor_pos, c);
|
||||
app.cursor_pos += 1;
|
||||
}
|
||||
if line == "/quit" {
|
||||
session.end().await?;
|
||||
println!("session ended.");
|
||||
KeyCode::Backspace => {
|
||||
if app.cursor_pos > 0 {
|
||||
app.cursor_pos -= 1;
|
||||
app.input.remove(app.cursor_pos);
|
||||
}
|
||||
}
|
||||
KeyCode::Left => app.cursor_pos = app.cursor_pos.saturating_sub(1),
|
||||
KeyCode::Right => app.cursor_pos = (app.cursor_pos + 1).min(app.input.len()),
|
||||
KeyCode::Up => app.scroll_offset = app.scroll_offset.saturating_sub(1),
|
||||
KeyCode::Down => app.scroll_offset = app.scroll_offset.saturating_add(1),
|
||||
KeyCode::Enter => {
|
||||
if !app.input.is_empty() {
|
||||
let text = app.input.clone();
|
||||
app.input.clear();
|
||||
app.cursor_pos = 0;
|
||||
|
||||
if text == "/exit" {
|
||||
let _ = session.end().await;
|
||||
break Ok(());
|
||||
}
|
||||
|
||||
app.push_log(tui::LogEntry::UserInput(text.clone()));
|
||||
app.is_thinking = true;
|
||||
|
||||
// Force a redraw to show "thinking..."
|
||||
terminal.draw(|frame| tui::draw(frame, &app))?;
|
||||
|
||||
match session.chat(&text).await {
|
||||
Ok(response) => {
|
||||
app.is_thinking = false;
|
||||
app.push_log(tui::LogEntry::AssistantText(response));
|
||||
}
|
||||
Err(e) => {
|
||||
app.is_thinking = false;
|
||||
app.push_log(tui::LogEntry::Error(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tui::restore_terminal(&mut terminal)?;
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_demo() -> anyhow::Result<()> {
|
||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||
|
||||
let mut terminal = tui::setup_terminal()?;
|
||||
let mut app = tui::App::new("sol", "mainline ±", "devstral-2");
|
||||
|
||||
// Populate with sample conversation
|
||||
app.push_log(tui::LogEntry::UserInput("fix the token validation bug in auth.rs".into()));
|
||||
app.push_log(tui::LogEntry::AssistantText(
|
||||
"Looking at the auth module, I can see the issue on line 42 where the token \
|
||||
is not properly validated before use. The expiry check is missing entirely."
|
||||
.into(),
|
||||
));
|
||||
app.push_log(tui::LogEntry::ToolSuccess {
|
||||
name: "file_read".into(),
|
||||
detail: "src/auth.rs (127 lines)".into(),
|
||||
});
|
||||
app.push_log(tui::LogEntry::ToolOutput {
|
||||
lines: vec![
|
||||
"38│ fn validate_token(token: &str) -> bool {".into(),
|
||||
"39│ let decoded = decode(token);".into(),
|
||||
"40│ // BUG: missing expiry check".into(),
|
||||
"41│ decoded.is_ok()".into(),
|
||||
"42│ }".into(),
|
||||
"43│".into(),
|
||||
"44│ fn refresh_token(token: &str) -> Result<String> {".into(),
|
||||
"45│ let client = reqwest::Client::new();".into(),
|
||||
"46│ // ...".into(),
|
||||
],
|
||||
collapsed: true,
|
||||
});
|
||||
app.push_log(tui::LogEntry::ToolSuccess {
|
||||
name: "search_replace".into(),
|
||||
detail: "src/auth.rs — applied 1 replacement (line 41)".into(),
|
||||
});
|
||||
app.push_log(tui::LogEntry::ToolExecuting {
|
||||
name: "bash".into(),
|
||||
detail: "cargo test --lib".into(),
|
||||
});
|
||||
app.push_log(tui::LogEntry::ToolOutput {
|
||||
lines: vec![
|
||||
"running 23 tests".into(),
|
||||
"test auth::tests::test_validate_token ... ok".into(),
|
||||
"test auth::tests::test_expired_token ... ok".into(),
|
||||
"test auth::tests::test_refresh_flow ... ok".into(),
|
||||
"test result: ok. 23 passed; 0 failed".into(),
|
||||
],
|
||||
collapsed: false,
|
||||
});
|
||||
app.push_log(tui::LogEntry::AssistantText(
|
||||
"Fixed. The token validation now checks expiry before use. All 23 tests pass."
|
||||
.into(),
|
||||
));
|
||||
app.push_log(tui::LogEntry::UserInput("now add rate limiting to the auth endpoint".into()));
|
||||
app.push_log(tui::LogEntry::ToolExecuting {
|
||||
name: "file_read".into(),
|
||||
detail: "src/routes/auth.rs".into(),
|
||||
});
|
||||
app.is_thinking = true;
|
||||
app.input_tokens = 2400;
|
||||
app.output_tokens = 890;
|
||||
|
||||
loop {
|
||||
terminal.draw(|frame| tui::draw(frame, &app))?;
|
||||
|
||||
if event::poll(std::time::Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break,
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Char(c) => {
|
||||
app.input.insert(app.cursor_pos, c);
|
||||
app.cursor_pos += 1;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
if app.cursor_pos > 0 {
|
||||
app.cursor_pos -= 1;
|
||||
app.input.remove(app.cursor_pos);
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.cursor_pos = app.cursor_pos.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.cursor_pos = (app.cursor_pos + 1).min(app.input.len());
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if !app.input.is_empty() {
|
||||
let text = app.input.clone();
|
||||
app.input.clear();
|
||||
app.cursor_pos = 0;
|
||||
|
||||
if text == "/exit" {
|
||||
break;
|
||||
}
|
||||
|
||||
print!("> ");
|
||||
match session.chat(&line).await {
|
||||
Ok(response) => {
|
||||
println!("\n{}\n", response);
|
||||
app.push_log(tui::LogEntry::UserInput(text));
|
||||
app.is_thinking = true;
|
||||
}
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.scroll_offset = app.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.scroll_offset = app.scroll_offset.saturating_add(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tui::restore_terminal(&mut terminal)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
437
sunbeam/src/code/tui.rs
Normal file
437
sunbeam/src/code/tui.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
use std::io;
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use crossterm::execute;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::Terminal;
|
||||
|
||||
// ── Sol color palette ──────────────────────────────────────────────────────
|
||||
|
||||
const SOL_YELLOW: Color = Color::Rgb(245, 197, 66);
|
||||
const SOL_AMBER: Color = Color::Rgb(232, 168, 64);
|
||||
const SOL_BLUE: Color = Color::Rgb(108, 166, 224);
|
||||
const SOL_RED: Color = Color::Rgb(224, 88, 88);
|
||||
const SOL_DIM: Color = Color::Rgb(138, 122, 90);
|
||||
const SOL_GRAY: Color = Color::Rgb(112, 112, 112);
|
||||
const SOL_FAINT: Color = Color::Rgb(80, 80, 80);
|
||||
const SOL_STATUS: Color = Color::Rgb(106, 96, 80);
|
||||
const SOL_APPROVAL_BG: Color = Color::Rgb(50, 42, 20);
|
||||
const SOL_APPROVAL_CMD: Color = Color::Rgb(200, 180, 120);
|
||||
|
||||
// ── Message types for the conversation log ─────────────────────────────────
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum LogEntry {
|
||||
UserInput(String),
|
||||
AssistantText(String),
|
||||
ToolSuccess { name: String, detail: String },
|
||||
ToolExecuting { name: String, detail: String },
|
||||
ToolFailed { name: String, detail: String },
|
||||
ToolOutput { lines: Vec<String>, collapsed: bool },
|
||||
Status(String),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
// ── Approval state ─────────────────────────────────────────────────────────
|
||||
|
||||
pub struct ApprovalPrompt {
|
||||
pub tool_name: String,
|
||||
pub command: String,
|
||||
pub options: Vec<String>,
|
||||
pub selected: usize,
|
||||
}
|
||||
|
||||
// ── App state ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct App {
|
||||
pub log: Vec<LogEntry>,
|
||||
pub input: String,
|
||||
pub cursor_pos: usize,
|
||||
pub scroll_offset: u16,
|
||||
pub project_name: String,
|
||||
pub branch: String,
|
||||
pub model: String,
|
||||
pub input_tokens: u32,
|
||||
pub output_tokens: u32,
|
||||
pub approval: Option<ApprovalPrompt>,
|
||||
pub is_thinking: bool,
|
||||
pub should_quit: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(project_name: &str, branch: &str, model: &str) -> Self {
|
||||
Self {
|
||||
log: Vec::new(),
|
||||
input: String::new(),
|
||||
cursor_pos: 0,
|
||||
scroll_offset: 0,
|
||||
project_name: project_name.into(),
|
||||
branch: branch.into(),
|
||||
model: model.into(),
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
approval: None,
|
||||
is_thinking: false,
|
||||
should_quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_log(&mut self, entry: LogEntry) {
|
||||
self.log.push(entry);
|
||||
// Auto-scroll to bottom
|
||||
self.scroll_offset = u16::MAX;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rendering ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn draw(frame: &mut ratatui::Frame, app: &App) {
|
||||
let area = frame.area();
|
||||
|
||||
// Layout: title (1) + log (flex) + input (3) + status (1)
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(1), // title bar
|
||||
Constraint::Min(5), // conversation log
|
||||
Constraint::Length(3), // input area
|
||||
Constraint::Length(1), // status bar
|
||||
])
|
||||
.split(area);
|
||||
|
||||
draw_title_bar(frame, chunks[0], app);
|
||||
draw_log(frame, chunks[1], app);
|
||||
|
||||
if let Some(ref approval) = app.approval {
|
||||
draw_approval(frame, chunks[2], approval);
|
||||
} else {
|
||||
draw_input(frame, chunks[2], app);
|
||||
}
|
||||
|
||||
draw_status_bar(frame, chunks[3], app);
|
||||
}
|
||||
|
||||
fn draw_title_bar(frame: &mut ratatui::Frame, area: Rect, app: &App) {
|
||||
let left = vec![
|
||||
Span::styled("sunbeam code", Style::default().fg(SOL_YELLOW).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(" · ", Style::default().fg(SOL_FAINT)),
|
||||
Span::raw(&app.project_name),
|
||||
Span::styled(" · ", Style::default().fg(SOL_FAINT)),
|
||||
Span::styled(&app.branch, Style::default().fg(SOL_DIM)),
|
||||
];
|
||||
|
||||
let right = Span::styled(&app.model, Style::default().fg(SOL_DIM));
|
||||
|
||||
// Render left-aligned title and right-aligned model
|
||||
let title_line = Line::from(left);
|
||||
frame.render_widget(Paragraph::new(title_line), area);
|
||||
|
||||
let right_area = Rect {
|
||||
x: area.width.saturating_sub(right.width() as u16 + 1),
|
||||
y: area.y,
|
||||
width: right.width() as u16 + 1,
|
||||
height: 1,
|
||||
};
|
||||
frame.render_widget(Paragraph::new(Line::from(right)), right_area);
|
||||
}
|
||||
|
||||
fn draw_log(frame: &mut ratatui::Frame, area: Rect, app: &App) {
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
for entry in &app.log {
|
||||
match entry {
|
||||
LogEntry::UserInput(text) => {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("> ", Style::default().fg(SOL_DIM)),
|
||||
Span::raw(text.as_str()),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
LogEntry::AssistantText(text) => {
|
||||
for line in text.lines() {
|
||||
lines.push(Line::from(Span::styled(line, Style::default().fg(SOL_YELLOW))));
|
||||
}
|
||||
}
|
||||
LogEntry::ToolSuccess { name, detail } => {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ✓ ", Style::default().fg(SOL_BLUE)),
|
||||
Span::styled(name.as_str(), Style::default().fg(SOL_AMBER)),
|
||||
Span::styled(format!(" {detail}"), Style::default().fg(SOL_DIM)),
|
||||
]));
|
||||
}
|
||||
LogEntry::ToolExecuting { name, detail } => {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ● ", Style::default().fg(SOL_AMBER)),
|
||||
Span::styled(name.as_str(), Style::default().fg(SOL_AMBER)),
|
||||
Span::styled(format!(" {detail}"), Style::default().fg(SOL_DIM)),
|
||||
]));
|
||||
}
|
||||
LogEntry::ToolFailed { name, detail } => {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ✗ ", Style::default().fg(SOL_RED)),
|
||||
Span::styled(name.as_str(), Style::default().fg(SOL_RED)),
|
||||
Span::styled(format!(" {detail}"), Style::default().fg(SOL_DIM)),
|
||||
]));
|
||||
}
|
||||
LogEntry::ToolOutput { lines: output_lines, collapsed } => {
|
||||
let show = if *collapsed { 5 } else { output_lines.len() };
|
||||
for line in output_lines.iter().take(show) {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {line}"),
|
||||
Style::default().fg(SOL_GRAY),
|
||||
)));
|
||||
}
|
||||
if *collapsed && output_lines.len() > 5 {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" … +{} lines (ctrl+o to expand)", output_lines.len() - 5),
|
||||
Style::default().fg(SOL_FAINT),
|
||||
)));
|
||||
}
|
||||
}
|
||||
LogEntry::Status(msg) => {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" [{msg}]"),
|
||||
Style::default().fg(SOL_DIM),
|
||||
)));
|
||||
}
|
||||
LogEntry::Error(msg) => {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" error: {msg}"),
|
||||
Style::default().fg(SOL_RED),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if app.is_thinking {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" thinking...",
|
||||
Style::default().fg(SOL_DIM).add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
}
|
||||
|
||||
let total_lines = lines.len() as u16;
|
||||
let visible = area.height;
|
||||
let max_scroll = total_lines.saturating_sub(visible);
|
||||
let scroll = if app.scroll_offset == u16::MAX {
|
||||
max_scroll
|
||||
} else {
|
||||
app.scroll_offset.min(max_scroll)
|
||||
};
|
||||
|
||||
let log_widget = Paragraph::new(Text::from(lines))
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((scroll, 0));
|
||||
|
||||
frame.render_widget(log_widget, area);
|
||||
}
|
||||
|
||||
fn draw_input(frame: &mut ratatui::Frame, area: Rect, app: &App) {
|
||||
let input_block = Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::default().fg(SOL_FAINT));
|
||||
|
||||
let input_text = Line::from(vec![
|
||||
Span::styled("> ", Style::default().fg(SOL_DIM)),
|
||||
Span::raw(&app.input),
|
||||
]);
|
||||
|
||||
let input_widget = Paragraph::new(input_text)
|
||||
.block(input_block)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
frame.render_widget(input_widget, area);
|
||||
|
||||
// Position cursor
|
||||
let cursor_x = area.x + 2 + app.cursor_pos as u16;
|
||||
let cursor_y = area.y + 1;
|
||||
frame.set_cursor_position((cursor_x, cursor_y));
|
||||
}
|
||||
|
||||
fn draw_approval(frame: &mut ratatui::Frame, area: Rect, approval: &ApprovalPrompt) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::default().fg(SOL_FAINT));
|
||||
|
||||
let mut lines = vec![
|
||||
Line::from(vec![
|
||||
Span::styled(" ⚠ ", Style::default().fg(SOL_YELLOW)),
|
||||
Span::styled(&approval.tool_name, Style::default().fg(SOL_YELLOW).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(format!(" {}", approval.command), Style::default().fg(SOL_APPROVAL_CMD)),
|
||||
]),
|
||||
];
|
||||
|
||||
for (i, opt) in approval.options.iter().enumerate() {
|
||||
let prefix = if i == approval.selected { " › " } else { " " };
|
||||
let style = if i == approval.selected {
|
||||
Style::default().fg(SOL_YELLOW)
|
||||
} else {
|
||||
Style::default().fg(SOL_DIM)
|
||||
};
|
||||
lines.push(Line::from(Span::styled(format!("{prefix}{opt}"), style)));
|
||||
}
|
||||
|
||||
let widget = Paragraph::new(Text::from(lines))
|
||||
.block(block)
|
||||
.style(Style::default().bg(SOL_APPROVAL_BG));
|
||||
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
|
||||
fn draw_status_bar(frame: &mut ratatui::Frame, area: Rect, app: &App) {
|
||||
let status = Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" ~/…/{}", app.project_name),
|
||||
Style::default().fg(SOL_STATUS),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" {} ±", app.branch),
|
||||
Style::default().fg(SOL_STATUS),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" {}k in · {}k out", app.input_tokens / 1000, app.output_tokens / 1000),
|
||||
Style::default().fg(SOL_STATUS),
|
||||
),
|
||||
]);
|
||||
|
||||
frame.render_widget(Paragraph::new(status), area);
|
||||
}
|
||||
|
||||
// ── Terminal setup/teardown ────────────────────────────────────────────────
|
||||
|
||||
pub fn setup_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||
terminal::enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
Terminal::new(backend)
|
||||
}
|
||||
|
||||
pub fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
|
||||
terminal::disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_app_creation() {
|
||||
let app = App::new("sol", "mainline", "devstral-2");
|
||||
assert_eq!(app.project_name, "sol");
|
||||
assert!(!app.should_quit);
|
||||
assert!(app.log.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_log_auto_scrolls() {
|
||||
let mut app = App::new("sol", "main", "devstral-2");
|
||||
app.scroll_offset = 0;
|
||||
app.push_log(LogEntry::Status("test".into()));
|
||||
assert_eq!(app.scroll_offset, u16::MAX); // auto-scroll to bottom
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color_constants() {
|
||||
assert!(matches!(SOL_YELLOW, Color::Rgb(245, 197, 66)));
|
||||
assert!(matches!(SOL_AMBER, Color::Rgb(232, 168, 64)));
|
||||
assert!(matches!(SOL_BLUE, Color::Rgb(108, 166, 224)));
|
||||
assert!(matches!(SOL_RED, Color::Rgb(224, 88, 88)));
|
||||
// No green in the palette
|
||||
assert!(!matches!(SOL_YELLOW, Color::Rgb(_, 255, _)));
|
||||
assert!(!matches!(SOL_BLUE, Color::Rgb(_, 255, _)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entries_all_variants() {
|
||||
let mut app = App::new("test", "main", "devstral-2");
|
||||
app.push_log(LogEntry::UserInput("hello".into()));
|
||||
app.push_log(LogEntry::AssistantText("response".into()));
|
||||
app.push_log(LogEntry::ToolSuccess { name: "file_read".into(), detail: "src/main.rs".into() });
|
||||
app.push_log(LogEntry::ToolExecuting { name: "bash".into(), detail: "cargo test".into() });
|
||||
app.push_log(LogEntry::ToolFailed { name: "grep".into(), detail: "no matches".into() });
|
||||
app.push_log(LogEntry::ToolOutput { lines: vec!["line 1".into(), "line 2".into()], collapsed: true });
|
||||
app.push_log(LogEntry::Status("thinking".into()));
|
||||
app.push_log(LogEntry::Error("connection lost".into()));
|
||||
assert_eq!(app.log.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_output_collapse_threshold() {
|
||||
// Collapsed output shows max 5 lines + "... +N lines"
|
||||
let lines: Vec<String> = (0..20).map(|i| format!("line {i}")).collect();
|
||||
let entry = LogEntry::ToolOutput { lines: lines.clone(), collapsed: true };
|
||||
if let LogEntry::ToolOutput { lines, collapsed } = &entry {
|
||||
assert!(lines.len() > 5);
|
||||
assert!(*collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_approval_prompt() {
|
||||
let approval = ApprovalPrompt {
|
||||
tool_name: "bash".into(),
|
||||
command: "cargo test".into(),
|
||||
options: vec![
|
||||
"Yes".into(),
|
||||
"Yes, always allow bash".into(),
|
||||
"No".into(),
|
||||
],
|
||||
selected: 0,
|
||||
};
|
||||
assert_eq!(approval.options.len(), 3);
|
||||
assert_eq!(approval.selected, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_approval_navigation() {
|
||||
let mut approval = ApprovalPrompt {
|
||||
tool_name: "bash".into(),
|
||||
command: "rm -rf".into(),
|
||||
options: vec!["Yes".into(), "No".into()],
|
||||
selected: 0,
|
||||
};
|
||||
// Navigate down
|
||||
approval.selected = (approval.selected + 1).min(approval.options.len() - 1);
|
||||
assert_eq!(approval.selected, 1);
|
||||
// Navigate down again (clamped)
|
||||
approval.selected = (approval.selected + 1).min(approval.options.len() - 1);
|
||||
assert_eq!(approval.selected, 1);
|
||||
// Navigate up
|
||||
approval.selected = approval.selected.saturating_sub(1);
|
||||
assert_eq!(approval.selected, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thinking_state() {
|
||||
let mut app = App::new("sol", "main", "devstral-2");
|
||||
assert!(!app.is_thinking);
|
||||
app.is_thinking = true;
|
||||
assert!(app.is_thinking);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_cursor() {
|
||||
let mut app = App::new("sol", "main", "devstral-2");
|
||||
app.input = "hello world".into();
|
||||
app.cursor_pos = 5;
|
||||
assert_eq!(&app.input[..app.cursor_pos], "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_tracking() {
|
||||
let mut app = App::new("sol", "main", "devstral-2");
|
||||
app.input_tokens = 1200;
|
||||
app.output_tokens = 340;
|
||||
assert_eq!(app.input_tokens / 1000, 1);
|
||||
assert_eq!(app.output_tokens / 1000, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user