mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
feat(dev): Enso by default, /login /auth /model <name>, Dev in every string, codex 11da6b9
This commit is contained in:
Generated
+47
-102
@@ -252,27 +252,6 @@ version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "appcontainer_common"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
dependencies = [
|
||||
"flatbuffers",
|
||||
"getrandom 0.2.17",
|
||||
"learning_mode_core",
|
||||
"learning_mode_windows",
|
||||
"process_security_environment_spec",
|
||||
"sandbox_spec",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"widestring",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"winreg 0.55.0",
|
||||
"wxc_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
@@ -3458,19 +3437,17 @@ name = "codex-mxc-sandbox"
|
||||
version = "0.7.0-dev.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"appcontainer_common",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-path-uri",
|
||||
"codex-windows-sandbox",
|
||||
"learning_mode_windows",
|
||||
"mxc-sdk",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tracelogging",
|
||||
"windows-sys 0.61.2",
|
||||
"wxc_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4615,6 +4592,12 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "const_format"
|
||||
version = "0.2.35"
|
||||
@@ -5254,7 +5237,7 @@ version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"const-oid 0.9.6",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5390,7 +5373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"const-oid",
|
||||
"const-oid 0.9.6",
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
@@ -5402,6 +5385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.0",
|
||||
"const-oid 0.10.2",
|
||||
"crypto-common 0.2.2",
|
||||
"ctutils",
|
||||
]
|
||||
@@ -7456,6 +7440,9 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hanzo-tui"
|
||||
version = "0.7.0-dev.1"
|
||||
dependencies = [
|
||||
"hanzo-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
@@ -8680,32 +8667,6 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "learning_mode_core"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "learning_mode_windows"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
dependencies = [
|
||||
"learning_mode_core",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.18",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"wxc_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -9116,22 +9077,37 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mxc_config_contract"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
name = "mxc-sdk"
|
||||
version = "1.0.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=a846837d827b496fc4a08dfe60554515758c6647#a846837d827b496fc4a08dfe60554515758c6647"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"cc",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"flatbuffers",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"nix 0.29.0",
|
||||
"same-file",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sha2 0.11.0",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mxc_telemetry"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
"tracelogging",
|
||||
"unicode-general-category",
|
||||
"url",
|
||||
"uuid",
|
||||
"widestring",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"winreg 0.55.0",
|
||||
"winresource",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10451,14 +10427,6 @@ dependencies = [
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "process_security_environment_spec"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
dependencies = [
|
||||
"flatbuffers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prodash"
|
||||
version = "31.0.0"
|
||||
@@ -11807,14 +11775,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sandbox_spec"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
dependencies = [
|
||||
"flatbuffers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
@@ -15273,6 +15233,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
@@ -15420,30 +15389,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "wxc_common"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"cidr",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"mxc_config_contract",
|
||||
"mxc_telemetry",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-general-category",
|
||||
"url",
|
||||
"widestring",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.13.2"
|
||||
|
||||
+1
-3
@@ -172,12 +172,9 @@ codex-windows-sandbox-test-support = { path = "upstream/codex/codex-rs/windows-s
|
||||
core_test_support = { path = "upstream/codex/codex-rs/core/tests/common" }
|
||||
|
||||
# External
|
||||
learning_mode_windows = { git = "https://github.com/microsoft/mxc", rev = "6cd3d58f05d3447e67109cfb75e042803b843ca4" }
|
||||
wxc_common = { git = "https://github.com/microsoft/mxc", rev = "6cd3d58f05d3447e67109cfb75e042803b843ca4" }
|
||||
age = "0.12.1"
|
||||
ansi-to-tui = "8.0.1"
|
||||
anyhow = "1"
|
||||
appcontainer_common = { git = "https://github.com/microsoft/mxc", rev = "6cd3d58f05d3447e67109cfb75e042803b843ca4" }
|
||||
arboard = { version = "3", features = ["wayland-data-control"] }
|
||||
arc-swap = "1.9.0"
|
||||
assert_cmd = "2"
|
||||
@@ -264,6 +261,7 @@ maplit = "1.0.2"
|
||||
memchr = "2.7.6"
|
||||
mime_guess = "2.0.5"
|
||||
multimap = "0.10.0"
|
||||
mxc-sdk = { git = "https://github.com/microsoft/mxc", rev = "a846837d827b496fc4a08dfe60554515758c6647" }
|
||||
notify = "8.2.0"
|
||||
nucleo = { git = "https://github.com/helix-editor/nucleo.git", rev = "4253de9faabb4e5c6d81d946a5e35a90f87347ee" }
|
||||
once_cell = "1.20.2"
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod kai;
|
||||
mod provider;
|
||||
pub use provider::activate_provider;
|
||||
pub use provider::clear_hanzo_api_key;
|
||||
pub use provider::default_model_override;
|
||||
pub use provider::hanzo_credential;
|
||||
pub use provider::save_hanzo_api_key;
|
||||
pub use provider::set_feature;
|
||||
|
||||
@@ -181,7 +181,8 @@ pub(super) fn load_hanzo_credentials(home: &Path) {
|
||||
|
||||
/// Move a profile still on the startup-only credential to the refreshing one.
|
||||
///
|
||||
/// Earlier releases wrote the Hanzo provider with `env_key = "HANZO_USER_KEY"`,
|
||||
/// Earlier releases wrote the Hanzo provider with `env_key = "HANZO_USER_KEY"`
|
||||
/// (or `HANZO_API_KEY`, which nothing here ever read),
|
||||
/// read once when the agent starts, so a session outlived its sign-in token and
|
||||
/// failed with 401 an hour in. A provider block still holding exactly that
|
||||
/// default is rewritten to ask `dev token` instead; a provider the user changed
|
||||
@@ -201,7 +202,10 @@ pub(super) fn upgrade_auth(home: &Path) {
|
||||
return;
|
||||
};
|
||||
if provider.get("auth").is_some()
|
||||
|| provider.get("env_key").and_then(toml_edit::Item::as_str) != Some("HANZO_USER_KEY")
|
||||
|| !matches!(
|
||||
provider.get("env_key").and_then(toml_edit::Item::as_str),
|
||||
Some("HANZO_USER_KEY" | "HANZO_API_KEY")
|
||||
)
|
||||
|| provider.get("base_url").and_then(toml_edit::Item::as_str) != Some(super::API_BASE)
|
||||
{
|
||||
return;
|
||||
@@ -219,6 +223,17 @@ pub(super) fn upgrade_auth(home: &Path) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The model a profile gets when it uses Hanzo and names none: Enso, whatever
|
||||
/// the catalog lists first. A model the profile names, or another provider, is
|
||||
/// the profile's own.
|
||||
pub fn default_model_override(home: &Path) -> Option<String> {
|
||||
let text = std::fs::read_to_string(home.join("config.toml")).ok()?;
|
||||
let config: toml::Table = text.parse().ok()?;
|
||||
(config.get("model_provider").and_then(toml::Value::as_str) == Some("hanzo")
|
||||
&& !config.contains_key("model"))
|
||||
.then(|| format!("model=\"{}\"", super::DEFAULT_MODEL))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "provider_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -98,3 +98,36 @@ fn a_provider_the_user_changed_is_left_alone() {
|
||||
upgrade_auth(home.path());
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), theirs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hanzo_profile_without_a_model_gets_enso() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::write(home.path().join("config.toml"), "model_provider = \"hanzo\"\n").expect("write");
|
||||
assert_eq!(
|
||||
default_model_override(home.path()).as_deref(),
|
||||
Some("model=\"enso-auto\"")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_model_the_profile_names_or_another_provider_is_left_alone() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let path = home.path().join("config.toml");
|
||||
std::fs::write(&path, "model_provider = \"hanzo\"\nmodel = \"mine\"\n").expect("write");
|
||||
assert_eq!(default_model_override(home.path()), None);
|
||||
std::fs::write(&path, "model_provider = \"pool\"\n").expect("write");
|
||||
assert_eq!(default_model_override(home.path()), None);
|
||||
std::fs::remove_file(&path).expect("remove");
|
||||
assert_eq!(default_model_override(home.path()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_api_key_name_nothing_reads_is_upgraded_too() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let stale = OLD_PROVIDER.replace("HANZO_USER_KEY", "HANZO_API_KEY");
|
||||
std::fs::write(home.path().join("config.toml"), stale).expect("write");
|
||||
upgrade_auth(home.path());
|
||||
let text = std::fs::read_to_string(home.path().join("config.toml")).expect("read");
|
||||
assert!(text.contains("command = \"dev\""), "{text}");
|
||||
assert!(!text.contains("HANZO_API_KEY"), "{text}");
|
||||
}
|
||||
|
||||
@@ -203,9 +203,13 @@ fn main() -> Result<()> {
|
||||
async fn run(paths: Arg0DispatchPaths) -> Result<()> {
|
||||
let Dev {
|
||||
mut interactive,
|
||||
overrides,
|
||||
mut overrides,
|
||||
command,
|
||||
} = Dev::parse();
|
||||
// A flag the user passes comes later, so it wins.
|
||||
if let Some(model) = hanzo_config::default_model_override(&hanzo_config::home()) {
|
||||
overrides.raw_overrides.insert(0, model);
|
||||
}
|
||||
let sandbox = paths.codex_linux_sandbox_exe.clone();
|
||||
|
||||
match command {
|
||||
|
||||
@@ -5,3 +5,6 @@ edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
description = "Hanzo Dev tui integration"
|
||||
repository = "https://github.com/hanzoai/dev"
|
||||
|
||||
[dependencies]
|
||||
hanzo-config = { path = "../hanzo-config", version = "0.7.0-dev.1" }
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//! The Hanzo sign-in as the interface shows it. The credential lives in the
|
||||
//! Hanzo CLI, so each call here asks that CLI and hands back what it printed,
|
||||
//! one line at a time.
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::ErrorKind;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
const PROGRAM: &str = "hanzo";
|
||||
const MISSING: &str =
|
||||
"The Hanzo CLI is not installed. Install it with `curl -fsSL https://hanzo.sh | sh`.";
|
||||
const PATIENCE: Duration = Duration::from_secs(15);
|
||||
|
||||
/// Who is signed in, as `hanzo auth show` reports it.
|
||||
pub fn show(mut emit: impl FnMut(String)) {
|
||||
let text = capture(PROGRAM, &["auth", "show"]);
|
||||
if text.to_ascii_lowercase().contains("not signed in") {
|
||||
emit("Not signed in. Use /login to sign in.".to_string());
|
||||
return;
|
||||
}
|
||||
each_line(&text, &mut emit);
|
||||
}
|
||||
|
||||
/// Sign out of the active Hanzo identity.
|
||||
pub fn logout(mut emit: impl FnMut(String)) {
|
||||
each_line(&capture(PROGRAM, &["auth", "logout"]), &mut emit);
|
||||
}
|
||||
|
||||
/// Sign in through Hanzo IAM without leaving the interface. The sign-in runs
|
||||
/// beside it, and every line it prints, such as the address to open, reaches
|
||||
/// `emit` as it arrives.
|
||||
pub fn login(emit: impl Fn(String) + Send + Sync + 'static) {
|
||||
std::thread::spawn(move || stream(PROGRAM, &["auth", "login", "--provider", "hanzo"], emit));
|
||||
}
|
||||
|
||||
fn each_line(text: &str, emit: &mut impl FnMut(String)) {
|
||||
text.lines()
|
||||
.map(str::trim_end)
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.for_each(|line| emit(line.to_string()));
|
||||
}
|
||||
|
||||
/// What `program args` printed, or why it could not run.
|
||||
fn capture(program: &str, args: &[&str]) -> String {
|
||||
let mut command = Command::new(program);
|
||||
command.args(args).stdin(Stdio::null());
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let _ = sender.send(command.output());
|
||||
});
|
||||
match receiver.recv_timeout(PATIENCE) {
|
||||
Ok(Ok(output)) => {
|
||||
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
|
||||
text.push('\n');
|
||||
text.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||
text
|
||||
}
|
||||
Ok(Err(error)) if error.kind() == ErrorKind::NotFound => MISSING.to_string(),
|
||||
Ok(Err(error)) => format!("Could not run the Hanzo CLI: {error}"),
|
||||
Err(_) => "The Hanzo CLI did not answer.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `program args` to the end, passing along each line of output as it is
|
||||
/// printed, then say how it ended.
|
||||
fn stream(program: &str, args: &[&str], emit: impl Fn(String) + Send + Sync + 'static) {
|
||||
let emit = Arc::new(emit);
|
||||
let mut child = match Command::new(program)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => child,
|
||||
Err(error) if error.kind() == ErrorKind::NotFound => return emit(MISSING.to_string()),
|
||||
Err(error) => return emit(format!("Could not run the Hanzo CLI: {error}")),
|
||||
};
|
||||
let readers: Vec<_> = [
|
||||
child.stdout.take().map(|pipe| Box::new(pipe) as Box<dyn std::io::Read + Send>),
|
||||
child.stderr.take().map(|pipe| Box::new(pipe) as Box<dyn std::io::Read + Send>),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|pipe| {
|
||||
let emit = Arc::clone(&emit);
|
||||
std::thread::spawn(move || {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
if !line.trim().is_empty() {
|
||||
emit(line);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let status = child.wait();
|
||||
for reader in readers {
|
||||
let _ = reader.join();
|
||||
}
|
||||
match status {
|
||||
Ok(status) if status.success() => emit("Signed in to Hanzo.".to_string()),
|
||||
Ok(status) => emit(format!("Sign-in did not complete ({status}).")),
|
||||
Err(error) => emit(format!("Sign-in did not complete: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// A stand-in for the Hanzo CLI that prints a fixed script and exits.
|
||||
fn cli(name: &str, body: &str) -> PathBuf {
|
||||
let path = std::env::temp_dir().join(format!("hanzo-tui-{}-{name}", std::process::id()));
|
||||
std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).expect("write the stand-in");
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
|
||||
.expect("make it executable");
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_cli_says_how_to_install_it() {
|
||||
assert_eq!(capture("hanzo-tui-no-such-program", &[]), MISSING);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_arrive_in_order_and_the_end_is_reported() {
|
||||
let program = cli("login", "echo 'Open https://hanzo.id/device'; echo 'Code: ABCD' >&2");
|
||||
let seen = Arc::new(Mutex::new(Vec::new()));
|
||||
let sink = Arc::clone(&seen);
|
||||
stream(program.to_str().expect("utf-8 path"), &[], move |line| {
|
||||
sink.lock().expect("lock").push(line);
|
||||
});
|
||||
let mut seen = seen.lock().expect("lock").clone();
|
||||
assert_eq!(seen.pop().as_deref(), Some("Signed in to Hanzo."));
|
||||
seen.sort();
|
||||
assert_eq!(seen, ["Code: ABCD", "Open https://hanzo.id/device"]);
|
||||
std::fs::remove_file(program).expect("clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_sign_in_is_not_reported_as_one() {
|
||||
let program = cli("fail", "echo 'denied'; exit 3");
|
||||
let seen = Arc::new(Mutex::new(Vec::new()));
|
||||
let sink = Arc::clone(&seen);
|
||||
stream(program.to_str().expect("utf-8 path"), &[], move |line| {
|
||||
sink.lock().expect("lock").push(line);
|
||||
});
|
||||
let seen = seen.lock().expect("lock").clone();
|
||||
assert_eq!(seen.first().map(String::as_str), Some("denied"));
|
||||
assert!(seen.last().is_some_and(|line| line.starts_with("Sign-in did not complete")));
|
||||
std::fs::remove_file(program).expect("clean up");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
//! Hanzo-owned presentation; the upstream TUI owns interaction and animation.
|
||||
|
||||
pub mod account;
|
||||
pub mod model;
|
||||
|
||||
pub const PRODUCT_NAME: &str = "Hanzo Dev";
|
||||
pub const INPUT_PLACEHOLDER: &str = "Ask Hanzo Dev to do anything";
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const STARTUP_TIP: &str = "Use **/mcp** to inspect connected tools. Hanzo MCP provides cloud controls and local development tools.";
|
||||
|
||||
/// Keep light terminals readable and dark input bars much subtler than Codex.
|
||||
/// Keep light terminals readable and dark input bars much subtler than the upstream default.
|
||||
pub fn input_background(background: (u8, u8, u8), light: bool) -> (u8, u8, u8) {
|
||||
let (target, alpha) = if light { (0.0, 0.04) } else { (255.0, 0.055) };
|
||||
let mix = |channel: u8| (channel as f32 * (1.0 - alpha) + target * alpha).round() as u8;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Finding the model a user means by `/model <name>`.
|
||||
|
||||
/// What a typed name came to.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Pick {
|
||||
One(String),
|
||||
/// More than one model fits; the user names one of these.
|
||||
Many(Vec<String>),
|
||||
None,
|
||||
}
|
||||
|
||||
/// Match `wanted` against the models the session offers and the ones Hanzo
|
||||
/// serves: the exact name, then `<name>-auto`, then the only model whose name
|
||||
/// contains it. Case does not matter.
|
||||
pub fn pick(wanted: &str, offered: &[String]) -> Pick {
|
||||
let wanted = wanted.trim().to_ascii_lowercase();
|
||||
if wanted.is_empty() {
|
||||
return Pick::None;
|
||||
}
|
||||
let mut names: Vec<String> = offered.to_vec();
|
||||
for served in hanzo_config::catalog::MODELS {
|
||||
if !names.iter().any(|name| name == served.slug) {
|
||||
names.push(served.slug.to_string());
|
||||
}
|
||||
}
|
||||
let lower = |name: &String| name.to_ascii_lowercase();
|
||||
let exact = |target: &str| names.iter().find(|name| lower(name) == target).cloned();
|
||||
if let Some(name) = exact(&wanted).or_else(|| exact(&format!("{wanted}-auto"))) {
|
||||
return Pick::One(name);
|
||||
}
|
||||
let mut near: Vec<String> = names
|
||||
.iter()
|
||||
.filter(|name| lower(name).contains(&wanted))
|
||||
.cloned()
|
||||
.collect();
|
||||
match near.len() {
|
||||
0 => Pick::None,
|
||||
1 => Pick::One(near.remove(0)),
|
||||
_ => Pick::Many(near),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn offered() -> Vec<String> {
|
||||
["gpt-6-astra", "gpt-6-sol"].map(String::from).to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_served_name_is_taken_as_it_is() {
|
||||
assert_eq!(pick("enso", &offered()), Pick::One("enso".into()));
|
||||
assert_eq!(pick("ENSO-PRO", &offered()), Pick::One("enso-pro".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_family_name_finds_its_auto_model() {
|
||||
assert_eq!(pick("zen6", &[]), Pick::One("zen6".into()));
|
||||
assert_eq!(pick("gpt-6-astra", &offered()), Pick::One("gpt-6-astra".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_only_model_containing_the_name_is_the_one() {
|
||||
assert_eq!(pick("astra", &offered()), Pick::One("gpt-6-astra".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_that_fits_several_asks_which() {
|
||||
let Pick::Many(names) = pick("flash", &offered()) else {
|
||||
panic!("flash fits several");
|
||||
};
|
||||
assert!(names.contains(&"enso-flash".to_string()));
|
||||
assert!(names.contains(&"zen5-flash".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_that_fits_nothing_is_not_guessed() {
|
||||
assert_eq!(pick("nonesuch", &offered()), Pick::None);
|
||||
assert_eq!(pick(" ", &offered()), Pick::None);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,15 @@ impl Edit {
|
||||
/// still there" cannot mean "not yet applied". Finding the *result* is what
|
||||
/// means done, and it is what stops a second run from stacking the edit.
|
||||
fn apply(&self, text: &str) -> Result<Option<String>> {
|
||||
if text.contains(&self.new) {
|
||||
// An edit that keeps its anchor is done once its result is there. One
|
||||
// that replaces its anchor is done once the anchor is gone: a short
|
||||
// result such as "Dev" is in most files before any edit has run.
|
||||
let done = if self.new.contains(&self.old) {
|
||||
text.contains(&self.new)
|
||||
} else {
|
||||
text.contains(&self.new) && !text.contains(&self.old)
|
||||
};
|
||||
if done {
|
||||
return Ok(None);
|
||||
}
|
||||
let found = text.matches(&self.old).count();
|
||||
@@ -102,6 +110,15 @@ mod tests {
|
||||
assert!(e.apply("Hanzo Dev").unwrap().is_none());
|
||||
}
|
||||
|
||||
/// A replacement whose result is shorter than its anchor, or common, must
|
||||
/// still run: "Dev" is in most files before the first edit.
|
||||
#[test]
|
||||
fn a_result_already_present_elsewhere_does_not_hide_the_anchor() {
|
||||
let e = edit("Codex", "Dev", 1);
|
||||
assert_eq!(e.apply("Dev tools, Codex").unwrap().unwrap(), "Dev tools, Dev");
|
||||
assert!(e.apply("Dev tools, Dev").unwrap().is_none(), "applied once the anchor is gone");
|
||||
}
|
||||
|
||||
/// The whole point of the count: upstream moving the ground under an anchor
|
||||
/// must stop preparation, not brand whatever happens to match.
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
Submodule upstream/codex updated: 0e1520605f...11da6b9edc
Reference in New Issue
Block a user