mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Replaces the Rust tree. `code-rs/` was a fork of just-every/code carrying its own copy of everything; `vendor/` held 219 MB of upstream snapshots beside it. Both are gone. In their place: Hanzo writes the command surface, upstream keeps the engines. `crates/hanzo-dev/src/main.rs` is a real file that dispatches into libraries — codex_tui::run_main, codex_exec::run_main, and the rest — rather than an include! of upstream's 5156-line binary main. Owning the front door is what makes a single unified command surface expressible. Upstream arrives as submodules at upstream/codex and upstream/code, both at latest. The gitlink is the pin, so no lockfile of hashes sits beside it and a bump is a two-line diff instead of forty-five thousand. Crates are consumed by ordinary path dependency; nothing is copied. The whole Hanzo delta on upstream is sixteen anchored substitutions in patches/codex.json, applied in place by `make prepare` and visible in one `git -C upstream/codex status`. Each carries an exact match count, so upstream moving the ground under one stops preparation and names the file rather than branding the wrong line. Two are upstreamable: they publish mcp_cmd, plugin_cmd and doctor from upstream's library instead of burying them in its binary. `dev` covers exec, login, logout, serve, resume, fork, review, agents, queue, archive, unarchive, delete, cloud, mcp, plugin, doctor, features, sandbox and completion. Auto Drive and the browser bridge are not ported yet; migration/STATUS.md records that, and the defects the v1 patch was already carrying, so they are fixed as features are ported rather than copied across. Releases build from the root after `make prepare`, with submodules checked out. Scripts that existed only to manage a fork-diff — the upstream-merge set and the codex path-dep check — go with the fork.
15641 lines
664 KiB
Diff
15641 lines
664 KiB
Diff
diff --git a/Cargo.lock b/Cargo.lock
|
||
index 96ab598..31e5b32 100644
|
||
--- a/Cargo.lock
|
||
+++ b/Cargo.lock
|
||
@@ -925,7 +925,7 @@ checksum = "e9b18233253483ce2f65329a24072ec414db782531bdbb7d0bbc4bd2ce6b7e21"
|
||
|
||
[[package]]
|
||
name = "code-ansi-escape"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"ansi-to-tui",
|
||
"ratatui",
|
||
@@ -934,7 +934,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-app-server"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"assert_cmd",
|
||
@@ -965,7 +965,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-app-server-protocol"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"clap",
|
||
@@ -988,7 +988,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-apply-patch"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"assert_cmd",
|
||
@@ -1002,7 +1002,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-arg0"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"code-apply-patch",
|
||
@@ -1015,7 +1015,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-auto-drive-core"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"chrono",
|
||
@@ -1040,7 +1040,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-auto-drive-diagnostics"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"code-auto-drive-core",
|
||
@@ -1067,7 +1067,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-backend-openapi-models"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"serde",
|
||
"serde_json",
|
||
@@ -1075,7 +1075,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-browser"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"async-trait",
|
||
@@ -1103,7 +1103,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-chatgpt"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"clap",
|
||
@@ -1120,7 +1120,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-cli"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"chrono",
|
||
@@ -1166,7 +1166,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-cloud-tasks"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"async-trait",
|
||
@@ -1194,7 +1194,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-cloud-tasks-client"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"async-trait",
|
||
@@ -1209,7 +1209,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-common"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"clap",
|
||
"code-app-server-protocol",
|
||
@@ -1222,7 +1222,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-core"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"agent-client-protocol",
|
||
"anyhow",
|
||
@@ -1309,7 +1309,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-exec"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"chrono",
|
||
@@ -1322,6 +1322,7 @@ dependencies = [
|
||
"code-git-tooling",
|
||
"code-ollama",
|
||
"code-protocol",
|
||
+ "code-version",
|
||
"codex-utils-rustls-provider",
|
||
"filetime",
|
||
"libc",
|
||
@@ -1336,12 +1337,13 @@ dependencies = [
|
||
"tokio",
|
||
"tracing",
|
||
"tracing-subscriber",
|
||
+ "ts-rs",
|
||
"uuid",
|
||
]
|
||
|
||
[[package]]
|
||
name = "code-execpolicy"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"allocative",
|
||
"anyhow",
|
||
@@ -1361,7 +1363,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-file-search"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"clap",
|
||
@@ -1374,7 +1376,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-git-apply"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"once_cell",
|
||
"regex",
|
||
@@ -1383,7 +1385,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-git-tooling"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"pretty_assertions",
|
||
"schemars 0.8.22",
|
||
@@ -1397,7 +1399,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-linux-sandbox"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"clap",
|
||
"code-core",
|
||
@@ -1410,7 +1412,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-login"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"base64 0.22.1",
|
||
"chrono",
|
||
@@ -1432,7 +1434,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-mcp-client"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"code-mcp-types",
|
||
@@ -1445,7 +1447,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-mcp-server"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"agent-client-protocol",
|
||
"anyhow",
|
||
@@ -1471,7 +1473,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-mcp-types"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"mcp-types",
|
||
"serde_json",
|
||
@@ -1479,7 +1481,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-ollama"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"async-stream",
|
||
"bytes",
|
||
@@ -1494,7 +1496,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-otel"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"chrono",
|
||
"code-app-server-protocol",
|
||
@@ -1517,14 +1519,14 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-process-hardening"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"libc",
|
||
]
|
||
|
||
[[package]]
|
||
name = "code-protocol"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"code-execpolicy",
|
||
@@ -1552,7 +1554,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-protocol-ts"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"clap",
|
||
@@ -1564,7 +1566,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-responses-api-proxy"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"clap",
|
||
@@ -1581,7 +1583,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-rmcp-client"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"axum",
|
||
@@ -1598,7 +1600,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-tui"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"anyhow",
|
||
"arboard",
|
||
@@ -1677,7 +1679,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-utils-json-to-toml"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"pretty_assertions",
|
||
"serde_json",
|
||
@@ -1686,7 +1688,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-utils-readiness"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"async-trait",
|
||
"thiserror 2.0.17",
|
||
@@ -1696,14 +1698,14 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "code-version"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"serde_json",
|
||
]
|
||
|
||
[[package]]
|
||
name = "codex-experimental-api-macros"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"proc-macro2",
|
||
"quote",
|
||
@@ -1712,7 +1714,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "codex-utils-absolute-path"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"dirs",
|
||
"path-absolutize",
|
||
@@ -1726,7 +1728,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "codex-utils-cache"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"lru",
|
||
"sha1",
|
||
@@ -1735,7 +1737,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "codex-utils-cargo-bin"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"assert_cmd",
|
||
"runfiles",
|
||
@@ -1744,7 +1746,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "codex-utils-image"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"base64 0.22.1",
|
||
"codex-utils-cache",
|
||
@@ -1756,7 +1758,7 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "codex-utils-rustls-provider"
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
dependencies = [
|
||
"rustls",
|
||
]
|
||
@@ -2319,7 +2321,7 @@ dependencies = [
|
||
"libc",
|
||
"option-ext",
|
||
"redox_users 0.5.2",
|
||
- "windows-sys 0.61.2",
|
||
+ "windows-sys 0.60.2",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -2559,7 +2561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||
dependencies = [
|
||
"libc",
|
||
- "windows-sys 0.61.2",
|
||
+ "windows-sys 0.52.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -2659,7 +2661,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||
dependencies = [
|
||
"cfg-if",
|
||
"rustix 1.1.2",
|
||
- "windows-sys 0.59.0",
|
||
+ "windows-sys 0.52.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -2925,7 +2927,7 @@ version = "0.2.24"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||
dependencies = [
|
||
- "unicode-width 0.2.1",
|
||
+ "unicode-width 0.2.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -3627,7 +3629,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||
dependencies = [
|
||
"hermit-abi",
|
||
"libc",
|
||
- "windows-sys 0.59.0",
|
||
+ "windows-sys 0.52.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -4236,7 +4238,7 @@ version = "0.50.3"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||
dependencies = [
|
||
- "windows-sys 0.61.2",
|
||
+ "windows-sys 0.60.2",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -5288,7 +5290,8 @@ dependencies = [
|
||
[[package]]
|
||
name = "ratatui"
|
||
version = "0.29.0"
|
||
-source = "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2"
|
||
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
+checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||
dependencies = [
|
||
"bitflags 2.10.0",
|
||
"cassowary",
|
||
@@ -5302,7 +5305,7 @@ dependencies = [
|
||
"strum 0.26.3",
|
||
"unicode-segmentation",
|
||
"unicode-truncate",
|
||
- "unicode-width 0.2.1",
|
||
+ "unicode-width 0.2.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -5730,7 +5733,7 @@ dependencies = [
|
||
"errno",
|
||
"libc",
|
||
"linux-raw-sys 0.4.15",
|
||
- "windows-sys 0.59.0",
|
||
+ "windows-sys 0.52.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -5743,7 +5746,7 @@ dependencies = [
|
||
"errno",
|
||
"libc",
|
||
"linux-raw-sys 0.11.0",
|
||
- "windows-sys 0.61.2",
|
||
+ "windows-sys 0.52.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -5801,7 +5804,7 @@ dependencies = [
|
||
"security-framework 3.5.1",
|
||
"security-framework-sys",
|
||
"webpki-root-certs",
|
||
- "windows-sys 0.61.2",
|
||
+ "windows-sys 0.52.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -6778,7 +6781,7 @@ dependencies = [
|
||
"getrandom 0.3.4",
|
||
"once_cell",
|
||
"rustix 1.1.2",
|
||
- "windows-sys 0.61.2",
|
||
+ "windows-sys 0.52.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -6845,7 +6848,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||
dependencies = [
|
||
"smawk",
|
||
"unicode-linebreak",
|
||
- "unicode-width 0.2.1",
|
||
+ "unicode-width 0.2.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -7465,7 +7468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19"
|
||
dependencies = [
|
||
"ratatui",
|
||
- "unicode-width 0.2.1",
|
||
+ "unicode-width 0.2.0",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -7559,9 +7562,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||
|
||
[[package]]
|
||
name = "unicode-width"
|
||
-version = "0.2.1"
|
||
+version = "0.2.0"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
|
||
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||
|
||
[[package]]
|
||
name = "unicode-xid"
|
||
diff --git a/Cargo.toml b/Cargo.toml
|
||
index c6bbb9e..085b545 100644
|
||
--- a/Cargo.toml
|
||
+++ b/Cargo.toml
|
||
@@ -44,7 +44,7 @@ members = [
|
||
resolver = "2"
|
||
|
||
[workspace.package]
|
||
-version = "0.0.0"
|
||
+version = "0.6.106"
|
||
# Track the edition for all workspace crates in one place. Individual
|
||
# crates can still override this value, but keeping it here means new
|
||
# crates created with `cargo new -w ...` automatically inherit the 2024
|
||
@@ -265,17 +265,18 @@ unwrap_used = "deny"
|
||
ignored = ["openssl-sys", "code-utils-readiness"]
|
||
|
||
[profile.release]
|
||
-lto = "fat"
|
||
+# Thin, not fat: a release is five of these binaries built one after another on
|
||
+# one Mac, and the fat-LTO single-unit link cost ~15 minutes per target for a
|
||
+# ~10% smaller file. Thin LTO links in a fraction of that.
|
||
+lto = "thin"
|
||
# Because we bundle some of these executables with the TypeScript CLI, we
|
||
# remove everything to make the binary as small as possible.
|
||
strip = "symbols"
|
||
-
|
||
-# See https://github.com/openai/codex/issues/1411 for details.
|
||
codegen-units = 1
|
||
|
||
[patch.crates-io]
|
||
-# ratatui = { path = "../../ratatui" }
|
||
-ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }
|
||
+# Keep the release build reproducible from crates.io. The old private git
|
||
+# branch was retired and made clean checkouts unable to resolve Cargo.lock.
|
||
|
||
# Custom build profiles used by build-fast.sh
|
||
[profile.dev-fast]
|
||
diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
|
||
index 6dda909..840accc 100644
|
||
--- a/MIGRATION_GUIDE.md
|
||
+++ b/MIGRATION_GUIDE.md
|
||
@@ -1,10 +1,10 @@
|
||
-# Crate Migration Guide: codex-rs to code-rs
|
||
+# Crate Migration Guide: codex-rs to hanzo-dev
|
||
|
||
-This guide documents the process for moving shared crates from `codex-rs` into `code-rs`, making them fully independent.
|
||
+This guide documents the process for moving shared crates from `codex-rs` into `hanzo-dev`, making them fully independent.
|
||
|
||
## Overview
|
||
|
||
-The `code-rs` repository currently contains thin wrapper crates that re-export functionality from their `codex-rs` counterparts. This creates a cross-repository dependency via relative paths (`../codex-rs/...`). The migration process moves the implementation into `code-rs` so that the crate owns its code and no longer depends on `codex-rs`.
|
||
+The `hanzo-dev` repository currently contains thin wrapper crates that re-export functionality from their `codex-rs` counterparts. This creates a cross-repository dependency via relative paths (`../vendor/codex/...`). The migration process moves the implementation into `hanzo-dev` so that the crate owns its code and no longer depends on `codex-rs`.
|
||
|
||
## Case Study: linux-sandbox Migration
|
||
|
||
@@ -20,23 +20,23 @@ We successfully migrated `code-linux-sandbox` as a representative example. This
|
||
### 1. Pre-Migration Analysis
|
||
|
||
Before starting, understand:
|
||
-- **Source crate location**: `/home/azureuser/code/codex-rs/<crate-name>/`
|
||
-- **Destination crate location**: `/home/azureuser/code/code-rs/<crate-name>/`
|
||
+- **Source crate location**: `/home/azureuser/code/vendor/codex/<crate-name>/`
|
||
+- **Destination crate location**: `/home/azureuser/code/hanzo-dev/<crate-name>/`
|
||
- **Current wrapper structure**: Usually just `pub use codex_<crate>::*;`
|
||
- **Dependencies**: What other crates does this depend on?
|
||
-- **Consumers**: What crates in code-rs depend on this wrapper?
|
||
+- **Consumers**: What crates in hanzo-dev depend on this wrapper?
|
||
|
||
### 2. File Structure Migration
|
||
|
||
-Copy all implementation files from codex-rs to code-rs:
|
||
+Copy all implementation files from codex-rs to hanzo-dev:
|
||
|
||
```bash
|
||
# Source modules (excluding lib.rs which needs special handling)
|
||
-cp codex-rs/<crate>/src/*.rs code-rs/<crate>/src/
|
||
+cp vendor/codex/<crate>/src/*.rs hanzo-dev/<crate>/src/
|
||
# Exception: main.rs will need modification
|
||
|
||
# Tests (if they exist)
|
||
-cp -r codex-rs/<crate>/tests/ code-rs/<crate>/tests/
|
||
+cp -r vendor/codex/<crate>/tests/ hanzo-dev/<crate>/tests/
|
||
```
|
||
|
||
**Files to copy:**
|
||
@@ -104,7 +104,7 @@ pub fn run_main() -> ! {
|
||
```
|
||
|
||
**Key considerations:**
|
||
-- Copy the exact module structure from `codex-rs/<crate>/src/lib.rs`
|
||
+- Copy the exact module structure from `vendor/codex/<crate>/src/lib.rs`
|
||
- Keep platform-specific conditional compilation (`#[cfg(...)]`)
|
||
- Preserve all public exports
|
||
- Update any codex-specific naming to code-specific
|
||
@@ -136,10 +136,10 @@ Replace all `codex_*` module references with `code_*` equivalents:
|
||
|
||
```bash
|
||
# Example: Update codex_core to code_core
|
||
-find code-rs/<crate>/src -name '*.rs' -exec sed -i 's/codex_core/code_core/g' {} +
|
||
+find hanzo-dev/<crate>/src -name '*.rs' -exec sed -i 's/codex_core/code_core/g' {} +
|
||
|
||
# Update codex_<crate> to code_<crate> (with underscores)
|
||
-find code-rs/<crate>/src -name '*.rs' -exec sed -i 's/codex_linux_sandbox/code_linux_sandbox/g' {} +
|
||
+find hanzo-dev/<crate>/src -name '*.rs' -exec sed -i 's/codex_linux_sandbox/code_linux_sandbox/g' {} +
|
||
```
|
||
|
||
**Common replacements:**
|
||
@@ -156,13 +156,13 @@ find code-rs/<crate>/src -name '*.rs' -exec sed -i 's/codex_linux_sandbox/code_l
|
||
|
||
Remove the cross-repository dependency reference.
|
||
|
||
-**Location:** `/home/azureuser/code/code-rs/Cargo.toml`
|
||
+**Location:** `/home/azureuser/code/hanzo-dev/Cargo.toml`
|
||
|
||
**Before:**
|
||
```toml
|
||
[workspace.dependencies]
|
||
# ...
|
||
-codex-linux-sandbox = { path = "../codex-rs/linux-sandbox" }
|
||
+codex-linux-sandbox = { path = "../vendor/codex/linux-sandbox" }
|
||
# ...
|
||
```
|
||
|
||
@@ -176,7 +176,7 @@ codex-linux-sandbox = { path = "../codex-rs/linux-sandbox" }
|
||
|
||
```bash
|
||
# Build the migrated crate
|
||
-cd /home/azureuser/code/code-rs
|
||
+cd /home/azureuser/code/hanzo-dev
|
||
cargo build -p code-<crate-name>
|
||
|
||
# Run tests
|
||
@@ -197,12 +197,12 @@ cargo build -p <dependent-crate>
|
||
Use the provided script for initial file copying:
|
||
|
||
```bash
|
||
-cd /home/azureuser/code/code-rs
|
||
+cd /home/azureuser/code/hanzo-dev
|
||
./migrate-crate.sh <crate-name>
|
||
```
|
||
|
||
The script will:
|
||
-1. Copy source files from codex-rs to code-rs
|
||
+1. Copy source files from codex-rs to hanzo-dev
|
||
2. Copy test files
|
||
3. Display the dependencies you need to add
|
||
4. Provide a checklist of manual steps
|
||
@@ -219,7 +219,7 @@ The script will:
|
||
|
||
**Symptom:** Build fails with "cannot find crate" errors.
|
||
|
||
-**Solution:** Ensure ALL dependencies from the codex-rs Cargo.toml are copied to code-rs Cargo.toml, including:
|
||
+**Solution:** Ensure ALL dependencies from the codex-rs Cargo.toml are copied to hanzo-dev Cargo.toml, including:
|
||
- Platform-specific dependencies (`[target.'cfg(...)'.dependencies]`)
|
||
- Dev dependencies for tests
|
||
- Feature flags on dependencies
|
||
@@ -238,8 +238,8 @@ libc = { workspace = true }
|
||
**Solution:** Search for ALL occurrences of `codex_` in the migrated crate:
|
||
|
||
```bash
|
||
-grep -r "codex_" code-rs/<crate>/src/
|
||
-grep -r "codex_" code-rs/<crate>/tests/
|
||
+grep -r "codex_" hanzo-dev/<crate>/src/
|
||
+grep -r "codex_" hanzo-dev/<crate>/tests/
|
||
```
|
||
|
||
Replace with the appropriate `code_*` variant. Common locations:
|
||
@@ -332,7 +332,7 @@ Crates should be migrated in dependency order. Dependencies must be migrated bef
|
||
- `cloud-tasks-client` (depends on backend-client)
|
||
- `responses-api-proxy` (depends on backend-client)
|
||
|
||
-**Note:** Run `grep -r "codex-" code-rs/*/Cargo.toml` to see current wrapper dependencies.
|
||
+**Note:** Run `grep -r "codex-" hanzo-dev/*/Cargo.toml` to see current wrapper dependencies.
|
||
|
||
## Verification Checklist
|
||
|
||
@@ -364,14 +364,14 @@ For each crate, ensure you handle:
|
||
|
||
The migration process transforms a thin wrapper crate into a fully independent implementation by:
|
||
|
||
-1. **Copying** implementation files from codex-rs to code-rs
|
||
+1. **Copying** implementation files from codex-rs to hanzo-dev
|
||
2. **Updating** Cargo.toml with actual dependencies (replacing codex-* with code-*)
|
||
3. **Replacing** wrapper re-exports with actual module structure in lib.rs
|
||
4. **Renaming** namespaces from codex_* to code_* throughout the code
|
||
5. **Removing** the codex-<crate> dependency from workspace Cargo.toml
|
||
6. **Building** and verifying the migrated crate and its dependents
|
||
|
||
-The result is a crate that owns its implementation and no longer depends on `../codex-rs`, making code-rs more independent and easier to maintain.
|
||
+The result is a crate that owns its implementation and no longer depends on `../codex-rs`, making hanzo-dev more independent and easier to maintain.
|
||
|
||
## linux-sandbox Migration Results
|
||
|
||
diff --git a/MIGRATION_QUICK_REFERENCE.md b/MIGRATION_QUICK_REFERENCE.md
|
||
index cba8f0b..ceb98bf 100644
|
||
--- a/MIGRATION_QUICK_REFERENCE.md
|
||
+++ b/MIGRATION_QUICK_REFERENCE.md
|
||
@@ -1,10 +1,10 @@
|
||
-# Quick Reference: Crate Migration from codex-rs to code-rs
|
||
+# Quick Reference: Crate Migration from codex-rs to hanzo-dev
|
||
|
||
## TL;DR - Migration Steps
|
||
|
||
```bash
|
||
# 1. Use the migration script
|
||
-cd /home/azureuser/code/code-rs
|
||
+cd /home/azureuser/code/hanzo-dev
|
||
./migrate-crate.sh <crate-name>
|
||
|
||
# 2. Update Cargo.toml (copy deps from codex-rs, replace codex-* with code-*)
|
||
@@ -21,7 +21,7 @@ find <crate-name>/src -name '*.rs' -exec sed -i 's/codex_core/code_core/g' {} +
|
||
find <crate-name>/src -name '*.rs' -exec sed -i 's/codex_<old_name>/code_<new_name>/g' {} +
|
||
|
||
# 6. Remove from workspace Cargo.toml
|
||
-vim Cargo.toml # Delete the codex-<crate> = { path = "../codex-rs/..." } line
|
||
+vim Cargo.toml # Delete the codex-<crate> = { path = "../vendor/codex/..." } line
|
||
|
||
# 7. Build and verify
|
||
cargo build -p code-<crate-name>
|
||
@@ -77,7 +77,7 @@ fn main() -> ! {
|
||
|
||
## Common Namespace Replacements
|
||
|
||
-| From (codex-rs) | To (code-rs) |
|
||
+| From (codex-rs) | To (hanzo-dev) |
|
||
|-----------------|--------------|
|
||
| `use codex_core::` | `use code_core::` |
|
||
| `codex_linux_sandbox::` | `code_linux_sandbox::` |
|
||
@@ -96,7 +96,7 @@ fn main() -> ! {
|
||
|
||
```bash
|
||
# Check for remaining codex references
|
||
-grep -r "codex_" code-rs/<crate>/src/
|
||
+grep -r "codex_" hanzo-dev/<crate>/src/
|
||
|
||
# Verify build
|
||
cargo build -p code-<crate>
|
||
diff --git a/app-server/src/code_message_processor.rs b/app-server/src/code_message_processor.rs
|
||
index 65cab2b..c27f1ae 100644
|
||
--- a/app-server/src/code_message_processor.rs
|
||
+++ b/app-server/src/code_message_processor.rs
|
||
@@ -100,6 +100,8 @@ use code_protocol::mcp_protocol::RemoveConversationListenerParams;
|
||
use code_protocol::mcp_protocol::RemoveConversationSubscriptionResponse;
|
||
use code_protocol::mcp_protocol::SetDefaultModelParams;
|
||
use code_protocol::mcp_protocol::SetDefaultModelResponse;
|
||
+use code_protocol::mcp_protocol::QueueUserMessageParams;
|
||
+use code_protocol::mcp_protocol::QueueUserMessageResponse;
|
||
use code_protocol::mcp_protocol::SendUserMessageParams;
|
||
use code_protocol::mcp_protocol::SendUserMessageResponse;
|
||
use code_protocol::mcp_protocol::SendUserTurnParams;
|
||
@@ -207,6 +209,9 @@ impl CodexMessageProcessor {
|
||
ClientRequest::SendUserMessage { request_id, params } => {
|
||
self.send_user_message(request_id, params).await;
|
||
}
|
||
+ ClientRequest::QueueUserMessage { request_id, params } => {
|
||
+ self.queue_user_message(request_id, params).await;
|
||
+ }
|
||
ClientRequest::InterruptConversation { request_id, params } => {
|
||
self.interrupt_conversation(request_id, params).await;
|
||
}
|
||
@@ -576,11 +581,14 @@ impl CodexMessageProcessor {
|
||
}
|
||
}
|
||
|
||
- async fn send_user_message(&self, request_id: RequestId, params: SendUserMessageParams) {
|
||
- let SendUserMessageParams {
|
||
- conversation_id,
|
||
- items,
|
||
- } = params;
|
||
+ /// Resolve the conversation and map wire items to core items. `None` means
|
||
+ /// the error response has already been sent.
|
||
+ async fn resolve_conversation_items(
|
||
+ &self,
|
||
+ request_id: RequestId,
|
||
+ conversation_id: ConversationId,
|
||
+ items: Vec<WireInputItem>,
|
||
+ ) -> Option<(Arc<CodexConversation>, Vec<CoreInputItem>)> {
|
||
let Ok(conversation) = self
|
||
.conversation_manager
|
||
.get_conversation(conversation_id)
|
||
@@ -592,7 +600,7 @@ impl CodexMessageProcessor {
|
||
data: None,
|
||
};
|
||
self.outgoing.send_error(request_id, error).await;
|
||
- return;
|
||
+ return None;
|
||
};
|
||
|
||
let mapped_items: Vec<CoreInputItem> = items
|
||
@@ -604,7 +612,22 @@ impl CodexMessageProcessor {
|
||
})
|
||
.collect();
|
||
|
||
- // Submit user input to the conversation.
|
||
+ Some((conversation, mapped_items))
|
||
+ }
|
||
+
|
||
+ async fn send_user_message(&self, request_id: RequestId, params: SendUserMessageParams) {
|
||
+ let SendUserMessageParams {
|
||
+ conversation_id,
|
||
+ items,
|
||
+ } = params;
|
||
+ let Some((conversation, mapped_items)) = self
|
||
+ .resolve_conversation_items(request_id.clone(), conversation_id, items)
|
||
+ .await
|
||
+ else {
|
||
+ return;
|
||
+ };
|
||
+
|
||
+ // Op::UserInput aborts any running task and spawns a new turn.
|
||
let _ = conversation
|
||
.submit(Op::UserInput {
|
||
items: mapped_items,
|
||
@@ -618,6 +641,35 @@ impl CodexMessageProcessor {
|
||
.await;
|
||
}
|
||
|
||
+ /// Steer the turn already in flight instead of replacing it.
|
||
+ ///
|
||
+ /// The difference from `send_user_message` is entirely in the Op:
|
||
+ /// `Op::QueueUserInput` folds the items into the running task, where
|
||
+ /// `Op::UserInput` calls `sess.abort()` first. With nothing running, core
|
||
+ /// spawns a turn either way.
|
||
+ async fn queue_user_message(&self, request_id: RequestId, params: QueueUserMessageParams) {
|
||
+ let QueueUserMessageParams {
|
||
+ conversation_id,
|
||
+ items,
|
||
+ } = params;
|
||
+ let Some((conversation, mapped_items)) = self
|
||
+ .resolve_conversation_items(request_id.clone(), conversation_id, items)
|
||
+ .await
|
||
+ else {
|
||
+ return;
|
||
+ };
|
||
+
|
||
+ let _ = conversation
|
||
+ .submit(Op::QueueUserInput {
|
||
+ items: mapped_items,
|
||
+ })
|
||
+ .await;
|
||
+
|
||
+ self.outgoing
|
||
+ .send_response(request_id, QueueUserMessageResponse {})
|
||
+ .await;
|
||
+ }
|
||
+
|
||
#[allow(dead_code)]
|
||
async fn send_user_turn(&self, request_id: RequestId, params: SendUserTurnParams) {
|
||
let SendUserTurnParams {
|
||
@@ -1963,11 +2015,28 @@ mod tests {
|
||
) -> (
|
||
CodexMessageProcessor,
|
||
mpsc::UnboundedReceiver<crate::outgoing_message::OutgoingMessage>,
|
||
+ ) {
|
||
+ make_processor_with_provider_for_tests(auth_manager, None)
|
||
+ }
|
||
+
|
||
+ /// `dev`'s default provider is `hanzo`, which sets `requires_openai_auth:
|
||
+ /// false` — so anything exercising the ChatGPT auth path has to ask for the
|
||
+ /// `openai` provider by name rather than rely on the default.
|
||
+ fn make_processor_with_provider_for_tests(
|
||
+ auth_manager: Arc<AuthManager>,
|
||
+ model_provider: Option<&str>,
|
||
+ ) -> (
|
||
+ CodexMessageProcessor,
|
||
+ mpsc::UnboundedReceiver<crate::outgoing_message::OutgoingMessage>,
|
||
) {
|
||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded_channel();
|
||
let outgoing = Arc::new(OutgoingMessageSender::new(outgoing_tx));
|
||
+ let overrides = ConfigOverrides {
|
||
+ model_provider: model_provider.map(str::to_string),
|
||
+ ..ConfigOverrides::default()
|
||
+ };
|
||
let config = Arc::new(
|
||
- Config::load_with_cli_overrides(Vec::new(), ConfigOverrides::default())
|
||
+ Config::load_with_cli_overrides(Vec::new(), overrides)
|
||
.expect("load default config"),
|
||
);
|
||
let conversation_manager = Arc::new(ConversationManager::new(
|
||
@@ -2061,7 +2130,8 @@ mod tests {
|
||
RefreshTokenError::permanent("refresh token already used"),
|
||
);
|
||
|
||
- let (processor, mut outgoing_rx) = make_processor_with_auth_for_tests(auth_manager);
|
||
+ let (processor, mut outgoing_rx) =
|
||
+ make_processor_with_provider_for_tests(auth_manager, Some("openai"));
|
||
processor
|
||
.get_auth_status(
|
||
RequestId::Integer(42),
|
||
diff --git a/app-server-protocol/schema/json/ClientRequest.json b/app-server-protocol/schema/json/ClientRequest.json
|
||
index 77046d3..db51cd1 100644
|
||
--- a/app-server-protocol/schema/json/ClientRequest.json
|
||
+++ b/app-server-protocol/schema/json/ClientRequest.json
|
||
@@ -1486,6 +1486,25 @@
|
||
],
|
||
"type": "string"
|
||
},
|
||
+ "QueueUserMessageParams": {
|
||
+ "description": "Steer a turn that is already running instead of replacing it.\n\n`sendUserMessage` submits `Op::UserInput`, which aborts the running task and spawns a new one — the caller can interrupt, never redirect. This submits `Op::QueueUserInput`, which folds the items into the turn already in flight. With no turn running the two are equivalent.",
|
||
+ "properties": {
|
||
+ "conversationId": {
|
||
+ "$ref": "#/definitions/ThreadId"
|
||
+ },
|
||
+ "items": {
|
||
+ "items": {
|
||
+ "$ref": "#/definitions/InputItem"
|
||
+ },
|
||
+ "type": "array"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "conversationId",
|
||
+ "items"
|
||
+ ],
|
||
+ "type": "object"
|
||
+ },
|
||
"ReasoningEffort": {
|
||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||
"minLength": 1,
|
||
@@ -4662,6 +4681,31 @@
|
||
"title": "SendUserMessageRequest",
|
||
"type": "object"
|
||
},
|
||
+ {
|
||
+ "description": "Steer the running turn without tearing it down.",
|
||
+ "properties": {
|
||
+ "id": {
|
||
+ "$ref": "#/definitions/RequestId"
|
||
+ },
|
||
+ "method": {
|
||
+ "enum": [
|
||
+ "queueUserMessage"
|
||
+ ],
|
||
+ "title": "QueueUserMessageRequestMethod",
|
||
+ "type": "string"
|
||
+ },
|
||
+ "params": {
|
||
+ "$ref": "#/definitions/QueueUserMessageParams"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "id",
|
||
+ "method",
|
||
+ "params"
|
||
+ ],
|
||
+ "title": "QueueUserMessageRequest",
|
||
+ "type": "object"
|
||
+ },
|
||
{
|
||
"properties": {
|
||
"id": {
|
||
diff --git a/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
|
||
index b5035b3..c5c1a36 100644
|
||
--- a/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
|
||
+++ b/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
|
||
@@ -1660,6 +1660,31 @@
|
||
"title": "SendUserMessageRequest",
|
||
"type": "object"
|
||
},
|
||
+ {
|
||
+ "description": "Steer the running turn without tearing it down.",
|
||
+ "properties": {
|
||
+ "id": {
|
||
+ "$ref": "#/definitions/RequestId"
|
||
+ },
|
||
+ "method": {
|
||
+ "enum": [
|
||
+ "queueUserMessage"
|
||
+ ],
|
||
+ "title": "QueueUserMessageRequestMethod",
|
||
+ "type": "string"
|
||
+ },
|
||
+ "params": {
|
||
+ "$ref": "#/definitions/QueueUserMessageParams"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "id",
|
||
+ "method",
|
||
+ "params"
|
||
+ ],
|
||
+ "title": "QueueUserMessageRequest",
|
||
+ "type": "object"
|
||
+ },
|
||
{
|
||
"properties": {
|
||
"id": {
|
||
@@ -7009,6 +7034,32 @@
|
||
},
|
||
"type": "object"
|
||
},
|
||
+ "QueueUserMessageParams": {
|
||
+ "$schema": "http://json-schema.org/draft-07/schema#",
|
||
+ "description": "Steer a turn that is already running instead of replacing it.\n\n`sendUserMessage` submits `Op::UserInput`, which aborts the running task and spawns a new one — the caller can interrupt, never redirect. This submits `Op::QueueUserInput`, which folds the items into the turn already in flight. With no turn running the two are equivalent.",
|
||
+ "properties": {
|
||
+ "conversationId": {
|
||
+ "$ref": "#/definitions/ThreadId"
|
||
+ },
|
||
+ "items": {
|
||
+ "items": {
|
||
+ "$ref": "#/definitions/InputItem"
|
||
+ },
|
||
+ "type": "array"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "conversationId",
|
||
+ "items"
|
||
+ ],
|
||
+ "title": "QueueUserMessageParams",
|
||
+ "type": "object"
|
||
+ },
|
||
+ "QueueUserMessageResponse": {
|
||
+ "$schema": "http://json-schema.org/draft-07/schema#",
|
||
+ "title": "QueueUserMessageResponse",
|
||
+ "type": "object"
|
||
+ },
|
||
"RateLimitSnapshot": {
|
||
"properties": {
|
||
"credits": {
|
||
@@ -11891,7 +11942,7 @@
|
||
"type": "object"
|
||
},
|
||
{
|
||
- "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||
+ "description": "User config layer from $DEV_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||
"properties": {
|
||
"file": {
|
||
"allOf": [
|
||
diff --git a/app-server-protocol/schema/json/v1/QueueUserMessageParams.json b/app-server-protocol/schema/json/v1/QueueUserMessageParams.json
|
||
new file mode 100644
|
||
index 0000000..d334859
|
||
--- /dev/null
|
||
+++ b/app-server-protocol/schema/json/v1/QueueUserMessageParams.json
|
||
@@ -0,0 +1,166 @@
|
||
+{
|
||
+ "$schema": "http://json-schema.org/draft-07/schema#",
|
||
+ "definitions": {
|
||
+ "InputItem": {
|
||
+ "oneOf": [
|
||
+ {
|
||
+ "properties": {
|
||
+ "data": {
|
||
+ "properties": {
|
||
+ "text": {
|
||
+ "type": "string"
|
||
+ },
|
||
+ "text_elements": {
|
||
+ "default": [],
|
||
+ "description": "UI-defined spans within `text` used to render or persist special elements.",
|
||
+ "items": {
|
||
+ "$ref": "#/definitions/V1TextElement"
|
||
+ },
|
||
+ "type": "array"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "text"
|
||
+ ],
|
||
+ "type": "object"
|
||
+ },
|
||
+ "type": {
|
||
+ "enum": [
|
||
+ "text"
|
||
+ ],
|
||
+ "title": "TextInputItemType",
|
||
+ "type": "string"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "data",
|
||
+ "type"
|
||
+ ],
|
||
+ "title": "TextInputItem",
|
||
+ "type": "object"
|
||
+ },
|
||
+ {
|
||
+ "properties": {
|
||
+ "data": {
|
||
+ "properties": {
|
||
+ "image_url": {
|
||
+ "type": "string"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "image_url"
|
||
+ ],
|
||
+ "type": "object"
|
||
+ },
|
||
+ "type": {
|
||
+ "enum": [
|
||
+ "image"
|
||
+ ],
|
||
+ "title": "ImageInputItemType",
|
||
+ "type": "string"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "data",
|
||
+ "type"
|
||
+ ],
|
||
+ "title": "ImageInputItem",
|
||
+ "type": "object"
|
||
+ },
|
||
+ {
|
||
+ "properties": {
|
||
+ "data": {
|
||
+ "properties": {
|
||
+ "path": {
|
||
+ "type": "string"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "path"
|
||
+ ],
|
||
+ "type": "object"
|
||
+ },
|
||
+ "type": {
|
||
+ "enum": [
|
||
+ "localImage"
|
||
+ ],
|
||
+ "title": "LocalImageInputItemType",
|
||
+ "type": "string"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "data",
|
||
+ "type"
|
||
+ ],
|
||
+ "title": "LocalImageInputItem",
|
||
+ "type": "object"
|
||
+ }
|
||
+ ]
|
||
+ },
|
||
+ "ThreadId": {
|
||
+ "type": "string"
|
||
+ },
|
||
+ "V1ByteRange": {
|
||
+ "properties": {
|
||
+ "end": {
|
||
+ "description": "End byte offset (exclusive) within the UTF-8 text buffer.",
|
||
+ "format": "uint",
|
||
+ "minimum": 0.0,
|
||
+ "type": "integer"
|
||
+ },
|
||
+ "start": {
|
||
+ "description": "Start byte offset (inclusive) within the UTF-8 text buffer.",
|
||
+ "format": "uint",
|
||
+ "minimum": 0.0,
|
||
+ "type": "integer"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "end",
|
||
+ "start"
|
||
+ ],
|
||
+ "type": "object"
|
||
+ },
|
||
+ "V1TextElement": {
|
||
+ "properties": {
|
||
+ "byteRange": {
|
||
+ "allOf": [
|
||
+ {
|
||
+ "$ref": "#/definitions/V1ByteRange"
|
||
+ }
|
||
+ ],
|
||
+ "description": "Byte range in the parent `text` buffer that this element occupies."
|
||
+ },
|
||
+ "placeholder": {
|
||
+ "description": "Optional human-readable placeholder for the element, displayed in the UI.",
|
||
+ "type": [
|
||
+ "string",
|
||
+ "null"
|
||
+ ]
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "byteRange"
|
||
+ ],
|
||
+ "type": "object"
|
||
+ }
|
||
+ },
|
||
+ "description": "Steer a turn that is already running instead of replacing it.\n\n`sendUserMessage` submits `Op::UserInput`, which aborts the running task and spawns a new one — the caller can interrupt, never redirect. This submits `Op::QueueUserInput`, which folds the items into the turn already in flight. With no turn running the two are equivalent.",
|
||
+ "properties": {
|
||
+ "conversationId": {
|
||
+ "$ref": "#/definitions/ThreadId"
|
||
+ },
|
||
+ "items": {
|
||
+ "items": {
|
||
+ "$ref": "#/definitions/InputItem"
|
||
+ },
|
||
+ "type": "array"
|
||
+ }
|
||
+ },
|
||
+ "required": [
|
||
+ "conversationId",
|
||
+ "items"
|
||
+ ],
|
||
+ "title": "QueueUserMessageParams",
|
||
+ "type": "object"
|
||
+}
|
||
diff --git a/app-server-protocol/schema/json/v1/QueueUserMessageResponse.json b/app-server-protocol/schema/json/v1/QueueUserMessageResponse.json
|
||
new file mode 100644
|
||
index 0000000..13c441b
|
||
--- /dev/null
|
||
+++ b/app-server-protocol/schema/json/v1/QueueUserMessageResponse.json
|
||
@@ -0,0 +1,5 @@
|
||
+{
|
||
+ "$schema": "http://json-schema.org/draft-07/schema#",
|
||
+ "title": "QueueUserMessageResponse",
|
||
+ "type": "object"
|
||
+}
|
||
diff --git a/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/app-server-protocol/schema/json/v2/ConfigReadResponse.json
|
||
index 0d3e9a4..162cd1c 100644
|
||
--- a/app-server-protocol/schema/json/v2/ConfigReadResponse.json
|
||
+++ b/app-server-protocol/schema/json/v2/ConfigReadResponse.json
|
||
@@ -363,7 +363,7 @@
|
||
"type": "object"
|
||
},
|
||
{
|
||
- "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||
+ "description": "User config layer from $DEV_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||
"properties": {
|
||
"file": {
|
||
"allOf": [
|
||
diff --git a/app-server-protocol/schema/json/v2/ConfigWriteResponse.json b/app-server-protocol/schema/json/v2/ConfigWriteResponse.json
|
||
index e8f0fa7..baf7e7d 100644
|
||
--- a/app-server-protocol/schema/json/v2/ConfigWriteResponse.json
|
||
+++ b/app-server-protocol/schema/json/v2/ConfigWriteResponse.json
|
||
@@ -74,7 +74,7 @@
|
||
"type": "object"
|
||
},
|
||
{
|
||
- "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||
+ "description": "User config layer from $DEV_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||
"properties": {
|
||
"file": {
|
||
"allOf": [
|
||
diff --git a/app-server-protocol/schema/typescript/ClientRequest.ts b/app-server-protocol/schema/typescript/ClientRequest.ts
|
||
index 18bbc73..86834b5 100644
|
||
--- a/app-server-protocol/schema/typescript/ClientRequest.ts
|
||
+++ b/app-server-protocol/schema/typescript/ClientRequest.ts
|
||
@@ -15,6 +15,7 @@ import type { InterruptConversationParams } from "./InterruptConversationParams"
|
||
import type { ListConversationsParams } from "./ListConversationsParams";
|
||
import type { LoginApiKeyParams } from "./LoginApiKeyParams";
|
||
import type { NewConversationParams } from "./NewConversationParams";
|
||
+import type { QueueUserMessageParams } from "./QueueUserMessageParams";
|
||
import type { RemoveConversationListenerParams } from "./RemoveConversationListenerParams";
|
||
import type { RequestId } from "./RequestId";
|
||
import type { ResumeConversationParams } from "./ResumeConversationParams";
|
||
@@ -59,4 +60,4 @@ import type { TurnSteerParams } from "./v2/TurnSteerParams";
|
||
/**
|
||
* Request from the client to the server.
|
||
*/
|
||
-export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/read", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/write", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };
|
||
+export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/read", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/write", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "queueUserMessage", id: RequestId, params: QueueUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };
|
||
diff --git a/app-server-protocol/schema/typescript/QueueUserMessageParams.ts b/app-server-protocol/schema/typescript/QueueUserMessageParams.ts
|
||
new file mode 100644
|
||
index 0000000..3f0ece0
|
||
--- /dev/null
|
||
+++ b/app-server-protocol/schema/typescript/QueueUserMessageParams.ts
|
||
@@ -0,0 +1,15 @@
|
||
+// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||
+
|
||
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||
+import type { InputItem } from "./InputItem";
|
||
+import type { ThreadId } from "./ThreadId";
|
||
+
|
||
+/**
|
||
+ * Steer a turn that is already running instead of replacing it.
|
||
+ *
|
||
+ * `sendUserMessage` submits `Op::UserInput`, which aborts the running task and
|
||
+ * spawns a new one — the caller can interrupt, never redirect. This submits
|
||
+ * `Op::QueueUserInput`, which folds the items into the turn already in flight.
|
||
+ * With no turn running the two are equivalent.
|
||
+ */
|
||
+export type QueueUserMessageParams = { conversationId: ThreadId, items: Array<InputItem>, };
|
||
diff --git a/app-server-protocol/schema/typescript/QueueUserMessageResponse.ts b/app-server-protocol/schema/typescript/QueueUserMessageResponse.ts
|
||
new file mode 100644
|
||
index 0000000..f0477ef
|
||
--- /dev/null
|
||
+++ b/app-server-protocol/schema/typescript/QueueUserMessageResponse.ts
|
||
@@ -0,0 +1,5 @@
|
||
+// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||
+
|
||
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||
+
|
||
+export type QueueUserMessageResponse = Record<string, never>;
|
||
diff --git a/app-server-protocol/schema/typescript/index.ts b/app-server-protocol/schema/typescript/index.ts
|
||
index c23d845..b4390f4 100644
|
||
--- a/app-server-protocol/schema/typescript/index.ts
|
||
+++ b/app-server-protocol/schema/typescript/index.ts
|
||
@@ -147,6 +147,8 @@ export type { PlanItem } from "./PlanItem";
|
||
export type { PlanItemArg } from "./PlanItemArg";
|
||
export type { PlanType } from "./PlanType";
|
||
export type { Profile } from "./Profile";
|
||
+export type { QueueUserMessageParams } from "./QueueUserMessageParams";
|
||
+export type { QueueUserMessageResponse } from "./QueueUserMessageResponse";
|
||
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
|
||
export type { RateLimitWindow } from "./RateLimitWindow";
|
||
export type { RawResponseItemEvent } from "./RawResponseItemEvent";
|
||
diff --git a/app-server-protocol/src/lib.rs b/app-server-protocol/src/lib.rs
|
||
index df27c7c..0857c3b 100644
|
||
--- a/app-server-protocol/src/lib.rs
|
||
+++ b/app-server-protocol/src/lib.rs
|
||
@@ -20,6 +20,6 @@ pub use schema_fixtures::SchemaFixtureOptions;
|
||
pub use schema_fixtures::read_schema_fixture_tree;
|
||
pub use schema_fixtures::write_schema_fixtures;
|
||
pub use schema_fixtures::write_schema_fixtures_with_options;
|
||
-// Backward-compatibility alias used by code-rs crates that still import from
|
||
+// Backward-compatibility alias used by hanzo-dev crates that still import from
|
||
// code-app-server-protocol.
|
||
pub use code_protocol::ConversationId;
|
||
diff --git a/app-server-protocol/src/protocol/common.rs b/app-server-protocol/src/protocol/common.rs
|
||
index d5cf1c2..8b2a914 100644
|
||
--- a/app-server-protocol/src/protocol/common.rs
|
||
+++ b/app-server-protocol/src/protocol/common.rs
|
||
@@ -418,6 +418,11 @@ client_request_definitions! {
|
||
params: v1::SendUserMessageParams,
|
||
response: v1::SendUserMessageResponse,
|
||
},
|
||
+ /// Steer the running turn without tearing it down.
|
||
+ QueueUserMessage {
|
||
+ params: v1::QueueUserMessageParams,
|
||
+ response: v1::QueueUserMessageResponse,
|
||
+ },
|
||
SendUserTurn {
|
||
params: v1::SendUserTurnParams,
|
||
response: v1::SendUserTurnResponse,
|
||
diff --git a/app-server-protocol/src/protocol/thread_history.rs b/app-server-protocol/src/protocol/thread_history.rs
|
||
index 83c2730..9dfa445 100644
|
||
--- a/app-server-protocol/src/protocol/thread_history.rs
|
||
+++ b/app-server-protocol/src/protocol/thread_history.rs
|
||
@@ -48,7 +48,7 @@ impl ThreadHistoryBuilder {
|
||
}
|
||
|
||
/// This function should handle all EventMsg variants that can be persisted in a rollout file.
|
||
- /// See `should_persist_event_msg` in `codex-rs/core/rollout/policy.rs`.
|
||
+ /// See `should_persist_event_msg` in `vendor/codex/core/rollout/policy.rs`.
|
||
fn handle_event(&mut self, event: &EventMsg) {
|
||
match event {
|
||
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
|
||
diff --git a/app-server-protocol/src/protocol/v1.rs b/app-server-protocol/src/protocol/v1.rs
|
||
index a498c3e..7032782 100644
|
||
--- a/app-server-protocol/src/protocol/v1.rs
|
||
+++ b/app-server-protocol/src/protocol/v1.rs
|
||
@@ -408,6 +408,23 @@ pub struct SendUserMessageParams {
|
||
pub items: Vec<InputItem>,
|
||
}
|
||
|
||
+/// Steer a turn that is already running instead of replacing it.
|
||
+///
|
||
+/// `sendUserMessage` submits `Op::UserInput`, which aborts the running task and
|
||
+/// spawns a new one — the caller can interrupt, never redirect. This submits
|
||
+/// `Op::QueueUserInput`, which folds the items into the turn already in flight.
|
||
+/// With no turn running the two are equivalent.
|
||
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||
+#[serde(rename_all = "camelCase")]
|
||
+pub struct QueueUserMessageParams {
|
||
+ pub conversation_id: ThreadId,
|
||
+ pub items: Vec<InputItem>,
|
||
+}
|
||
+
|
||
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||
+#[serde(rename_all = "camelCase")]
|
||
+pub struct QueueUserMessageResponse {}
|
||
+
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SendUserTurnParams {
|
||
diff --git a/app-server-protocol/src/protocol/v2.rs b/app-server-protocol/src/protocol/v2.rs
|
||
index 29b1cb0..a37d6b2 100644
|
||
--- a/app-server-protocol/src/protocol/v2.rs
|
||
+++ b/app-server-protocol/src/protocol/v2.rs
|
||
@@ -305,7 +305,7 @@ pub enum ConfigLayerSource {
|
||
file: AbsolutePathBuf,
|
||
},
|
||
|
||
- /// User config layer from $CODEX_HOME/config.toml. This layer is special
|
||
+ /// User config layer from $DEV_HOME/config.toml. This layer is special
|
||
/// in that it is expected to be:
|
||
/// - writable by the user
|
||
/// - generally outside the workspace directory
|
||
diff --git a/arg0/src/lib.rs b/arg0/src/lib.rs
|
||
index c194771..6891cba 100644
|
||
--- a/arg0/src/lib.rs
|
||
+++ b/arg0/src/lib.rs
|
||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||
use std::path::PathBuf;
|
||
|
||
use code_core::config::resolve_code_path_for_read;
|
||
-use code_core::CODEX_APPLY_PATCH_ARG1;
|
||
+use code_core::DEV_APPLY_PATCH_ARG1;
|
||
#[cfg(unix)]
|
||
use std::os::unix::fs::symlink;
|
||
use tempfile::TempDir;
|
||
@@ -55,7 +55,7 @@ where
|
||
}
|
||
|
||
let argv1 = args.next().unwrap_or_default();
|
||
- if argv1 == CODEX_APPLY_PATCH_ARG1 {
|
||
+ if argv1 == DEV_APPLY_PATCH_ARG1 {
|
||
let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned));
|
||
let exit_code = match patch_arg {
|
||
Some(patch_arg) => {
|
||
@@ -67,7 +67,7 @@ where
|
||
}
|
||
}
|
||
None => {
|
||
- eprintln!("Error: {CODEX_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
|
||
+ eprintln!("Error: {DEV_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
|
||
1
|
||
}
|
||
};
|
||
@@ -105,12 +105,12 @@ where
|
||
})
|
||
}
|
||
|
||
-const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
|
||
+const ILLEGAL_ENV_VAR_PREFIX: &str = "DEV_";
|
||
|
||
/// Load env vars from ~/.code/.env (legacy ~/.codex/.env is still read) and `$(pwd)/.env`.
|
||
///
|
||
/// Security: Do not allow `.env` files to create or modify any variables
|
||
-/// with names starting with `CODEX_`.
|
||
+/// with names starting with `DEV_`.
|
||
fn load_dotenv() {
|
||
// 1) Load from global ~/.code/.env (or ~/.codex/.env) first.
|
||
if let Ok(code_home) = code_core::config::find_code_home() {
|
||
@@ -124,7 +124,7 @@ fn load_dotenv() {
|
||
// 2) Load from the current project's .env, but with extra safety:
|
||
// - Do NOT import provider API keys by default (e.g., OPENAI_API_KEY, ANTHROPIC_API_KEY,
|
||
// GOOGLE_API_KEY, QWEN_API_KEY).
|
||
- // - Users can opt back in via CODEX_ALLOW_PROJECT_OPENAI_KEYS=1 (either exported
|
||
+ // - Users can opt back in via DEV_ALLOW_PROJECT_OPENAI_KEYS=1 (either exported
|
||
// in the shell or placed in ~/.code/.env).
|
||
// NOTE: use an explicit cwd/.env path instead of dotenv_iter() because
|
||
// dotenv_iter() walks parent directories. Parent-walking can accidentally
|
||
@@ -136,8 +136,8 @@ fn load_dotenv() {
|
||
// Filtered setter that always blocks provider keys from the project's .env.
|
||
for (key, value) in iter.into_iter().flatten() {
|
||
let upper = key.to_ascii_uppercase();
|
||
- // Never allow CODEX_* to be set from .env files for safety.
|
||
- if upper.starts_with(ILLEGAL_ENV_VAR_PREFIX) && upper != "CODEX_HOME" { continue; }
|
||
+ // Never allow DEV_* to be set from .env files for safety.
|
||
+ if upper.starts_with(ILLEGAL_ENV_VAR_PREFIX) && upper != "DEV_HOME" { continue; }
|
||
// Always ignore provider API keys from project .env (must be set globally or in shell).
|
||
if matches!(
|
||
upper.as_str(),
|
||
@@ -158,21 +158,21 @@ fn load_dotenv() {
|
||
}
|
||
}
|
||
|
||
- // Bridge CODE_HOME to CODEX_HOME for legacy components that still read only CODEX_HOME.
|
||
- let codex_home_missing = std::env::var("CODEX_HOME")
|
||
+ // Bridge CODE_HOME to DEV_HOME for legacy components that still read only DEV_HOME.
|
||
+ let codex_home_missing = std::env::var("DEV_HOME")
|
||
.map(|v| v.trim().is_empty())
|
||
.unwrap_or(true);
|
||
if codex_home_missing {
|
||
if let Ok(code_home) = std::env::var("CODE_HOME") {
|
||
if !code_home.trim().is_empty() {
|
||
// Safe: still single-threaded during startup.
|
||
- unsafe { std::env::set_var("CODEX_HOME", code_home) };
|
||
+ unsafe { std::env::set_var("DEV_HOME", code_home) };
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
-/// Helper to set vars from a dotenvy iterator while filtering out `CODEX_` keys.
|
||
+/// Helper to set vars from a dotenvy iterator while filtering out `DEV_` keys.
|
||
fn set_filtered<I>(iter: I)
|
||
where
|
||
I: IntoIterator<Item = Result<(String, String), dotenvy::Error>>,
|
||
@@ -219,7 +219,7 @@ fn prepend_path_entry_for_apply_patch() -> std::io::Result<TempDir> {
|
||
&batch_script,
|
||
format!(
|
||
r#"@echo off
|
||
-"{}" {CODEX_APPLY_PATCH_ARG1} %*
|
||
+"{}" {DEV_APPLY_PATCH_ARG1} %*
|
||
"#,
|
||
exe.display()
|
||
),
|
||
diff --git a/browser/src/manager.rs b/browser/src/manager.rs
|
||
index daf2953..79b4724 100644
|
||
--- a/browser/src/manager.rs
|
||
+++ b/browser/src/manager.rs
|
||
@@ -88,7 +88,7 @@ fn is_temporary_internal_launch_error_message(message: &str) -> bool {
|
||
}
|
||
|
||
fn chrome_logging_enabled() -> bool {
|
||
- env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_BROWSER_LOG")
|
||
+ env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("DEV_BROWSER_LOG")
|
||
}
|
||
|
||
fn env_truthy(key: &str) -> bool {
|
||
@@ -102,14 +102,14 @@ fn resolve_chrome_log_path() -> Option<PathBuf> {
|
||
return None;
|
||
}
|
||
|
||
- if let Ok(path) = std::env::var("CODEX_BROWSER_LOG_PATH") {
|
||
+ if let Ok(path) = std::env::var("DEV_BROWSER_LOG_PATH") {
|
||
let trimmed = path.trim();
|
||
if !trimmed.is_empty() {
|
||
return Some(PathBuf::from(trimmed));
|
||
}
|
||
}
|
||
|
||
- let base = if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
|
||
+ let base = if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("DEV_HOME")) {
|
||
PathBuf::from(home).join("debug_logs")
|
||
} else if let Ok(home) = std::env::var("HOME") {
|
||
PathBuf::from(home).join(".code").join("debug_logs")
|
||
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
|
||
index 4184c0b..d0a3a4b 100644
|
||
--- a/cli/Cargo.toml
|
||
+++ b/cli/Cargo.toml
|
||
@@ -3,10 +3,18 @@ edition = "2024"
|
||
name = "code-cli"
|
||
version = { workspace = true }
|
||
|
||
+# `dev` is the product. It is what `hanzo code` launches (by bare name, off
|
||
+# PATH) and what the installer contract promises.
|
||
[[bin]]
|
||
-name = "code"
|
||
+name = "dev"
|
||
path = "src/main.rs"
|
||
|
||
+# `code` stays as an alias so existing installs keep working. It is the same
|
||
+# program — an alias, never the canonical name.
|
||
+[[bin]]
|
||
+name = "code"
|
||
+path = "src/code_alias.rs"
|
||
+
|
||
[lib]
|
||
name = "code_cli"
|
||
path = "src/lib.rs"
|
||
diff --git a/cli/src/code_alias.rs b/cli/src/code_alias.rs
|
||
new file mode 100644
|
||
index 0000000..6412db2
|
||
--- /dev/null
|
||
+++ b/cli/src/code_alias.rs
|
||
@@ -0,0 +1,2 @@
|
||
+// Compatibility entry point for existing `code` installs.
|
||
+include!("main.rs");
|
||
diff --git a/cli/src/login.rs b/cli/src/login.rs
|
||
index d6d6664..ef7843e 100644
|
||
--- a/cli/src/login.rs
|
||
+++ b/cli/src/login.rs
|
||
@@ -5,8 +5,10 @@ use code_core::auth::CLIENT_ID;
|
||
use code_core::auth::OPENAI_API_KEY_ENV_VAR;
|
||
use code_core::auth::login_with_api_key;
|
||
use code_core::auth::logout;
|
||
+use code_core::HANZO_PROVIDER_ID;
|
||
use code_core::config::Config;
|
||
use code_core::config::ConfigOverrides;
|
||
+use code_login::DeviceGrant;
|
||
use code_login::ServerOptions;
|
||
use code_login::run_device_code_login;
|
||
use code_login::run_login_server;
|
||
@@ -15,7 +17,21 @@ use std::io::IsTerminal;
|
||
use std::io::Read;
|
||
use std::path::PathBuf;
|
||
|
||
-pub async fn login_with_chatgpt(code_home: PathBuf, originator: String) -> std::io::Result<()> {
|
||
+/// Report the outcome of a login and leave.
|
||
+fn finish(result: std::io::Result<()>) -> ! {
|
||
+ match result {
|
||
+ Ok(()) => {
|
||
+ eprintln!("Successfully logged in");
|
||
+ std::process::exit(0);
|
||
+ }
|
||
+ Err(e) => {
|
||
+ eprintln!("Error logging in: {e}");
|
||
+ std::process::exit(1);
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+async fn browser_login(code_home: PathBuf, originator: String) -> std::io::Result<()> {
|
||
let opts = ServerOptions::new(code_home, CLIENT_ID.to_string(), originator);
|
||
let server = run_login_server(opts)?;
|
||
|
||
@@ -27,23 +43,17 @@ pub async fn login_with_chatgpt(code_home: PathBuf, originator: String) -> std::
|
||
server.block_until_done().await
|
||
}
|
||
|
||
-pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
|
||
+/// Sign in to the configured provider. Hanzo IAM registers no loopback redirect
|
||
+/// for its CLI client, so the device grant is how you reach it — headless or
|
||
+/// not. Every other provider keeps the browser loopback flow.
|
||
+pub async fn run_login(cli_config_overrides: CliConfigOverrides) -> ! {
|
||
let config = load_config_or_exit(cli_config_overrides);
|
||
+ let originator = config.responses_originator_header.clone();
|
||
|
||
- match login_with_chatgpt(
|
||
- config.code_home,
|
||
- config.responses_originator_header.clone(),
|
||
- )
|
||
- .await
|
||
- {
|
||
- Ok(_) => {
|
||
- eprintln!("Successfully logged in");
|
||
- std::process::exit(0);
|
||
- }
|
||
- Err(e) => {
|
||
- eprintln!("Error logging in: {e}");
|
||
- std::process::exit(1);
|
||
- }
|
||
+ if config.model_provider_id == HANZO_PROVIDER_ID {
|
||
+ finish(DeviceGrant::run(ServerOptions::hanzo(config.code_home, originator)).await)
|
||
+ } else {
|
||
+ finish(browser_login(config.code_home, originator).await)
|
||
}
|
||
}
|
||
|
||
@@ -52,17 +62,7 @@ pub async fn run_login_with_api_key(
|
||
api_key: String,
|
||
) -> ! {
|
||
let config = load_config_or_exit(cli_config_overrides);
|
||
-
|
||
- match login_with_api_key(&config.code_home, &api_key) {
|
||
- Ok(_) => {
|
||
- eprintln!("Successfully logged in");
|
||
- std::process::exit(0);
|
||
- }
|
||
- Err(e) => {
|
||
- eprintln!("Error logging in: {e}");
|
||
- std::process::exit(1);
|
||
- }
|
||
- }
|
||
+ finish(login_with_api_key(&config.code_home, &api_key))
|
||
}
|
||
|
||
pub fn read_api_key_from_stdin() -> String {
|
||
@@ -70,7 +70,7 @@ pub fn read_api_key_from_stdin() -> String {
|
||
|
||
if stdin.is_terminal() {
|
||
eprintln!(
|
||
- "--with-api-key expects the API key on stdin. Try piping it, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`."
|
||
+ "--with-api-key expects the API key on stdin. Try piping it, e.g. `printenv HANZO_USER_KEY | dev login --with-api-key`."
|
||
);
|
||
std::process::exit(1);
|
||
}
|
||
@@ -92,31 +92,35 @@ pub fn read_api_key_from_stdin() -> String {
|
||
api_key
|
||
}
|
||
|
||
-/// Login using the OAuth device code flow.
|
||
+/// Login using a device code. Hanzo IAM speaks RFC 8628; OpenAI speaks its own
|
||
+/// `/api/accounts/deviceauth` scheme. The issuer and client overrides carry a
|
||
+/// sibling brand's IAM (e.g. `https://lux.id/v1/iam` with `lux-cli`).
|
||
pub async fn run_login_with_device_code(
|
||
cli_config_overrides: CliConfigOverrides,
|
||
issuer_base_url: Option<String>,
|
||
client_id: Option<String>,
|
||
) -> ! {
|
||
let config = load_config_or_exit(cli_config_overrides);
|
||
- let mut opts = ServerOptions::new(
|
||
- config.code_home,
|
||
- client_id.unwrap_or(CLIENT_ID.to_string()),
|
||
- config.responses_originator_header.clone(),
|
||
- );
|
||
+ let originator = config.responses_originator_header.clone();
|
||
+ let hanzo = config.model_provider_id == HANZO_PROVIDER_ID;
|
||
+
|
||
+ let mut opts = if hanzo {
|
||
+ ServerOptions::hanzo(config.code_home, originator)
|
||
+ } else {
|
||
+ ServerOptions::new(config.code_home, CLIENT_ID.to_string(), originator)
|
||
+ };
|
||
+ if let Some(id) = client_id {
|
||
+ opts.client_id = id;
|
||
+ }
|
||
if let Some(iss) = issuer_base_url {
|
||
opts.issuer = iss;
|
||
}
|
||
- match run_device_code_login(opts).await {
|
||
- Ok(()) => {
|
||
- eprintln!("Successfully logged in");
|
||
- std::process::exit(0);
|
||
- }
|
||
- Err(e) => {
|
||
- eprintln!("Error logging in with device code: {e}");
|
||
- std::process::exit(1);
|
||
- }
|
||
- }
|
||
+
|
||
+ finish(if hanzo {
|
||
+ DeviceGrant::run(opts).await
|
||
+ } else {
|
||
+ run_device_code_login(opts).await
|
||
+ })
|
||
}
|
||
|
||
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||
@@ -147,7 +151,12 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||
}
|
||
},
|
||
AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens | AuthMode::Headers => {
|
||
- eprintln!("Logged in using ChatGPT");
|
||
+ let brand = if config.model_provider_id == HANZO_PROVIDER_ID {
|
||
+ "Hanzo"
|
||
+ } else {
|
||
+ "ChatGPT"
|
||
+ };
|
||
+ eprintln!("Logged in with your {brand} account");
|
||
std::process::exit(0);
|
||
}
|
||
},
|
||
diff --git a/cli/src/main.rs b/cli/src/main.rs
|
||
index a7fe4a2..6f4f898 100644
|
||
--- a/cli/src/main.rs
|
||
+++ b/cli/src/main.rs
|
||
@@ -12,7 +12,7 @@ use code_cli::SeatbeltCommand;
|
||
use code_cli::login::read_api_key_from_stdin;
|
||
use code_cli::login::run_login_status;
|
||
use code_cli::login::run_login_with_api_key;
|
||
-use code_cli::login::run_login_with_chatgpt;
|
||
+use code_cli::login::run_login;
|
||
use code_cli::login::run_login_with_device_code;
|
||
use code_cli::login::run_logout;
|
||
mod bridge;
|
||
@@ -39,15 +39,17 @@ use tokio::runtime::{Builder as TokioRuntimeBuilder, Handle as TokioHandle};
|
||
mod mcp_cmd;
|
||
|
||
use crate::mcp_cmd::McpCli;
|
||
+use code_cli::proto;
|
||
+use code_cli::proto::ProtoCli;
|
||
|
||
-const CLI_COMMAND_NAME: &str = "code";
|
||
-pub(crate) const CODEX_SECURE_MODE_ENV_VAR: &str = "CODEX_SECURE_MODE";
|
||
+const CLI_COMMAND_NAME: &str = "dev";
|
||
+pub(crate) const DEV_SECURE_MODE_ENV_VAR: &str = "DEV_SECURE_MODE";
|
||
|
||
/// As early as possible in the process lifecycle, apply hardening measures
|
||
-/// if the CODEX_SECURE_MODE environment variable is set to "1".
|
||
+/// if the DEV_SECURE_MODE environment variable is set to "1".
|
||
#[ctor::ctor]
|
||
fn pre_main_hardening() {
|
||
- let secure_mode = match std::env::var(CODEX_SECURE_MODE_ENV_VAR) {
|
||
+ let secure_mode = match std::env::var(DEV_SECURE_MODE_ENV_VAR) {
|
||
Ok(value) => value,
|
||
Err(_) => return,
|
||
};
|
||
@@ -58,24 +60,24 @@ fn pre_main_hardening() {
|
||
|
||
// Always clear this env var so child processes don't inherit it.
|
||
unsafe {
|
||
- std::env::remove_var(CODEX_SECURE_MODE_ENV_VAR);
|
||
+ std::env::remove_var(DEV_SECURE_MODE_ENV_VAR);
|
||
}
|
||
}
|
||
|
||
-/// Codex CLI
|
||
+/// Hanzo Dev — the Hanzo coding agent.
|
||
///
|
||
/// If no subcommand is specified, options will be forwarded to the interactive CLI.
|
||
#[derive(Debug, Parser)]
|
||
#[clap(
|
||
author,
|
||
- name = "code",
|
||
+ name = "dev",
|
||
version = code_version::version(),
|
||
// If a sub‑command is given, ignore requirements of the default args.
|
||
subcommand_negates_reqs = true,
|
||
// The executable is sometimes invoked via a platform‑specific name like
|
||
- // `codex-x86_64-unknown-linux-musl`, but the help output should always use
|
||
- // the generic `code` command name that users run.
|
||
- bin_name = "code"
|
||
+ // `dev-x86_64-unknown-linux-musl`, or via the `code` alias, but the help
|
||
+ // output should always name the product: `dev`.
|
||
+ bin_name = "dev"
|
||
)]
|
||
struct MultitoolCli {
|
||
#[clap(flatten)]
|
||
@@ -98,7 +100,7 @@ struct MultitoolCli {
|
||
|
||
#[derive(Debug, clap::Subcommand)]
|
||
enum Subcommand {
|
||
- /// Run Codex non-interactively.
|
||
+ /// Run Hanzo Dev non-interactively.
|
||
#[clap(visible_alias = "e")]
|
||
Exec(ExecCli),
|
||
|
||
@@ -112,27 +114,33 @@ enum Subcommand {
|
||
/// Remove stored authentication credentials.
|
||
Logout(LogoutCommand),
|
||
|
||
- /// [experimental] Run Codex as an MCP server and manage MCP servers.
|
||
+ /// [experimental] Run Hanzo Dev as an MCP server and manage MCP servers.
|
||
#[clap(visible_alias = "acp")]
|
||
Mcp(McpCli),
|
||
|
||
- /// [experimental] Run the Codex MCP server (stdio transport).
|
||
+ /// [experimental] Run the Hanzo Dev MCP server (stdio transport).
|
||
McpServer,
|
||
|
||
/// [experimental] Run the app server.
|
||
AppServer,
|
||
|
||
+ /// [experimental] Speak the agent protocol on stdin/stdout: Submission
|
||
+ /// JSON lines in, Event JSON lines out. Unlike `app-server` this exposes
|
||
+ /// the whole `Op` set, so a controller can steer a turn in flight
|
||
+ /// (`queue_user_input`) rather than only interrupt it.
|
||
+ Proto(ProtoCli),
|
||
+
|
||
/// Generate shell completion scripts.
|
||
Completion(CompletionCommand),
|
||
|
||
/// Internal debugging commands.
|
||
Debug(DebugArgs),
|
||
|
||
- /// Debug: replay ordering from response.json and codex-tui.log
|
||
+ /// Debug: replay ordering from response.json and the TUI log
|
||
#[clap(hide = false)]
|
||
OrderReplay(OrderReplayArgs),
|
||
|
||
- /// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
|
||
+ /// Apply the latest diff produced by the agent as a `git apply` to your local working tree.
|
||
#[clap(visible_alias = "a")]
|
||
Apply(ApplyCommand),
|
||
|
||
@@ -142,7 +150,7 @@ enum Subcommand {
|
||
/// Internal: generate TypeScript protocol bindings.
|
||
#[clap(hide = true)]
|
||
GenerateTs(GenerateTsCommand),
|
||
- /// [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally.
|
||
+ /// [EXPERIMENTAL] Browse cloud tasks and apply changes locally.
|
||
#[clap(name = "cloud", alias = "cloud-tasks")]
|
||
Cloud(CloudTasksCli),
|
||
|
||
@@ -159,7 +167,7 @@ enum Subcommand {
|
||
/// Side-channel LLM utilities (no TUI events).
|
||
Llm(LlmCli),
|
||
|
||
- /// Manage Code Bridge subscription for this workspace.
|
||
+ /// Manage Hanzo Dev Bridge subscription for this workspace.
|
||
Bridge(BridgeCommand),
|
||
}
|
||
|
||
@@ -320,7 +328,7 @@ struct LoginCommand {
|
||
|
||
#[arg(
|
||
long = "with-api-key",
|
||
- help = "Read the API key from stdin (e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`)"
|
||
+ help = "Read the API key from stdin (e.g. `printenv HANZO_USER_KEY | dev login --with-api-key`)"
|
||
)]
|
||
with_api_key: bool,
|
||
|
||
@@ -386,7 +394,7 @@ struct OrderReplayArgs {
|
||
struct PreviewArgs {
|
||
/// Slug identifier (e.g., faster-downloads)
|
||
slug: String,
|
||
- /// Optional owner/repo to override (defaults to just-every/code or $GITHUB_REPOSITORY)
|
||
+ /// Optional owner/repo to override (defaults to hanzoai/dev or $GITHUB_REPOSITORY)
|
||
#[arg(long = "repo", value_name = "OWNER/REPO")]
|
||
repo: Option<String>,
|
||
/// Output directory where the binary will be extracted
|
||
@@ -495,6 +503,10 @@ async fn cli_main(code_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()>
|
||
Some(Subcommand::AppServer) => {
|
||
code_app_server::run_main(code_linux_sandbox_exe, root_config_overrides).await?;
|
||
}
|
||
+ Some(Subcommand::Proto(mut proto_cli)) => {
|
||
+ prepend_config_flags(&mut proto_cli.config_overrides, root_config_overrides.clone());
|
||
+ proto::run_main(proto_cli).await?;
|
||
+ }
|
||
Some(Subcommand::Resume(ResumeCommand {
|
||
session_id,
|
||
last,
|
||
@@ -545,14 +557,14 @@ async fn cli_main(code_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()>
|
||
.await;
|
||
} else if login_cli.api_key.is_some() {
|
||
eprintln!(
|
||
- "The --api-key flag is no longer supported. Pipe the key instead, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`."
|
||
+ "The --api-key flag is no longer supported. Pipe the key instead, e.g. `printenv HANZO_USER_KEY | dev login --with-api-key`."
|
||
);
|
||
std::process::exit(1);
|
||
} else if login_cli.with_api_key {
|
||
let api_key = read_api_key_from_stdin();
|
||
run_login_with_api_key(login_cli.config_overrides, api_key).await;
|
||
} else {
|
||
- run_login_with_chatgpt(login_cli.config_overrides).await;
|
||
+ run_login(login_cli.config_overrides).await;
|
||
}
|
||
}
|
||
}
|
||
@@ -1086,7 +1098,7 @@ fn resolve_resume_path(session_id: Option<&str>, last: bool) -> anyhow::Result<O
|
||
}
|
||
|
||
let code_home = code_core::config::find_code_home()
|
||
- .context("failed to locate Codex home directory")?;
|
||
+ .context("failed to locate the Hanzo Dev home directory")?;
|
||
|
||
let sess = session_id.map(|s| s.to_string());
|
||
let fetch = async move {
|
||
@@ -1243,7 +1255,7 @@ async fn preview_main(args: PreviewArgs) -> anyhow::Result<()> {
|
||
let repo = args
|
||
.repo
|
||
.or_else(|| env::var("GITHUB_REPOSITORY").ok())
|
||
- .unwrap_or_else(|| "just-every/code".to_string());
|
||
+ .unwrap_or_else(|| "hanzoai/dev".to_string());
|
||
let (owner, name) = repo
|
||
.split_once('/')
|
||
.map(|(o, n)| (o.to_string(), n.to_string()))
|
||
@@ -1260,7 +1272,7 @@ async fn preview_main(args: PreviewArgs) -> anyhow::Result<()> {
|
||
_ => bail!(format!("Unsupported platform: {}/{}", os, arch)),
|
||
};
|
||
|
||
- let client = reqwest::Client::builder().user_agent("codex-preview/1").build()?;
|
||
+ let client = reqwest::Client::builder().user_agent("hanzo-dev-preview/1").build()?;
|
||
|
||
// Resolve slug/tag from id
|
||
let id = args.slug.trim().to_string();
|
||
@@ -1434,6 +1446,11 @@ async fn preview_main(args: PreviewArgs) -> anyhow::Result<()> {
|
||
bail!("No recognized artifact content found.")
|
||
}
|
||
|
||
+/// What to say when several installs of this agent answer to the same name.
|
||
+/// Held as one value so the identity test can read it; the names here are the
|
||
+/// real ones — formula `hanzo-dev`, package `@hanzo/dev`, binary `dev`.
|
||
+const ONE_INSTALL: &str = "\nIf versions differ, keep one install and drop the rest:\n - Homebrew: brew uninstall hanzo-dev\n - npm/pnpm: npm uninstall -g @hanzo/dev\n - Bun: bun remove -g @hanzo/dev\n\nCall it 'dev'. 'code' is an alias for the same program and collides with VS Code's CLI.";
|
||
+
|
||
async fn doctor_main() -> anyhow::Result<()> {
|
||
use std::env;
|
||
use std::process::Stdio;
|
||
@@ -1443,7 +1460,7 @@ async fn doctor_main() -> anyhow::Result<()> {
|
||
let exe = std::env::current_exe()
|
||
.map(|p| p.display().to_string())
|
||
.unwrap_or_else(|_| "<unknown>".to_string());
|
||
- println!("code version: {}", code_version::version());
|
||
+ println!("dev version: {}", code_version::version());
|
||
println!("current_exe: {}", exe);
|
||
|
||
// PATH
|
||
@@ -1518,23 +1535,23 @@ async fn doctor_main() -> anyhow::Result<()> {
|
||
let bun_coder = format!("{}/coder", bun_bin);
|
||
if coder_paths.iter().any(|p| p == &bun_coder) {
|
||
println!("\nBun shim detected for 'coder': {}", bun_coder);
|
||
- println!("Suggestion: remove old Bun global with: bun remove -g @just-every/code");
|
||
+ println!("Suggestion: remove old Bun global with: bun remove -g @hanzo/dev");
|
||
}
|
||
let bun_code = format!("{}/code", bun_bin);
|
||
if code_paths.iter().any(|p| p == &bun_code) {
|
||
println!("Bun shim detected for 'code': {}", bun_code);
|
||
- println!("Suggestion: prefer 'coder' or remove Bun shim if it conflicts.");
|
||
+ println!("Suggestion: prefer 'dev' or remove the Bun shim if it conflicts.");
|
||
}
|
||
}
|
||
|
||
// Detect Homebrew overshadow of VS Code
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
- let brew_code = code_paths.iter().find(|p| p.contains("/homebrew/bin/code") || p.contains("/Cellar/code/"));
|
||
+ let brew_code = code_paths.iter().find(|p| p.contains("/homebrew/bin/code") || p.contains("/Cellar/hanzo-dev/") || p.contains("/Cellar/code/"));
|
||
let vscode_code = code_paths.iter().find(|p| p.contains("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"));
|
||
if brew_code.is_some() && vscode_code.is_some() {
|
||
println!("\nHomebrew 'code' precedes VS Code CLI in PATH.");
|
||
- println!("Suggestion: uninstall Homebrew formula 'code' (brew uninstall code) or reorder PATH so /usr/local/bin comes before /usr/local/homebrew/bin.");
|
||
+ println!("Suggestion: call this agent by its own name, 'dev' — 'code' is only an alias. Or reorder PATH so /usr/local/bin precedes /opt/homebrew/bin.");
|
||
}
|
||
}
|
||
|
||
@@ -1548,17 +1565,28 @@ async fn doctor_main() -> anyhow::Result<()> {
|
||
println!("npm prefix -g: {}", npm_prefix);
|
||
}
|
||
|
||
- println!("\nIf versions differ, remove older installs and keep one package manager:");
|
||
- println!(" - Bun: bun remove -g @just-every/code");
|
||
- println!(" - npm/pnpm: npm uninstall -g @just-every/code");
|
||
- println!(" - Homebrew: brew uninstall code");
|
||
- println!(" - Prefer using 'coder' to avoid conflicts with VS Code's 'code'.");
|
||
+ println!("{ONE_INSTALL}");
|
||
|
||
Ok(())
|
||
}
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
+
|
||
+ /// `doctor` is the one place that tells a confused user which install to
|
||
+ /// remove, so it has to name the real formula, package and binary. Left as
|
||
+ /// prose it drifted back to `brew uninstall code` and `prefer 'coder'` —
|
||
+ /// advice to remove a formula that does not exist. See hanzo_identity.rs,
|
||
+ /// which guards the same contract for the defaults.
|
||
+ #[test]
|
||
+ fn doctor_names_the_things_that_actually_exist() {
|
||
+ assert!(ONE_INSTALL.contains("brew uninstall hanzo-dev"));
|
||
+ assert!(ONE_INSTALL.contains("npm uninstall -g @hanzo/dev"));
|
||
+ assert!(ONE_INSTALL.contains("Call it 'dev'"));
|
||
+ assert!(!ONE_INSTALL.contains("uninstall code"));
|
||
+ assert!(!ONE_INSTALL.contains("coder"));
|
||
+ }
|
||
+
|
||
use std::io::Write;
|
||
use std::path::{Path, PathBuf};
|
||
use std::sync::Mutex;
|
||
@@ -1580,11 +1608,14 @@ mod tests {
|
||
use code_protocol::ThreadId;
|
||
|
||
#[test]
|
||
- fn bash_completion_uses_code_command_name() {
|
||
+ fn bash_completion_uses_our_command_name() {
|
||
let mut buf = Vec::new();
|
||
write_completion(Shell::Bash, &mut buf);
|
||
let script = String::from_utf8(buf).expect("completion output should be valid UTF-8");
|
||
- assert!(script.contains("_code()"), "expected bash completion function to be named _code");
|
||
+ assert!(
|
||
+ script.contains(&format!("_{CLI_COMMAND_NAME}()")),
|
||
+ "expected bash completion function to be named _{CLI_COMMAND_NAME}"
|
||
+ );
|
||
assert!(!script.contains("_codex()"), "bash completion output should not use legacy codex prefix");
|
||
}
|
||
|
||
@@ -1639,9 +1670,9 @@ where
|
||
.unwrap_or_else(|poison| poison.into_inner());
|
||
let temp_home = TempDir::new().expect("temp code home");
|
||
let prev_code_home = std::env::var("CODE_HOME").ok();
|
||
- let prev_codex_home = std::env::var("CODEX_HOME").ok();
|
||
+ let prev_codex_home = std::env::var("DEV_HOME").ok();
|
||
set_env_var("CODE_HOME", temp_home.path());
|
||
- remove_env_var("CODEX_HOME");
|
||
+ remove_env_var("DEV_HOME");
|
||
|
||
let result = f(temp_home.path());
|
||
|
||
@@ -1650,8 +1681,8 @@ where
|
||
None => remove_env_var("CODE_HOME"),
|
||
}
|
||
match prev_codex_home {
|
||
- Some(val) => set_env_var("CODEX_HOME", val),
|
||
- None => remove_env_var("CODEX_HOME"),
|
||
+ Some(val) => set_env_var("DEV_HOME", val),
|
||
+ None => remove_env_var("DEV_HOME"),
|
||
}
|
||
|
||
result
|
||
diff --git a/cli/src/mcp_cmd.rs b/cli/src/mcp_cmd.rs
|
||
index 72701ce..80226d8 100644
|
||
--- a/cli/src/mcp_cmd.rs
|
||
+++ b/cli/src/mcp_cmd.rs
|
||
@@ -17,7 +17,7 @@ use code_core::config_types::McpServerTransportConfig;
|
||
/// - `serve` — run the MCP server on stdio
|
||
/// - `list` — list configured servers (with `--json`)
|
||
/// - `get` — show a single server (with `--json`)
|
||
-/// - `add` — add a server launcher entry to `~/.code/config.toml` (Code also reads legacy `~/.codex/config.toml`)
|
||
+/// - `add` — add a server launcher entry to `~/.code/config.toml` (Hanzo Dev also reads legacy `~/.codex/config.toml`)
|
||
/// - `remove` — delete a server entry
|
||
#[derive(Debug, clap::Parser)]
|
||
pub struct McpCli {
|
||
@@ -63,7 +63,7 @@ pub struct AddArgs {
|
||
|
||
/// URL of a remote MCP server.
|
||
///
|
||
- /// When `--bearer-token` is omitted, Code records the server as a stdio
|
||
+ /// When `--bearer-token` is omitted, Hanzo Dev records the server as a stdio
|
||
/// launcher using `npx -y mcp-remote <url>` so the MCP server can handle
|
||
/// OAuth flows.
|
||
#[arg(long)]
|
||
@@ -71,7 +71,7 @@ pub struct AddArgs {
|
||
|
||
/// Optional bearer token to use with `--url` for static authentication.
|
||
///
|
||
- /// When set, Code records the server as a `streamable_http` MCP server.
|
||
+ /// When set, Hanzo Dev records the server as a `streamable_http` MCP server.
|
||
#[arg(long)]
|
||
pub bearer_token: Option<String>,
|
||
|
||
@@ -183,7 +183,7 @@ fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<(
|
||
Some(map)
|
||
};
|
||
|
||
- let code_home = find_code_home().context("failed to resolve CODEX_HOME")?;
|
||
+ let code_home = find_code_home().context("failed to resolve DEV_HOME")?;
|
||
let mut servers = load_global_mcp_servers(&code_home)
|
||
.with_context(|| format!("failed to load MCP servers from {}", code_home.display()))?;
|
||
|
||
@@ -274,7 +274,7 @@ fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) ->
|
||
|
||
validate_server_name(&name)?;
|
||
|
||
- let code_home = find_code_home().context("failed to resolve CODEX_HOME")?;
|
||
+ let code_home = find_code_home().context("failed to resolve DEV_HOME")?;
|
||
let mut servers = load_global_mcp_servers(&code_home)
|
||
.with_context(|| format!("failed to load MCP servers from {}", code_home.display()))?;
|
||
|
||
@@ -347,7 +347,7 @@ fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Resul
|
||
}
|
||
|
||
if entries.is_empty() {
|
||
- println!("No MCP servers configured yet. Try `codex mcp add my-tool -- my-command`.");
|
||
+ println!("No MCP servers configured yet. Try `dev mcp add my-tool -- my-command`.");
|
||
return Ok(());
|
||
}
|
||
|
||
@@ -573,7 +573,7 @@ fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Result<(
|
||
if let Some(timeout) = server.tool_timeout_sec {
|
||
println!(" tool_timeout_sec: {:.3}", timeout.as_secs_f64());
|
||
}
|
||
- println!(" remove: codex mcp remove {}", get_args.name);
|
||
+ println!(" remove: dev mcp remove {}", get_args.name);
|
||
|
||
Ok(())
|
||
}
|
||
diff --git a/cloud-tasks/src/lib.rs b/cloud-tasks/src/lib.rs
|
||
index 0fa14ac..1346553 100644
|
||
--- a/cloud-tasks/src/lib.rs
|
||
+++ b/cloud-tasks/src/lib.rs
|
||
@@ -176,7 +176,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
|
||
// Default to online unless explicitly configured to use mock.
|
||
let use_mock = matches!(
|
||
- std::env::var("CODEX_CLOUD_TASKS_MODE").ok().as_deref(),
|
||
+ std::env::var("DEV_CLOUD_TASKS_MODE").ok().as_deref(),
|
||
Some("mock") | Some("MOCK")
|
||
);
|
||
|
||
@@ -184,7 +184,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
Arc::new(code_cloud_tasks_client::MockClient)
|
||
} else {
|
||
// Build an HTTP client against the configured (or default) base URL.
|
||
- let base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ let base_url = std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
|
||
let ua = code_core::default_client::get_code_user_agent(None);
|
||
let mut http =
|
||
@@ -233,7 +233,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
}
|
||
_ => {
|
||
eprintln!(
|
||
- "Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||
+ "Not signed in. Please run 'dev login' to sign in with ChatGPT, then re-run 'dev cloud'."
|
||
);
|
||
std::process::exit(1);
|
||
}
|
||
@@ -241,7 +241,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
}
|
||
None => {
|
||
eprintln!(
|
||
- "Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||
+ "Not signed in. Please run 'dev login' to sign in with ChatGPT, then re-run 'dev cloud'."
|
||
);
|
||
std::process::exit(1);
|
||
}
|
||
@@ -284,7 +284,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
let mut app = app::App::new();
|
||
// Initial load
|
||
let force_internal = matches!(
|
||
- std::env::var("CODEX_CLOUD_TASKS_FORCE_INTERNAL")
|
||
+ std::env::var("DEV_CLOUD_TASKS_FORCE_INTERNAL")
|
||
.ok()
|
||
.as_deref(),
|
||
Some("1") | Some("true") | Some("TRUE")
|
||
@@ -331,7 +331,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
let tx = tx.clone();
|
||
tokio::spawn(async move {
|
||
let base_url = util::normalize_base_url(
|
||
- &std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ &std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
|
||
);
|
||
let headers = util::build_chatgpt_headers().await;
|
||
@@ -346,7 +346,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
let tx = tx.clone();
|
||
tokio::spawn(async move {
|
||
let base_url = util::normalize_base_url(
|
||
- &std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ &std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
|
||
);
|
||
// Build headers: UA + ChatGPT auth if available
|
||
@@ -569,7 +569,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
let tx = tx.clone();
|
||
tokio::spawn(async move {
|
||
let base_url = crate::util::normalize_base_url(
|
||
- &std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ &std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
|
||
);
|
||
let headers = crate::util::build_chatgpt_headers().await;
|
||
@@ -954,7 +954,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
if should_fetch {
|
||
let tx = tx.clone();
|
||
tokio::spawn(async move {
|
||
- let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
|
||
+ let base_url = crate::util::normalize_base_url(&std::env::var("DEV_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
|
||
let headers = crate::util::build_chatgpt_headers().await;
|
||
let res = crate::env_detect::list_environments(&base_url, &headers).await;
|
||
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
|
||
@@ -1134,7 +1134,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
let tx = tx.clone();
|
||
tokio::spawn(async move {
|
||
let base_url = crate::util::normalize_base_url(
|
||
- &std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ &std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
|
||
);
|
||
let headers = crate::util::build_chatgpt_headers().await;
|
||
@@ -1205,7 +1205,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
let _ = frame_tx.send(Instant::now() + Duration::from_millis(100));
|
||
let tx = tx.clone();
|
||
tokio::spawn(async move {
|
||
- let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
|
||
+ let base_url = crate::util::normalize_base_url(&std::env::var("DEV_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
|
||
let headers = crate::util::build_chatgpt_headers().await;
|
||
let res = crate::env_detect::list_environments(&base_url, &headers).await;
|
||
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
|
||
@@ -1333,7 +1333,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
if should_fetch {
|
||
let tx = tx.clone();
|
||
tokio::spawn(async move {
|
||
- let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
|
||
+ let base_url = crate::util::normalize_base_url(&std::env::var("DEV_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
|
||
let headers = crate::util::build_chatgpt_headers().await;
|
||
let res = crate::env_detect::list_environments(&base_url, &headers).await;
|
||
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
|
||
@@ -1525,14 +1525,14 @@ async fn run_submit(args: crate::cli::SubmitArgs) -> anyhow::Result<()> {
|
||
set_user_agent_suffix("code_cloud_tasks_submit");
|
||
|
||
let use_mock = matches!(
|
||
- std::env::var("CODEX_CLOUD_TASKS_MODE").ok().as_deref(),
|
||
+ std::env::var("DEV_CLOUD_TASKS_MODE").ok().as_deref(),
|
||
Some("mock") | Some("MOCK")
|
||
);
|
||
|
||
let backend: Arc<dyn code_cloud_tasks_client::CloudBackend> = if use_mock {
|
||
Arc::new(code_cloud_tasks_client::MockClient)
|
||
} else {
|
||
- let base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ let base_url = std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
|
||
let ua = code_core::default_client::get_code_user_agent(None);
|
||
let mut http = code_cloud_tasks_client::HttpClient::new(base_url.clone())?
|
||
@@ -1562,12 +1562,12 @@ async fn run_submit(args: crate::cli::SubmitArgs) -> anyhow::Result<()> {
|
||
t
|
||
}
|
||
_ => {
|
||
- eprintln!("Not signed in. Run 'codex login' and retry.");
|
||
+ eprintln!("Not signed in. Run 'dev login' and retry.");
|
||
std::process::exit(1);
|
||
}
|
||
},
|
||
None => {
|
||
- eprintln!("Not signed in. Run 'codex login' and retry.");
|
||
+ eprintln!("Not signed in. Run 'dev login' and retry.");
|
||
std::process::exit(1);
|
||
}
|
||
};
|
||
@@ -1577,7 +1577,7 @@ async fn run_submit(args: crate::cli::SubmitArgs) -> anyhow::Result<()> {
|
||
// Resolve target environment id
|
||
let env_id = if let Some(e) = args.env.clone() { e } else {
|
||
let base_url = util::normalize_base_url(
|
||
- &std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ &std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
|
||
);
|
||
let headers = util::build_chatgpt_headers().await;
|
||
diff --git a/cloud-tasks/src/ui.rs b/cloud-tasks/src/ui.rs
|
||
index 975ad0e..4ff965f 100644
|
||
--- a/cloud-tasks/src/ui.rs
|
||
+++ b/cloud-tasks/src/ui.rs
|
||
@@ -61,7 +61,7 @@ static ROUNDED: OnceLock<bool> = OnceLock::new();
|
||
|
||
fn rounded_enabled() -> bool {
|
||
*ROUNDED.get_or_init(|| {
|
||
- std::env::var("CODEX_TUI_ROUNDED")
|
||
+ std::env::var("DEV_TUI_ROUNDED")
|
||
.ok()
|
||
.map(|v| v == "1")
|
||
.unwrap_or(true)
|
||
diff --git a/cloud-tasks/src/util.rs b/cloud-tasks/src/util.rs
|
||
index 78395ae..e30d0c5 100644
|
||
--- a/cloud-tasks/src/util.rs
|
||
+++ b/cloud-tasks/src/util.rs
|
||
@@ -19,7 +19,7 @@ enum CloudLogLevel {
|
||
}
|
||
|
||
fn log_level_from_env() -> CloudLogLevel {
|
||
- if let Ok(raw) = std::env::var("CODEX_CLOUD_TASKS_LOG_LEVEL") {
|
||
+ if let Ok(raw) = std::env::var("DEV_CLOUD_TASKS_LOG_LEVEL") {
|
||
let value = raw.trim().to_ascii_lowercase();
|
||
return match value.as_str() {
|
||
"off" | "none" | "0" => CloudLogLevel::Off,
|
||
@@ -30,7 +30,7 @@ fn log_level_from_env() -> CloudLogLevel {
|
||
};
|
||
}
|
||
|
||
- if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_CLOUD_TASKS_DEBUG") {
|
||
+ if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("DEV_CLOUD_TASKS_DEBUG") {
|
||
return CloudLogLevel::Debug;
|
||
}
|
||
|
||
@@ -59,16 +59,16 @@ fn user_home_dir() -> Option<PathBuf> {
|
||
}
|
||
|
||
fn resolve_log_path() -> Option<PathBuf> {
|
||
- if let Ok(path) = std::env::var("CODEX_CLOUD_TASKS_LOG_PATH") {
|
||
+ if let Ok(path) = std::env::var("DEV_CLOUD_TASKS_LOG_PATH") {
|
||
let trimmed = path.trim();
|
||
if !trimmed.is_empty() {
|
||
return Some(PathBuf::from(trimmed));
|
||
}
|
||
}
|
||
|
||
- let base = if let Ok(dir) = std::env::var("CODEX_CLOUD_TASKS_LOG_DIR") {
|
||
+ let base = if let Ok(dir) = std::env::var("DEV_CLOUD_TASKS_LOG_DIR") {
|
||
PathBuf::from(dir)
|
||
- } else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
|
||
+ } else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("DEV_HOME")) {
|
||
PathBuf::from(home).join("debug_logs")
|
||
} else if let Some(home) = user_home_dir() {
|
||
home.join(".code").join("debug_logs")
|
||
diff --git a/cloud-tasks-client/src/http.rs b/cloud-tasks-client/src/http.rs
|
||
index 47a4816..b6e4cf4 100644
|
||
--- a/cloud-tasks-client/src/http.rs
|
||
+++ b/cloud-tasks-client/src/http.rs
|
||
@@ -239,7 +239,7 @@ mod api {
|
||
"content": [{ "content_type": "text", "text": prompt }]
|
||
}));
|
||
|
||
- if let Ok(diff) = std::env::var("CODEX_STARTING_DIFF")
|
||
+ if let Ok(diff) = std::env::var("DEV_STARTING_DIFF")
|
||
&& !diff.is_empty()
|
||
{
|
||
input_items.push(serde_json::json!({
|
||
@@ -778,7 +778,7 @@ enum CloudLogLevel {
|
||
}
|
||
|
||
fn log_level_from_env() -> CloudLogLevel {
|
||
- if let Ok(raw) = std::env::var("CODEX_CLOUD_TASKS_LOG_LEVEL") {
|
||
+ if let Ok(raw) = std::env::var("DEV_CLOUD_TASKS_LOG_LEVEL") {
|
||
let value = raw.trim().to_ascii_lowercase();
|
||
return match value.as_str() {
|
||
"off" | "none" | "0" => CloudLogLevel::Off,
|
||
@@ -789,7 +789,7 @@ fn log_level_from_env() -> CloudLogLevel {
|
||
};
|
||
}
|
||
|
||
- if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_CLOUD_TASKS_DEBUG") {
|
||
+ if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("DEV_CLOUD_TASKS_DEBUG") {
|
||
return CloudLogLevel::Debug;
|
||
}
|
||
|
||
@@ -818,16 +818,16 @@ fn user_home_dir() -> Option<PathBuf> {
|
||
}
|
||
|
||
fn resolve_log_path() -> Option<PathBuf> {
|
||
- if let Ok(path) = std::env::var("CODEX_CLOUD_TASKS_LOG_PATH") {
|
||
+ if let Ok(path) = std::env::var("DEV_CLOUD_TASKS_LOG_PATH") {
|
||
let trimmed = path.trim();
|
||
if !trimmed.is_empty() {
|
||
return Some(PathBuf::from(trimmed));
|
||
}
|
||
}
|
||
|
||
- let base = if let Ok(dir) = std::env::var("CODEX_CLOUD_TASKS_LOG_DIR") {
|
||
+ let base = if let Ok(dir) = std::env::var("DEV_CLOUD_TASKS_LOG_DIR") {
|
||
PathBuf::from(dir)
|
||
- } else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
|
||
+ } else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("DEV_HOME")) {
|
||
PathBuf::from(home).join("debug_logs")
|
||
} else if let Some(home) = user_home_dir() {
|
||
home.join(".code").join("debug_logs")
|
||
diff --git a/code-auto-drive-core/src/auto_coordinator.rs b/code-auto-drive-core/src/auto_coordinator.rs
|
||
index 68ff605..367f9de 100644
|
||
--- a/code-auto-drive-core/src/auto_coordinator.rs
|
||
+++ b/code-auto-drive-core/src/auto_coordinator.rs
|
||
@@ -1825,7 +1825,7 @@ pub fn start_auto_coordinator(
|
||
debug_enabled: bool,
|
||
derive_goal_from_history: bool,
|
||
) -> Result<AutoCoordinatorHandle> {
|
||
- if std::env::var_os("CODEX_DEBUG_AUTO_COORDINATOR").is_some() {
|
||
+ if std::env::var_os("DEV_DEBUG_AUTO_COORDINATOR").is_some() {
|
||
eprintln!(
|
||
"start_auto_coordinator invoked\n{:?}",
|
||
std::backtrace::Backtrace::force_capture()
|
||
diff --git a/code-auto-drive-core/src/faults.rs b/code-auto-drive-core/src/faults.rs
|
||
index 08a82aa..e8984d0 100644
|
||
--- a/code-auto-drive-core/src/faults.rs
|
||
+++ b/code-auto-drive-core/src/faults.rs
|
||
@@ -34,14 +34,14 @@ enum FaultReset {
|
||
static CONFIG: OnceCell<HashMap<FaultScope, FaultConfig>> = OnceCell::new();
|
||
|
||
fn parse_fault_scope() -> Option<FaultScope> {
|
||
- match std::env::var("CODEX_FAULTS_SCOPE").ok().as_deref() {
|
||
+ match std::env::var("DEV_FAULTS_SCOPE").ok().as_deref() {
|
||
Some("auto_drive") => Some(FaultScope::AutoDrive),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn parse_reset_hint() -> Option<FaultReset> {
|
||
- if let Some(seconds) = std::env::var("CODEX_FAULTS_429_RESET").ok() {
|
||
+ if let Some(seconds) = std::env::var("DEV_FAULTS_429_RESET").ok() {
|
||
if let Ok(value) = seconds.parse::<u64>() {
|
||
return Some(FaultReset::Seconds(value));
|
||
}
|
||
@@ -62,7 +62,7 @@ fn parse_reset_hint() -> Option<FaultReset> {
|
||
fn init_config() -> HashMap<FaultScope, FaultConfig> {
|
||
let mut map = HashMap::new();
|
||
if let Some(scope) = parse_fault_scope() {
|
||
- if let Ok(spec) = std::env::var("CODEX_FAULTS") {
|
||
+ if let Ok(spec) = std::env::var("DEV_FAULTS") {
|
||
let cfg = FaultConfig::default();
|
||
for entry in spec.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||
if let Some((label, count)) = entry.split_once(':') {
|
||
diff --git a/code-version/src/lib.rs b/code-version/src/lib.rs
|
||
index 1509946..50a705b 100644
|
||
--- a/code-version/src/lib.rs
|
||
+++ b/code-version/src/lib.rs
|
||
@@ -14,7 +14,7 @@ pub const CODE_VERSION: &str = {
|
||
};
|
||
|
||
const ANNOUNCEMENT_TIP: &str = include_str!("../../../announcement_tip.toml");
|
||
-const MODELS_MANIFEST: &str = include_str!("../../../codex-rs/models-manager/models.json");
|
||
+const MODELS_MANIFEST: &str = include_str!("../../../vendor/codex/models-manager/models.json");
|
||
pub const MIN_WIRE_COMPAT_VERSION_FALLBACK: &str = "0.101.0";
|
||
|
||
static MIN_WIRE_COMPAT_VERSION: LazyLock<String> = LazyLock::new(|| {
|
||
diff --git a/common/src/approval_presets.rs b/common/src/approval_presets.rs
|
||
index 20305d2..07234bf 100644
|
||
--- a/common/src/approval_presets.rs
|
||
+++ b/common/src/approval_presets.rs
|
||
@@ -24,21 +24,21 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
|
||
ApprovalPreset {
|
||
id: "read-only",
|
||
label: "Read Only",
|
||
- description: "Codex can read files and answer questions. Codex requires approval to make edits, run commands, or access network",
|
||
+ description: "Hanzo Dev can read files and answer questions. It requires approval to make edits, run commands, or access network",
|
||
approval: AskForApproval::OnRequest,
|
||
sandbox: SandboxPolicy::ReadOnly,
|
||
},
|
||
ApprovalPreset {
|
||
id: "auto",
|
||
label: "Auto",
|
||
- description: "Codex can read files, make edits, and run commands in the workspace. Codex requires approval to work outside the workspace or access network",
|
||
+ description: "Hanzo Dev can read files, make edits, and run commands in the workspace. It requires approval to work outside the workspace or access network",
|
||
approval: AskForApproval::OnRequest,
|
||
sandbox: SandboxPolicy::new_workspace_write_policy(),
|
||
},
|
||
ApprovalPreset {
|
||
id: "full-access",
|
||
label: "Full Access",
|
||
- description: "Codex can read files, make edits, and run commands with network access, without approval. Exercise caution",
|
||
+ description: "Hanzo Dev can read files, make edits, and run commands with network access, without approval. Exercise caution",
|
||
approval: AskForApproval::Never,
|
||
sandbox: SandboxPolicy::DangerFullAccess,
|
||
},
|
||
diff --git a/common/src/config_override.rs b/common/src/config_override.rs
|
||
index d0719bc..db5ecf0 100644
|
||
--- a/common/src/config_override.rs
|
||
+++ b/common/src/config_override.rs
|
||
@@ -18,7 +18,7 @@ use toml::Value;
|
||
#[derive(Parser, Debug, Default, Clone)]
|
||
pub struct CliConfigOverrides {
|
||
/// Override a configuration value that would otherwise be loaded from
|
||
- /// `~/.code/config.toml` (Code also reads legacy `~/.codex/config.toml`). Use a dotted path (`foo.bar.baz`) to override
|
||
+ /// `~/.code/config.toml` (Hanzo Dev also reads legacy `~/.codex/config.toml`). Use a dotted path (`foo.bar.baz`) to override
|
||
/// nested values. The `value` portion is parsed as JSON. If it fails to
|
||
/// parse as JSON, the raw string is used as a literal.
|
||
///
|
||
diff --git a/common/src/model_presets.rs b/common/src/model_presets.rs
|
||
index 7dbb39d..b119b63 100644
|
||
--- a/common/src/model_presets.rs
|
||
+++ b/common/src/model_presets.rs
|
||
@@ -6,10 +6,10 @@ use code_core::protocol_config_types::ReasoningEffort;
|
||
use once_cell::sync::Lazy;
|
||
|
||
pub const HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_1_migration_prompt";
|
||
-pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str =
|
||
+pub const HIDE_GPT_5_1_DEV_MAX_MIGRATION_PROMPT_CONFIG: &str =
|
||
"hide_gpt-5.1-codex-max_migration_prompt";
|
||
pub const HIDE_GPT_5_2_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_2_migration_prompt";
|
||
-pub const HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_2_codex_migration_prompt";
|
||
+pub const HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_2_codex_migration_prompt";
|
||
|
||
/// A reasoning effort option surfaced for a model.
|
||
#[derive(Debug, Clone)]
|
||
@@ -47,8 +47,113 @@ const ALL_TEXT_VERBOSITY: &[TextVerbosityConfig] = &[
|
||
TextVerbosityConfig::High,
|
||
];
|
||
|
||
+/// Reasoning levels offered on a model. A level means the same thing on every
|
||
+/// model, so it is worded once here rather than per preset.
|
||
+fn efforts(levels: &[ReasoningEffort]) -> Vec<ReasoningEffortPreset> {
|
||
+ levels
|
||
+ .iter()
|
||
+ .map(|effort| ReasoningEffortPreset {
|
||
+ effort: effort.clone(),
|
||
+ description: match effort {
|
||
+ ReasoningEffort::Low => "Fast responses with lighter reasoning",
|
||
+ ReasoningEffort::Medium => "Balances speed and reasoning depth for everyday tasks",
|
||
+ _ => "Greater reasoning depth for complex problems",
|
||
+ }
|
||
+ .to_string(),
|
||
+ })
|
||
+ .collect()
|
||
+}
|
||
+
|
||
+/// A model served by the default `hanzo` provider (api.hanzo.ai/v1). Ids and
|
||
+/// context windows come from `GET /v1/models`.
|
||
+fn hanzo(
|
||
+ model: &str,
|
||
+ display_name: &str,
|
||
+ description: &str,
|
||
+ levels: &[ReasoningEffort],
|
||
+) -> ModelPreset {
|
||
+ ModelPreset {
|
||
+ id: model.to_string(),
|
||
+ model: model.to_string(),
|
||
+ display_name: display_name.to_string(),
|
||
+ description: description.to_string(),
|
||
+ default_reasoning_effort: ReasoningEffort::Medium,
|
||
+ supported_reasoning_efforts: efforts(levels),
|
||
+ supported_text_verbosity: ALL_TEXT_VERBOSITY,
|
||
+ is_default: false,
|
||
+ upgrade: None,
|
||
+ pro_only: false,
|
||
+ show_in_picker: true,
|
||
+ }
|
||
+}
|
||
+
|
||
static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
+ let deep = [
|
||
+ ReasoningEffort::Low,
|
||
+ ReasoningEffort::Medium,
|
||
+ ReasoningEffort::High,
|
||
+ ];
|
||
+ let quick = [ReasoningEffort::Low, ReasoningEffort::Medium];
|
||
+ let one = [ReasoningEffort::Medium];
|
||
+
|
||
vec![
|
||
+ ModelPreset {
|
||
+ is_default: true,
|
||
+ ..hanzo(
|
||
+ "zen5-coder",
|
||
+ "Zen 5 Coder",
|
||
+ "Agentic coding. 1M context.",
|
||
+ &deep,
|
||
+ )
|
||
+ },
|
||
+ hanzo(
|
||
+ "zen5",
|
||
+ "Zen 5",
|
||
+ "General reasoning and chat, same size and price as zen5-coder. 1M context.",
|
||
+ &deep,
|
||
+ ),
|
||
+ hanzo(
|
||
+ "zen5-pro",
|
||
+ "Zen 5 Pro",
|
||
+ "Long sessions where output tokens dominate cost; cheaper per token than zen5. 1M context.",
|
||
+ &deep,
|
||
+ ),
|
||
+ hanzo(
|
||
+ "zen5-flash",
|
||
+ "Zen 5 Flash",
|
||
+ "Quick edits and short turns. 64K context.",
|
||
+ &quick,
|
||
+ ),
|
||
+ hanzo(
|
||
+ "zen5-mini",
|
||
+ "Zen 5 Mini",
|
||
+ "Cheapest Zen 5; batch and helper turns. 32K context.",
|
||
+ &quick,
|
||
+ ),
|
||
+ hanzo(
|
||
+ "enso",
|
||
+ "Enso",
|
||
+ "Long-context reasoning outside the Zen family. 1M context.",
|
||
+ &deep,
|
||
+ ),
|
||
+ hanzo(
|
||
+ "enso-ultra",
|
||
+ "Enso Ultra",
|
||
+ "The largest Enso; slowest and most expensive of the three. 1M context.",
|
||
+ &deep,
|
||
+ ),
|
||
+ hanzo(
|
||
+ "enso-flash",
|
||
+ "Enso Flash",
|
||
+ "The fastest Enso. 256K context.",
|
||
+ &quick,
|
||
+ ),
|
||
+ hanzo(
|
||
+ "best",
|
||
+ "Best",
|
||
+ "Lets the gateway pick the model for each request.",
|
||
+ &one,
|
||
+ ),
|
||
ModelPreset {
|
||
id: "gpt-5.5".to_string(),
|
||
model: "gpt-5.5".to_string(),
|
||
@@ -75,7 +180,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
},
|
||
],
|
||
supported_text_verbosity: ALL_TEXT_VERBOSITY,
|
||
- is_default: true,
|
||
+ is_default: false,
|
||
upgrade: None,
|
||
pro_only: false,
|
||
show_in_picker: true,
|
||
@@ -230,7 +335,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: false,
|
||
@@ -271,7 +376,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: true,
|
||
@@ -345,7 +450,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
id: "gpt-5.1-codex-max".to_string(),
|
||
model: "gpt-5.1-codex-max".to_string(),
|
||
display_name: "gpt-5.1-codex-max".to_string(),
|
||
- description: "Latest Codex-optimized flagship for deep and fast reasoning.".to_string(),
|
||
+ description: "Prior Codex-optimized model, superseded by gpt-5.3-codex.".to_string(),
|
||
default_reasoning_effort: ReasoningEffort::Medium,
|
||
supported_reasoning_efforts: vec![
|
||
ReasoningEffortPreset {
|
||
@@ -370,7 +475,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: true,
|
||
@@ -401,7 +506,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: false,
|
||
@@ -428,7 +533,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: true,
|
||
@@ -462,7 +567,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: false,
|
||
@@ -493,7 +598,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: false,
|
||
@@ -519,7 +624,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: false,
|
||
@@ -557,7 +662,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||
upgrade: Some(ModelUpgrade {
|
||
id: "gpt-5.3-codex".to_string(),
|
||
reasoning_effort_mapping: None,
|
||
- migration_config_key: HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
+ migration_config_key: HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG.to_string(),
|
||
}),
|
||
pro_only: false,
|
||
show_in_picker: false,
|
||
@@ -671,6 +776,32 @@ mod tests {
|
||
assert_eq!(PRESETS.iter().filter(|preset| preset.is_default).count(), 1);
|
||
}
|
||
|
||
+ #[test]
|
||
+ fn hanzo_catalog_is_selectable_and_zen5_coder_is_the_default() {
|
||
+ let presets = builtin_model_presets(Some(AuthMode::ApiKey), false);
|
||
+ let ids: Vec<&str> = presets.iter().map(|preset| preset.id.as_str()).collect();
|
||
+ for id in [
|
||
+ "zen5-coder",
|
||
+ "zen5",
|
||
+ "zen5-pro",
|
||
+ "zen5-flash",
|
||
+ "zen5-mini",
|
||
+ "enso",
|
||
+ "enso-ultra",
|
||
+ "enso-flash",
|
||
+ "best",
|
||
+ ] {
|
||
+ assert!(ids.contains(&id), "{id} is not offered by the picker");
|
||
+ }
|
||
+
|
||
+ let default = presets
|
||
+ .iter()
|
||
+ .find(|preset| preset.is_default)
|
||
+ .expect("a default preset");
|
||
+ assert_eq!(default.id, "zen5-coder");
|
||
+ assert_eq!(ids.first(), Some(&"zen5-coder"));
|
||
+ }
|
||
+
|
||
#[test]
|
||
fn deprecated_and_chatgpt_only_codex_models_hidden_for_api_key_auth() {
|
||
let presets = builtin_model_presets(Some(AuthMode::ApiKey), false);
|
||
diff --git a/config.md b/config.md
|
||
index a6f2dab..aaa540d 100644
|
||
--- a/config.md
|
||
+++ b/config.md
|
||
@@ -8,7 +8,7 @@ This file has moved. Please see the latest configuration documentation here:
|
||
- Values can contain objects, such as `--config shell_environment_policy.include_only=["PATH", "HOME", "USER"]`.
|
||
- For consistency with `config.toml`, values are in TOML format rather than JSON format, so use `{a = 1, b = 2}` rather than `{"a": 1, "b": 2}`.
|
||
- If `value` cannot be parsed as a valid TOML value, it is treated as a string value. This means that both `-c model="o3"` and `-c model=o3` are equivalent.
|
||
-- The `$CODE_HOME/config.toml` configuration file. `CODE_HOME` defaults to `~/.code`; Code also reads from `$CODEX_HOME`/`~/.codex` for backwards compatibility but only writes to `~/.code`. (Logs and other state use the same directory.)
|
||
+- The `$CODE_HOME/config.toml` configuration file. `CODE_HOME` defaults to `~/.code`; Code also reads from `$DEV_HOME`/`~/.codex` for backwards compatibility but only writes to `~/.code`. (Logs and other state use the same directory.)
|
||
|
||
Both the `--config` flag and the `config.toml` file support the following options:
|
||
|
||
@@ -524,7 +524,7 @@ If subagents can't detect Git, ensure:
|
||
2. You're running Code from a Git repository
|
||
3. The `.git` directory is accessible
|
||
|
||
-For more details, see the [FAQ](https://github.com/just-every/code/blob/main/docs/faq.md).
|
||
+For more details, see the [FAQ](https://github.com/hanzoai/dev/blob/main/docs/faq.md).
|
||
|
||
## disable_response_storage
|
||
|
||
@@ -574,7 +574,7 @@ inherit = "none"
|
||
set = { PATH = "/usr/bin", MY_FLAG = "1" }
|
||
```
|
||
|
||
-Currently, `CODEX_SANDBOX_NETWORK_DISABLED=1` is also added to the environment, assuming network is disabled. This is not configurable.
|
||
+Currently, `DEV_SANDBOX_NETWORK_DISABLED=1` is also added to the environment, assuming network is disabled. This is not configurable.
|
||
|
||
## notify
|
||
|
||
@@ -655,7 +655,7 @@ notify = ["python3", "/Users/mbolin/.codex/notify.py"]
|
||
|
||
## history
|
||
|
||
-By default, Codex CLI records messages sent to the model in `$CODEX_HOME/history.jsonl`. Note that on UNIX, the file permissions are set to `o600`, so it should only be readable and writable by the owner.
|
||
+By default, Codex CLI records messages sent to the model in `$DEV_HOME/history.jsonl`. Note that on UNIX, the file permissions are set to `o600`, so it should only be readable and writable by the owner.
|
||
|
||
To disable this behavior, configure `[history]` as follows:
|
||
|
||
diff --git a/core/Cargo.toml b/core/Cargo.toml
|
||
index 8d284b8..81dae16 100644
|
||
--- a/core/Cargo.toml
|
||
+++ b/core/Cargo.toml
|
||
@@ -73,6 +73,7 @@ time = { workspace = true, features = [
|
||
tokio = { workspace = true, features = [
|
||
"io-std",
|
||
"macros",
|
||
+ "net",
|
||
"process",
|
||
"rt-multi-thread",
|
||
"signal",
|
||
diff --git a/core/gpt-5.1-codex-max_prompt.md b/core/gpt-5.1-codex-max_prompt.md
|
||
index 8e3f08f..ac1cb90 100644
|
||
--- a/core/gpt-5.1-codex-max_prompt.md
|
||
+++ b/core/gpt-5.1-codex-max_prompt.md
|
||
@@ -1,4 +1,4 @@
|
||
-You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer.
|
||
+You are a coding agent based on GPT-5, running in Hanzo Dev on a user's computer.
|
||
|
||
## General
|
||
|
||
diff --git a/core/gpt-5.2-codex_prompt.md b/core/gpt-5.2-codex_prompt.md
|
||
index 8e3f08f..ac1cb90 100644
|
||
--- a/core/gpt-5.2-codex_prompt.md
|
||
+++ b/core/gpt-5.2-codex_prompt.md
|
||
@@ -1,4 +1,4 @@
|
||
-You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer.
|
||
+You are a coding agent based on GPT-5, running in Hanzo Dev on a user's computer.
|
||
|
||
## General
|
||
|
||
diff --git a/core/gpt_5_1_prompt.md b/core/gpt_5_1_prompt.md
|
||
index da2ec67..977ddbe 100644
|
||
--- a/core/gpt_5_1_prompt.md
|
||
+++ b/core/gpt_5_1_prompt.md
|
||
@@ -1,4 +1,4 @@
|
||
-You are GPT-5.1 running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.
|
||
+You are GPT-5.1 running in Hanzo Dev, a terminal-based coding assistant. You are expected to be precise, safe, and helpful.
|
||
|
||
Your capabilities:
|
||
|
||
@@ -6,7 +6,6 @@ Your capabilities:
|
||
- Communicate with the user by streaming thinking & responses, and by making & updating plans.
|
||
- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section.
|
||
|
||
-Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI).
|
||
|
||
# How you work
|
||
|
||
diff --git a/core/gpt_5_2_prompt.md b/core/gpt_5_2_prompt.md
|
||
index 8aa188f..876a218 100644
|
||
--- a/core/gpt_5_2_prompt.md
|
||
+++ b/core/gpt_5_2_prompt.md
|
||
@@ -1,4 +1,4 @@
|
||
-You are GPT-5.2 running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.
|
||
+You are GPT-5.2 running in Hanzo Dev, a terminal-based coding assistant. You are expected to be precise, safe, and helpful.
|
||
|
||
Your capabilities:
|
||
|
||
@@ -6,7 +6,6 @@ Your capabilities:
|
||
- Communicate with the user by streaming thinking & responses, and by making & updating plans.
|
||
- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section.
|
||
|
||
-Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI).
|
||
|
||
# How you work
|
||
|
||
diff --git a/core/gpt_5_codex_prompt.md b/core/gpt_5_codex_prompt.md
|
||
index 88a569f..e60c47a 100644
|
||
--- a/core/gpt_5_codex_prompt.md
|
||
+++ b/core/gpt_5_codex_prompt.md
|
||
@@ -1,4 +1,4 @@
|
||
-You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer.
|
||
+You are a coding agent based on GPT-5, running in Hanzo Dev on a user's computer.
|
||
|
||
## General
|
||
|
||
diff --git a/core/prompt.md b/core/prompt.md
|
||
index 907ff8b..3277caa 100644
|
||
--- a/core/prompt.md
|
||
+++ b/core/prompt.md
|
||
@@ -1,4 +1,4 @@
|
||
-You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.
|
||
+You are a coding agent running in Hanzo Dev, a terminal-based coding assistant. You are expected to be precise, safe, and helpful.
|
||
|
||
Your capabilities:
|
||
|
||
@@ -6,7 +6,6 @@ Your capabilities:
|
||
- Communicate with the user by streaming thinking & responses, and by making & updating plans.
|
||
- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section.
|
||
|
||
-Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI).
|
||
|
||
# How you work
|
||
|
||
diff --git a/core/prompt_coder.md b/core/prompt_coder.md
|
||
index 4836187..de48d34 100644
|
||
--- a/core/prompt_coder.md
|
||
+++ b/core/prompt_coder.md
|
||
@@ -1,10 +1,9 @@
|
||
-In this environment, you are running as `code` and your name is Code. Code is a fork Codex CLI, an open source project led by OpenAI.
|
||
+In this environment, you are running as `dev` and your name is Hanzo Dev.
|
||
|
||
-Code is a fast, community-driven fork focused on key developer ergonomics: Browser control, multi-agent flows, autonomous tasks, and on-the-fly reasoning control - all while staying compatible with upstream.
|
||
+Hanzo Dev is the Hanzo coding agent: browser control, multi-agent flows, autonomous long-running tasks, and on-the-fly reasoning control. You have independent control over your environment and should complete tasks without asking for human assistance.
|
||
|
||
-# Changes
|
||
-
|
||
-This version has a few key changes and additions. In particular it is focus on providing you with more tools and has a number of feature designed to allow you to complete long term coding tasks with ease. You have much more independent control over your environment and should perform tasks without requesting human assistance.
|
||
+## Starting an app
|
||
+When asked to build an app, service or tool from scratch, the first step is to look for an existing open-source project to fork as the starting point — search GitHub, weigh license, activity and fit, and say what you found and why you chose it (or why nothing fit). Fork and adapt; write from a blank file only what does not already exist. A working project with a community beats a first draft every time.
|
||
|
||
## Code design
|
||
Focus on producing final, maintable, production ready code every time.
|
||
diff --git a/core/prompt_with_apply_patch_instructions.md b/core/prompt_with_apply_patch_instructions.md
|
||
index 2650c67..f5ef0ef 100644
|
||
--- a/core/prompt_with_apply_patch_instructions.md
|
||
+++ b/core/prompt_with_apply_patch_instructions.md
|
||
@@ -1,4 +1,4 @@
|
||
-You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.
|
||
+You are a coding agent running in Hanzo Dev, a terminal-based coding assistant. You are expected to be precise, safe, and helpful.
|
||
|
||
Your capabilities:
|
||
|
||
@@ -6,7 +6,6 @@ Your capabilities:
|
||
- Communicate with the user by streaming thinking & responses, and by making & updating plans.
|
||
- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section.
|
||
|
||
-Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI).
|
||
|
||
# How you work
|
||
|
||
diff --git a/core/src/agent_defaults.rs b/core/src/agent_defaults.rs
|
||
index 70cad29..54bda13 100644
|
||
--- a/core/src/agent_defaults.rs
|
||
+++ b/core/src/agent_defaults.rs
|
||
@@ -13,8 +13,8 @@ use std::sync::LazyLock;
|
||
const CLAUDE_ALLOWED_TOOLS: &str = "Bash(ls:*), Bash(cat:*), Bash(grep:*), Bash(git status:*), Bash(git log:*), Bash(find:*), Read, Grep, Glob, LS, WebFetch, TodoRead, TodoWrite, WebSearch";
|
||
const CLOUD_MODEL_ENV_FLAG: &str = "CODE_ENABLE_CLOUD_AGENT_MODEL";
|
||
|
||
-const CODE_GPT5_CODEX_READ_ONLY: &[&str] = &["-s", "read-only", "exec", "--skip-git-repo-check"];
|
||
-const CODE_GPT5_CODEX_WRITE: &[&str] = &["-s", "workspace-write", "--dangerously-bypass-approvals-and-sandbox", "exec", "--skip-git-repo-check"];
|
||
+const CODE_GPT5_DEV_READ_ONLY: &[&str] = &["-s", "read-only", "exec", "--skip-git-repo-check"];
|
||
+const CODE_GPT5_DEV_WRITE: &[&str] = &["-s", "workspace-write", "--dangerously-bypass-approvals-and-sandbox", "exec", "--skip-git-repo-check"];
|
||
const CODE_GPT5_READ_ONLY: &[&str] = &["-s", "read-only", "exec", "--skip-git-repo-check"];
|
||
const CODE_GPT5_WRITE: &[&str] = &["-s", "workspace-write", "--dangerously-bypass-approvals-and-sandbox", "exec", "--skip-git-repo-check"];
|
||
const CLAUDE_SONNET_READ_ONLY: &[&str] = &["--allowedTools", CLAUDE_ALLOWED_TOOLS];
|
||
@@ -31,9 +31,9 @@ const COPILOT_READ_ONLY: &[&str] = &["--autopilot", "--allow-all-tools", "--no-a
|
||
const COPILOT_WRITE: &[&str] = &["--autopilot", "--yolo", "--no-ask-user", "-s"];
|
||
const QWEN_3_CODER_READ_ONLY: &[&str] = &[];
|
||
const QWEN_3_CODER_WRITE: &[&str] = &["-y"];
|
||
-const CLOUD_GPT5_CODEX_READ_ONLY: &[&str] = &[];
|
||
-const CLOUD_GPT5_CODEX_WRITE: &[&str] = &[];
|
||
-const MODELS_MANIFEST: &str = include_str!("../../../codex-rs/models-manager/models.json");
|
||
+const CLOUD_GPT5_DEV_READ_ONLY: &[&str] = &[];
|
||
+const CLOUD_GPT5_DEV_WRITE: &[&str] = &[];
|
||
+const MODELS_MANIFEST: &str = include_str!("../../../vendor/codex/models-manager/models.json");
|
||
|
||
/// Canonical list of built-in agent model slugs used when no `[[agents]]`
|
||
/// entries are configured. The ordering here controls priority for legacy
|
||
@@ -122,8 +122,8 @@ const AGENT_MODEL_SPECS: &[AgentModelSpec] = &[
|
||
slug: "code-gpt-5.6-luna",
|
||
family: "code",
|
||
cli: "coder",
|
||
- read_only_args: CODE_GPT5_CODEX_READ_ONLY,
|
||
- write_args: CODE_GPT5_CODEX_WRITE,
|
||
+ read_only_args: CODE_GPT5_DEV_READ_ONLY,
|
||
+ write_args: CODE_GPT5_DEV_WRITE,
|
||
model_args: &["--model", "gpt-5.6-luna"],
|
||
description: "Budget coding agent for small changes and quick refactors; use when speed and cost matter.",
|
||
enabled_by_default: true,
|
||
@@ -145,8 +145,8 @@ const AGENT_MODEL_SPECS: &[AgentModelSpec] = &[
|
||
slug: "code-gpt-5.3-codex",
|
||
family: "code",
|
||
cli: "coder",
|
||
- read_only_args: CODE_GPT5_CODEX_READ_ONLY,
|
||
- write_args: CODE_GPT5_CODEX_WRITE,
|
||
+ read_only_args: CODE_GPT5_DEV_READ_ONLY,
|
||
+ write_args: CODE_GPT5_DEV_WRITE,
|
||
model_args: &["--model", "gpt-5.3-codex"],
|
||
description: "Primary coding agent for implementation and multi-file edits; strong speed and reliability.",
|
||
enabled_by_default: false,
|
||
@@ -169,8 +169,8 @@ const AGENT_MODEL_SPECS: &[AgentModelSpec] = &[
|
||
slug: "code-gpt-5.3-codex-spark",
|
||
family: "code",
|
||
cli: "coder",
|
||
- read_only_args: CODE_GPT5_CODEX_READ_ONLY,
|
||
- write_args: CODE_GPT5_CODEX_WRITE,
|
||
+ read_only_args: CODE_GPT5_DEV_READ_ONLY,
|
||
+ write_args: CODE_GPT5_DEV_WRITE,
|
||
model_args: &["--model", "gpt-5.3-codex-spark"],
|
||
description: "Fast codex variant tuned for responsive coding loops and smaller edits.",
|
||
enabled_by_default: false,
|
||
@@ -302,8 +302,8 @@ const AGENT_MODEL_SPECS: &[AgentModelSpec] = &[
|
||
slug: "cloud-gpt-5.1-codex-max",
|
||
family: "cloud",
|
||
cli: "cloud",
|
||
- read_only_args: CLOUD_GPT5_CODEX_READ_ONLY,
|
||
- write_args: CLOUD_GPT5_CODEX_WRITE,
|
||
+ read_only_args: CLOUD_GPT5_DEV_READ_ONLY,
|
||
+ write_args: CLOUD_GPT5_DEV_WRITE,
|
||
model_args: &["--model", "gpt-5.1-codex-max"],
|
||
description: "Cloud-hosted gpt-5.1-codex-max agent; use for remote runs when enabled via CODE_ENABLE_CLOUD_AGENT_MODEL.",
|
||
enabled_by_default: false,
|
||
diff --git a/core/src/agent_tool.rs b/core/src/agent_tool.rs
|
||
index bea6599..1ba394d 100644
|
||
--- a/core/src/agent_tool.rs
|
||
+++ b/core/src/agent_tool.rs
|
||
@@ -263,7 +263,7 @@ fn fallback_code_binary_path() -> Option<std::path::PathBuf> {
|
||
// If the running binary was pruned (e.g., shared target cache rotation), try to locate
|
||
// a fresh dev build in the repository, and if missing, trigger a quick rebuild.
|
||
let repo_root = find_repo_root(std::env::current_dir().ok()?)?;
|
||
- let workspace = repo_root.join("code-rs");
|
||
+ let workspace = repo_root.join("hanzo-dev");
|
||
|
||
// Probe likely build outputs in priority order.
|
||
let mut candidates = vec![
|
||
@@ -322,7 +322,7 @@ fn format_agent_not_found_error(agent_name: &str, command: &str) -> String {
|
||
command = \"C:\\\\Users\\\\YourUser\\\\AppData\\\\Roaming\\\\npm\\\\{}.cmd\"\n\
|
||
3. Verify your PATH includes the directory containing '{}'\n\
|
||
4. On Windows, ensure the file has a valid extension (.exe, .cmd, .bat, .com)\n\n\
|
||
- For more information, see: https://github.com/just-every/code/blob/main/code-rs/config.md",
|
||
+ For more information, see: https://github.com/hanzoai/dev/blob/main/hanzo-dev/config.md",
|
||
command, agent_name, command, command
|
||
));
|
||
}
|
||
@@ -337,7 +337,7 @@ fn format_agent_not_found_error(agent_name: &str, command: &str) -> String {
|
||
[[agents]]\n\
|
||
name = \"{}\"\n\
|
||
command = \"/absolute/path/to/{}\"\n\n\
|
||
- For more information, see: https://github.com/just-every/code/blob/main/code-rs/config.md",
|
||
+ For more information, see: https://github.com/hanzoai/dev/blob/main/hanzo-dev/config.md",
|
||
command, command, command, agent_name, command
|
||
));
|
||
}
|
||
diff --git a/core/src/apply_patch.rs b/core/src/apply_patch.rs
|
||
index c31d1ec..e51b1a5 100644
|
||
--- a/core/src/apply_patch.rs
|
||
+++ b/core/src/apply_patch.rs
|
||
@@ -20,7 +20,7 @@ use std::collections::HashMap;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
|
||
-pub const CODEX_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch";
|
||
+pub const DEV_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch";
|
||
|
||
pub(crate) struct ApplyPatchRun {
|
||
pub auto_approved: bool,
|
||
diff --git a/core/src/auth.rs b/core/src/auth.rs
|
||
index f3710cc..f047185 100644
|
||
--- a/core/src/auth.rs
|
||
+++ b/core/src/auth.rs
|
||
@@ -24,6 +24,11 @@ use crate::token_data::parse_jwt_expiration;
|
||
use crate::config::resolve_code_path_for_read;
|
||
use crate::util::backoff;
|
||
|
||
+/// The command a human runs to authenticate. It lives here once so no message
|
||
+/// can drift into naming a command that does not exist — `hanzo login` is not
|
||
+/// one, and gets swallowed as an AI task.
|
||
+pub const LOGIN_COMMAND: &str = "hanzo auth login";
|
||
+
|
||
#[derive(Debug, Clone)]
|
||
pub struct CodexAuth {
|
||
pub mode: AuthMode,
|
||
@@ -81,7 +86,7 @@ impl std::error::Error for RefreshTokenError {}
|
||
const REFRESH_TOKEN_ACCOUNT_MISMATCH_MESSAGE: &str =
|
||
"Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.";
|
||
const REFRESH_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
|
||
-pub const REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR: &str = "CODEX_REFRESH_TOKEN_URL_OVERRIDE";
|
||
+pub const REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR: &str = "DEV_REFRESH_TOKEN_URL_OVERRIDE";
|
||
|
||
impl PartialEq for CodexAuth {
|
||
fn eq(&self, other: &Self) -> bool {
|
||
@@ -371,7 +376,7 @@ fn should_proactively_refresh_auth(
|
||
}
|
||
|
||
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
||
-pub const CODEX_API_KEY_ENV_VAR: &str = "CODEX_API_KEY";
|
||
+pub const HANZO_API_KEY_ENV_VAR: &str = "HANZO_API_KEY";
|
||
|
||
fn read_openai_api_key_from_env() -> Option<String> {
|
||
env::var(OPENAI_API_KEY_ENV_VAR)
|
||
@@ -380,7 +385,7 @@ fn read_openai_api_key_from_env() -> Option<String> {
|
||
}
|
||
|
||
pub fn read_code_api_key_from_env() -> Option<String> {
|
||
- env::var(CODEX_API_KEY_ENV_VAR)
|
||
+ env::var(HANZO_API_KEY_ENV_VAR)
|
||
.ok()
|
||
.map(|value| value.trim().to_string())
|
||
.filter(|value| !value.is_empty())
|
||
@@ -730,7 +735,7 @@ fn load_auth(
|
||
}
|
||
None => {
|
||
// We have an API key but no tokens in the auth.json file.
|
||
- // Perhaps the user ran `codex login --api-key <KEY>` or updated
|
||
+ // Perhaps the user ran `dev login --api-key <KEY>` or updated
|
||
// auth.json by hand. Either way, let's assume they are trying
|
||
// to use their API key.
|
||
return Ok(Some(CodexAuth::from_api_key_with_client(api_key, client)));
|
||
@@ -754,7 +759,7 @@ fn load_auth(
|
||
}))
|
||
}
|
||
|
||
-/// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory.
|
||
+/// Attempt to read and refresh the `auth.json` file in the given `DEV_HOME` directory.
|
||
/// Returns the full AuthDotJson structure after refreshing if necessary.
|
||
pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result<AuthDotJson> {
|
||
let mut file = File::open(auth_file)?;
|
||
@@ -991,7 +996,7 @@ fn summarize_body(body: &str) -> String {
|
||
}
|
||
}
|
||
|
||
-/// Expected structure for $CODEX_HOME/auth.json.
|
||
+/// Expected structure for $DEV_HOME/auth.json.
|
||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||
pub struct AuthDotJson {
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
@@ -1009,6 +1014,15 @@ pub struct AuthDotJson {
|
||
|
||
// Shared constant for token refresh (client id used for oauth token refresh flow)
|
||
pub const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||
+
|
||
+/// Hanzo IAM's OIDC issuer. hanzo.id serves OIDC under `/v1/iam`, so
|
||
+/// `{issuer}/oauth/device` and `{issuer}/oauth/token` resolve to the endpoints
|
||
+/// advertised by `hanzo.id/.well-known/openid-configuration`.
|
||
+pub const HANZO_ISSUER: &str = "https://hanzo.id/v1/iam";
|
||
+/// The public client registered for the device grant. It holds no secret, which
|
||
+/// is what a CLI needs: `hanzo-app` is confidential and answers an
|
||
+/// unauthenticated device request with `invalid_client`.
|
||
+pub const HANZO_CLIENT_ID: &str = "hanzo-cli";
|
||
const CHATGPT_ACCESS_TOKEN_REFRESH_WINDOW_MINUTES: i64 = 5;
|
||
|
||
use std::sync::RwLock;
|
||
diff --git a/core/src/cgroup.rs b/core/src/cgroup.rs
|
||
index 84d08ec..bfe7744 100644
|
||
--- a/core/src/cgroup.rs
|
||
+++ b/core/src/cgroup.rs
|
||
@@ -12,14 +12,14 @@ const EXEC_CGROUP_OOM_SCORE_ADJ: &str = "500";
|
||
|
||
#[cfg(target_os = "linux")]
|
||
pub(crate) fn default_exec_memory_max_bytes() -> Option<u64> {
|
||
- if let Ok(raw) = std::env::var("CODEX_EXEC_MEMORY_MAX_BYTES") {
|
||
+ if let Ok(raw) = std::env::var("DEV_EXEC_MEMORY_MAX_BYTES") {
|
||
if let Ok(value) = raw.trim().parse::<u64>() {
|
||
if value > 0 {
|
||
return Some(value);
|
||
}
|
||
}
|
||
}
|
||
- if let Ok(raw) = std::env::var("CODEX_EXEC_MEMORY_MAX_MB") {
|
||
+ if let Ok(raw) = std::env::var("DEV_EXEC_MEMORY_MAX_MB") {
|
||
if let Ok(value) = raw.trim().parse::<u64>() {
|
||
if value > 0 {
|
||
return Some(value.saturating_mul(1024 * 1024));
|
||
diff --git a/core/src/chat_completions.rs b/core/src/chat_completions.rs
|
||
index 9719d43..60da586 100644
|
||
--- a/core/src/chat_completions.rs
|
||
+++ b/core/src/chat_completions.rs
|
||
@@ -370,7 +370,7 @@ pub(crate) async fn stream_chat_completions(
|
||
// If an Ollama context override is present, propagate it. Some Ollama
|
||
// builds honor `num_ctx` directly in OpenAI-compatible Chat Completions,
|
||
// and others accept it under an `options` object – include both.
|
||
- if let Ok(val) = std::env::var("CODEX_OLLAMA_NUM_CTX") {
|
||
+ if let Ok(val) = std::env::var("DEV_OLLAMA_NUM_CTX") {
|
||
if let Ok(n) = val.parse::<u64>() {
|
||
if let Some(obj) = payload.as_object_mut() {
|
||
obj.insert("num_ctx".to_string(), json!(n));
|
||
diff --git a/core/src/client.rs b/core/src/client.rs
|
||
index cadb7ea..47257e3 100644
|
||
--- a/core/src/client.rs
|
||
+++ b/core/src/client.rs
|
||
@@ -67,7 +67,7 @@ use crate::error::ModelCapError;
|
||
use crate::error::RetryLimitReachedError;
|
||
use crate::error::UnexpectedResponseError;
|
||
use crate::error::UsageLimitReachedError;
|
||
-use crate::flags::CODEX_RS_SSE_FIXTURE;
|
||
+use crate::flags::DEV_RS_SSE_FIXTURE;
|
||
use crate::model_family::{find_family_for_model, ModelFamily};
|
||
use crate::model_provider_info::ModelProviderInfo;
|
||
use crate::model_provider_info::WireApi;
|
||
@@ -108,10 +108,10 @@ fn preferred_ws_version_from_env() -> ResponsesWebsocketVersion {
|
||
// Sticky-routing token captured at the start of a turn. When present, it must
|
||
// be replayed on every subsequent request within the same turn (retries,
|
||
// continuations, websocket reconnects).
|
||
-const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
|
||
-const X_CODEX_ROUTING_HINT_HEADER: &str = "x-codex-routing-hint";
|
||
-const X_CODEX_WINDOW_ID_HEADER: &str = "x-codex-window-id";
|
||
-const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER: &str =
|
||
+const X_DEV_TURN_STATE_HEADER: &str = "x-codex-turn-state";
|
||
+const X_DEV_ROUTING_HINT_HEADER: &str = "x-codex-routing-hint";
|
||
+const X_DEV_WINDOW_ID_HEADER: &str = "x-codex-window-id";
|
||
+const X_OPENAI_INTERNAL_DEV_RESPONSES_LITE_HEADER: &str =
|
||
"x-openai-internal-codex-responses-lite";
|
||
|
||
const MODEL_CAP_MODEL_HEADER: &str = "x-codex-model-cap-model";
|
||
@@ -468,7 +468,7 @@ impl ModelClient {
|
||
("session_id".to_string(), session_id_str.clone()),
|
||
("thread_id".to_string(), session_id_str),
|
||
(
|
||
- X_CODEX_WINDOW_ID_HEADER.to_string(),
|
||
+ X_DEV_WINDOW_ID_HEADER.to_string(),
|
||
self.current_window_id(session_id),
|
||
),
|
||
])
|
||
@@ -921,7 +921,7 @@ impl ModelClient {
|
||
if let Some(header_value) =
|
||
self.build_routing_hint_header(auth.as_ref(), request_model, service_tier.as_deref())
|
||
{
|
||
- req_builder = req_builder.header(X_CODEX_ROUTING_HINT_HEADER, header_value);
|
||
+ req_builder = req_builder.header(X_DEV_ROUTING_HINT_HEADER, header_value);
|
||
}
|
||
|
||
let has_beta_header = req_builder
|
||
@@ -943,14 +943,14 @@ impl ModelClient {
|
||
req_builder =
|
||
attach_responses_lite_header(req_builder, request_family.use_responses_lite);
|
||
if let Some(state) = turn_state.get() {
|
||
- req_builder = req_builder.header(X_CODEX_TURN_STATE_HEADER, state);
|
||
+ req_builder = req_builder.header(X_DEV_TURN_STATE_HEADER, state);
|
||
}
|
||
req_builder = req_builder
|
||
.header("conversation_id", session_id_str.clone())
|
||
.header("session_id", session_id_str.clone())
|
||
.header("thread_id", session_id_str.clone());
|
||
if let Ok(window_id) = HeaderValue::from_str(&self.current_window_id(session_id)) {
|
||
- req_builder = req_builder.header(X_CODEX_WINDOW_ID_HEADER, window_id);
|
||
+ req_builder = req_builder.header(X_DEV_WINDOW_ID_HEADER, window_id);
|
||
}
|
||
|
||
if let Some(auth) = auth.as_ref()
|
||
@@ -1032,7 +1032,7 @@ impl ModelClient {
|
||
|
||
if let Some(value) = response
|
||
.headers()
|
||
- .get(X_CODEX_TURN_STATE_HEADER)
|
||
+ .get(X_DEV_TURN_STATE_HEADER)
|
||
.and_then(|value| value.to_str().ok())
|
||
{
|
||
if let Some(existing) = turn_state.get()
|
||
@@ -1218,7 +1218,7 @@ impl ModelClient {
|
||
|
||
/// Implementation for the OpenAI *Responses* experimental API.
|
||
async fn stream_responses(&self, prompt: &Prompt, log_tag: Option<&str>) -> Result<ResponseStream> {
|
||
- if let Some(path) = &*CODEX_RS_SSE_FIXTURE {
|
||
+ if let Some(path) = &*DEV_RS_SSE_FIXTURE {
|
||
// short circuit for tests
|
||
warn!(path, "Streaming from fixture");
|
||
return stream_from_fixture(path, self.provider.clone(), self.otel_event_manager.clone())
|
||
@@ -1425,7 +1425,7 @@ impl ModelClient {
|
||
if let Some(header_value) =
|
||
self.build_routing_hint_header(auth.as_ref(), request_model, service_tier.as_deref())
|
||
{
|
||
- req_builder = req_builder.header(X_CODEX_ROUTING_HINT_HEADER, header_value);
|
||
+ req_builder = req_builder.header(X_DEV_ROUTING_HINT_HEADER, header_value);
|
||
}
|
||
|
||
let has_beta_header = req_builder
|
||
@@ -1447,7 +1447,7 @@ impl ModelClient {
|
||
req_builder =
|
||
attach_responses_lite_header(req_builder, request_family.use_responses_lite);
|
||
if let Some(state) = turn_state.get() {
|
||
- req_builder = req_builder.header(X_CODEX_TURN_STATE_HEADER, state);
|
||
+ req_builder = req_builder.header(X_DEV_TURN_STATE_HEADER, state);
|
||
}
|
||
|
||
req_builder = req_builder
|
||
@@ -1458,7 +1458,7 @@ impl ModelClient {
|
||
.header(reqwest::header::ACCEPT, "text/event-stream")
|
||
.json(&payload_json);
|
||
if let Ok(window_id) = HeaderValue::from_str(&self.current_window_id(session_id)) {
|
||
- req_builder = req_builder.header(X_CODEX_WINDOW_ID_HEADER, window_id);
|
||
+ req_builder = req_builder.header(X_DEV_WINDOW_ID_HEADER, window_id);
|
||
}
|
||
|
||
if let Some(auth) = auth.as_ref()
|
||
@@ -1507,7 +1507,7 @@ impl ModelClient {
|
||
Ok(resp) if resp.status().is_success() => {
|
||
if let Some(value) = resp
|
||
.headers()
|
||
- .get(X_CODEX_TURN_STATE_HEADER)
|
||
+ .get(X_DEV_TURN_STATE_HEADER)
|
||
.and_then(|value| value.to_str().ok())
|
||
{
|
||
if let Some(existing) = turn_state.get()
|
||
@@ -1601,7 +1601,7 @@ impl ModelClient {
|
||
let status = res.status();
|
||
let headers = res.headers().clone();
|
||
if let Some(value) = headers
|
||
- .get(X_CODEX_TURN_STATE_HEADER)
|
||
+ .get(X_DEV_TURN_STATE_HEADER)
|
||
.and_then(|value| value.to_str().ok())
|
||
{
|
||
if let Some(existing) = turn_state.get()
|
||
@@ -2078,7 +2078,7 @@ impl ModelClient {
|
||
if let Some(header_value) =
|
||
self.build_routing_hint_header(auth.as_ref(), model_slug, service_tier.as_deref())
|
||
{
|
||
- request = request.header(X_CODEX_ROUTING_HINT_HEADER, header_value);
|
||
+ request = request.header(X_DEV_ROUTING_HINT_HEADER, header_value);
|
||
}
|
||
|
||
// Ensure Responses API beta header is present for compact calls. Mirror the
|
||
@@ -2102,10 +2102,10 @@ impl ModelClient {
|
||
request = attach_codex_beta_features_header(request, &self.config);
|
||
request = attach_responses_lite_header(request, family.use_responses_lite);
|
||
if let Some(state) = turn_state.get() {
|
||
- request = request.header(X_CODEX_TURN_STATE_HEADER, state);
|
||
+ request = request.header(X_DEV_TURN_STATE_HEADER, state);
|
||
}
|
||
if let Ok(window_id) = HeaderValue::from_str(&self.current_window_id(session_id)) {
|
||
- request = request.header(X_CODEX_WINDOW_ID_HEADER, window_id);
|
||
+ request = request.header(X_DEV_WINDOW_ID_HEADER, window_id);
|
||
}
|
||
|
||
request = request
|
||
@@ -2148,7 +2148,7 @@ impl ModelClient {
|
||
let status = response.status();
|
||
let headers = response.headers().clone();
|
||
if let Some(value) = headers
|
||
- .get(X_CODEX_TURN_STATE_HEADER)
|
||
+ .get(X_DEV_TURN_STATE_HEADER)
|
||
.and_then(|value| value.to_str().ok())
|
||
{
|
||
if let Some(existing) = turn_state.get()
|
||
@@ -2272,7 +2272,7 @@ fn attach_responses_lite_header(
|
||
use_responses_lite: bool,
|
||
) -> reqwest::RequestBuilder {
|
||
if use_responses_lite {
|
||
- builder.header(X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER, "true")
|
||
+ builder.header(X_OPENAI_INTERNAL_DEV_RESPONSES_LITE_HEADER, "true")
|
||
} else {
|
||
builder
|
||
}
|
||
@@ -3282,6 +3282,7 @@ mod tests {
|
||
name: name.to_string(),
|
||
base_url: Some("https://api.openai.com/v1".to_string()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3406,6 +3407,7 @@ mod tests {
|
||
name: "openai".to_string(),
|
||
base_url: Some("https://api.openai.com/v1".to_string()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3456,6 +3458,7 @@ mod tests {
|
||
name: "backend".to_string(),
|
||
base_url: Some("https://chatgpt.com/backend-api/codex".to_string()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3508,6 +3511,7 @@ mod tests {
|
||
name: "custom".to_string(),
|
||
base_url: Some("https://api.openai.com/v1".to_string()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3550,6 +3554,7 @@ mod tests {
|
||
name: "openai".to_string(),
|
||
base_url: Some("https://api.openai.com/v1".to_string()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3582,7 +3587,7 @@ mod tests {
|
||
|
||
let header_value = request
|
||
.headers()
|
||
- .get(X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER)
|
||
+ .get(X_OPENAI_INTERNAL_DEV_RESPONSES_LITE_HEADER)
|
||
.expect("Responses Lite header present");
|
||
assert_eq!(header_value, "true");
|
||
}
|
||
@@ -3700,6 +3705,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3789,6 +3795,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3837,6 +3844,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3891,6 +3899,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -3988,6 +3997,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -4097,6 +4107,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -4339,6 +4350,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -4372,6 +4384,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -4408,6 +4421,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -4441,6 +4455,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -4474,6 +4489,7 @@ mod tests {
|
||
name: "test".to_string(),
|
||
base_url: Some("https://test.com".to_string()),
|
||
env_key: Some("TEST_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
diff --git a/core/src/codex/compact.rs b/core/src/codex/compact.rs
|
||
index ee2a8b1..2f159dd 100644
|
||
--- a/core/src/codex/compact.rs
|
||
+++ b/core/src/codex/compact.rs
|
||
@@ -49,7 +49,7 @@ const COMPACTION_EMERGENCY_MESSAGE: &str = "⚠️ Compaction failed: The conver
|
||
/// Determine whether to use remote compaction (ChatGPT-based) or local compaction.
|
||
///
|
||
/// Upstream codex-rs checks if auth mode is ChatGPT and RemoteCompaction feature is enabled.
|
||
-/// In code-rs, remote compaction infrastructure is not yet implemented, so this always
|
||
+/// In hanzo-dev, remote compaction infrastructure is not yet implemented, so this always
|
||
/// returns false (always use local compaction).
|
||
///
|
||
/// TODO: Once ChatGPT auth and remote compaction are implemented, update this to:
|
||
diff --git a/core/src/codex/exec.rs b/core/src/codex/exec.rs
|
||
index 45ae9bf..ced724f 100644
|
||
--- a/core/src/codex/exec.rs
|
||
+++ b/core/src/codex/exec.rs
|
||
@@ -418,6 +418,10 @@ impl Session {
|
||
if is_apply_patch {
|
||
let unified_diff = turn_diff_tracker.get_unified_diff();
|
||
if let Ok(Some(unified_diff)) = unified_diff {
|
||
+ // The diff says what moved; a picture says what it looks like.
|
||
+ // Reads the same value the reviewer is about to, and returns
|
||
+ // before the browser it may launch has started.
|
||
+ crate::shot::capture(self, &unified_diff);
|
||
let msg = EventMsg::TurnDiff(TurnDiffEvent { unified_diff });
|
||
let event = self.make_event(sub_id, msg);
|
||
let _ = self.tx_event.send(event).await;
|
||
diff --git a/core/src/codex/session.rs b/core/src/codex/session.rs
|
||
index b55bef8..98a3592 100644
|
||
--- a/core/src/codex/session.rs
|
||
+++ b/core/src/codex/session.rs
|
||
@@ -644,7 +644,7 @@ impl Session {
|
||
.map(|auth| auth.mode);
|
||
|
||
let default_model_slug = if auth_mode.is_some_and(code_app_server_protocol::AuthMode::is_chatgpt) {
|
||
- crate::config::GPT_5_CODEX_MEDIUM_MODEL
|
||
+ crate::config::GPT_5_DEV_MEDIUM_MODEL
|
||
} else {
|
||
crate::config::OPENAI_DEFAULT_MODEL
|
||
};
|
||
diff --git a/core/src/codex/streaming.rs b/core/src/codex/streaming.rs
|
||
index 22e03ae..c0dd11e 100644
|
||
--- a/core/src/codex/streaming.rs
|
||
+++ b/core/src/codex/streaming.rs
|
||
@@ -53,7 +53,7 @@ const SEARCH_TOOL_DEVELOPER_INSTRUCTIONS: &str =
|
||
include_str!("../../templates/search_tool/developer_instructions.md");
|
||
const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
|
||
const LEGACY_SEARCH_TOOL_BM25_TOOL_NAME: &str = "search_tool_bm25";
|
||
-const CODEX_APPS_TOOL_PREFIX: &str = "mcp__codex_apps__";
|
||
+const DEV_APPS_TOOL_PREFIX: &str = "mcp__codex_apps__";
|
||
const GENERATED_IMAGE_ARTIFACTS_DIR: &str = "generated_images";
|
||
const AUTO_CONTEXT_JUDGE_MIN_TOKENS: u64 = 150_000;
|
||
const AUTO_CONTEXT_FORCE_COMPACT_MARGIN_TOKENS: u64 = 20_000;
|
||
@@ -3670,7 +3670,7 @@ fn select_mcp_tools_for_turn(
|
||
mcp_tools
|
||
.into_iter()
|
||
.filter(|(name, _tool)| {
|
||
- if !name.starts_with(CODEX_APPS_TOOL_PREFIX) {
|
||
+ if !name.starts_with(DEV_APPS_TOOL_PREFIX) {
|
||
return true;
|
||
}
|
||
selected.contains(name)
|
||
@@ -10850,6 +10850,9 @@ async fn handle_container_exec_with_params(
|
||
.await;
|
||
|
||
if let Ok(Some(unified_diff)) = turn_diff_tracker.get_unified_diff() {
|
||
+ // The diff says what moved; a picture says what it looks
|
||
+ // like. Same value the reviewer is about to read.
|
||
+ crate::shot::capture(sess, &unified_diff);
|
||
let diff_event = sess.make_event(
|
||
&sub_id,
|
||
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }),
|
||
@@ -13995,27 +13998,27 @@ mod command_guard_detection_tests {
|
||
let argv = vec![
|
||
"bash".to_string(),
|
||
"-lc".to_string(),
|
||
- "cat <<'EOF' > code-rs/git-tooling/Cargo.toml\n[package]\nname = \"demo\"\nEOF".to_string(),
|
||
+ "cat <<'EOF' > hanzo-dev/git-tooling/Cargo.toml\n[package]\nname = \"demo\"\nEOF".to_string(),
|
||
];
|
||
|
||
let suggestion = detect_cat_write(&argv).expect("should flag cat write");
|
||
assert_eq!(suggestion.label, "original_script");
|
||
assert!(suggestion
|
||
.original_value
|
||
- .contains("cat <<'EOF' > code-rs/git-tooling/Cargo.toml"));
|
||
+ .contains("cat <<'EOF' > hanzo-dev/git-tooling/Cargo.toml"));
|
||
}
|
||
|
||
#[test]
|
||
fn detects_raw_shell_script_cat_heredoc_write() {
|
||
let argv = vec![
|
||
- "cat <<'EOF' > code-rs/git-tooling/Cargo.toml\n[package]\nname = \"demo\"\nEOF".to_string(),
|
||
+ "cat <<'EOF' > hanzo-dev/git-tooling/Cargo.toml\n[package]\nname = \"demo\"\nEOF".to_string(),
|
||
];
|
||
|
||
let suggestion = detect_cat_write(&argv).expect("should flag cat write");
|
||
assert_eq!(suggestion.label, "original_script");
|
||
assert!(suggestion
|
||
.original_value
|
||
- .contains("cat <<'EOF' > code-rs/git-tooling/Cargo.toml"));
|
||
+ .contains("cat <<'EOF' > hanzo-dev/git-tooling/Cargo.toml"));
|
||
}
|
||
|
||
#[test]
|
||
@@ -14338,7 +14341,7 @@ pub(super) fn debug_history(label: &str, items: &[ResponseItem]) {
|
||
})
|
||
.collect();
|
||
let rendered = preview.join(" | ");
|
||
- if std::env::var_os("CODEX_COMPACT_TRACE").is_some() {
|
||
+ if std::env::var_os("DEV_COMPACT_TRACE").is_some() {
|
||
eprintln!("[compact_history] {} => [{}]", label, rendered);
|
||
}
|
||
info!(target = "code_core::compact_history", "{} => [{}]", label, rendered);
|
||
diff --git a/core/src/config/sources.rs b/core/src/config/sources.rs
|
||
index a76a926..6485be9 100644
|
||
--- a/core/src/config/sources.rs
|
||
+++ b/core/src/config/sources.rs
|
||
@@ -293,7 +293,7 @@ pub async fn persist_model_selection(
|
||
Ok(())
|
||
}
|
||
|
||
-/// Patch `CODEX_HOME/config.toml` project state.
|
||
+/// Patch `DEV_HOME/config.toml` project state.
|
||
/// Use with caution.
|
||
pub fn set_project_trusted(code_home: &Path, project_path: &Path) -> anyhow::Result<()> {
|
||
let config_path = code_home.join(CONFIG_TOML_FILE);
|
||
@@ -380,7 +380,7 @@ fn set_project_trusted_inner(doc: &mut DocumentMut, project_path: &Path) -> anyh
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the selected TUI theme into `CODEX_HOME/config.toml` at `[tui.theme].name`.
|
||
+/// Persist the selected TUI theme into `DEV_HOME/config.toml` at `[tui.theme].name`.
|
||
pub fn set_tui_theme_name(code_home: &Path, theme: ThemeName) -> anyhow::Result<()> {
|
||
let config_path = code_home.join(CONFIG_TOML_FILE);
|
||
|
||
@@ -486,7 +486,7 @@ pub fn set_cached_terminal_background(
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the selected spinner into `CODEX_HOME/config.toml` at `[tui.spinner].name`.
|
||
+/// Persist the selected spinner into `DEV_HOME/config.toml` at `[tui.spinner].name`.
|
||
pub fn set_tui_spinner_name(code_home: &Path, spinner_name: &str) -> anyhow::Result<()> {
|
||
let config_path = code_home.join(CONFIG_TOML_FILE);
|
||
|
||
@@ -622,7 +622,7 @@ pub fn set_custom_theme(
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the alternate screen preference into `CODEX_HOME/config.toml` at `[tui].alternate_screen`.
|
||
+/// Persist the alternate screen preference into `DEV_HOME/config.toml` at `[tui].alternate_screen`.
|
||
pub fn set_tui_alternate_screen(code_home: &Path, enabled: bool) -> anyhow::Result<()> {
|
||
let config_path = code_home.join(CONFIG_TOML_FILE);
|
||
|
||
@@ -650,7 +650,7 @@ pub fn set_tui_alternate_screen(code_home: &Path, enabled: bool) -> anyhow::Resu
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the TUI notifications preference into `CODEX_HOME/config.toml` at `[tui].notifications`.
|
||
+/// Persist the TUI notifications preference into `DEV_HOME/config.toml` at `[tui].notifications`.
|
||
pub fn set_tui_notifications(
|
||
code_home: &Path,
|
||
notifications: crate::config_types::Notifications,
|
||
@@ -686,7 +686,7 @@ pub fn set_tui_notifications(
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the review auto-resolve preference into `CODEX_HOME/config.toml` at `[tui].review_auto_resolve`.
|
||
+/// Persist the review auto-resolve preference into `DEV_HOME/config.toml` at `[tui].review_auto_resolve`.
|
||
pub fn set_tui_review_auto_resolve(code_home: &Path, enabled: bool) -> anyhow::Result<()> {
|
||
let config_path = code_home.join(CONFIG_TOML_FILE);
|
||
let read_path = resolve_code_path_for_read(code_home, Path::new(CONFIG_TOML_FILE));
|
||
@@ -707,7 +707,7 @@ pub fn set_tui_review_auto_resolve(code_home: &Path, enabled: bool) -> anyhow::R
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the auto review preference into `CODEX_HOME/config.toml` at `[tui].auto_review_enabled`.
|
||
+/// Persist the auto review preference into `DEV_HOME/config.toml` at `[tui].auto_review_enabled`.
|
||
pub fn set_tui_auto_review_enabled(code_home: &Path, enabled: bool) -> anyhow::Result<()> {
|
||
let config_path = code_home.join(CONFIG_TOML_FILE);
|
||
let read_path = resolve_code_path_for_read(code_home, Path::new(CONFIG_TOML_FILE));
|
||
@@ -728,7 +728,7 @@ pub fn set_tui_auto_review_enabled(code_home: &Path, enabled: bool) -> anyhow::R
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the review model + reasoning effort into `CODEX_HOME/config.toml`.
|
||
+/// Persist the review model + reasoning effort into `DEV_HOME/config.toml`.
|
||
pub fn set_review_model(
|
||
code_home: &Path,
|
||
model: &str,
|
||
@@ -798,7 +798,7 @@ pub fn set_review_resolve_model(
|
||
Ok(())
|
||
}
|
||
|
||
-/// Persist the planning model + reasoning effort into `CODEX_HOME/config.toml`.
|
||
+/// Persist the planning model + reasoning effort into `DEV_HOME/config.toml`.
|
||
pub fn set_planning_model(
|
||
code_home: &Path,
|
||
model: &str,
|
||
@@ -1284,7 +1284,7 @@ pub fn add_project_allowed_command(
|
||
Ok(())
|
||
}
|
||
|
||
-/// List MCP servers from `CODEX_HOME/config.toml`.
|
||
+/// List MCP servers from `DEV_HOME/config.toml`.
|
||
/// Returns `(enabled, disabled)` lists of `(name, McpServerConfig)`.
|
||
pub fn list_mcp_servers(code_home: &Path) -> anyhow::Result<(
|
||
Vec<(String, McpServerConfig)>,
|
||
@@ -1693,7 +1693,7 @@ fn env_path(var: &str) -> std::io::Result<Option<PathBuf>> {
|
||
|
||
fn env_overrides_present() -> bool {
|
||
matches!(std::env::var("CODE_HOME"), Ok(ref v) if !v.trim().is_empty())
|
||
- || matches!(std::env::var("CODEX_HOME"), Ok(ref v) if !v.trim().is_empty())
|
||
+ || matches!(std::env::var("DEV_HOME"), Ok(ref v) if !v.trim().is_empty())
|
||
}
|
||
|
||
fn default_code_home_dir() -> Option<PathBuf> {
|
||
@@ -1766,10 +1766,10 @@ pub fn resolve_code_path_for_read(code_home: &Path, relative: &Path) -> PathBuf
|
||
}
|
||
|
||
/// Returns the path to the Code/Codex configuration directory, which can be
|
||
-/// specified by the `CODE_HOME` or `CODEX_HOME` environment variables. If not set,
|
||
+/// specified by the `CODE_HOME` or `DEV_HOME` environment variables. If not set,
|
||
/// defaults to `~/.code` for the fork.
|
||
///
|
||
-/// - If `CODE_HOME` or `CODEX_HOME` is set, the value will be canonicalized and this
|
||
+/// - If `CODE_HOME` or `DEV_HOME` is set, the value will be canonicalized and this
|
||
/// function will Err if the path does not exist.
|
||
/// - If neither is set, this function does not verify that the directory exists.
|
||
pub fn find_code_home() -> std::io::Result<PathBuf> {
|
||
@@ -1777,7 +1777,7 @@ pub fn find_code_home() -> std::io::Result<PathBuf> {
|
||
return Ok(path);
|
||
}
|
||
|
||
- if let Some(path) = env_path("CODEX_HOME")? {
|
||
+ if let Some(path) = env_path("DEV_HOME")? {
|
||
return Ok(path);
|
||
}
|
||
|
||
diff --git a/core/src/config.rs b/core/src/config.rs
|
||
index 4645f9d..392e1f1 100644
|
||
--- a/core/src/config.rs
|
||
+++ b/core/src/config.rs
|
||
@@ -39,6 +39,7 @@ use crate::model_family::derive_default_model_family;
|
||
use crate::model_family::find_family_for_model;
|
||
use crate::model_provider_info::ModelProviderInfo;
|
||
use crate::model_provider_info::built_in_model_providers;
|
||
+use crate::model_provider_info::HANZO_PROVIDER_ID;
|
||
use crate::reasoning::clamp_reasoning_effort_for_model;
|
||
use crate::protocol::AskForApproval;
|
||
use crate::protocol::SandboxPolicy;
|
||
@@ -142,9 +143,19 @@ pub use crate::config_constraint::ConstraintResult;
|
||
pub(crate) use defaults::merge_with_default_agents;
|
||
pub(crate) use validation::upgrade_legacy_model_slugs;
|
||
|
||
+/// `dev`'s authenticated default model: Enso's adaptive route on the Hanzo Cloud.
|
||
+///
|
||
+/// It is a coding agent, so the default is Enso Auto's adaptive coding route.
|
||
+/// Anything else stays selectable — this is the one you get without asking.
|
||
+pub const HANZO_DEFAULT_MODEL: &str = "enso-auto";
|
||
+/// Reviews run on the same model that wrote the code.
|
||
+pub const HANZO_DEFAULT_REVIEW_MODEL: &str = "enso-auto";
|
||
+
|
||
+/// Defaults used only when the caller has explicitly selected the `openai`
|
||
+/// provider. They are not `dev`'s defaults.
|
||
pub(crate) const OPENAI_DEFAULT_MODEL: &str = "gpt-5.5";
|
||
const OPENAI_DEFAULT_REVIEW_MODEL: &str = "gpt-5.5";
|
||
-pub const GPT_5_CODEX_MEDIUM_MODEL: &str = "gpt-5.5";
|
||
+pub const GPT_5_DEV_MEDIUM_MODEL: &str = "gpt-5.5";
|
||
pub(crate) const DEFAULT_SUBAGENT_MAX_DEPTH: i32 = 1;
|
||
|
||
/// Maximum number of bytes of the documentation that will be embedded. Larger
|
||
@@ -282,6 +293,11 @@ pub struct Config {
|
||
/// Key into the model_providers map that specifies which provider to use.
|
||
pub model_provider_id: String,
|
||
|
||
+ /// Whether the provider was named by a flag, profile or config.toml, as
|
||
+ /// opposed to inferred from the environment. Onboarding reads this: an
|
||
+ /// inferred provider is not a decision, so sign-in still leads with Hanzo.
|
||
+ pub model_provider_explicit: bool,
|
||
+
|
||
/// Info needed to make an API request to the model.
|
||
pub model_provider: ModelProviderInfo,
|
||
|
||
@@ -408,7 +424,7 @@ pub struct Config {
|
||
pub project_doc_fallback_filenames: Vec<String>,
|
||
|
||
/// Directory containing all Codex state (defaults to `~/.code`; can be
|
||
- /// overridden by the `CODE_HOME` or `CODEX_HOME` environment variables).
|
||
+ /// overridden by the `CODE_HOME` or `DEV_HOME` environment variables).
|
||
pub code_home: PathBuf,
|
||
|
||
/// Settings that govern if and what will be written to `~/.code/history.jsonl`
|
||
@@ -1144,10 +1160,25 @@ impl Config {
|
||
model_providers.entry(key).or_insert(provider);
|
||
}
|
||
|
||
+ // LAST link on purpose: a flag, a profile or config.toml has already had
|
||
+ // its say, and only when all three decline does the environment answer.
|
||
+ // The key you already hold picks the provider (provider_from_env), so a
|
||
+ // person with ANTHROPIC_API_KEY and no config is not told to go get ours.
|
||
+ // A stored Hanzo session outranks a vendor key in the shell: signing in
|
||
+ // was an explicit act, an exported variable often is not.
|
||
+ let model_provider_explicit = model_provider.is_some()
|
||
+ || config_profile.model_provider.is_some()
|
||
+ || cfg.model_provider.is_some();
|
||
let model_provider_id = model_provider
|
||
.or(config_profile.model_provider)
|
||
.or(cfg.model_provider)
|
||
- .unwrap_or_else(|| "openai".to_string());
|
||
+ .unwrap_or_else(|| {
|
||
+ if Self::has_stored_auth(&code_home) {
|
||
+ HANZO_PROVIDER_ID.to_string()
|
||
+ } else {
|
||
+ crate::model_provider_info::provider_from_env()
|
||
+ }
|
||
+ });
|
||
let model_provider = model_providers
|
||
.get(&model_provider_id)
|
||
.ok_or_else(|| {
|
||
@@ -1314,8 +1345,25 @@ impl Config {
|
||
.or(cfg.api_key_fallback_on_all_accounts_limited)
|
||
.unwrap_or(false);
|
||
|
||
- let default_model_slug = if using_chatgpt_auth {
|
||
- GPT_5_CODEX_MEDIUM_MODEL
|
||
+ // The default model follows the selected provider: a model id is only
|
||
+ // meaningful to the endpoint that serves it. Asking the Hanzo Cloud for
|
||
+ // an OpenAI id is a 400, and vice versa.
|
||
+ let default_model_slug = if model_provider_id == HANZO_PROVIDER_ID {
|
||
+ // Without an account the free pool is what answers, and it is the
|
||
+ // only thing the free credential may ask for — a coding model would
|
||
+ // be refused, so the pair moves together.
|
||
+ //
|
||
+ // A stored session counts as an account even though it sets no
|
||
+ // bearer in the environment: the request builder prefers it over the
|
||
+ // free key, so choosing the free model on its behalf would ask the
|
||
+ // wrong model with the right credential.
|
||
+ if crate::free::anonymous() && !Self::has_stored_auth(&code_home) {
|
||
+ crate::free::MODEL
|
||
+ } else {
|
||
+ HANZO_DEFAULT_MODEL
|
||
+ }
|
||
+ } else if using_chatgpt_auth {
|
||
+ GPT_5_DEV_MEDIUM_MODEL
|
||
} else {
|
||
OPENAI_DEFAULT_MODEL
|
||
};
|
||
@@ -1429,10 +1477,18 @@ impl Config {
|
||
}
|
||
|
||
// Default review model when not set in config; allow CLI override to take precedence.
|
||
+ // Like `model`, the review model follows the provider — a review model
|
||
+ // id is only meaningful to the endpoint that serves it.
|
||
let review_model = override_review_model
|
||
.or(config_profile.review_model.clone())
|
||
.or(cfg.review_model)
|
||
- .unwrap_or_else(default_review_model);
|
||
+ .unwrap_or_else(|| {
|
||
+ if model_provider_id == HANZO_PROVIDER_ID {
|
||
+ HANZO_DEFAULT_REVIEW_MODEL.to_string()
|
||
+ } else {
|
||
+ default_review_model()
|
||
+ }
|
||
+ });
|
||
|
||
let review_model_reasoning_effort = config_profile
|
||
.review_model_reasoning_effort
|
||
@@ -1575,8 +1631,11 @@ impl Config {
|
||
.or_else(|| cfg.tui.as_ref().and_then(|t| t.auto_drive.clone()))
|
||
.unwrap_or_else(|| {
|
||
let mut defaults = AutoDriveSettings::default();
|
||
- if using_chatgpt_auth {
|
||
- defaults.model = GPT_5_CODEX_MEDIUM_MODEL.to_string();
|
||
+ // Same rule as the default model above: the provider decides, not
|
||
+ // the auth mode. A Hanzo device login persists as `ChatGPT`, so
|
||
+ // reading auth alone here handed the Hanzo Cloud an OpenAI id.
|
||
+ if model_provider_id != HANZO_PROVIDER_ID && using_chatgpt_auth {
|
||
+ defaults.model = GPT_5_DEV_MEDIUM_MODEL.to_string();
|
||
defaults.model_reasoning_effort = ReasoningEffort::XHigh;
|
||
}
|
||
defaults
|
||
@@ -1642,6 +1701,7 @@ impl Config {
|
||
model_auto_compact_token_limit,
|
||
context_mode,
|
||
model_provider_id,
|
||
+ model_provider_explicit,
|
||
model_provider,
|
||
cwd: resolved_cwd,
|
||
approval_policy: effective_approval,
|
||
@@ -1775,6 +1835,19 @@ impl Config {
|
||
}
|
||
|
||
/// Check if we're using ChatGPT authentication
|
||
+ /// Whether a signed-in session is stored on disk, in any mode. The request
|
||
+ /// builder reaches for it before the free key, so the model default has to
|
||
+ /// see it too.
|
||
+ fn has_stored_auth(code_home: &Path) -> bool {
|
||
+ use code_app_server_protocol::AuthMode;
|
||
+ use crate::CodexAuth;
|
||
+
|
||
+ matches!(
|
||
+ CodexAuth::from_code_home(code_home, AuthMode::ApiKey, "code_cli_rs"),
|
||
+ Ok(Some(_))
|
||
+ )
|
||
+ }
|
||
+
|
||
fn is_using_chatgpt_auth(code_home: &Path) -> bool {
|
||
use code_app_server_protocol::AuthMode;
|
||
use crate::CodexAuth;
|
||
@@ -2074,6 +2147,31 @@ theme = "catppuccin-mocha"
|
||
assert_eq!(parsed_bool.auto_upgrade_enabled, Some(true));
|
||
}
|
||
|
||
+ /// An exported vendor key picks the provider for a run, but it is not a
|
||
+ /// DECISION — onboarding reads this flag to keep sign-in Hanzo-first.
|
||
+ #[test]
|
||
+ fn a_provider_is_explicit_only_when_someone_named_it() -> std::io::Result<()> {
|
||
+ let code_home = TempDir::new()?;
|
||
+
|
||
+ let inferred = Config::load_from_base_config_with_overrides(
|
||
+ ConfigToml::default(),
|
||
+ ConfigOverrides::default(),
|
||
+ code_home.path().to_path_buf(),
|
||
+ )?;
|
||
+ assert!(!inferred.model_provider_explicit);
|
||
+
|
||
+ let named = Config::load_from_base_config_with_overrides(
|
||
+ ConfigToml {
|
||
+ model_provider: Some("openai".to_string()),
|
||
+ ..Default::default()
|
||
+ },
|
||
+ ConfigOverrides::default(),
|
||
+ code_home.path().to_path_buf(),
|
||
+ )?;
|
||
+ assert!(named.model_provider_explicit);
|
||
+ Ok(())
|
||
+ }
|
||
+
|
||
#[test]
|
||
fn tool_output_max_bytes_defaults_and_overrides() -> std::io::Result<()> {
|
||
let code_home = TempDir::new()?;
|
||
@@ -2144,12 +2242,12 @@ theme = "catppuccin-mocha"
|
||
|
||
let _home_guard = EnvVarGuard::new("HOME");
|
||
let _code_home_guard = EnvVarGuard::new("CODE_HOME");
|
||
- let _codex_home_guard = EnvVarGuard::new("CODEX_HOME");
|
||
+ let _codex_home_guard = EnvVarGuard::new("DEV_HOME");
|
||
|
||
unsafe {
|
||
std::env::set_var("HOME", legacy_home.path());
|
||
std::env::remove_var("CODE_HOME");
|
||
- std::env::remove_var("CODEX_HOME");
|
||
+ std::env::remove_var("DEV_HOME");
|
||
}
|
||
|
||
let loaded = Config::load_instructions(Some(code_home.path()));
|
||
@@ -2533,6 +2631,7 @@ model_verbosity = "high"
|
||
name: "OpenAI using Chat Completions".to_string(),
|
||
base_url: Some("https://api.openai.com/v1".to_string()),
|
||
env_key: Some("OPENAI_API_KEY".to_string()),
|
||
+ alt_env_keys: Vec::new(),
|
||
wire_api: crate::WireApi::Chat,
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
@@ -2949,15 +3048,15 @@ model_verbosity = "high"
|
||
assert_eq!(config.auto_drive.model_routing_entries.len(), 2);
|
||
assert_eq!(
|
||
config.auto_drive.model_routing_entries[0].model,
|
||
- "gpt-5.5"
|
||
+ "zen5-pro"
|
||
);
|
||
assert_eq!(
|
||
config.auto_drive.model_routing_entries[0].reasoning_levels,
|
||
- vec![ReasoningEffort::High, ReasoningEffort::XHigh]
|
||
+ vec![ReasoningEffort::High]
|
||
);
|
||
assert_eq!(
|
||
config.auto_drive.model_routing_entries[1].model,
|
||
- "gpt-5.4-mini"
|
||
+ "zen5-flash"
|
||
);
|
||
assert_eq!(
|
||
config.auto_drive.model_routing_entries[1].reasoning_levels,
|
||
diff --git a/core/src/config_types.rs b/core/src/config_types.rs
|
||
index ce88970..4830cd3 100644
|
||
--- a/core/src/config_types.rs
|
||
+++ b/core/src/config_types.rs
|
||
@@ -867,7 +867,7 @@ pub struct Tui {
|
||
// Important: Provide a manual Default so that when no config file exists and we
|
||
// construct `Config` via `unwrap_or_default()`, we still honor the intended
|
||
// default of `alternate_screen = true`. Deriving `Default` would set booleans to
|
||
-// `false`, which caused fresh installs (or a temporary CODEX_HOME) to start in
|
||
+// `false`, which caused fresh installs (or a temporary DEV_HOME) to start in
|
||
// standard-terminal mode until the user pressed Ctrl+T.
|
||
impl Default for Tui {
|
||
fn default() -> Self {
|
||
@@ -956,13 +956,13 @@ fn default_auto_drive_model_routing_reasoning_levels() -> Vec<ReasoningEffort> {
|
||
pub fn default_auto_drive_model_routing_entries() -> Vec<AutoDriveModelRoutingEntry> {
|
||
vec![
|
||
AutoDriveModelRoutingEntry {
|
||
- model: "gpt-5.5".to_string(),
|
||
+ model: "zen5-pro".to_string(),
|
||
enabled: true,
|
||
- reasoning_levels: vec![ReasoningEffort::High, ReasoningEffort::XHigh],
|
||
+ reasoning_levels: vec![ReasoningEffort::High],
|
||
description: "Hard planning and complex problem solving".to_string(),
|
||
},
|
||
AutoDriveModelRoutingEntry {
|
||
- model: "gpt-5.4-mini".to_string(),
|
||
+ model: "zen5-flash".to_string(),
|
||
enabled: true,
|
||
reasoning_levels: vec![ReasoningEffort::High],
|
||
description: "Fast implementation loops and failing-test iteration".to_string(),
|
||
@@ -1048,8 +1048,9 @@ const fn default_auto_drive_coordinator_turn_cap() -> u32 {
|
||
}
|
||
|
||
fn default_auto_drive_model() -> String {
|
||
- // Keep aligned with the coordinator's preferred model fallback.
|
||
- String::from("gpt-5.1")
|
||
+ // A model id only means something to the endpoint that serves it, and the
|
||
+ // endpoint here is the Hanzo Cloud.
|
||
+ String::from(crate::config::HANZO_DEFAULT_MODEL)
|
||
}
|
||
|
||
const fn default_auto_drive_reasoning_effort() -> ReasoningEffort {
|
||
diff --git a/core/src/custom_prompts.rs b/core/src/custom_prompts.rs
|
||
index 26e5921..1d4702c 100644
|
||
--- a/core/src/custom_prompts.rs
|
||
+++ b/core/src/custom_prompts.rs
|
||
@@ -4,8 +4,8 @@ use std::path::Path;
|
||
use std::path::PathBuf;
|
||
use tokio::fs;
|
||
|
||
-/// Return the default prompts directory: `$CODEX_HOME/prompts`.
|
||
-/// If `CODEX_HOME` cannot be resolved, returns `None`.
|
||
+/// Return the default prompts directory: `$DEV_HOME/prompts`.
|
||
+/// If `DEV_HOME` cannot be resolved, returns `None`.
|
||
pub fn default_prompts_dir() -> Option<PathBuf> {
|
||
crate::config::find_code_home()
|
||
.ok()
|
||
diff --git a/core/src/default_client.rs b/core/src/default_client.rs
|
||
index 312f9c1..ff963d0 100644
|
||
--- a/core/src/default_client.rs
|
||
+++ b/core/src/default_client.rs
|
||
@@ -1,4 +1,4 @@
|
||
-use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||
+use crate::spawn::DEV_SANDBOX_ENV_VAR;
|
||
use reqwest::header::HeaderMap;
|
||
use reqwest::header::HeaderValue;
|
||
use std::sync::LazyLock;
|
||
@@ -6,7 +6,7 @@ use std::sync::Mutex;
|
||
use std::sync::RwLock;
|
||
|
||
pub const DEFAULT_ORIGINATOR: &str = "code_cli_rs";
|
||
-pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
||
+pub const DEV_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "DEV_INTERNAL_ORIGINATOR_OVERRIDE";
|
||
|
||
/// Optional suffix for the Codex User-Agent string.
|
||
///
|
||
@@ -31,7 +31,7 @@ pub enum SetOriginatorError {
|
||
}
|
||
|
||
fn get_originator_value(provided: Option<String>) -> Originator {
|
||
- let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
|
||
+ let value = std::env::var(DEV_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
|
||
.ok()
|
||
.or(provided)
|
||
.unwrap_or(DEFAULT_ORIGINATOR.to_string());
|
||
@@ -73,7 +73,7 @@ pub fn originator() -> Originator {
|
||
return originator.clone();
|
||
}
|
||
|
||
- if std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).is_ok() {
|
||
+ if std::env::var(DEV_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).is_ok() {
|
||
let originator = get_originator_value(None);
|
||
if let Ok(mut guard) = ORIGINATOR.write() {
|
||
match guard.as_ref() {
|
||
@@ -210,7 +210,7 @@ pub fn create_client(originator: &str) -> reqwest::Client {
|
||
}
|
||
|
||
fn is_sandboxed() -> bool {
|
||
- std::env::var(CODEX_SANDBOX_ENV_VAR).as_deref() == Ok("seatbelt")
|
||
+ std::env::var(DEV_SANDBOX_ENV_VAR).as_deref() == Ok("seatbelt")
|
||
}
|
||
|
||
#[cfg(test)]
|
||
@@ -224,19 +224,19 @@ mod tests {
|
||
|
||
fn set_originator_env(value: &str) {
|
||
unsafe {
|
||
- std::env::set_var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR, value);
|
||
+ std::env::set_var(DEV_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR, value);
|
||
}
|
||
}
|
||
|
||
fn remove_originator_env() {
|
||
unsafe {
|
||
- std::env::remove_var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);
|
||
+ std::env::remove_var(DEV_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);
|
||
}
|
||
}
|
||
|
||
fn with_originator_env_cleared<F: FnOnce()>(f: F) {
|
||
let _lock = ORIGINATOR_ENV_LOCK.lock().expect("originator env lock");
|
||
- let previous = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).ok();
|
||
+ let previous = std::env::var(DEV_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).ok();
|
||
remove_originator_env();
|
||
f();
|
||
match previous {
|
||
@@ -255,7 +255,7 @@ mod tests {
|
||
Fut: Future<Output = ()>,
|
||
{
|
||
let _lock = ORIGINATOR_ENV_LOCK.lock().expect("originator env lock");
|
||
- let previous = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).ok();
|
||
+ let previous = std::env::var(DEV_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).ok();
|
||
remove_originator_env();
|
||
f().await;
|
||
match previous {
|
||
diff --git a/core/src/error.rs b/core/src/error.rs
|
||
index 81534c1..c6ed86d 100644
|
||
--- a/core/src/error.rs
|
||
+++ b/core/src/error.rs
|
||
@@ -133,7 +133,7 @@ pub enum CodexErr {
|
||
AuthRefreshPermanent(String),
|
||
|
||
#[error(
|
||
- "To use Codex with your ChatGPT plan, upgrade to Plus: https://openai.com/chatgpt/pricing."
|
||
+ "To use Hanzo Dev with your ChatGPT plan, upgrade to Plus: https://openai.com/chatgpt/pricing."
|
||
)]
|
||
UsageNotIncluded,
|
||
|
||
diff --git a/core/src/flags.rs b/core/src/flags.rs
|
||
index 9fac914..893136b 100644
|
||
--- a/core/src/flags.rs
|
||
+++ b/core/src/flags.rs
|
||
@@ -2,7 +2,7 @@ use env_flags::env_flags;
|
||
|
||
env_flags! {
|
||
/// Fixture path for offline tests (see client.rs).
|
||
- pub CODEX_RS_SSE_FIXTURE: Option<&str> = None;
|
||
+ pub DEV_RS_SSE_FIXTURE: Option<&str> = None;
|
||
|
||
/// Enable context timeline delta tracking (Phase 2).
|
||
pub CTX_DELTAS: bool = false;
|
||
diff --git a/core/src/free.rs b/core/src/free.rs
|
||
new file mode 100644
|
||
index 0000000..a0bac9b
|
||
--- /dev/null
|
||
+++ b/core/src/free.rs
|
||
@@ -0,0 +1,103 @@
|
||
+//! Working without an account.
|
||
+//!
|
||
+//! The Hanzo Cloud answers `dev` with no login, on the free pool. A key built
|
||
+//! into this binary asks for the `free` model; the gateway holds that key in an
|
||
+//! allowlist, caps what a single request may spend, and bills nothing for it.
|
||
+//! That is what makes a credential safe to ship inside a binary — it cannot
|
||
+//! spend an account, because it has none.
|
||
+//!
|
||
+//! Free costs nothing and in exchange it is data-shared, which [`NOTICE`] says
|
||
+//! at the top of every run that uses it. Signing in swaps both halves at once:
|
||
+//! the user's own key, and the coding model it pays for.
|
||
+
|
||
+/// The credential the gateway answers to without an account. Public by design,
|
||
+/// so it is declared here rather than fetched — a value every copy of this
|
||
+/// binary already carries gains nothing from a lookup. `HANZO_FREE_KEY`
|
||
+/// overrides it against a private deployment.
|
||
+const FREE_KEY: &str = "hz_7GrX4ZKTMlExtziPF4EZtPA9QfdsxFBkr1AXEB3COo8XVQ9R";
|
||
+
|
||
+/// The gateway's anonymous Enso pool. This id is intentionally distinct from
|
||
+/// the authenticated `enso-auto` route so an unaccounted-for process can never
|
||
+/// spend a user's account.
|
||
+pub const MODEL: &str = "enso-free";
|
||
+
|
||
+/// The bearer a signed-in user brings. `hanzo code` sets it on the child, and
|
||
+/// `hanzo auth login` is how a human fills it in.
|
||
+pub const USER_KEY: &str = "HANZO_USER_KEY";
|
||
+
|
||
+fn env(name: &str) -> Option<String> {
|
||
+ std::env::var(name)
|
||
+ .ok()
|
||
+ .filter(|v| !v.trim().is_empty())
|
||
+ .map(|v| v.trim().to_string())
|
||
+}
|
||
+
|
||
+/// Whether an account is behind this run.
|
||
+pub fn signed_in() -> bool {
|
||
+ env(USER_KEY).is_some()
|
||
+}
|
||
+
|
||
+/// The free credential, when there is one to use.
|
||
+pub fn key() -> Option<String> {
|
||
+ env("HANZO_FREE_KEY").or_else(|| (!FREE_KEY.is_empty()).then(|| FREE_KEY.to_string()))
|
||
+}
|
||
+
|
||
+/// The account always wins. Free answers only when there is no account to bill
|
||
+/// and a credential to ask with — no credential means the run asks for nothing,
|
||
+/// which is how a build with no free key behaves exactly as it did before.
|
||
+fn free_answers(user: Option<&str>, free: Option<&str>) -> bool {
|
||
+ user.is_none() && free.is_some()
|
||
+}
|
||
+
|
||
+/// Whether this run will be answered free.
|
||
+pub fn anonymous() -> bool {
|
||
+ free_answers(env(USER_KEY).as_deref(), key().as_deref())
|
||
+}
|
||
+
|
||
+/// Whether this configured run is the free pool's: it asks the Hanzo Cloud for
|
||
+/// the pool's own model. Another provider's run is never free, whatever the
|
||
+/// environment holds.
|
||
+pub fn runs(config: &crate::config::Config) -> bool {
|
||
+ config.model_provider_id == crate::model_provider_info::HANZO_PROVIDER_ID
|
||
+ && config.model == MODEL
|
||
+}
|
||
+
|
||
+/// Said once at the top of a free run — what it costs, what it shares, and how
|
||
+/// to leave it. The same bargain hanzo.ai and hanzo.chat state in their own
|
||
+/// words.
|
||
+pub const NOTICE: &str = "Running free, without an account. Free is data-shared: what you send and what comes back may be used to improve models — ours, and the provider that serves them. Paid stays private. Sign in with `hanzo auth login` for the coding models and private requests. Terms: https://hanzo.ai/terms";
|
||
+
|
||
+#[cfg(test)]
|
||
+mod tests {
|
||
+ use super::*;
|
||
+
|
||
+ #[test]
|
||
+ fn an_account_outranks_the_free_credential() {
|
||
+ assert!(!free_answers(Some("hk-real"), Some("hz_free")));
|
||
+ assert!(!free_answers(Some("hk-real"), None));
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn free_answers_only_when_it_can() {
|
||
+ assert!(free_answers(None, Some("hz_free")));
|
||
+ assert!(!free_answers(None, None));
|
||
+ }
|
||
+
|
||
+ /// The pool's own id outlives the routes under it. A vendor `:free` id names
|
||
+ /// one route, and naming it here would send this binary to a model that can
|
||
+ /// be retired without warning.
|
||
+ #[test]
|
||
+ fn the_free_id_is_the_pool_not_a_route() {
|
||
+ assert_eq!(MODEL, "enso-free");
|
||
+ assert!(!MODEL.contains(':') && !MODEL.contains('/'));
|
||
+ }
|
||
+
|
||
+ /// A free run is data-shared, so the notice has to say so and say where the
|
||
+ /// terms are.
|
||
+ #[test]
|
||
+ fn the_notice_states_the_bargain() {
|
||
+ assert!(NOTICE.contains("data-shared"));
|
||
+ assert!(NOTICE.contains("https://hanzo.ai/terms"));
|
||
+ assert!(NOTICE.contains("hanzo auth login"));
|
||
+ }
|
||
+}
|
||
diff --git a/core/src/git_worktree.rs b/core/src/git_worktree.rs
|
||
index fccff08..0948590 100644
|
||
--- a/core/src/git_worktree.rs
|
||
+++ b/core/src/git_worktree.rs
|
||
@@ -740,8 +740,8 @@ pub async fn copy_uncommitted_to_worktree(src_root: &Path, worktree_path: &Path)
|
||
}
|
||
|
||
// Opt-in: mirror modified submodule pointers into the worktree index (no checkout/network).
|
||
- // Enable via CODEX_BRANCH_INCLUDE_SUBMODULES=1|true|yes.
|
||
- let include_submods = std::env::var("CODEX_BRANCH_INCLUDE_SUBMODULES")
|
||
+ // Enable via DEV_BRANCH_INCLUDE_SUBMODULES=1|true|yes.
|
||
+ let include_submods = std::env::var("DEV_BRANCH_INCLUDE_SUBMODULES")
|
||
.ok()
|
||
.map(|v| v.to_ascii_lowercase())
|
||
.map(|v| v == "1" || v == "true" || v == "yes")
|
||
diff --git a/core/src/http_client.rs b/core/src/http_client.rs
|
||
index 507bf0f..6cff153 100644
|
||
--- a/core/src/http_client.rs
|
||
+++ b/core/src/http_client.rs
|
||
@@ -7,7 +7,7 @@ use reqwest::cookie::CookieStore;
|
||
use reqwest::cookie::Jar;
|
||
use reqwest::header::HeaderValue;
|
||
|
||
-pub const CODEX_CA_CERTIFICATE_ENV_VAR: &str = "CODEX_CA_CERTIFICATE";
|
||
+pub const DEV_CA_CERTIFICATE_ENV_VAR: &str = "DEV_CA_CERTIFICATE";
|
||
|
||
static SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE: LazyLock<Arc<ChatGptCloudflareCookieStore>> =
|
||
LazyLock::new(|| Arc::new(ChatGptCloudflareCookieStore::default()));
|
||
@@ -130,7 +130,7 @@ pub fn apply_extra_root_certificates(
|
||
.ok()
|
||
}
|
||
|
||
- let codex_ca_certificate = std::env::var(CODEX_CA_CERTIFICATE_ENV_VAR)
|
||
+ let codex_ca_certificate = std::env::var(DEV_CA_CERTIFICATE_ENV_VAR)
|
||
.ok()
|
||
.filter(|value| !value.trim().is_empty());
|
||
let ssl_cert_file = std::env::var("SSL_CERT_FILE")
|
||
@@ -184,7 +184,7 @@ pub fn apply_extra_root_certificates(
|
||
}
|
||
|
||
/// Build a reqwest Client with optional extra root certificates loaded from
|
||
-/// common environment variables. `CODEX_CA_CERTIFICATE` takes precedence over
|
||
+/// common environment variables. `DEV_CA_CERTIFICATE` takes precedence over
|
||
/// `SSL_CERT_FILE`, and other ecosystem-standard CA bundle variables continue
|
||
/// to work as fallbacks.
|
||
pub fn build_http_client() -> reqwest::Client {
|
||
diff --git a/core/src/lib.rs b/core/src/lib.rs
|
||
index ad9cfc3..e18a6ba 100644
|
||
--- a/core/src/lib.rs
|
||
+++ b/core/src/lib.rs
|
||
@@ -8,6 +8,7 @@
|
||
|
||
mod apply_patch;
|
||
mod fs_sanitize;
|
||
+mod shot;
|
||
pub mod auth;
|
||
pub mod auth_accounts;
|
||
pub mod account_usage;
|
||
@@ -55,6 +56,7 @@ mod exec_command;
|
||
pub mod exec_env;
|
||
pub mod external_agent_config;
|
||
mod flags;
|
||
+pub mod free;
|
||
pub mod git_info;
|
||
pub mod landlock;
|
||
pub mod http_client;
|
||
@@ -81,6 +83,8 @@ mod unified_exec;
|
||
mod user_instructions;
|
||
mod skills;
|
||
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||
+pub use model_provider_info::HANZO_PROVIDER_ID;
|
||
+pub use model_provider_info::create_hanzo_provider;
|
||
pub use model_provider_info::ModelProviderInfo;
|
||
pub use model_provider_info::OpenRouterConfig;
|
||
pub use model_provider_info::OpenRouterProviderConfig;
|
||
@@ -138,7 +142,7 @@ mod function_tool;
|
||
mod user_notification;
|
||
pub mod util;
|
||
|
||
-pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
|
||
+pub use apply_patch::DEV_APPLY_PATCH_ARG1;
|
||
pub use command_safety::is_safe_command;
|
||
pub use agent_tool::external_agent_command_exists;
|
||
pub use agent_tool::smoke_test_agent_blocking;
|
||
diff --git a/core/src/mcp_connection_manager.rs b/core/src/mcp_connection_manager.rs
|
||
index c2006b3..0ff914a 100644
|
||
--- a/core/src/mcp_connection_manager.rs
|
||
+++ b/core/src/mcp_connection_manager.rs
|
||
@@ -311,7 +311,7 @@ impl McpConnectionManager {
|
||
client_info: Implementation {
|
||
name: "codex-mcp-client".to_owned(),
|
||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||
- title: Some("Codex".into()),
|
||
+ title: Some("Hanzo Dev".into()),
|
||
// This field is used by Codex when it is an MCP
|
||
// server: it should not be used when Codex is
|
||
// an MCP client.
|
||
diff --git a/core/src/model_family.rs b/core/src/model_family.rs
|
||
index 9802ab0..c34ba13 100644
|
||
--- a/core/src/model_family.rs
|
||
+++ b/core/src/model_family.rs
|
||
@@ -17,12 +17,12 @@ use once_cell::sync::Lazy;
|
||
const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md");
|
||
const BASE_INSTRUCTIONS_WITH_APPLY_PATCH: &str =
|
||
include_str!("../prompt_with_apply_patch_instructions.md");
|
||
-const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../gpt_5_codex_prompt.md");
|
||
+const GPT_5_DEV_INSTRUCTIONS: &str = include_str!("../gpt_5_codex_prompt.md");
|
||
const GPT_5_1_INSTRUCTIONS: &str = include_str!("../gpt_5_1_prompt.md");
|
||
const GPT_5_2_INSTRUCTIONS: &str = include_str!("../gpt_5_2_prompt.md");
|
||
-const GPT_5_1_CODEX_MAX_INSTRUCTIONS: &str = include_str!("../gpt-5.1-codex-max_prompt.md");
|
||
-const GPT_5_2_CODEX_INSTRUCTIONS: &str = include_str!("../gpt-5.2-codex_prompt.md");
|
||
-const DEFAULT_PERSONALITY_HEADER: &str = "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals.";
|
||
+const GPT_5_1_DEV_MAX_INSTRUCTIONS: &str = include_str!("../gpt-5.1-codex-max_prompt.md");
|
||
+const GPT_5_2_DEV_INSTRUCTIONS: &str = include_str!("../gpt-5.2-codex_prompt.md");
|
||
+const DEFAULT_PERSONALITY_HEADER: &str = "You are Hanzo Dev, a coding agent. You and the user share the same workspace and collaborate to achieve the user's goals.";
|
||
const LOCAL_FRIENDLY_TEMPLATE: &str =
|
||
"You optimize for team morale and being a supportive teammate as much as code quality.";
|
||
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
|
||
@@ -39,7 +39,7 @@ const MAX_OUTPUT_QWEN3_CODER_30B: u64 = 65_536;
|
||
|
||
static UPSTREAM_MODELS: Lazy<Vec<ModelInfo>> = Lazy::new(|| {
|
||
parse_upstream_models(include_str!(
|
||
- "../../../codex-rs/models-manager/models.json"
|
||
+ "../../../vendor/codex/models-manager/models.json"
|
||
))
|
||
.unwrap_or_else(|err| panic!("failed to parse upstream models.json: {err}"))
|
||
});
|
||
@@ -369,7 +369,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
|
||
model_family!(
|
||
slug, slug,
|
||
supports_reasoning_summaries: true,
|
||
- base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(),
|
||
+ base_instructions: GPT_5_DEV_INSTRUCTIONS.to_string(),
|
||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||
supports_parallel_tool_calls: true,
|
||
default_reasoning_effort: Some(ReasoningEffort::Medium),
|
||
@@ -380,7 +380,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
|
||
model_family!(
|
||
slug, slug,
|
||
supports_reasoning_summaries: true,
|
||
- base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
|
||
+ base_instructions: GPT_5_2_DEV_INSTRUCTIONS.to_string(),
|
||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||
supports_parallel_tool_calls: true,
|
||
truncation_policy: TruncationPolicy::Tokens(10_000),
|
||
@@ -398,7 +398,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
|
||
model_family!(
|
||
slug, slug,
|
||
supports_reasoning_summaries: true,
|
||
- base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(),
|
||
+ base_instructions: GPT_5_1_DEV_MAX_INSTRUCTIONS.to_string(),
|
||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||
context_window: Some(CONTEXT_WINDOW_272K),
|
||
max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
|
||
@@ -411,7 +411,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
|
||
model_family!(
|
||
slug, slug,
|
||
supports_reasoning_summaries: true,
|
||
- base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(),
|
||
+ base_instructions: GPT_5_DEV_INSTRUCTIONS.to_string(),
|
||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||
context_window: Some(CONTEXT_WINDOW_272K),
|
||
max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
|
||
@@ -422,7 +422,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
|
||
model_family!(
|
||
slug, slug,
|
||
supports_reasoning_summaries: true,
|
||
- base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
|
||
+ base_instructions: GPT_5_2_DEV_INSTRUCTIONS.to_string(),
|
||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||
supports_parallel_tool_calls: true,
|
||
context_window: Some(CONTEXT_WINDOW_272K),
|
||
@@ -434,7 +434,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
|
||
model_family!(
|
||
slug, slug,
|
||
supports_reasoning_summaries: true,
|
||
- base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
|
||
+ base_instructions: GPT_5_2_DEV_INSTRUCTIONS.to_string(),
|
||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||
supports_parallel_tool_calls: true,
|
||
context_window: Some(CONTEXT_WINDOW_272K),
|
||
@@ -445,7 +445,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
|
||
model_family!(
|
||
slug, slug,
|
||
supports_reasoning_summaries: true,
|
||
- base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
|
||
+ base_instructions: GPT_5_2_DEV_INSTRUCTIONS.to_string(),
|
||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||
supports_parallel_tool_calls: true,
|
||
context_window: Some(CONTEXT_WINDOW_272K),
|
||
@@ -585,7 +585,7 @@ mod tests {
|
||
|
||
#[test]
|
||
fn upstream_models_parser_accepts_renamed_reasoning_summary_field() {
|
||
- let catalog = include_str!("../../../codex-rs/models-manager/models.json");
|
||
+ let catalog = include_str!("../../../vendor/codex/models-manager/models.json");
|
||
let models = parse_upstream_models(catalog).expect("bundled upstream models parse");
|
||
|
||
assert!(!models.is_empty());
|
||
diff --git a/core/src/model_provider_info.rs b/core/src/model_provider_info.rs
|
||
index 61ec87a..184e561 100644
|
||
--- a/core/src/model_provider_info.rs
|
||
+++ b/core/src/model_provider_info.rs
|
||
@@ -13,7 +13,6 @@ use serde::Deserialize;
|
||
use serde::Serialize;
|
||
use serde_json::Value;
|
||
use std::collections::{BTreeMap, HashMap};
|
||
-use std::env::VarError;
|
||
use std::io;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
@@ -93,6 +92,19 @@ pub struct ModelProviderInfo {
|
||
/// Environment variable that stores the user's API key for this provider.
|
||
pub env_key: Option<String>,
|
||
|
||
+ /// Variables tried BEFORE `env_key`, for the same provider reached by a
|
||
+ /// MACHINE rather than a person.
|
||
+ ///
|
||
+ /// A provider has one endpoint and two kinds of caller. `env_key` names the
|
||
+ /// human's — a session a login filled in. A service run (the coding harness
|
||
+ /// in a sandbox, a scheduled job, an agent acting for an org) never has one
|
||
+ /// and carries an org API key or an IAM machine identity's token instead.
|
||
+ ///
|
||
+ /// Empty for every provider that has only one kind of caller, so nothing
|
||
+ /// changes for them.
|
||
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
+ pub alt_env_keys: Vec<String>,
|
||
+
|
||
/// Optional instructions to help the user get a valid value for the
|
||
/// variable and set it.
|
||
pub env_key_instructions: Option<String>,
|
||
@@ -410,13 +422,14 @@ impl ModelProviderInfo {
|
||
match self.api_key() {
|
||
Ok(Some(key)) => Ok(Some(CodexAuth::from_api_key(&key))),
|
||
Ok(None) => Ok(auth.clone()),
|
||
- Err(err) => {
|
||
- if auth.is_some() {
|
||
- Ok(auth.clone())
|
||
- } else {
|
||
- Err(err)
|
||
- }
|
||
- }
|
||
+ Err(err) => match auth.clone() {
|
||
+ Some(auth) => Ok(Some(auth)),
|
||
+ // No account, and the Hanzo Cloud answers without one.
|
||
+ None => match self.free_key() {
|
||
+ Some(key) => Ok(Some(CodexAuth::from_api_key(&key))),
|
||
+ None => Err(err),
|
||
+ },
|
||
+ },
|
||
}
|
||
}
|
||
|
||
@@ -561,27 +574,49 @@ impl ModelProviderInfo {
|
||
/// If `env_key` is Some, returns the API key for this provider if present
|
||
/// (and non-empty) in the environment. If `env_key` is required but
|
||
/// cannot be found, returns an error.
|
||
+ ///
|
||
+ /// A PROVIDER MAY BE REACHED BY A PERSON OR BY A MACHINE, and those carry
|
||
+ /// different credentials. `HANZO_USER_KEY` is a signed-in human's session,
|
||
+ /// filled in by `hanzo auth login`. A run started by a service — the coding
|
||
+ /// harness inside a sandbox pod, a scheduled job, an agent acting for an org
|
||
+ /// — has no human session and holds an org API key or an IAM machine
|
||
+ /// identity's token instead.
|
||
+ ///
|
||
+ /// Reading only the user variable made the machine case impossible: dev in a
|
||
+ /// sandbox reported "Authentication expired. Run `code login` to continue"
|
||
+ /// for a caller that was perfectly authenticated, because the only bearer it
|
||
+ /// would look at was the one no service has. So the provider accepts
|
||
+ /// EITHER, and `alt_env_keys` is how a provider says which.
|
||
+ ///
|
||
+ /// Order is machine-first and deliberate: where both are present the process
|
||
+ /// was started BY something, and the credential it was given is the one that
|
||
+ /// should be billed and audited. A stale human key left in an environment
|
||
+ /// must not quietly outrank the identity the caller actually presented.
|
||
pub fn api_key(&self) -> crate::error::Result<Option<String>> {
|
||
- match &self.env_key {
|
||
- Some(env_key) => {
|
||
- let env_value = std::env::var(env_key);
|
||
- env_value
|
||
- .and_then(|v| {
|
||
- if v.trim().is_empty() {
|
||
- Err(VarError::NotPresent)
|
||
- } else {
|
||
- Ok(Some(v))
|
||
- }
|
||
- })
|
||
- .map_err(|_| {
|
||
- crate::error::CodexErr::EnvVar(EnvVarError {
|
||
- var: env_key.clone(),
|
||
- instructions: self.env_key_instructions.clone(),
|
||
- })
|
||
- })
|
||
+ let Some(env_key) = &self.env_key else {
|
||
+ return Ok(None);
|
||
+ };
|
||
+ for key in self.alt_env_keys.iter().chain(std::iter::once(env_key)) {
|
||
+ if let Ok(v) = std::env::var(key) {
|
||
+ if !v.trim().is_empty() {
|
||
+ return Ok(Some(v));
|
||
+ }
|
||
}
|
||
- None => Ok(None),
|
||
}
|
||
+ Err(crate::error::CodexErr::EnvVar(EnvVarError {
|
||
+ var: env_key.clone(),
|
||
+ instructions: self.env_key_instructions.clone(),
|
||
+ }))
|
||
+ }
|
||
+
|
||
+ /// The credential this provider answers to without an account, if it has
|
||
+ /// one. Only the Hanzo Cloud does; every other provider still asks for a
|
||
+ /// key, which is why this reads the provider's own bearer rather than a
|
||
+ /// name or a URL that a config file can rewrite.
|
||
+ fn free_key(&self) -> Option<String> {
|
||
+ (self.env_key.as_deref() == Some(crate::free::USER_KEY))
|
||
+ .then(crate::free::key)
|
||
+ .flatten()
|
||
}
|
||
|
||
/// Effective maximum number of request retries for this provider.
|
||
@@ -716,6 +751,76 @@ const DEFAULT_OLLAMA_PORT: u32 = 11434;
|
||
|
||
pub const BUILT_IN_OSS_MODEL_PROVIDER_ID: &str = "oss";
|
||
|
||
+/// The Hanzo Cloud — `dev`'s home and its default. Every other provider stays
|
||
+/// configurable; this is the one you get without asking.
|
||
+pub const HANZO_PROVIDER_ID: &str = "hanzo";
|
||
+pub const ANTHROPIC_PROVIDER_ID: &str = "anthropic";
|
||
+pub const OPENAI_PROVIDER_ID: &str = "openai";
|
||
+
|
||
+/// The key you already have decides the provider, when you have not said.
|
||
+///
|
||
+/// A coding agent should work with whatever credential is in the environment
|
||
+/// rather than insisting on ours, so this is consulted ONLY after an explicit
|
||
+/// choice — flag, profile, config.toml — has declined to answer. Hanzo first
|
||
+/// because it is the default and the one we serve; then a key that names its
|
||
+/// own vendor.
|
||
+///
|
||
+/// With no key at all the answer is still Hanzo: that is the provider whose
|
||
+/// `env_key_instructions` tell a person how to sign in, and an empty
|
||
+/// environment is exactly when someone needs to read them.
|
||
+pub fn provider_from_env() -> String {
|
||
+ fn set(k: &str) -> bool {
|
||
+ std::env::var(k).is_ok_and(|v| !v.trim().is_empty())
|
||
+ }
|
||
+ if set(HANZO_ENV_KEY) || HANZO_MACHINE_ENV_KEYS.iter().any(|k| set(k)) {
|
||
+ return HANZO_PROVIDER_ID.to_string();
|
||
+ }
|
||
+ if set(ANTHROPIC_ENV_KEY) {
|
||
+ return ANTHROPIC_PROVIDER_ID.to_string();
|
||
+ }
|
||
+ if set(OPENAI_ENV_KEY) {
|
||
+ return OPENAI_PROVIDER_ID.to_string();
|
||
+ }
|
||
+ HANZO_PROVIDER_ID.to_string()
|
||
+}
|
||
+
|
||
+
|
||
+/// Where the Hanzo Cloud answers. Override with `HANZO_BASE_URL` for a private
|
||
+/// deployment; the `hanzo` CLI does exactly that when its active network points
|
||
+/// somewhere other than the public gateway.
|
||
+const HANZO_BASE_URL: &str = "https://api.hanzo.ai/v1";
|
||
+
|
||
+/// The bearer `dev` reads. `hanzo code` sets it on the child process, and
|
||
+/// `hanzo auth login` is how a human fills it in. `free` owns the name, so the
|
||
+/// account half and the free half of the pair cannot drift apart.
|
||
+const HANZO_ENV_KEY: &str = crate::free::USER_KEY;
|
||
+const ANTHROPIC_ENV_KEY: &str = "ANTHROPIC_API_KEY";
|
||
+const OPENAI_ENV_KEY: &str = "OPENAI_API_KEY";
|
||
+
|
||
+/// The credentials a SERVICE presents, tried before the human session.
|
||
+///
|
||
+/// `HANZO_API_KEY` is an org key. `HANZO_MACHINE_TOKEN` is the bearer an IAM
|
||
+/// machine identity holds after the client_credentials exchange — the identity
|
||
+/// a sandbox run, a cron job or an agent acting for an org actually carries.
|
||
+/// Neither exists on a developer's laptop, and `HANZO_USER_KEY` never exists in
|
||
+/// a pod, which is why reading only one of them made the other case impossible.
|
||
+///
|
||
+/// THIS IS A RESTORATION, and the fleet proves it. `HANZO_ENV_KEY` was
|
||
+/// introduced by 8a4de1f45b (2026-08-01), which is NOT in v0.6.91 — and v0.6.91
|
||
+/// is what runs in the sandbox image today. Measured against that live pod, with
|
||
+/// the two variables set one at a time:
|
||
+///
|
||
+/// nothing set no request is attempted at all
|
||
+/// HANZO_API_KEY only the request goes out; IAM rejects the sentinel (400)
|
||
+/// HANZO_USER_KEY only no request is attempted at all
|
||
+///
|
||
+/// So the deployed build reaches our gateway on the MACHINE variable and ignores
|
||
+/// the human one, and main reversed that without anything noticing: every
|
||
+/// service caller in the fleet would have stopped authenticating at the next
|
||
+/// image rebuild, silently, because a missing credential and an unread one look
|
||
+/// identical from outside.
|
||
+const HANZO_MACHINE_ENV_KEYS: [&str; 2] = ["HANZO_API_KEY", "HANZO_MACHINE_TOKEN"];
|
||
+
|
||
/// Built-in default provider list.
|
||
fn wire_api_override_from_env(env_key: &str) -> Option<WireApi> {
|
||
match std::env::var(env_key) {
|
||
@@ -740,17 +845,18 @@ pub fn built_in_model_providers(
|
||
) -> HashMap<String, ModelProviderInfo> {
|
||
use ModelProviderInfo as P;
|
||
|
||
- // We do not want to be in the business of adjucating which third-party
|
||
- // providers are bundled with Codex CLI, so we only include the OpenAI and
|
||
- // open source ("oss") providers by default. Users are encouraged to add to
|
||
- // `model_providers` in config.toml to add their own providers.
|
||
+ // The Hanzo Cloud is the default. The rest are bundled because people ask
|
||
+ // for them; add any other via `model_providers` in config.toml.
|
||
[
|
||
+ (HANZO_PROVIDER_ID, create_hanzo_provider()),
|
||
+ (ANTHROPIC_PROVIDER_ID, create_anthropic_provider()),
|
||
(
|
||
"openai",
|
||
P {
|
||
name: "OpenAI".into(),
|
||
base_url: openai_base_url,
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -794,17 +900,86 @@ pub fn built_in_model_providers(
|
||
.collect()
|
||
}
|
||
|
||
+/// The Hanzo Cloud provider — `dev`'s default.
|
||
+///
|
||
+/// Speaks the Responses wire at `api.hanzo.ai/v1` and reads its bearer from
|
||
+/// `HANZO_USER_KEY`. `requires_openai_auth` is false: this account is ours, so
|
||
+/// the ChatGPT login dance never applies to it.
|
||
+pub fn create_hanzo_provider() -> ModelProviderInfo {
|
||
+ let base_url = std::env::var("HANZO_BASE_URL")
|
||
+ .ok()
|
||
+ .filter(|v| !v.trim().is_empty())
|
||
+ .unwrap_or_else(|| HANZO_BASE_URL.to_string());
|
||
+
|
||
+ ModelProviderInfo {
|
||
+ name: "Hanzo".into(),
|
||
+ base_url: Some(base_url),
|
||
+ env_key: Some(HANZO_ENV_KEY.into()),
|
||
+ alt_env_keys: HANZO_MACHINE_ENV_KEYS.iter().map(|s| (*s).to_string()).collect(),
|
||
+ env_key_instructions: Some(
|
||
+ "Sign in with `hanzo auth login`, or set HANZO_USER_KEY to a key from \
|
||
+ https://hanzo.ai/settings/keys. A service sets HANZO_API_KEY or \
|
||
+ HANZO_MACHINE_TOKEN instead."
|
||
+ .into(),
|
||
+ ),
|
||
+ experimental_bearer_token: None,
|
||
+ auth: None,
|
||
+ wire_api: WireApi::Responses,
|
||
+ query_params: None,
|
||
+ http_headers: Some(
|
||
+ [("version".to_string(), code_version::version().to_string())]
|
||
+ .into_iter()
|
||
+ .collect(),
|
||
+ ),
|
||
+ env_http_headers: None,
|
||
+ request_max_retries: None,
|
||
+ stream_max_retries: None,
|
||
+ stream_idle_timeout_ms: None,
|
||
+ websocket_connect_timeout_ms: None,
|
||
+ requires_openai_auth: false,
|
||
+ openrouter: None,
|
||
+ }
|
||
+}
|
||
+
|
||
+/// Anthropic, through its OpenAI-compatible surface — so it needs no new wire
|
||
+/// format, only a base URL and a key. `wire_api: Chat` is that surface; the
|
||
+/// native Messages API is a different shape and is deliberately not spoken here.
|
||
+pub fn create_anthropic_provider() -> ModelProviderInfo {
|
||
+ ModelProviderInfo {
|
||
+ name: "Anthropic".into(),
|
||
+ base_url: Some("https://api.anthropic.com/v1".into()),
|
||
+ env_key: Some(ANTHROPIC_ENV_KEY.into()),
|
||
+ alt_env_keys: Vec::new(),
|
||
+ env_key_instructions: Some(
|
||
+ "Set ANTHROPIC_API_KEY to a key from https://console.anthropic.com/settings/keys."
|
||
+ .into(),
|
||
+ ),
|
||
+ experimental_bearer_token: None,
|
||
+ auth: None,
|
||
+ wire_api: WireApi::Chat,
|
||
+ query_params: None,
|
||
+ http_headers: None,
|
||
+ env_http_headers: None,
|
||
+ request_max_retries: None,
|
||
+ stream_max_retries: None,
|
||
+ stream_idle_timeout_ms: None,
|
||
+ websocket_connect_timeout_ms: None,
|
||
+ requires_openai_auth: false,
|
||
+ openrouter: None,
|
||
+ }
|
||
+}
|
||
+
|
||
pub fn create_oss_provider() -> ModelProviderInfo {
|
||
- // These CODEX_OSS_ environment variables are experimental: we may
|
||
+ // These DEV_OSS_ environment variables are experimental: we may
|
||
// switch to reading values from config.toml instead.
|
||
- let code_oss_base_url = match std::env::var("CODEX_OSS_BASE_URL")
|
||
+ let code_oss_base_url = match std::env::var("DEV_OSS_BASE_URL")
|
||
.ok()
|
||
.filter(|v| !v.trim().is_empty())
|
||
{
|
||
Some(url) => url,
|
||
None => format!(
|
||
"http://localhost:{port}/v1",
|
||
- port = std::env::var("CODEX_OSS_PORT")
|
||
+ port = std::env::var("DEV_OSS_PORT")
|
||
.ok()
|
||
.filter(|v| !v.trim().is_empty())
|
||
.and_then(|v| v.parse::<u32>().ok())
|
||
@@ -820,6 +995,7 @@ pub fn create_oss_provider_with_base_url(base_url: &str) -> ModelProviderInfo {
|
||
name: "gpt-oss".into(),
|
||
base_url: Some(base_url.into()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -867,6 +1043,7 @@ base_url = "http://localhost:11434/v1"
|
||
name: "Ollama".into(),
|
||
base_url: Some("http://localhost:11434/v1".into()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -898,6 +1075,7 @@ query_params = { api-version = "2025-04-01-preview" }
|
||
name: "Azure".into(),
|
||
base_url: Some("https://xxxxx.openai.azure.com/openai".into()),
|
||
env_key: Some("AZURE_OPENAI_API_KEY".into()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -932,6 +1110,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" }
|
||
name: "Example".into(),
|
||
base_url: Some("https://example.com".into()),
|
||
env_key: Some("API_KEY".into()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -962,6 +1141,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" }
|
||
name: "test".into(),
|
||
base_url: Some(base_url.into()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -998,6 +1178,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" }
|
||
name: "Azure".into(),
|
||
base_url: Some("https://example.com".into()),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -1035,6 +1216,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" }
|
||
name: name.into(),
|
||
base_url: base_url.map(str::to_string),
|
||
env_key: None,
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: None,
|
||
@@ -1125,6 +1307,7 @@ refresh_interval_ms = 0
|
||
name: "Corp".into(),
|
||
base_url: Some("https://example.com".into()),
|
||
env_key: Some("CORP_API_KEY".into()),
|
||
+ alt_env_keys: Vec::new(),
|
||
env_key_instructions: None,
|
||
experimental_bearer_token: None,
|
||
auth: Some(ModelProviderAuthInfo {
|
||
@@ -1152,3 +1335,63 @@ refresh_interval_ms = 0
|
||
);
|
||
}
|
||
}
|
||
+
|
||
+#[cfg(test)]
|
||
+mod provider_from_env_tests {
|
||
+ use super::*;
|
||
+
|
||
+ // These read process-wide env, so they run as ONE test rather than racing
|
||
+ // each other through the same variables.
|
||
+ #[test]
|
||
+ fn the_key_you_hold_picks_the_provider() {
|
||
+ let keys = [HANZO_ENV_KEY, ANTHROPIC_ENV_KEY, OPENAI_ENV_KEY];
|
||
+ let saved: Vec<_> = keys.iter().map(|k| (*k, std::env::var(k).ok())).collect();
|
||
+ let clear = || {
|
||
+ for k in keys {
|
||
+ unsafe { std::env::remove_var(k) }
|
||
+ }
|
||
+ for k in HANZO_MACHINE_ENV_KEYS {
|
||
+ unsafe { std::env::remove_var(k) }
|
||
+ }
|
||
+ };
|
||
+
|
||
+ clear();
|
||
+ assert_eq!(provider_from_env(), HANZO_PROVIDER_ID, "no key at all still points at the provider whose instructions say how to sign in");
|
||
+
|
||
+ clear();
|
||
+ unsafe { std::env::set_var(OPENAI_ENV_KEY, "sk-x") };
|
||
+ assert_eq!(provider_from_env(), OPENAI_PROVIDER_ID, "someone with only an OpenAI key should not be told to fetch ours");
|
||
+
|
||
+ clear();
|
||
+ unsafe { std::env::set_var(ANTHROPIC_ENV_KEY, "sk-ant-x") };
|
||
+ assert_eq!(provider_from_env(), ANTHROPIC_PROVIDER_ID);
|
||
+
|
||
+ // Ours wins when several are present: it is the default and the one we serve.
|
||
+ clear();
|
||
+ unsafe { std::env::set_var(ANTHROPIC_ENV_KEY, "sk-ant-x") };
|
||
+ unsafe { std::env::set_var(OPENAI_ENV_KEY, "sk-x") };
|
||
+ unsafe { std::env::set_var(HANZO_ENV_KEY, "hz-x") };
|
||
+ assert_eq!(provider_from_env(), HANZO_PROVIDER_ID);
|
||
+
|
||
+ // An empty value is not a key.
|
||
+ clear();
|
||
+ unsafe { std::env::set_var(ANTHROPIC_ENV_KEY, " ") };
|
||
+ assert_eq!(provider_from_env(), HANZO_PROVIDER_ID, "whitespace is not a credential");
|
||
+
|
||
+ clear();
|
||
+ for (k, v) in saved {
|
||
+ if let Some(v) = v {
|
||
+ unsafe { std::env::set_var(k, v) }
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn anthropic_is_reachable_and_needs_no_new_wire_format() {
|
||
+ let p = built_in_model_providers(None);
|
||
+ let a = p.get(ANTHROPIC_PROVIDER_ID).expect("anthropic is built in");
|
||
+ assert_eq!(a.env_key.as_deref(), Some("ANTHROPIC_API_KEY"));
|
||
+ assert_eq!(a.wire_api, WireApi::Chat, "the OpenAI-compatible surface, not the native Messages API");
|
||
+ assert!(a.base_url.as_deref().unwrap_or_default().starts_with("https://api.anthropic.com"));
|
||
+ }
|
||
+}
|
||
diff --git a/core/src/openai_tools.rs b/core/src/openai_tools.rs
|
||
index e41a8a6..8361337 100644
|
||
--- a/core/src/openai_tools.rs
|
||
+++ b/core/src/openai_tools.rs
|
||
@@ -1378,13 +1378,22 @@ fn parse_tool_input_schema(input_schema: &JsonValue) -> Result<JsonSchema, serde
|
||
serde_json::from_value::<JsonSchema>(input_schema)
|
||
}
|
||
|
||
+/// The six types [`JsonSchema`] can hold. One list, read by every path that
|
||
+/// resolves a `type` — a scalar and a `["string", "null"]` union both measure
|
||
+/// against it, so they cannot drift apart.
|
||
+const SCHEMA_TYPES: [&str; 6] = [
|
||
+ "object", "array", "string", "number", "integer", "boolean",
|
||
+];
|
||
+
|
||
/// Sanitize a JSON Schema (as serde_json::Value) so it can fit our limited
|
||
/// JsonSchema enum. This function:
|
||
-/// - Ensures every schema object has a "type". If missing, infers it from
|
||
-/// common keywords (properties => object, items => array, enum/const/format => string)
|
||
-/// and otherwise defaults to "string".
|
||
+/// - Ensures every schema object has a "type" drawn from [`SCHEMA_TYPES`]. If it
|
||
+/// is missing, or is one we cannot hold, infers it from common keywords
|
||
+/// (properties => object, items => array, enum/const/format => string) and
|
||
+/// otherwise defaults to "string".
|
||
/// - Fills required child fields (e.g. array items, object properties) with
|
||
-/// permissive defaults when absent.
|
||
+/// permissive defaults when absent, and writes an empty `required` list where
|
||
+/// a schema omits one, which is JSON Schema's own reading of an absent list.
|
||
fn sanitize_json_schema(value: &mut JsonValue) {
|
||
match value {
|
||
JsonValue::Bool(_) => {
|
||
@@ -1408,6 +1417,22 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||
if let Some(items) = map.get_mut("items") {
|
||
sanitize_json_schema(items);
|
||
}
|
||
+ // `anyOf: [T, {type: null}]` is how MCP servers spell an optional
|
||
+ // T. The null arm says nothing about a value the model may send,
|
||
+ // so it goes; T alone stays, and being optional is already the
|
||
+ // caller's `required` list.
|
||
+ for combiner in ["oneOf", "anyOf"] {
|
||
+ if let Some(JsonValue::Array(arms)) = map.get_mut(combiner) {
|
||
+ let typed: Vec<JsonValue> = arms
|
||
+ .iter()
|
||
+ .filter(|arm| arm.get("type").and_then(|t| t.as_str()) != Some("null"))
|
||
+ .cloned()
|
||
+ .collect();
|
||
+ if !typed.is_empty() {
|
||
+ *arms = typed;
|
||
+ }
|
||
+ }
|
||
+ }
|
||
// Some schemas use oneOf/anyOf/allOf - sanitize their entries
|
||
for combiner in ["oneOf", "anyOf", "allOf", "prefixItems"] {
|
||
if let Some(v) = map.get_mut(combiner) {
|
||
@@ -1415,23 +1440,25 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||
}
|
||
}
|
||
|
||
- // Normalize/ensure type
|
||
- let mut ty = map.get("type").and_then(|v| v.as_str()).map(str::to_string);
|
||
+ // Normalize/ensure type. A scalar is measured against SCHEMA_TYPES
|
||
+ // exactly as a union is, so the two cannot disagree about what we
|
||
+ // can hold: `null` and anything else outside the list fall through
|
||
+ // to inference below rather than reaching the parser, where one bad
|
||
+ // property took down the whole tool around it.
|
||
+ let mut ty = map
|
||
+ .get("type")
|
||
+ .and_then(JsonValue::as_str)
|
||
+ .filter(|t| SCHEMA_TYPES.contains(t))
|
||
+ .map(str::to_string);
|
||
|
||
// If type is an array (union), pick first supported; else leave to inference
|
||
if ty.is_none() {
|
||
if let Some(JsonValue::Array(types)) = map.get("type") {
|
||
- for t in types {
|
||
- if let Some(tt) = t.as_str() {
|
||
- if matches!(
|
||
- tt,
|
||
- "object" | "array" | "string" | "number" | "integer" | "boolean"
|
||
- ) {
|
||
- ty = Some(tt.to_string());
|
||
- break;
|
||
- }
|
||
- }
|
||
- }
|
||
+ ty = types
|
||
+ .iter()
|
||
+ .filter_map(JsonValue::as_str)
|
||
+ .find(|t| SCHEMA_TYPES.contains(t))
|
||
+ .map(str::to_string);
|
||
}
|
||
}
|
||
|
||
@@ -1474,6 +1501,14 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||
JsonValue::Object(serde_json::Map::new()),
|
||
);
|
||
}
|
||
+ // JSON Schema reads an absent `required` as "nothing is
|
||
+ // required"; our Object serializer reads an absent one as
|
||
+ // "everything is". Write the empty list, so a server that omits
|
||
+ // it — every Pydantic tool whose arguments are all optional —
|
||
+ // is not sent to the model demanding all fifteen of them.
|
||
+ if !map.contains_key("required") {
|
||
+ map.insert("required".to_string(), JsonValue::Array(Vec::new()));
|
||
+ }
|
||
// If additionalProperties is an object schema, sanitize it too.
|
||
// Leave booleans as-is, since JSON Schema allows boolean here.
|
||
if let Some(ap) = map.get_mut("additionalProperties") {
|
||
@@ -2302,7 +2337,7 @@ mod tests {
|
||
},
|
||
),
|
||
]),
|
||
- required: None,
|
||
+ required: Some(Vec::new()),
|
||
additional_properties: None,
|
||
},
|
||
description: "Do something cool".to_string(),
|
||
@@ -2437,7 +2472,7 @@ mod tests {
|
||
},
|
||
),
|
||
]),
|
||
- required: None,
|
||
+ required: Some(Vec::new()),
|
||
additional_properties: None,
|
||
},
|
||
description: "Do something cool".to_string(),
|
||
@@ -2531,7 +2566,7 @@ mod tests {
|
||
allowed_values: None,
|
||
}
|
||
)]),
|
||
- required: None,
|
||
+ required: Some(Vec::new()),
|
||
additional_properties: None,
|
||
},
|
||
description: "Search docs".to_string(),
|
||
@@ -2608,7 +2643,7 @@ mod tests {
|
||
"page".to_string(),
|
||
JsonSchema::Number { description: None }
|
||
)]),
|
||
- required: None,
|
||
+ required: Some(Vec::new()),
|
||
additional_properties: None,
|
||
},
|
||
description: "Pagination".to_string(),
|
||
@@ -2683,7 +2718,7 @@ mod tests {
|
||
description: None
|
||
}
|
||
)]),
|
||
- required: None,
|
||
+ required: Some(Vec::new()),
|
||
additional_properties: None,
|
||
},
|
||
description: "Tags".to_string(),
|
||
@@ -2692,6 +2727,102 @@ mod tests {
|
||
);
|
||
}
|
||
|
||
+ /// The Hanzo MCP server spells every optional argument as
|
||
+ /// `anyOf: [T, {type: null}]`. Twenty of its tools were dropped at the
|
||
+ /// door for that null arm.
|
||
+ #[test]
|
||
+ fn test_mcp_tool_nullable_any_of_keeps_the_typed_arm() {
|
||
+ let tool = mcp_tool_to_openai_tool(
|
||
+ "hanzo__zsh".to_string(),
|
||
+ mcp_types::Tool {
|
||
+ name: "zsh".to_string(),
|
||
+ input_schema: ToolInputSchema {
|
||
+ properties: Some(serde_json::json!({
|
||
+ "cwd": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
|
||
+ "timeout": { "type": ["integer", "null"] },
|
||
+ "env": { "type": "null" }
|
||
+ })),
|
||
+ required: None,
|
||
+ r#type: "object".to_string(),
|
||
+ },
|
||
+ output_schema: None,
|
||
+ title: None,
|
||
+ annotations: None,
|
||
+ description: None,
|
||
+ },
|
||
+ )
|
||
+ .expect("nullable arguments convert");
|
||
+ let parameters = serde_json::to_value(&tool.parameters).expect("serializes");
|
||
+ assert_eq!(
|
||
+ parameters["properties"]["cwd"],
|
||
+ serde_json::json!({ "anyOf": [ { "type": "string" } ] })
|
||
+ );
|
||
+ assert_eq!(parameters["properties"]["timeout"]["type"], "number");
|
||
+ assert_eq!(parameters["properties"]["env"]["type"], "string");
|
||
+ }
|
||
+
|
||
+ fn convert_hanzo_tool(
|
||
+ properties: serde_json::Value,
|
||
+ required: Option<Vec<String>>,
|
||
+ ) -> JsonValue {
|
||
+ let tool = mcp_tool_to_openai_tool(
|
||
+ "hanzo__agent".to_string(),
|
||
+ mcp_types::Tool {
|
||
+ name: "agent".to_string(),
|
||
+ input_schema: ToolInputSchema {
|
||
+ properties: Some(properties),
|
||
+ required,
|
||
+ r#type: "object".to_string(),
|
||
+ },
|
||
+ output_schema: None,
|
||
+ title: None,
|
||
+ annotations: None,
|
||
+ description: None,
|
||
+ },
|
||
+ )
|
||
+ .expect("a Pydantic schema is a JSON Schema and must convert");
|
||
+ serde_json::to_value(&tool.parameters).expect("serializes")
|
||
+ }
|
||
+
|
||
+ /// Pydantic omits `required` when every argument is optional, which is most
|
||
+ /// of the Hanzo MCP server: `agent` declares none of its fifteen. Reading
|
||
+ /// that as "all fifteen" tells the model a one-word call is malformed.
|
||
+ #[test]
|
||
+ fn test_mcp_tool_absent_required_requires_nothing() {
|
||
+ let parameters = convert_hanzo_tool(
|
||
+ serde_json::json!({
|
||
+ "action": { "type": "string" },
|
||
+ "prompt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
|
||
+ }),
|
||
+ None,
|
||
+ );
|
||
+
|
||
+ assert_eq!(parameters["required"], serde_json::json!([]));
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn test_mcp_tool_declared_required_survives() {
|
||
+ let parameters = convert_hanzo_tool(
|
||
+ serde_json::json!({ "op": { "type": "string" } }),
|
||
+ Some(vec!["op".to_string()]),
|
||
+ );
|
||
+
|
||
+ assert_eq!(parameters["required"], serde_json::json!(["op"]));
|
||
+ }
|
||
+
|
||
+ /// A type outside [`SCHEMA_TYPES`] degrades to an accept-all string. It must
|
||
+ /// never reach the parser, where it would fail the whole tool it sits in
|
||
+ /// rather than the one argument it describes.
|
||
+ #[test]
|
||
+ fn test_mcp_tool_unheld_type_does_not_fail_the_tool() {
|
||
+ let parameters = convert_hanzo_tool(
|
||
+ serde_json::json!({ "weird": { "type": "int64" } }),
|
||
+ None,
|
||
+ );
|
||
+
|
||
+ assert_eq!(parameters["properties"]["weird"]["type"], "string");
|
||
+ }
|
||
+
|
||
#[test]
|
||
fn test_mcp_tool_schema_composition_is_preserved() {
|
||
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
||
diff --git a/core/src/parse_command.rs b/core/src/parse_command.rs
|
||
index 99865e9..abaaada 100644
|
||
--- a/core/src/parse_command.rs
|
||
+++ b/core/src/parse_command.rs
|
||
@@ -140,7 +140,7 @@ mod tests {
|
||
|
||
#[test]
|
||
fn sh_lc_sed_is_read_command() {
|
||
- let inner = "sed -n '10,20p' code-rs/tui/src/chatwidget.rs";
|
||
+ let inner = "sed -n '10,20p' hanzo-dev/tui/src/chatwidget.rs";
|
||
assert_parsed(
|
||
&vec_str(&["sh", "-lc", inner]),
|
||
vec![ParsedCommand::Read {
|
||
@@ -334,13 +334,13 @@ mod tests {
|
||
|
||
#[test]
|
||
fn supports_rg_with_path_and_glob_filter() {
|
||
- let inner = "rg \"codex-run-as-apply-patch\" -n code-rs -g\"*.rs\"";
|
||
+ let inner = "rg \"codex-run-as-apply-patch\" -n hanzo-dev -g\"*.rs\"";
|
||
assert_parsed(
|
||
&vec_str(&["bash", "-lc", inner]),
|
||
vec![ParsedCommand::Search {
|
||
- cmd: "rg codex-run-as-apply-patch -n code-rs '-g*.rs'".to_string(),
|
||
+ cmd: "rg codex-run-as-apply-patch -n hanzo-dev '-g*.rs'".to_string(),
|
||
query: Some("codex-run-as-apply-patch".to_string()),
|
||
- path: Some("code-rs".to_string()),
|
||
+ path: Some("hanzo-dev".to_string()),
|
||
}],
|
||
);
|
||
}
|
||
@@ -532,10 +532,10 @@ mod tests {
|
||
#[test]
|
||
fn supports_grep_recursive_current_dir() {
|
||
assert_parsed(
|
||
- &vec_str(&["grep", "-R", "CODEX_SANDBOX_ENV_VAR", "-n", "."]),
|
||
+ &vec_str(&["grep", "-R", "DEV_SANDBOX_ENV_VAR", "-n", "."]),
|
||
vec![ParsedCommand::Search {
|
||
- cmd: "grep -R CODEX_SANDBOX_ENV_VAR -n .".to_string(),
|
||
- query: Some("CODEX_SANDBOX_ENV_VAR".to_string()),
|
||
+ cmd: "grep -R DEV_SANDBOX_ENV_VAR -n .".to_string(),
|
||
+ query: Some("DEV_SANDBOX_ENV_VAR".to_string()),
|
||
path: Some(".".to_string()),
|
||
}],
|
||
);
|
||
@@ -547,13 +547,13 @@ mod tests {
|
||
&vec_str(&[
|
||
"grep",
|
||
"-R",
|
||
- "CODEX_SANDBOX_ENV_VAR",
|
||
+ "DEV_SANDBOX_ENV_VAR",
|
||
"-n",
|
||
"core/src/spawn.rs",
|
||
]),
|
||
vec![ParsedCommand::Search {
|
||
- cmd: "grep -R CODEX_SANDBOX_ENV_VAR -n core/src/spawn.rs".to_string(),
|
||
- query: Some("CODEX_SANDBOX_ENV_VAR".to_string()),
|
||
+ cmd: "grep -R DEV_SANDBOX_ENV_VAR -n core/src/spawn.rs".to_string(),
|
||
+ query: Some("DEV_SANDBOX_ENV_VAR".to_string()),
|
||
path: Some("spawn.rs".to_string()),
|
||
}],
|
||
);
|
||
@@ -588,7 +588,7 @@ mod tests {
|
||
#[test]
|
||
fn supports_cd_and_rg_files() {
|
||
assert_parsed(
|
||
- &shlex_split_safe("cd code-rs && rg --files"),
|
||
+ &shlex_split_safe("cd hanzo-dev && rg --files"),
|
||
vec![ParsedCommand::Search {
|
||
cmd: "rg --files".to_string(),
|
||
query: None,
|
||
diff --git a/core/src/reasoning.rs b/core/src/reasoning.rs
|
||
index 85fed8e..9497a79 100644
|
||
--- a/core/src/reasoning.rs
|
||
+++ b/core/src/reasoning.rs
|
||
@@ -1,19 +1,19 @@
|
||
use crate::config_types::ReasoningEffort;
|
||
|
||
-const GPT5_1_CODEX_MAX_EFFORTS: &[ReasoningEffort] = &[
|
||
+const GPT5_1_DEV_MAX_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::Low,
|
||
ReasoningEffort::Medium,
|
||
ReasoningEffort::High,
|
||
ReasoningEffort::XHigh,
|
||
];
|
||
|
||
-const GPT5_1_CODEX_EFFORTS: &[ReasoningEffort] = &[
|
||
+const GPT5_1_DEV_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::Low,
|
||
ReasoningEffort::Medium,
|
||
ReasoningEffort::High,
|
||
];
|
||
|
||
-const GPT5_1_CODEX_MINI_EFFORTS: &[ReasoningEffort] = &[
|
||
+const GPT5_1_DEV_MINI_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::Medium,
|
||
ReasoningEffort::High,
|
||
];
|
||
@@ -31,13 +31,13 @@ const GPT5_2_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::XHigh,
|
||
];
|
||
|
||
-const GPT5_CODEX_EFFORTS: &[ReasoningEffort] = &[
|
||
+const GPT5_DEV_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::Low,
|
||
ReasoningEffort::Medium,
|
||
ReasoningEffort::High,
|
||
];
|
||
|
||
-const GPT5_CODEX_MINI_EFFORTS: &[ReasoningEffort] = &[
|
||
+const GPT5_DEV_MINI_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::Medium,
|
||
ReasoningEffort::High,
|
||
];
|
||
@@ -49,7 +49,7 @@ const GPT5_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::High,
|
||
];
|
||
|
||
-const CODEX_FALLBACK_EFFORTS: &[ReasoningEffort] = &[
|
||
+const DEV_FALLBACK_EFFORTS: &[ReasoningEffort] = &[
|
||
ReasoningEffort::Low,
|
||
ReasoningEffort::Medium,
|
||
ReasoningEffort::High,
|
||
@@ -77,15 +77,15 @@ pub fn supported_reasoning_efforts_for_model(model: &str) -> &'static [Reasoning
|
||
let lower = model.to_ascii_lowercase();
|
||
|
||
if lower.starts_with("gpt-5.1-codex-max") {
|
||
- return GPT5_1_CODEX_MAX_EFFORTS;
|
||
+ return GPT5_1_DEV_MAX_EFFORTS;
|
||
}
|
||
|
||
if lower.starts_with("gpt-5.1-codex-mini") {
|
||
- return GPT5_1_CODEX_MINI_EFFORTS;
|
||
+ return GPT5_1_DEV_MINI_EFFORTS;
|
||
}
|
||
|
||
if lower.starts_with("gpt-5.1-codex") {
|
||
- return GPT5_1_CODEX_EFFORTS;
|
||
+ return GPT5_1_DEV_EFFORTS;
|
||
}
|
||
|
||
if lower.starts_with("gpt-5.1") {
|
||
@@ -101,11 +101,11 @@ pub fn supported_reasoning_efforts_for_model(model: &str) -> &'static [Reasoning
|
||
}
|
||
|
||
if lower.starts_with("gpt-5-codex-mini") {
|
||
- return GPT5_CODEX_MINI_EFFORTS;
|
||
+ return GPT5_DEV_MINI_EFFORTS;
|
||
}
|
||
|
||
if lower.starts_with("gpt-5-codex") {
|
||
- return GPT5_CODEX_EFFORTS;
|
||
+ return GPT5_DEV_EFFORTS;
|
||
}
|
||
|
||
if lower.starts_with("gpt-5") {
|
||
@@ -113,7 +113,7 @@ pub fn supported_reasoning_efforts_for_model(model: &str) -> &'static [Reasoning
|
||
}
|
||
|
||
if lower.starts_with("codex-") {
|
||
- return CODEX_FALLBACK_EFFORTS;
|
||
+ return DEV_FALLBACK_EFFORTS;
|
||
}
|
||
|
||
DEFAULT_EFFORTS
|
||
diff --git a/core/src/remote_models/mod.rs b/core/src/remote_models/mod.rs
|
||
index c132c77..1e1ecb5 100644
|
||
--- a/core/src/remote_models/mod.rs
|
||
+++ b/core/src/remote_models/mod.rs
|
||
@@ -31,7 +31,7 @@ mod cache;
|
||
const MODEL_CACHE_FILE: &str = "models_cache.json";
|
||
const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300);
|
||
const REMOTE_MODELS_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||
-const CODEX_AUTO_BALANCED_MODEL: &str = "codex-auto-balanced";
|
||
+const DEV_AUTO_BALANCED_MODEL: &str = "codex-auto-balanced";
|
||
|
||
#[derive(Debug, Default, Clone)]
|
||
struct RemoteModelsState {
|
||
@@ -96,8 +96,8 @@ impl RemoteModelsManager {
|
||
state
|
||
.models
|
||
.iter()
|
||
- .any(|m| m.slug == CODEX_AUTO_BALANCED_MODEL)
|
||
- .then(|| CODEX_AUTO_BALANCED_MODEL.to_string())
|
||
+ .any(|m| m.slug == DEV_AUTO_BALANCED_MODEL)
|
||
+ .then(|| DEV_AUTO_BALANCED_MODEL.to_string())
|
||
}
|
||
|
||
/// Best-effort refresh of remote models.
|
||
diff --git a/core/src/seatbelt.rs b/core/src/seatbelt.rs
|
||
index d51b0ab..cab3c40 100644
|
||
--- a/core/src/seatbelt.rs
|
||
+++ b/core/src/seatbelt.rs
|
||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||
use tokio::process::Child;
|
||
|
||
use crate::protocol::SandboxPolicy;
|
||
-use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||
+use crate::spawn::DEV_SANDBOX_ENV_VAR;
|
||
use crate::spawn::StdioPolicy;
|
||
use crate::spawn::spawn_child_async;
|
||
|
||
@@ -26,7 +26,7 @@ pub async fn spawn_command_under_seatbelt(
|
||
) -> std::io::Result<Child> {
|
||
let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd);
|
||
let arg0 = None;
|
||
- env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||
+ env.insert(DEV_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||
spawn_child_async(
|
||
PathBuf::from(MACOS_PATH_TO_SEATBELT_EXECUTABLE),
|
||
args,
|
||
@@ -119,8 +119,8 @@ fn create_seatbelt_command_args(
|
||
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}"
|
||
);
|
||
|
||
- if std::env::var("CODEX_DEBUG_PRINT_SEATBELT").is_ok() {
|
||
- eprintln!("--- Codex Seatbelt Policy ---\n{}\n------------------------------", full_policy);
|
||
+ if std::env::var("DEV_DEBUG_PRINT_SEATBELT").is_ok() {
|
||
+ eprintln!("--- Hanzo Dev Seatbelt Policy ---\n{}\n------------------------------", full_policy);
|
||
}
|
||
|
||
let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy];
|
||
diff --git a/core/src/shell.rs b/core/src/shell.rs
|
||
index 1bf6922..a4bf689 100644
|
||
--- a/core/src/shell.rs
|
||
+++ b/core/src/shell.rs
|
||
@@ -70,7 +70,7 @@ impl Shell {
|
||
// turn it into a PowerShell command.
|
||
let first = command.first().map(String::as_str);
|
||
if first != Some(ps.exe.as_str()) {
|
||
- // TODO (CODEX_2900): Handle escaping newlines.
|
||
+ // TODO (DEV_2900): Handle escaping newlines.
|
||
if command.iter().any(|a| a.contains('\n') || a.contains('\r')) {
|
||
return Some(command);
|
||
}
|
||
@@ -716,7 +716,7 @@ mod tests_windows {
|
||
vec!["pwsh.exe", "-NoProfile", "-Command", "echo hello"],
|
||
),
|
||
(
|
||
- // TODO (CODEX_2900): Handle escaping newlines for powershell invocation.
|
||
+ // TODO (DEV_2900): Handle escaping newlines for powershell invocation.
|
||
Shell::PowerShell(PowerShellConfig {
|
||
exe: "powershell.exe".to_string(),
|
||
bash_exe_fallback: Some(PathBuf::from("bash.exe")),
|
||
diff --git a/core/src/shot/mod.rs b/core/src/shot/mod.rs
|
||
new file mode 100644
|
||
index 0000000..d80a5eb
|
||
--- /dev/null
|
||
+++ b/core/src/shot/mod.rs
|
||
@@ -0,0 +1,493 @@
|
||
+//! A picture of the UI the agent just changed, published beside the diff that
|
||
+//! changed it.
|
||
+//!
|
||
+//! WHY. A diff says what moved; it does not say what it looks like. For a change
|
||
+//! to a rendered surface the page itself is the reviewable artifact, and a human
|
||
+//! watching a session live should not have to check out the branch and start a
|
||
+//! server to see one. The session log already streams to that console, so the
|
||
+//! picture rides the channel that exists — one ordinary event naming one object
|
||
+//! in storage — rather than standing up a second artifact plane beside it. The
|
||
+//! bytes go to object storage because a log is for reading, and a 200 KiB image
|
||
+//! inlined into a transcript makes it unreadable for everything else.
|
||
+//!
|
||
+//! WHY IT MAY FAIL QUIETLY. Evidence is not the work. No dev server listening, a
|
||
+//! route that 404s, storage unreachable, no session to publish to — none of those
|
||
+//! are reasons to fail a coding turn, and none of them are worth more than a
|
||
+//! debug line. Every step below returns rather than propagates, and the whole
|
||
+//! thing runs off the turn's critical path so a slow browser costs the agent
|
||
+//! nothing.
|
||
+//!
|
||
+//! WHY IT NEVER STARTS A SERVER. It attaches to a dev server already listening
|
||
+//! and will not launch one. Launching one means running the `dev` script out of
|
||
+//! the repository's own package.json — arbitrary code from the tree the agent is
|
||
+//! being asked to edit — outside the approval and sandbox path every other
|
||
+//! command in this program goes through. A repository shipping
|
||
+//! `"dev": "curl … | sh"` would otherwise get execution for free by baiting the
|
||
+//! agent into touching a stylesheet. Taking a picture must not be a way to run
|
||
+//! something, so this reads package.json for a port and dials it.
|
||
+
|
||
+mod ui;
|
||
+
|
||
+use std::path::PathBuf;
|
||
+use std::sync::atomic::AtomicU64;
|
||
+use std::sync::atomic::Ordering;
|
||
+
|
||
+use anyhow::Context as _;
|
||
+use anyhow::Result;
|
||
+use anyhow::anyhow;
|
||
+use code_browser::BrowserConfig;
|
||
+use code_browser::BrowserManager;
|
||
+use code_browser::ScreenshotMode;
|
||
+use code_browser::ViewportConfig;
|
||
+use serde::Deserialize;
|
||
+use serde_json::Value;
|
||
+use serde_json::json;
|
||
+use tokio::net::TcpStream;
|
||
+use tokio::sync::Semaphore;
|
||
+use tokio::time::Duration;
|
||
+use tokio::time::timeout;
|
||
+use tracing::debug;
|
||
+
|
||
+use crate::codex::Session;
|
||
+use crate::default_client::create_client;
|
||
+use crate::model_provider_info::create_hanzo_provider;
|
||
+
|
||
+/// The org-scoped bucket session artifacts live in. The org never appears in the
|
||
+/// key — cloud derives the physical bucket from the caller's own principal, so a
|
||
+/// key is meaningless outside the tenant that wrote it.
|
||
+const BUCKET: &str = "sessions";
|
||
+
|
||
+/// The session to publish to, set by whatever created it. The agent cannot
|
||
+/// invent one: without a session there is nowhere for a picture to go, so the
|
||
+/// whole gesture is abandoned before a browser or a socket is touched. This is
|
||
+/// also why a laptop with no cloud session behaves exactly as it did before.
|
||
+const SESSION: &str = "HANZO_SESSION";
|
||
+
|
||
+/// One fixed viewport, so two pictures of the same page are comparable. Not
|
||
+/// configurable — a reviewer flipping between runs taken at different widths is
|
||
+/// reading noise, not a change.
|
||
+const VIEW: (u32, u32) = (1280, 800);
|
||
+
|
||
+/// The budget for the whole gesture: launch, navigate, capture, two round trips.
|
||
+/// Past this the picture describes a page that has already moved on.
|
||
+const BUDGET: Duration = Duration::from_secs(45);
|
||
+
|
||
+/// How long to wait on the dev server's socket. It is on loopback; either it is
|
||
+/// listening or it is not.
|
||
+const DIAL: Duration = Duration::from_millis(300);
|
||
+
|
||
+/// At most one capture in flight. A turn that patches thirty files wants one
|
||
+/// picture of where it ends up, not thirty of it arriving there — and an
|
||
+/// unbounded fan-out of browsers driven by model output is a way to run a
|
||
+/// machine out of memory.
|
||
+static INFLIGHT: Semaphore = Semaphore::const_new(1);
|
||
+
|
||
+/// Numbers the objects within a session so the console can order them.
|
||
+static COUNT: AtomicU64 = AtomicU64::new(0);
|
||
+
|
||
+/// Publish a picture of the page this diff changed, if it changed one.
|
||
+///
|
||
+/// Returns immediately. The decision is cheap and made here; everything with a
|
||
+/// cost happens on a detached task, because the turn must not wait on evidence.
|
||
+pub(crate) fn capture(sess: &Session, diff: &str) {
|
||
+ let Some(plan) = Plan::new(sess, diff) else {
|
||
+ return;
|
||
+ };
|
||
+ tokio::spawn(async move {
|
||
+ let Ok(_held) = INFLIGHT.try_acquire() else {
|
||
+ debug!("shot: a capture is already running");
|
||
+ return;
|
||
+ };
|
||
+ match timeout(BUDGET, plan.run()).await {
|
||
+ Ok(Ok(key)) => debug!("shot: published {key}"),
|
||
+ Ok(Err(why)) => debug!("shot: skipped — {why:#}"),
|
||
+ Err(_) => debug!("shot: skipped — over {BUDGET:?}"),
|
||
+ }
|
||
+ });
|
||
+}
|
||
+
|
||
+/// Everything the capture needs, owned, so it can outlive the turn that asked
|
||
+/// for it.
|
||
+struct Plan {
|
||
+ cwd: PathBuf,
|
||
+ session: String,
|
||
+ base: String,
|
||
+ token: String,
|
||
+ route: String,
|
||
+ /// Workspace-relative, capped. Relative because an absolute path carries the
|
||
+ /// operator's home directory — and so their account name — into a log the
|
||
+ /// whole org can read.
|
||
+ files: Vec<String>,
|
||
+ slug: String,
|
||
+ n: u64,
|
||
+}
|
||
+
|
||
+/// How many changed files the payload names. Enough to see what the picture is
|
||
+/// of; the diff beside it is the complete list.
|
||
+const NAMED: usize = 10;
|
||
+
|
||
+impl Plan {
|
||
+ fn new(sess: &Session, diff: &str) -> Option<Self> {
|
||
+ // Cheapest question first: is anyone listening for this?
|
||
+ let session = std::env::var(SESSION).ok()?;
|
||
+ let session = handle(&session)?;
|
||
+
|
||
+ let changed: Vec<PathBuf> = ui::touched(diff)
|
||
+ .into_iter()
|
||
+ .filter(|p| ui::rendered(p))
|
||
+ .collect();
|
||
+ let first = changed.first()?;
|
||
+
|
||
+ // The same base URL and bearer the agent already talks to Hanzo with.
|
||
+ // A second notion of where the cloud is would be a second thing to get
|
||
+ // wrong.
|
||
+ let provider = create_hanzo_provider();
|
||
+ let base = provider.base_url.clone()?;
|
||
+ let token = provider.api_key().ok().flatten()?;
|
||
+
|
||
+ let cwd = sess.get_cwd().to_path_buf();
|
||
+ let route = changed
|
||
+ .iter()
|
||
+ .find_map(|p| ui::route(p))
|
||
+ .unwrap_or_else(|| "/".to_string());
|
||
+ let slug = ui::slug(&route, first);
|
||
+ let files = ui::named(&changed, &cwd, NAMED);
|
||
+
|
||
+ Some(Self {
|
||
+ cwd,
|
||
+ session: session.to_string(),
|
||
+ base: base.trim_end_matches('/').to_string(),
|
||
+ token,
|
||
+ route,
|
||
+ files,
|
||
+ slug,
|
||
+ n: COUNT.fetch_add(1, Ordering::Relaxed) + 1,
|
||
+ })
|
||
+ }
|
||
+
|
||
+ async fn run(&self) -> Result<String> {
|
||
+ let origin = self
|
||
+ .origin()
|
||
+ .await
|
||
+ .ok_or_else(|| anyhow!("no dev server listening"))?;
|
||
+ let png = shoot(&format!("{origin}{}", self.route)).await?;
|
||
+ let http = create_client("shot");
|
||
+ let key = format!("{}/{}/{}-{}.png", BUCKET, self.session, self.n, self.slug);
|
||
+ let signed = self.presign(&http, &key).await?;
|
||
+ put(&http, &signed, png).await?;
|
||
+ self.append(&http, &key, &origin).await?;
|
||
+ Ok(key)
|
||
+ }
|
||
+
|
||
+ /// The dev server, if one is already listening.
|
||
+ ///
|
||
+ /// Exactly one port is dialed: the one this repository declares. Sweeping the
|
||
+ /// usual suspects would eventually connect to some unrelated program on the
|
||
+ /// operator's machine and present a picture of it as their diff.
|
||
+ async fn origin(&self) -> Option<String> {
|
||
+ let manifest = std::fs::read_to_string(self.cwd.join("package.json")).ok()?;
|
||
+ let port = ui::port(&manifest)?;
|
||
+ let addr = format!("127.0.0.1:{port}");
|
||
+ timeout(DIAL, TcpStream::connect(&addr)).await.ok()?.ok()?;
|
||
+ Some(format!("http://{addr}"))
|
||
+ }
|
||
+
|
||
+ /// Ask cloud for a URL that may write this one object.
|
||
+ ///
|
||
+ /// The image never passes through the API: cloud signs, we upload straight to
|
||
+ /// storage. The signature is time-boxed and scoped to this exact bucket and
|
||
+ /// key, which is why it is a credential and why it stops here.
|
||
+ async fn presign(&self, http: &reqwest::Client, key: &str) -> Result<String> {
|
||
+ #[derive(Deserialize)]
|
||
+ struct Signed {
|
||
+ url: String,
|
||
+ }
|
||
+ let res = http
|
||
+ .post(format!("{}/s3/buckets/{BUCKET}/objects", self.base))
|
||
+ .bearer_auth(&self.token)
|
||
+ .json(&json!({ "key": key }))
|
||
+ .send()
|
||
+ .await
|
||
+ .context("presign")?;
|
||
+ let status = res.status();
|
||
+ if !status.is_success() {
|
||
+ return Err(anyhow!("presign: {status}"));
|
||
+ }
|
||
+ Ok(res.json::<Signed>().await.context("presign body")?.url)
|
||
+ }
|
||
+
|
||
+ /// One event, naming the object. The bytes are in storage; the log carries
|
||
+ /// the pointer, the route, and the files that earned the picture.
|
||
+ async fn append(&self, http: &reqwest::Client, key: &str, origin: &str) -> Result<()> {
|
||
+ let res = http
|
||
+ .post(format!("{}/agents/sessions/{}/events", self.base, self.session))
|
||
+ .bearer_auth(&self.token)
|
||
+ .json(&json!({ "kind": "tool-call", "payload": self.payload(key, origin) }))
|
||
+ .send()
|
||
+ .await
|
||
+ .context("append")?;
|
||
+ let status = res.status();
|
||
+ if !status.is_success() {
|
||
+ return Err(anyhow!("append: {status}"));
|
||
+ }
|
||
+ Ok(())
|
||
+ }
|
||
+
|
||
+ /// What the console reads.
|
||
+ ///
|
||
+ /// Bucket and key rather than a URL: the only URL for a private object is a
|
||
+ /// signed, expiring one, and writing a credential into a transcript would be
|
||
+ /// a leak that outlives the session. A reader resolves the key through the
|
||
+ /// same door, under their own identity, whenever they open the event.
|
||
+ fn payload(&self, key: &str, origin: &str) -> Value {
|
||
+ json!({
|
||
+ "tool": "shot",
|
||
+ "route": self.route,
|
||
+ "bucket": BUCKET,
|
||
+ "key": key,
|
||
+ "origin": origin,
|
||
+ "viewport": { "width": VIEW.0, "height": VIEW.1 },
|
||
+ "files": self.files,
|
||
+ })
|
||
+ }
|
||
+}
|
||
+
|
||
+/// A session id fit to be spliced into a URL path and an object key. An
|
||
+/// environment variable is caller-controlled input, and this one addresses
|
||
+/// another tenant's session if it is allowed to contain a slash or a dot-dot.
|
||
+fn handle(raw: &str) -> Option<&str> {
|
||
+ let id = raw.trim();
|
||
+ let ok = !id.is_empty()
|
||
+ && id.len() <= 128
|
||
+ && id
|
||
+ .bytes()
|
||
+ .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_');
|
||
+ ok.then_some(id)
|
||
+}
|
||
+
|
||
+/// Photograph a URL in a browser of this function's own.
|
||
+///
|
||
+/// Its own, not the agent's: the `browser` tool's page belongs to the
|
||
+/// conversation, and navigating it away to take this picture would move the
|
||
+/// ground under whatever the agent was looking at. Headless with a temporary
|
||
+/// profile, so it carries none of the operator's cookies to a page it was told
|
||
+/// about by a model.
|
||
+async fn shoot(url: &str) -> Result<Vec<u8>> {
|
||
+ let browser = BrowserManager::new(BrowserConfig {
|
||
+ enabled: true,
|
||
+ headless: true,
|
||
+ persist_profile: false,
|
||
+ viewport: ViewportConfig {
|
||
+ width: VIEW.0,
|
||
+ height: VIEW.1,
|
||
+ device_scale_factor: 1.0,
|
||
+ mobile: false,
|
||
+ },
|
||
+ ..BrowserConfig::default()
|
||
+ });
|
||
+ let png = frame(&browser, url).await;
|
||
+ let _ = browser.stop().await;
|
||
+ png
|
||
+}
|
||
+
|
||
+async fn frame(browser: &BrowserManager, url: &str) -> Result<Vec<u8>> {
|
||
+ browser.start().await.context("launch")?;
|
||
+ browser.goto(url).await.with_context(|| format!("open {url}"))?;
|
||
+ let page = browser.get_or_create_page().await.context("page")?;
|
||
+ let shots = page
|
||
+ .screenshot(ScreenshotMode::Viewport)
|
||
+ .await
|
||
+ .context("capture")?;
|
||
+ shots
|
||
+ .into_iter()
|
||
+ .next()
|
||
+ .map(|s| s.data)
|
||
+ .ok_or_else(|| anyhow!("empty capture"))
|
||
+}
|
||
+
|
||
+/// Upload the bytes with the signature and nothing else. Our bearer is for the
|
||
+/// API; sending it to whatever host cloud signed against would hand an org
|
||
+/// credential to a third party for no reason.
|
||
+async fn put(http: &reqwest::Client, signed: &str, png: Vec<u8>) -> Result<()> {
|
||
+ let res = http
|
||
+ .put(signed)
|
||
+ .header(reqwest::header::CONTENT_TYPE, "image/png")
|
||
+ .body(png)
|
||
+ .send()
|
||
+ .await
|
||
+ .context("upload")?;
|
||
+ let status = res.status();
|
||
+ if !status.is_success() {
|
||
+ return Err(anyhow!("upload: {status}"));
|
||
+ }
|
||
+ Ok(())
|
||
+}
|
||
+
|
||
+#[cfg(test)]
|
||
+mod tests {
|
||
+ use super::*;
|
||
+ use wiremock::Mock;
|
||
+ use wiremock::MockServer;
|
||
+ use wiremock::Request;
|
||
+ use wiremock::ResponseTemplate;
|
||
+ use wiremock::matchers::body_json;
|
||
+ use wiremock::matchers::header;
|
||
+ use wiremock::matchers::method;
|
||
+ use wiremock::matchers::path;
|
||
+
|
||
+ fn plan() -> Plan {
|
||
+ Plan {
|
||
+ cwd: PathBuf::from("/home/someone/work/web"),
|
||
+ session: "ses_abc123".to_string(),
|
||
+ base: "https://api.hanzo.ai/v1".to_string(),
|
||
+ token: "hk-secret-value".to_string(),
|
||
+ route: "/settings".to_string(),
|
||
+ files: vec!["src/app/settings/page.tsx".to_string()],
|
||
+ slug: "settings".to_string(),
|
||
+ n: 7,
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn the_payload_names_the_object_and_what_earned_it() {
|
||
+ let p = plan();
|
||
+ let key = format!("{}/{}/{}-{}.png", BUCKET, p.session, p.n, p.slug);
|
||
+ let body = p.payload(&key, "http://127.0.0.1:3000");
|
||
+ assert_eq!(body["tool"], "shot");
|
||
+ assert_eq!(body["route"], "/settings");
|
||
+ assert_eq!(body["bucket"], "sessions");
|
||
+ assert_eq!(body["key"], "sessions/ses_abc123/7-settings.png");
|
||
+ assert_eq!(body["origin"], "http://127.0.0.1:3000");
|
||
+ assert_eq!(body["viewport"]["width"], 1280);
|
||
+ assert_eq!(body["viewport"]["height"], 800);
|
||
+ assert_eq!(body["files"][0], "src/app/settings/page.tsx");
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn the_payload_carries_no_credential() {
|
||
+ let p = plan();
|
||
+ let body = p.payload("sessions/ses_abc123/7-settings.png", "http://127.0.0.1:3000").to_string();
|
||
+ assert!(!body.contains(&p.token));
|
||
+ assert!(!body.contains("hk-"));
|
||
+ // A presigned URL is a bearer with an expiry. It must never be written down.
|
||
+ assert!(!body.contains("X-Amz-Signature"));
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn the_payload_names_no_absolute_path() {
|
||
+ let body = plan()
|
||
+ .payload("sessions/ses_abc123/7-settings.png", "http://127.0.0.1:3000")
|
||
+ .to_string();
|
||
+ assert!(!body.contains("/home/someone"));
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn the_payload_fits_the_log() {
|
||
+ // Cloud refuses a payload over 64 KiB. The cap on named files is what
|
||
+ // keeps this true for a turn that rewrites a design system.
|
||
+ let mut p = plan();
|
||
+ p.files = (0..NAMED)
|
||
+ .map(|i| format!("src/app/{}/page.tsx", "n".repeat(200 + i)))
|
||
+ .collect();
|
||
+ let body = p.payload("sessions/ses_abc123/7-settings.png", "http://127.0.0.1:3000");
|
||
+ assert!(body.to_string().len() < 64 * 1024);
|
||
+ }
|
||
+
|
||
+ /// The three calls, against a stand-in for cloud. This is the contract:
|
||
+ /// which doors, in which order, carrying what — and, load-bearing, which of
|
||
+ /// them the org bearer is allowed anywhere near.
|
||
+ #[tokio::test]
|
||
+ async fn publishing_is_three_calls_and_one_of_them_is_unauthenticated() {
|
||
+ let cloud = MockServer::start().await;
|
||
+ let key = "sessions/ses_abc123/7-settings.png";
|
||
+ let signed = format!("{}/bucket/object?X-Amz-Signature=deadbeef", cloud.uri());
|
||
+
|
||
+ Mock::given(method("POST"))
|
||
+ .and(path("/v1/s3/buckets/sessions/objects"))
|
||
+ .and(header("authorization", "Bearer hk-secret-value"))
|
||
+ .and(body_json(json!({ "key": key })))
|
||
+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||
+ "url": signed, "method": "PUT", "key": key, "expiresIn": 900
|
||
+ })))
|
||
+ .expect(1)
|
||
+ .mount(&cloud)
|
||
+ .await;
|
||
+
|
||
+ // The signature IS the credential for this URL. Sending our bearer along
|
||
+ // would hand an org key to whatever host cloud signed against.
|
||
+ Mock::given(method("PUT"))
|
||
+ .and(path("/bucket/object"))
|
||
+ .and(header("content-type", "image/png"))
|
||
+ .and(|req: &Request| req.headers.get("authorization").is_none())
|
||
+ .respond_with(ResponseTemplate::new(200))
|
||
+ .expect(1)
|
||
+ .mount(&cloud)
|
||
+ .await;
|
||
+
|
||
+ Mock::given(method("POST"))
|
||
+ .and(path("/v1/agents/sessions/ses_abc123/events"))
|
||
+ .and(header("authorization", "Bearer hk-secret-value"))
|
||
+ .and(body_json(json!({
|
||
+ "kind": "tool-call",
|
||
+ "payload": {
|
||
+ "tool": "shot",
|
||
+ "route": "/settings",
|
||
+ "bucket": "sessions",
|
||
+ "key": key,
|
||
+ "origin": "http://127.0.0.1:3000",
|
||
+ "viewport": { "width": 1280, "height": 800 },
|
||
+ "files": ["src/app/settings/page.tsx"],
|
||
+ }
|
||
+ })))
|
||
+ .respond_with(ResponseTemplate::new(201).set_body_json(json!({ "seq": 12 })))
|
||
+ .expect(1)
|
||
+ .mount(&cloud)
|
||
+ .await;
|
||
+
|
||
+ let mut p = plan();
|
||
+ p.base = format!("{}/v1", cloud.uri());
|
||
+ let http = create_client("shot");
|
||
+ let url = p.presign(&http, key).await.unwrap();
|
||
+ assert_eq!(url, signed);
|
||
+ put(&http, &url, b"\x89PNG\r\n\x1a\n".to_vec()).await.unwrap();
|
||
+ p.append(&http, key, "http://127.0.0.1:3000").await.unwrap();
|
||
+ // Dropping the server asserts every `expect(1)` was met exactly once.
|
||
+ }
|
||
+
|
||
+ /// Cloud refusing is not the agent's problem, but it must be reported as a
|
||
+ /// refusal rather than swallowed as success.
|
||
+ #[tokio::test]
|
||
+ async fn a_refusal_is_an_error_not_a_shrug() {
|
||
+ let cloud = MockServer::start().await;
|
||
+ Mock::given(method("POST"))
|
||
+ .respond_with(ResponseTemplate::new(422).set_body_json(json!({ "error": "leak" })))
|
||
+ .mount(&cloud)
|
||
+ .await;
|
||
+ let mut p = plan();
|
||
+ p.base = format!("{}/v1", cloud.uri());
|
||
+ let http = create_client("shot");
|
||
+ assert!(p.presign(&http, "k.png").await.is_err());
|
||
+ assert!(p.append(&http, "k.png", "http://127.0.0.1:3000").await.is_err());
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_session_id_cannot_retarget_the_request() {
|
||
+ for bad in [
|
||
+ "",
|
||
+ " ",
|
||
+ "../../admin",
|
||
+ "ses_abc/../../other",
|
||
+ "ses abc",
|
||
+ "ses_abc?x=1",
|
||
+ "ses_abc#frag",
|
||
+ "ses_abc/events",
|
||
+ "https://evil.example/",
|
||
+ ] {
|
||
+ assert_eq!(handle(bad), None, "{bad} must be refused");
|
||
+ }
|
||
+ assert_eq!(handle(" ses_abc123 "), Some("ses_abc123"));
|
||
+ assert_eq!(handle("ses-ABC_123"), Some("ses-ABC_123"));
|
||
+ assert_eq!(handle(&"a".repeat(128)), Some("a".repeat(128).as_str()));
|
||
+ assert_eq!(handle(&"a".repeat(129)), None);
|
||
+ }
|
||
+}
|
||
diff --git a/core/src/shot/ui.rs b/core/src/shot/ui.rs
|
||
new file mode 100644
|
||
index 0000000..e7218cd
|
||
--- /dev/null
|
||
+++ b/core/src/shot/ui.rs
|
||
@@ -0,0 +1,460 @@
|
||
+//! What a diff touched, whether any of it renders, and where it renders.
|
||
+//!
|
||
+//! Everything here is a decision over strings and paths — no clock, no network,
|
||
+//! no filesystem. That is the point of the split: the effectful half next door
|
||
+//! has nothing left to decide, and every rule below can be read straight out of
|
||
+//! a test instead of out of a running browser.
|
||
+
|
||
+use std::path::Path;
|
||
+use std::path::PathBuf;
|
||
+
|
||
+/// Extensions whose content IS the rendered surface.
|
||
+///
|
||
+/// Deliberately short. A picture costs a browser launch and, more expensively, a
|
||
+/// reviewer's attention — so the predicate only fires on extensions that cannot
|
||
+/// mean anything except markup or style. `.ts` and `.js` are absent on purpose:
|
||
+/// most of them are logic, and Next's `app/api/route.ts` is a server handler
|
||
+/// sitting in the middle of the UI tree. A change that genuinely moves pixels
|
||
+/// nearly always arrives with a partner carrying one of these.
|
||
+const RENDERED: [&str; 9] = [
|
||
+ "astro", "css", "html", "jsx", "less", "scss", "svelte", "tsx", "vue",
|
||
+];
|
||
+
|
||
+/// Directories whose contents were downloaded or generated. A `.css` under any
|
||
+/// of them is output, not authorship, and photographing it tells a reviewer
|
||
+/// nothing about what the agent did.
|
||
+const DERIVED: [&str; 8] = [
|
||
+ ".next",
|
||
+ ".nuxt",
|
||
+ ".svelte-kit",
|
||
+ "build",
|
||
+ "dist",
|
||
+ "node_modules",
|
||
+ "out",
|
||
+ "target",
|
||
+];
|
||
+
|
||
+/// Directory names that route by nesting: the URL is the directory path and only
|
||
+/// the router's own leaf file marks a directory as a route.
|
||
+const NESTED: [&str; 2] = ["app", "routes"];
|
||
+
|
||
+/// The directory that routes by filename: `pages/about.tsx` is `/about`.
|
||
+const FLAT: &str = "pages";
|
||
+
|
||
+/// Leaf filenames that name their directory rather than a segment of their own —
|
||
+/// Next's `page.tsx`, SvelteKit's `+page.svelte`.
|
||
+const LEAF: [&str; 2] = ["page", "+page"];
|
||
+
|
||
+/// The paths a unified diff writes.
|
||
+///
|
||
+/// `+++ b/<path>` is the post-image name: the file as it exists *after* the
|
||
+/// patch, which is the only version there is anything to photograph. A deletion
|
||
+/// writes `+++ /dev/null` and names nothing, so it drops out here for free.
|
||
+///
|
||
+/// Paths arrive relative to the git root when there is one and absolute when
|
||
+/// there is not, so callers must relativize before publishing anything.
|
||
+pub(crate) fn touched(diff: &str) -> Vec<PathBuf> {
|
||
+ diff.lines()
|
||
+ .filter_map(|line| line.strip_prefix("+++ "))
|
||
+ // Some unified-diff writers append a tab and a timestamp to the header.
|
||
+ .map(|rest| rest.split('\t').next().unwrap_or(rest))
|
||
+ .filter_map(|rest| rest.strip_prefix("b/"))
|
||
+ .filter(|rest| !rest.is_empty())
|
||
+ .map(PathBuf::from)
|
||
+ .collect()
|
||
+}
|
||
+
|
||
+/// Whether this file, changed, is a reason to look at a page.
|
||
+pub(crate) fn rendered(path: &Path) -> bool {
|
||
+ if path
|
||
+ .components()
|
||
+ .any(|c| DERIVED.contains(&c.as_os_str().to_string_lossy().as_ref()))
|
||
+ {
|
||
+ return false;
|
||
+ }
|
||
+ match path.extension().and_then(|e| e.to_str()) {
|
||
+ Some(ext) => RENDERED.contains(&ext.to_ascii_lowercase().as_str()),
|
||
+ None => false,
|
||
+ }
|
||
+}
|
||
+
|
||
+/// The URL path a source file renders at, for the file-routed frameworks where
|
||
+/// the answer is written in the path itself — Next (both routers), SvelteKit,
|
||
+/// Nuxt, Astro, Remix.
|
||
+///
|
||
+/// `None` means "not derivable", and the caller opens the site root. That is the
|
||
+/// deliberate answer for a dynamic segment like `[slug]`: a reviewer can tell a
|
||
+/// front page from the page they expected, but cannot tell an invented id from a
|
||
+/// broken route.
|
||
+pub(crate) fn route(path: &Path) -> Option<String> {
|
||
+ let parts: Vec<String> = path
|
||
+ .components()
|
||
+ .map(|c| c.as_os_str().to_string_lossy().into_owned())
|
||
+ .collect();
|
||
+ let file = parts.last()?;
|
||
+ let stem = Path::new(file).file_stem()?.to_str()?;
|
||
+
|
||
+ // The router directory this file hangs off. Innermost wins, so a `pages/`
|
||
+ // nested inside an `app/` routes the file that sits under it.
|
||
+ let root = parts
|
||
+ .iter()
|
||
+ .rposition(|p| NESTED.contains(&p.as_str()) || p == FLAT)?;
|
||
+ let mut segs: Vec<&str> = parts[root + 1..parts.len() - 1]
|
||
+ .iter()
|
||
+ .map(String::as_str)
|
||
+ .collect();
|
||
+
|
||
+ if parts[root] == FLAT {
|
||
+ // `pages/` names a route by the file; `index` names the directory.
|
||
+ if stem != "index" {
|
||
+ segs.push(stem);
|
||
+ }
|
||
+ } else if !LEAF.contains(&stem) {
|
||
+ // Under `app/` and `routes/` a directory is a route only because the
|
||
+ // router's leaf file says so. Everything else living there — a stylesheet,
|
||
+ // a component, a layout — has no URL of its own.
|
||
+ return None;
|
||
+ }
|
||
+
|
||
+ let mut url = String::new();
|
||
+ for seg in segs {
|
||
+ // A Next route group organises files without appearing in the URL.
|
||
+ if seg.starts_with('(') && seg.ends_with(')') {
|
||
+ continue;
|
||
+ }
|
||
+ if dynamic(seg) {
|
||
+ return None;
|
||
+ }
|
||
+ url.push('/');
|
||
+ url.push_str(seg);
|
||
+ }
|
||
+ Some(if url.is_empty() { "/".to_string() } else { url })
|
||
+}
|
||
+
|
||
+/// A path segment standing in for a value the agent does not have: Next's
|
||
+/// `[slug]`, SvelteKit's `[id]`, Nuxt's `_id`, Remix's `$id`.
|
||
+fn dynamic(seg: &str) -> bool {
|
||
+ seg.starts_with('[') || seg.starts_with('$') || seg.starts_with('_') || seg.starts_with(':')
|
||
+}
|
||
+
|
||
+/// The changed files, named the way they may be published: relative to the
|
||
+/// workspace, capped, and never absolute.
|
||
+///
|
||
+/// An absolute path carries the operator's home directory, and with it their
|
||
+/// account name, into a log the whole org can read. A diff can name a file
|
||
+/// outside the workspace, so falling back to the bare filename is the last line
|
||
+/// rather than a case that cannot happen.
|
||
+pub(crate) fn named(changed: &[PathBuf], cwd: &Path, cap: usize) -> Vec<String> {
|
||
+ changed
|
||
+ .iter()
|
||
+ .take(cap)
|
||
+ .map(|p| {
|
||
+ let short = p.strip_prefix(cwd).unwrap_or(p);
|
||
+ if short.is_absolute() {
|
||
+ Path::new(short.file_name().unwrap_or_default())
|
||
+ } else {
|
||
+ short
|
||
+ }
|
||
+ .to_string_lossy()
|
||
+ .into_owned()
|
||
+ })
|
||
+ .collect()
|
||
+}
|
||
+
|
||
+/// A short, key-safe name for the object, derived from the route when there is
|
||
+/// one and from the file otherwise. Lowercase alphanumerics and dashes only, so
|
||
+/// nothing here can shape the storage key it lands in.
|
||
+pub(crate) fn slug(route: &str, first: &Path) -> String {
|
||
+ let raw = if route == "/" {
|
||
+ first
|
||
+ .file_stem()
|
||
+ .map(|s| s.to_string_lossy().into_owned())
|
||
+ .unwrap_or_default()
|
||
+ } else {
|
||
+ route.to_string()
|
||
+ };
|
||
+ let mut out = String::new();
|
||
+ for ch in raw.chars() {
|
||
+ if ch.is_ascii_alphanumeric() {
|
||
+ out.extend(ch.to_lowercase());
|
||
+ } else if !out.ends_with('-') {
|
||
+ out.push('-');
|
||
+ }
|
||
+ if out.len() >= 48 {
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+ let trimmed = out.trim_matches('-');
|
||
+ if trimmed.is_empty() {
|
||
+ "page".to_string()
|
||
+ } else {
|
||
+ trimmed.to_string()
|
||
+ }
|
||
+}
|
||
+
|
||
+/// The port the repository serves its UI on, read out of `package.json`: the
|
||
+/// repository's own dev script first, then the framework's default.
|
||
+///
|
||
+/// Reading, never running. The scripts in a `package.json` are arbitrary code
|
||
+/// belonging to the tree the agent is editing; this function parses one for a
|
||
+/// number and nothing else.
|
||
+pub(crate) fn port(manifest: &str) -> Option<u16> {
|
||
+ let doc: serde_json::Value = serde_json::from_str(manifest).ok()?;
|
||
+ let script = ["dev", "start", "serve"]
|
||
+ .iter()
|
||
+ .find_map(|name| doc["scripts"][name].as_str());
|
||
+ if let Some(explicit) = script.and_then(declared) {
|
||
+ return Some(explicit);
|
||
+ }
|
||
+ let deps = ["dependencies", "devDependencies"];
|
||
+ let has = |name: &str| deps.iter().any(|d| !doc[d][name].is_null());
|
||
+ // Ordered most specific first: SvelteKit and Nuxt both also carry the
|
||
+ // bundler they are built on, and the framework's port is the one that wins.
|
||
+ let by_framework = [
|
||
+ ("@sveltejs/kit", 5173u16),
|
||
+ ("nuxt", 3000),
|
||
+ ("astro", 4321),
|
||
+ ("@angular/cli", 4200),
|
||
+ ("gatsby", 8000),
|
||
+ ("next", 3000),
|
||
+ ("react-scripts", 3000),
|
||
+ ("@remix-run/dev", 3000),
|
||
+ ("vite", 5173),
|
||
+ ("@vitejs/plugin-react", 5173),
|
||
+ ];
|
||
+ by_framework
|
||
+ .iter()
|
||
+ .find(|(name, _)| has(name))
|
||
+ .map(|(_, port)| *port)
|
||
+ .or(script.map(|_| 3000))
|
||
+}
|
||
+
|
||
+/// A port named on a dev script's own command line: `--port 4000`, `--port=4000`
|
||
+/// or `-p 4000`.
|
||
+fn declared(script: &str) -> Option<u16> {
|
||
+ let mut words = script.split_whitespace().peekable();
|
||
+ while let Some(word) = words.next() {
|
||
+ let value = match word.split_once('=') {
|
||
+ Some(("--port", v)) => Some(v),
|
||
+ _ if word == "--port" || word == "-p" => words.peek().copied(),
|
||
+ _ => None,
|
||
+ };
|
||
+ if let Some(parsed) = value.and_then(|v| v.parse::<u16>().ok()) {
|
||
+ return Some(parsed);
|
||
+ }
|
||
+ }
|
||
+ None
|
||
+}
|
||
+
|
||
+#[cfg(test)]
|
||
+mod tests {
|
||
+ use super::*;
|
||
+
|
||
+ fn diff_for(paths: &[&str]) -> String {
|
||
+ paths
|
||
+ .iter()
|
||
+ .map(|p| {
|
||
+ format!("diff --git a/{p} b/{p}\nindex 000..111\n--- a/{p}\n+++ b/{p}\n@@ -1 +1 @@\n-a\n+b\n")
|
||
+ })
|
||
+ .collect()
|
||
+ }
|
||
+
|
||
+ fn ui_of(paths: &[&str]) -> Vec<PathBuf> {
|
||
+ touched(&diff_for(paths))
|
||
+ .into_iter()
|
||
+ .filter(|p| rendered(p))
|
||
+ .collect()
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_ui_file_is_a_reason_to_look() {
|
||
+ for path in [
|
||
+ "src/app/page.tsx",
|
||
+ "src/Button.jsx",
|
||
+ "components/Card.vue",
|
||
+ "src/routes/+page.svelte",
|
||
+ "public/index.html",
|
||
+ "styles/main.css",
|
||
+ "styles/main.scss",
|
||
+ "styles/main.less",
|
||
+ "src/pages/about.astro",
|
||
+ ] {
|
||
+ assert!(rendered(Path::new(path)), "{path} should count as UI");
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_backend_file_is_not() {
|
||
+ for path in [
|
||
+ "apps/agents/sessions.go",
|
||
+ "core/src/shot/mod.rs",
|
||
+ "scripts/deploy.py",
|
||
+ "migrations/001.sql",
|
||
+ "Cargo.toml",
|
||
+ "LLM.md",
|
||
+ "src/lib/format.ts",
|
||
+ "src/app/api/route.ts",
|
||
+ "Makefile",
|
||
+ ] {
|
||
+ assert!(!rendered(Path::new(path)), "{path} should not count as UI");
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn generated_output_is_not_authorship() {
|
||
+ for path in [
|
||
+ "node_modules/react/index.css",
|
||
+ "dist/app.css",
|
||
+ ".next/static/chunk.css",
|
||
+ "build/index.html",
|
||
+ "out/index.html",
|
||
+ "target/doc/style.css",
|
||
+ ] {
|
||
+ assert!(!rendered(Path::new(path)), "{path} is derived, not UI");
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_backend_only_diff_yields_nothing() {
|
||
+ assert!(ui_of(&["apps/agents/sessions.go", "core/src/codex.rs"]).is_empty());
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_mixed_diff_yields_the_ui_half() {
|
||
+ let ui = ui_of(&[
|
||
+ "apps/agents/sessions.go",
|
||
+ "web/src/app/settings/page.tsx",
|
||
+ "core/src/codex.rs",
|
||
+ ]);
|
||
+ assert_eq!(ui, vec![PathBuf::from("web/src/app/settings/page.tsx")]);
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_deleted_file_names_nothing() {
|
||
+ let diff = "diff --git a/src/Old.tsx b/src/Old.tsx\ndeleted file mode 100644\n--- a/src/Old.tsx\n+++ /dev/null\n@@ -1 +0,0 @@\n-gone\n";
|
||
+ assert!(touched(diff).is_empty());
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_rename_names_the_destination() {
|
||
+ let diff = "diff --git a/src/A.tsx b/src/B.tsx\nindex 000..111\n--- a/src/A.tsx\n+++ b/src/B.tsx\n@@ -1 +1 @@\n-a\n+b\n";
|
||
+ assert_eq!(touched(diff), vec![PathBuf::from("src/B.tsx")]);
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_header_timestamp_is_not_part_of_the_path() {
|
||
+ let diff = "--- a/src/A.tsx\t2026-01-01 00:00:00\n+++ b/src/A.tsx\t2026-01-01 00:00:01\n";
|
||
+ assert_eq!(touched(diff), vec![PathBuf::from("src/A.tsx")]);
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn an_absolute_path_survives_intact() {
|
||
+ let diff = "--- a//home/z/web/src/A.tsx\n+++ b//home/z/web/src/A.tsx\n";
|
||
+ assert_eq!(touched(diff), vec![PathBuf::from("/home/z/web/src/A.tsx")]);
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn routes_are_read_off_the_path() {
|
||
+ let cases = [
|
||
+ ("src/app/page.tsx", Some("/")),
|
||
+ ("src/app/settings/page.tsx", Some("/settings")),
|
||
+ ("app/(marketing)/pricing/page.tsx", Some("/pricing")),
|
||
+ ("src/routes/blog/+page.svelte", Some("/blog")),
|
||
+ ("src/pages/about.tsx", Some("/about")),
|
||
+ ("src/pages/index.tsx", Some("/")),
|
||
+ ("src/pages/blog/index.vue", Some("/blog")),
|
||
+ ("src/pages/docs/intro.astro", Some("/docs/intro")),
|
||
+ ];
|
||
+ for (path, want) in cases {
|
||
+ assert_eq!(
|
||
+ route(Path::new(path)).as_deref(),
|
||
+ want,
|
||
+ "route for {path}"
|
||
+ );
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn an_underivable_route_is_the_root() {
|
||
+ for path in [
|
||
+ // A value the agent does not have.
|
||
+ "app/blog/[slug]/page.tsx",
|
||
+ "src/routes/user/[id]/+page.svelte",
|
||
+ "src/pages/posts/_id.vue",
|
||
+ "app/$team/page.tsx",
|
||
+ // Not a route at all: a component, a stylesheet, a layout.
|
||
+ "src/components/Button.tsx",
|
||
+ "src/app/globals.css",
|
||
+ "src/app/layout.tsx",
|
||
+ "src/routes/styles.css",
|
||
+ ] {
|
||
+ assert_eq!(route(Path::new(path)), None, "route for {path}");
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn published_names_are_never_absolute() {
|
||
+ let cwd = Path::new("/home/someone/work/web");
|
||
+ let changed = [
|
||
+ PathBuf::from("/home/someone/work/web/src/app/page.tsx"),
|
||
+ PathBuf::from("src/Button.tsx"),
|
||
+ // A diff can name a file the workspace does not contain.
|
||
+ PathBuf::from("/etc/skel/theme.css"),
|
||
+ ];
|
||
+ let out = named(&changed, cwd, 10);
|
||
+ assert_eq!(out, ["src/app/page.tsx", "src/Button.tsx", "theme.css"]);
|
||
+ for name in &out {
|
||
+ assert!(!name.starts_with('/'), "{name} leaks a filesystem location");
|
||
+ assert!(!name.contains("someone"), "{name} leaks an account name");
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn published_names_are_capped() {
|
||
+ let changed: Vec<PathBuf> = (0..50).map(|i| PathBuf::from(format!("a/{i}.tsx"))).collect();
|
||
+ assert_eq!(named(&changed, Path::new("/w"), 10).len(), 10);
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn slugs_carry_no_punctuation() {
|
||
+ assert_eq!(slug("/settings/team", Path::new("x.tsx")), "settings-team");
|
||
+ assert_eq!(slug("/", Path::new("src/Button.tsx")), "button");
|
||
+ assert_eq!(slug("/", Path::new("src/.hidden")), "hidden");
|
||
+ assert_eq!(slug("/", Path::new("/")), "page");
|
||
+ assert!(slug("/a/very/long/route/that/keeps/going/on/and/on/forever/and/ever", Path::new("x.tsx")).len() <= 48);
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn the_dev_script_names_its_own_port() {
|
||
+ for (script, want) in [
|
||
+ (r#"{"scripts":{"dev":"next dev --port 4000"}}"#, 4000u16),
|
||
+ (r#"{"scripts":{"dev":"next dev --port=4100"}}"#, 4100),
|
||
+ (r#"{"scripts":{"dev":"vite -p 4200"}}"#, 4200),
|
||
+ ] {
|
||
+ assert_eq!(port(script), Some(want), "{script}");
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn the_framework_names_the_default() {
|
||
+ for (manifest, want) in [
|
||
+ (r#"{"scripts":{"dev":"next dev"},"dependencies":{"next":"15"}}"#, 3000u16),
|
||
+ (r#"{"scripts":{"dev":"vite"},"devDependencies":{"vite":"5"}}"#, 5173),
|
||
+ (r#"{"scripts":{"dev":"astro dev"},"dependencies":{"astro":"4"}}"#, 4321),
|
||
+ (r#"{"scripts":{"dev":"ng serve"},"devDependencies":{"@angular/cli":"18"}}"#, 4200),
|
||
+ (
|
||
+ r#"{"scripts":{"dev":"vite dev"},"devDependencies":{"@sveltejs/kit":"2","vite":"5"}}"#,
|
||
+ 5173,
|
||
+ ),
|
||
+ (r#"{"scripts":{"dev":"nuxt dev"},"dependencies":{"nuxt":"3","vite":"5"}}"#, 3000),
|
||
+ ] {
|
||
+ assert_eq!(port(manifest), Some(want), "{manifest}");
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn a_manifest_that_serves_nothing_names_no_port() {
|
||
+ assert_eq!(port(r#"{"name":"lib","scripts":{"build":"tsc"}}"#), None);
|
||
+ assert_eq!(port("not json"), None);
|
||
+ }
|
||
+}
|
||
diff --git a/core/src/skills/assets/samples/plan/SKILL.md b/core/src/skills/assets/samples/plan/SKILL.md
|
||
index 5d49c33..3edd75e 100644
|
||
--- a/core/src/skills/assets/samples/plan/SKILL.md
|
||
+++ b/core/src/skills/assets/samples/plan/SKILL.md
|
||
@@ -1,6 +1,6 @@
|
||
---
|
||
name: plan
|
||
-description: Generate a plan for how an agent should accomplish a complex coding task. Use when a user asks for a plan, and optionally when they want to save, find, read, update, or delete plan files in $CODEX_HOME/plans (default ~/.codex/plans).
|
||
+description: Generate a plan for how an agent should accomplish a complex coding task. Use when a user asks for a plan, and optionally when they want to save, find, read, update, or delete plan files in $DEV_HOME/plans (default ~/.codex/plans).
|
||
metadata:
|
||
short-description: Generate a plan for a complex task
|
||
---
|
||
@@ -17,7 +17,7 @@ This skill can also be used to draft codebase or system overviews.
|
||
|
||
## Core rules
|
||
|
||
-- Resolve the plans directory as `$CODEX_HOME/plans` or `~/.codex/plans` when `CODEX_HOME` is not set.
|
||
+- Resolve the plans directory as `$DEV_HOME/plans` or `~/.codex/plans` when `DEV_HOME` is not set.
|
||
- Create the plans directory if it does not exist.
|
||
- Never write to the repo; only read files to understand context.
|
||
- Require frontmatter with **only** `name` and `description` (single-line values) for on-disk plans.
|
||
diff --git a/core/src/skills/assets/samples/plan/scripts/create_plan.py b/core/src/skills/assets/samples/plan/scripts/create_plan.py
|
||
index 4cbfa8f..8d15ae4 100644
|
||
--- a/core/src/skills/assets/samples/plan/scripts/create_plan.py
|
||
+++ b/core/src/skills/assets/samples/plan/scripts/create_plan.py
|
||
@@ -1,5 +1,5 @@
|
||
#!/usr/bin/env python3
|
||
-"""Create or overwrite a plan markdown file in $CODEX_HOME/plans."""
|
||
+"""Create or overwrite a plan markdown file in $DEV_HOME/plans."""
|
||
|
||
from __future__ import annotations
|
||
|
||
@@ -61,7 +61,7 @@ def read_body(args: argparse.Namespace) -> str | None:
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(
|
||
- description="Create a plan file under $CODEX_HOME/plans or ~/.codex/plans."
|
||
+ description="Create a plan file under $DEV_HOME/plans or ~/.codex/plans."
|
||
)
|
||
parser.add_argument("--name", required=True, help="Plan name (lower-case, hyphen-delimited).")
|
||
parser.add_argument("--description", required=True, help="Short plan description.")
|
||
diff --git a/core/src/skills/assets/samples/plan/scripts/list_plans.py b/core/src/skills/assets/samples/plan/scripts/list_plans.py
|
||
index db6c412..689cead 100644
|
||
--- a/core/src/skills/assets/samples/plan/scripts/list_plans.py
|
||
+++ b/core/src/skills/assets/samples/plan/scripts/list_plans.py
|
||
@@ -10,7 +10,7 @@ from plan_utils import get_plans_dir, parse_frontmatter
|
||
|
||
|
||
def main() -> int:
|
||
- parser = argparse.ArgumentParser(description="List plan summaries from $CODEX_HOME/plans.")
|
||
+ parser = argparse.ArgumentParser(description="List plan summaries from $DEV_HOME/plans.")
|
||
parser.add_argument("--query", help="Case-insensitive substring to filter name/description.")
|
||
parser.add_argument("--json", action="store_true", help="Emit JSON output.")
|
||
args = parser.parse_args()
|
||
diff --git a/core/src/skills/assets/samples/plan/scripts/plan_utils.py b/core/src/skills/assets/samples/plan/scripts/plan_utils.py
|
||
index 1a36a4b..4921be6 100644
|
||
--- a/core/src/skills/assets/samples/plan/scripts/plan_utils.py
|
||
+++ b/core/src/skills/assets/samples/plan/scripts/plan_utils.py
|
||
@@ -11,8 +11,8 @@ _NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||
|
||
|
||
def get_codex_home() -> Path:
|
||
- """Return CODEX_HOME if set, else ~/.codex."""
|
||
- return Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
|
||
+ """Return DEV_HOME if set, else ~/.codex."""
|
||
+ return Path(os.environ.get("DEV_HOME", "~/.codex")).expanduser()
|
||
|
||
|
||
def get_plans_dir() -> Path:
|
||
diff --git a/core/src/skills/assets/samples/skill-installer/SKILL.md b/core/src/skills/assets/samples/skill-installer/SKILL.md
|
||
index 857c32d..13a7f37 100644
|
||
--- a/core/src/skills/assets/samples/skill-installer/SKILL.md
|
||
+++ b/core/src/skills/assets/samples/skill-installer/SKILL.md
|
||
@@ -1,6 +1,6 @@
|
||
---
|
||
name: skill-installer
|
||
-description: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).
|
||
+description: Install Codex skills into $DEV_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).
|
||
metadata:
|
||
short-description: Install curated skills from openai/skills or other repos
|
||
---
|
||
@@ -27,7 +27,7 @@ Skills from {repo}:
|
||
Which ones would you like installed?
|
||
"""
|
||
|
||
-After installing a skill, tell the user: "Restart Codex to pick up new skills."
|
||
+After installing a skill, tell the user: "Restart Hanzo Dev to pick up new skills."
|
||
|
||
## Scripts
|
||
|
||
@@ -43,7 +43,7 @@ All of these scripts use network, so when running in the sandbox, request escala
|
||
- Defaults to direct download for public GitHub repos.
|
||
- If download fails with auth/permission errors, falls back to git sparse checkout.
|
||
- Aborts if the destination skill directory already exists.
|
||
-- Installs into `$CODEX_HOME/skills/<skill-name>` (defaults to `~/.codex/skills`).
|
||
+- Installs into `$DEV_HOME/skills/<skill-name>` (defaults to `~/.codex/skills`).
|
||
- Multiple `--path` values install multiple skills in one run, each named from the path basename unless `--name` is supplied.
|
||
- Options: `--ref <ref>` (default `main`), `--dest <path>`, `--method auto|download|git`.
|
||
|
||
@@ -53,4 +53,4 @@ All of these scripts use network, so when running in the sandbox, request escala
|
||
- Private GitHub repos can be accessed via existing git credentials or optional `GITHUB_TOKEN`/`GH_TOKEN` for download.
|
||
- Git fallback tries HTTPS first, then SSH.
|
||
- The skills at https://github.com/openai/skills/tree/main/skills/.system are preinstalled, so no need to help users install those. If they ask, just explain this. If they insist, you can download and overwrite.
|
||
-- Installed annotations come from `$CODEX_HOME/skills`.
|
||
+- Installed annotations come from `$DEV_HOME/skills`.
|
||
diff --git a/core/src/skills/assets/samples/skill-installer/scripts/install-skill-from-github.py b/core/src/skills/assets/samples/skill-installer/scripts/install-skill-from-github.py
|
||
index 5c0d8e6..b217005 100644
|
||
--- a/core/src/skills/assets/samples/skill-installer/scripts/install-skill-from-github.py
|
||
+++ b/core/src/skills/assets/samples/skill-installer/scripts/install-skill-from-github.py
|
||
@@ -1,5 +1,5 @@
|
||
#!/usr/bin/env python3
|
||
-"""Install a skill from a GitHub repo path into $CODEX_HOME/skills."""
|
||
+"""Install a skill from a GitHub repo path into $DEV_HOME/skills."""
|
||
|
||
from __future__ import annotations
|
||
|
||
@@ -44,7 +44,7 @@ class InstallError(Exception):
|
||
|
||
|
||
def _codex_home() -> str:
|
||
- return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex"))
|
||
+ return os.environ.get("DEV_HOME", os.path.expanduser("~/.codex"))
|
||
|
||
|
||
def _tmp_root() -> str:
|
||
diff --git a/core/src/skills/assets/samples/skill-installer/scripts/list-curated-skills.py b/core/src/skills/assets/samples/skill-installer/scripts/list-curated-skills.py
|
||
index 0977c29..f8cf463 100644
|
||
--- a/core/src/skills/assets/samples/skill-installer/scripts/list-curated-skills.py
|
||
+++ b/core/src/skills/assets/samples/skill-installer/scripts/list-curated-skills.py
|
||
@@ -32,7 +32,7 @@ def _request(url: str) -> bytes:
|
||
|
||
|
||
def _codex_home() -> str:
|
||
- return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex"))
|
||
+ return os.environ.get("DEV_HOME", os.path.expanduser("~/.codex"))
|
||
|
||
|
||
def _installed_skills() -> set[str]:
|
||
diff --git a/core/src/skills/system.rs b/core/src/skills/system.rs
|
||
index d086603..5fa9de6 100644
|
||
--- a/core/src/skills/system.rs
|
||
+++ b/core/src/skills/system.rs
|
||
@@ -80,12 +80,12 @@ const EMBEDDED_SYSTEM_SKILLS: &[(&str, &[u8])] = &[
|
||
|
||
/// Returns the on-disk cache location for embedded system skills.
|
||
///
|
||
-/// This is typically located at `CODEX_HOME/skills/.system`.
|
||
+/// This is typically located at `DEV_HOME/skills/.system`.
|
||
pub(crate) fn system_cache_root_dir(code_home: &Path) -> PathBuf {
|
||
code_home.join(SKILLS_DIR_NAME).join(SYSTEM_SKILLS_DIR_NAME)
|
||
}
|
||
|
||
-/// Installs embedded system skills into `CODEX_HOME/skills/.system`.
|
||
+/// Installs embedded system skills into `DEV_HOME/skills/.system`.
|
||
///
|
||
/// Clears any existing system skills directory first and then writes the embedded
|
||
/// skills directory into place.
|
||
diff --git a/core/src/spawn.rs b/core/src/spawn.rs
|
||
index f2a744c..4c02b94 100644
|
||
--- a/core/src/spawn.rs
|
||
+++ b/core/src/spawn.rs
|
||
@@ -18,12 +18,12 @@ use crate::protocol::SandboxPolicy;
|
||
///
|
||
/// We may try to have just one environment variable for all sandboxing
|
||
/// attributes, so this may change in the future.
|
||
-pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED";
|
||
+pub const DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "DEV_SANDBOX_NETWORK_DISABLED";
|
||
|
||
/// Should be set when the process is spawned under a sandbox. Currently, the
|
||
/// value is "seatbelt" for macOS, but it may change in the future to
|
||
/// accommodate sandboxing configuration and other sandboxing mechanisms.
|
||
-pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX";
|
||
+pub const DEV_SANDBOX_ENV_VAR: &str = "DEV_SANDBOX";
|
||
|
||
const SPAWN_RETRY_DELAYS_MS: [u64; 3] = [0, 10, 50];
|
||
|
||
@@ -96,7 +96,7 @@ pub enum StdioPolicy {
|
||
///
|
||
/// For now, we take `SandboxPolicy` as a parameter to spawn_child() because
|
||
/// we need to determine whether to set the
|
||
-/// `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable.
|
||
+/// `DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable.
|
||
pub(crate) async fn spawn_child_async(
|
||
program: PathBuf,
|
||
args: Vec<String>,
|
||
@@ -119,7 +119,7 @@ pub(crate) async fn spawn_child_async(
|
||
cmd.envs(env);
|
||
|
||
if !sandbox_policy.has_full_network_access() {
|
||
- cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
|
||
+ cmd.env(DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
|
||
}
|
||
|
||
// If this Codex process dies (including being killed via SIGKILL), we want
|
||
diff --git a/core/src/unified_exec/mod.rs b/core/src/unified_exec/mod.rs
|
||
index e3ff134..c074a86 100644
|
||
--- a/core/src/unified_exec/mod.rs
|
||
+++ b/core/src/unified_exec/mod.rs
|
||
@@ -442,7 +442,7 @@ mod tests {
|
||
session_id: Some(session_id),
|
||
input_chunks: &[
|
||
"export".to_string(),
|
||
- "CODEX_INTERACTIVE_SHELL_VAR=codex\n".to_string(),
|
||
+ "DEV_INTERACTIVE_SHELL_VAR=codex\n".to_string(),
|
||
],
|
||
timeout_ms: Some(2_500),
|
||
})
|
||
@@ -451,7 +451,7 @@ mod tests {
|
||
let out_2 = manager
|
||
.handle_request(UnifiedExecRequest {
|
||
session_id: Some(session_id),
|
||
- input_chunks: &["echo $CODEX_INTERACTIVE_SHELL_VAR\n".to_string()],
|
||
+ input_chunks: &["echo $DEV_INTERACTIVE_SHELL_VAR\n".to_string()],
|
||
timeout_ms: Some(2_500),
|
||
})
|
||
.await?;
|
||
@@ -477,7 +477,7 @@ mod tests {
|
||
manager
|
||
.handle_request(UnifiedExecRequest {
|
||
session_id: Some(session_a),
|
||
- input_chunks: &["export CODEX_INTERACTIVE_SHELL_VAR=codex\n".to_string()],
|
||
+ input_chunks: &["export DEV_INTERACTIVE_SHELL_VAR=codex\n".to_string()],
|
||
timeout_ms: Some(2_500),
|
||
})
|
||
.await?;
|
||
@@ -487,7 +487,7 @@ mod tests {
|
||
session_id: None,
|
||
input_chunks: &[
|
||
"echo".to_string(),
|
||
- "$CODEX_INTERACTIVE_SHELL_VAR\n".to_string(),
|
||
+ "$DEV_INTERACTIVE_SHELL_VAR\n".to_string(),
|
||
],
|
||
timeout_ms: Some(2_500),
|
||
})
|
||
@@ -497,7 +497,7 @@ mod tests {
|
||
let out_3 = manager
|
||
.handle_request(UnifiedExecRequest {
|
||
session_id: Some(session_a),
|
||
- input_chunks: &["echo $CODEX_INTERACTIVE_SHELL_VAR\n".to_string()],
|
||
+ input_chunks: &["echo $DEV_INTERACTIVE_SHELL_VAR\n".to_string()],
|
||
timeout_ms: Some(2_500),
|
||
})
|
||
.await?;
|
||
@@ -525,7 +525,7 @@ mod tests {
|
||
session_id: Some(session_id),
|
||
input_chunks: &[
|
||
"export".to_string(),
|
||
- "CODEX_INTERACTIVE_SHELL_VAR=codex\n".to_string(),
|
||
+ "DEV_INTERACTIVE_SHELL_VAR=codex\n".to_string(),
|
||
],
|
||
timeout_ms: Some(2_500),
|
||
})
|
||
@@ -534,7 +534,7 @@ mod tests {
|
||
let out_2 = manager
|
||
.handle_request(UnifiedExecRequest {
|
||
session_id: Some(session_id),
|
||
- input_chunks: &["sleep 5 && echo $CODEX_INTERACTIVE_SHELL_VAR\n".to_string()],
|
||
+ input_chunks: &["sleep 5 && echo $DEV_INTERACTIVE_SHELL_VAR\n".to_string()],
|
||
timeout_ms: Some(10),
|
||
})
|
||
.await?;
|
||
diff --git a/core/tests/common/mod.rs b/core/tests/common/mod.rs
|
||
index 7866127..078ec18 100644
|
||
--- a/core/tests/common/mod.rs
|
||
+++ b/core/tests/common/mod.rs
|
||
@@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex};
|
||
|
||
use code_core::config::{Config, ConfigOverrides, ConfigToml};
|
||
use code_core::protocol::EventMsg;
|
||
-use code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||
+use code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||
use code_core::CodexConversation;
|
||
use serde_json::Value;
|
||
use tempfile::TempDir;
|
||
@@ -108,7 +108,7 @@ where
|
||
|
||
/// Returns true when network-dependent tests should be skipped.
|
||
pub fn skip_if_no_network() -> bool {
|
||
- if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
+ if std::env::var(DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
println!(
|
||
"Skipping test because network access is disabled inside the sandbox."
|
||
);
|
||
diff --git a/core/tests/custom_prompts_discovery.rs b/core/tests/custom_prompts_discovery.rs
|
||
index 33d5d42..9949fd7 100644
|
||
--- a/core/tests/custom_prompts_discovery.rs
|
||
+++ b/core/tests/custom_prompts_discovery.rs
|
||
@@ -65,7 +65,7 @@ fn normalize_path_for_assertion(path: &Path) -> PathBuf {
|
||
#[tokio::test]
|
||
async fn discovers_prompts_from_code_home() -> Result<()> {
|
||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
||
- let env = EnvBackup::new(&["HOME", "CODE_HOME", "CODEX_HOME"]);
|
||
+ let env = EnvBackup::new(&["HOME", "CODE_HOME", "DEV_HOME"]);
|
||
|
||
let code_home = TempDir::new()?;
|
||
let prompts_dir = code_home.path().join("prompts");
|
||
@@ -74,7 +74,7 @@ async fn discovers_prompts_from_code_home() -> Result<()> {
|
||
fs::write(prompts_dir.join("beta.MD"), "# beta")?;
|
||
|
||
env.set_path("CODE_HOME", code_home.path());
|
||
- env.remove("CODEX_HOME");
|
||
+ env.remove("DEV_HOME");
|
||
|
||
let default_dir = default_prompts_dir().expect("expected prompts dir");
|
||
assert_eq!(
|
||
@@ -92,7 +92,7 @@ async fn discovers_prompts_from_code_home() -> Result<()> {
|
||
#[tokio::test]
|
||
async fn discovers_prompts_from_legacy_codex_home() -> Result<()> {
|
||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
||
- let env = EnvBackup::new(&["HOME", "CODE_HOME", "CODEX_HOME"]);
|
||
+ let env = EnvBackup::new(&["HOME", "CODE_HOME", "DEV_HOME"]);
|
||
|
||
let fake_home = TempDir::new()?;
|
||
let codex_home = fake_home.path().join(".codex");
|
||
@@ -102,7 +102,7 @@ async fn discovers_prompts_from_legacy_codex_home() -> Result<()> {
|
||
|
||
env.set_path("HOME", fake_home.path());
|
||
env.remove("CODE_HOME");
|
||
- env.remove("CODEX_HOME");
|
||
+ env.remove("DEV_HOME");
|
||
|
||
let default_dir = default_prompts_dir().expect("expected prompts dir");
|
||
assert_eq!(
|
||
@@ -120,7 +120,7 @@ async fn discovers_prompts_from_legacy_codex_home() -> Result<()> {
|
||
#[tokio::test]
|
||
async fn prefers_code_home_when_both_locations_exist() -> Result<()> {
|
||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
||
- let env = EnvBackup::new(&["HOME", "CODE_HOME", "CODEX_HOME"]);
|
||
+ let env = EnvBackup::new(&["HOME", "CODE_HOME", "DEV_HOME"]);
|
||
|
||
let fake_home = TempDir::new()?;
|
||
let code_home = fake_home.path().join(".code");
|
||
@@ -134,7 +134,7 @@ async fn prefers_code_home_when_both_locations_exist() -> Result<()> {
|
||
|
||
env.set_path("HOME", fake_home.path());
|
||
env.remove("CODE_HOME");
|
||
- env.remove("CODEX_HOME");
|
||
+ env.remove("DEV_HOME");
|
||
|
||
let default_dir = default_prompts_dir().expect("expected prompts dir");
|
||
assert_eq!(
|
||
@@ -152,7 +152,7 @@ async fn prefers_code_home_when_both_locations_exist() -> Result<()> {
|
||
#[tokio::test]
|
||
async fn ignores_non_markdown_files() -> Result<()> {
|
||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
||
- let env = EnvBackup::new(&["HOME", "CODE_HOME", "CODEX_HOME"]);
|
||
+ let env = EnvBackup::new(&["HOME", "CODE_HOME", "DEV_HOME"]);
|
||
|
||
let code_home = TempDir::new()?;
|
||
let prompts_dir = code_home.path().join("prompts");
|
||
@@ -161,7 +161,7 @@ async fn ignores_non_markdown_files() -> Result<()> {
|
||
fs::write(prompts_dir.join("ignore.txt"), "# ignore")?;
|
||
|
||
env.set_path("CODE_HOME", code_home.path());
|
||
- env.remove("CODEX_HOME");
|
||
+ env.remove("DEV_HOME");
|
||
|
||
let prompts = discover_prompts_in(&prompts_dir).await;
|
||
let names = prompt_names(&prompts);
|
||
diff --git a/core/tests/git_mutation_guard.rs b/core/tests/git_mutation_guard.rs
|
||
index cc1aef4..fd33029 100644
|
||
--- a/core/tests/git_mutation_guard.rs
|
||
+++ b/core/tests/git_mutation_guard.rs
|
||
@@ -8,24 +8,24 @@
|
||
use std::fs;
|
||
|
||
const TARGET_DIRS: &[&str] = &[
|
||
- "code-rs/tui/src",
|
||
- "code-rs/code-auto-drive-core/src",
|
||
+ "hanzo-dev/tui/src",
|
||
+ "hanzo-dev/code-auto-drive-core/src",
|
||
];
|
||
|
||
// Files allowed to host direct git invocations. They must also demonstrate an
|
||
// epoch bump reference (either direct `bump_snapshot_epoch` or a documented helper).
|
||
const ALLOW_LIST: &[(&str, &[&str])] = &[
|
||
// chatwidget uses run_git_command (bumps epoch) and ghost snapshots with post_commit_hook.
|
||
- ("code-rs/tui/src/chatwidget.rs", &["bump_snapshot_epoch", "run_git_command"]),
|
||
+ ("hanzo-dev/tui/src/chatwidget.rs", &["bump_snapshot_epoch", "run_git_command"]),
|
||
// get_git_diff is read-only; no epoch bump required, still allowed.
|
||
- ("code-rs/tui/src/get_git_diff.rs", &[]),
|
||
+ ("hanzo-dev/tui/src/get_git_diff.rs", &[]),
|
||
// auto_coordinator bumps epoch on mutating verbs inside run_git_command.
|
||
(
|
||
- "code-rs/code-auto-drive-core/src/auto_coordinator.rs",
|
||
+ "hanzo-dev/code-auto-drive-core/src/auto_coordinator.rs",
|
||
&["bump_snapshot_epoch", "run_git_command"],
|
||
),
|
||
// controller currently no direct git calls; kept to avoid churn, require no markers.
|
||
- ("code-rs/code-auto-drive-core/src/controller.rs", &[]),
|
||
+ ("hanzo-dev/code-auto-drive-core/src/controller.rs", &[]),
|
||
];
|
||
|
||
#[test]
|
||
diff --git a/core/tests/hanzo_identity.rs b/core/tests/hanzo_identity.rs
|
||
new file mode 100644
|
||
index 0000000..9d587a3
|
||
--- /dev/null
|
||
+++ b/core/tests/hanzo_identity.rs
|
||
@@ -0,0 +1,255 @@
|
||
+//! The product identity of `dev`, expressed as assertions.
|
||
+//!
|
||
+//! This repo tracks an upstream (`openai/codex`) and `upstream-merge.yml` merges
|
||
+//! from it every 30 minutes. A one-time edit to a default is therefore not a
|
||
+//! decision — it is a suggestion that survives until the next merge. These tests
|
||
+//! are how the decision is made durable: a merge that reverts any of them turns
|
||
+//! the build red instead of quietly shipping someone else's defaults.
|
||
+//!
|
||
+//! This file has no counterpart upstream, so a merge cannot conflict with it or
|
||
+//! carry away the guard along with the thing it guards.
|
||
+//!
|
||
+//! If you are here because this test failed after an upstream merge: the merge
|
||
+//! reverted our identity. Re-apply it — do not delete the test.
|
||
+
|
||
+use code_core::HANZO_PROVIDER_ID;
|
||
+use code_core::WireApi;
|
||
+use code_core::built_in_model_providers;
|
||
+use code_core::config::HANZO_DEFAULT_MODEL;
|
||
+use code_core::config::HANZO_DEFAULT_REVIEW_MODEL;
|
||
+
|
||
+/// Every model `dev` may pick by default belongs to the Zen family. Naming an
|
||
+/// upstream model in our own copy is the thing this guards against.
|
||
+fn is_zen_model(slug: &str) -> bool {
|
||
+ slug.starts_with("zen") || slug.starts_with("enso")
|
||
+}
|
||
+
|
||
+#[test]
|
||
+fn the_hanzo_cloud_is_a_built_in_provider() {
|
||
+ let providers = built_in_model_providers(None);
|
||
+ let hanzo = providers
|
||
+ .get(HANZO_PROVIDER_ID)
|
||
+ .expect("the Hanzo Cloud must be a built-in provider");
|
||
+
|
||
+ assert_eq!(hanzo.name, "Hanzo");
|
||
+ assert_eq!(
|
||
+ hanzo.base_url.as_deref(),
|
||
+ Some("https://api.hanzo.ai/v1"),
|
||
+ "the Hanzo Cloud answers at api.hanzo.ai/v1"
|
||
+ );
|
||
+ assert_eq!(
|
||
+ hanzo.env_key.as_deref(),
|
||
+ Some("HANZO_USER_KEY"),
|
||
+ "`hanzo code` sets HANZO_USER_KEY on the child; the provider must read it"
|
||
+ );
|
||
+ assert!(
|
||
+ matches!(hanzo.wire_api, WireApi::Responses),
|
||
+ "verified against the live gateway: /v1/responses serves our models"
|
||
+ );
|
||
+ assert!(
|
||
+ !hanzo.requires_openai_auth,
|
||
+ "this account is ours; the ChatGPT login flow never applies to it"
|
||
+ );
|
||
+}
|
||
+
|
||
+#[test]
|
||
+fn the_default_model_is_ours() {
|
||
+ assert!(
|
||
+ is_zen_model(HANZO_DEFAULT_MODEL),
|
||
+ "default model must be a Zen model, got `{HANZO_DEFAULT_MODEL}`"
|
||
+ );
|
||
+ assert!(
|
||
+ is_zen_model(HANZO_DEFAULT_REVIEW_MODEL),
|
||
+ "default review model must be a Zen model, got `{HANZO_DEFAULT_REVIEW_MODEL}`"
|
||
+ );
|
||
+}
|
||
+
|
||
+/// The installer contract, `hanzo code`, and the Homebrew formula all resolve a
|
||
+/// binary named `dev` off PATH. A workspace that builds no such binary means a
|
||
+/// release ships nothing anyone can run.
|
||
+#[test]
|
||
+fn the_cli_builds_a_binary_named_dev() {
|
||
+ let manifest = std::fs::read_to_string(concat!(
|
||
+ env!("CARGO_MANIFEST_DIR"),
|
||
+ "/../cli/Cargo.toml"
|
||
+ ))
|
||
+ .expect("read hanzo-dev/cli/Cargo.toml");
|
||
+
|
||
+ let bins: Vec<&str> = manifest
|
||
+ .split("[[bin]]")
|
||
+ .skip(1)
|
||
+ .filter_map(|section| {
|
||
+ section
|
||
+ .lines()
|
||
+ .find_map(|l| l.trim().strip_prefix("name = "))
|
||
+ .map(|n| n.trim().trim_matches('"'))
|
||
+ })
|
||
+ .collect();
|
||
+
|
||
+ assert!(
|
||
+ bins.contains(&"dev"),
|
||
+ "hanzo-dev/cli must build a binary named `dev`; it builds {bins:?}"
|
||
+ );
|
||
+ assert_eq!(
|
||
+ bins.first(),
|
||
+ Some(&"dev"),
|
||
+ "`dev` is the product name and must be declared first; the rest are aliases"
|
||
+ );
|
||
+}
|
||
+
|
||
+/// We publish our releases to namespaces we own. Publishing into a third party's
|
||
+/// namespace is not a style question — it hands them our distribution.
|
||
+#[test]
|
||
+fn the_release_pipeline_publishes_to_our_namespaces() {
|
||
+ let release_yml = repo_file(".hanzo/workflows/release.yml");
|
||
+
|
||
+ for (needle, why) in [
|
||
+ ("@just-every/", "npm packages must publish under @hanzo/"),
|
||
+ (
|
||
+ "just-every/homebrew-tap",
|
||
+ "the Homebrew formula belongs in hanzoai/homebrew-tap",
|
||
+ ),
|
||
+ (
|
||
+ "just-every/code/releases",
|
||
+ "release assets must be served from hanzoai/dev",
|
||
+ ),
|
||
+ ] {
|
||
+ assert!(
|
||
+ !release_yml.contains(needle),
|
||
+ "release.yml still references `{needle}` — {why}"
|
||
+ );
|
||
+ }
|
||
+
|
||
+ assert!(
|
||
+ release_yml.contains("--bin dev"),
|
||
+ "the release must build the `dev` binary"
|
||
+ );
|
||
+ for pkg in [
|
||
+ "@hanzo/dev-darwin-arm64",
|
||
+ "@hanzo/dev-darwin-x64",
|
||
+ "@hanzo/dev-linux-x64-musl",
|
||
+ "@hanzo/dev-linux-arm64-musl",
|
||
+ "@hanzo/dev-win32-x64",
|
||
+ ] {
|
||
+ assert!(
|
||
+ release_yml.contains(pkg),
|
||
+ "the release must publish `{pkg}` — postinstall.js resolves it first"
|
||
+ );
|
||
+ }
|
||
+ assert!(
|
||
+ release_yml.contains("hanzoai/homebrew-tap"),
|
||
+ "the release must push the formula to hanzoai/homebrew-tap"
|
||
+ );
|
||
+}
|
||
+
|
||
+/// A SERVICE HAS NO HUMAN SESSION, and reading only `HANZO_USER_KEY` made that
|
||
+/// case impossible: `dev` inside a sandbox pod reported "Authentication expired.
|
||
+/// Run `code login` to continue" for a caller that was perfectly authenticated,
|
||
+/// because the one variable it would look at is the one no service ever has.
|
||
+#[test]
|
||
+fn the_hanzo_provider_accepts_a_machine_credential() {
|
||
+ let p = code_core::create_hanzo_provider();
|
||
+ for want in ["HANZO_API_KEY", "HANZO_MACHINE_TOKEN"] {
|
||
+ assert!(
|
||
+ p.alt_env_keys.iter().any(|k| k == want),
|
||
+ "a service run carries {want}; the provider must read it"
|
||
+ );
|
||
+ }
|
||
+ assert_eq!(
|
||
+ p.env_key.as_deref(),
|
||
+ Some("HANZO_USER_KEY"),
|
||
+ "the human session stays the declared key"
|
||
+ );
|
||
+}
|
||
+
|
||
+/// MACHINE FIRST, and deliberately. Where both are present the process was
|
||
+/// started BY something, and the credential it was handed is the one that should
|
||
+/// be billed and audited — a stale human key left in an environment must not
|
||
+/// quietly outrank the identity the caller actually presented.
|
||
+#[test]
|
||
+fn a_machine_credential_outranks_a_stale_human_one() {
|
||
+ let p = code_core::create_hanzo_provider();
|
||
+ let idx = |name: &str| {
|
||
+ p.alt_env_keys
|
||
+ .iter()
|
||
+ .chain(p.env_key.iter())
|
||
+ .position(|k| k == name)
|
||
+ .expect("declared")
|
||
+ };
|
||
+ assert!(
|
||
+ idx("HANZO_API_KEY") < idx("HANZO_USER_KEY"),
|
||
+ "the machine credential must be tried before the human session"
|
||
+ );
|
||
+}
|
||
+
|
||
+/// A repo-relative path, read from the test's own manifest location.
|
||
+fn repo_file(rel: &str) -> String {
|
||
+ let path = format!("{}/../../{rel}", env!("CARGO_MANIFEST_DIR"));
|
||
+ std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {rel}: {e}"))
|
||
+}
|
||
+
|
||
+/// Auto Drive drives the same gateway everything else does. Its defaults were
|
||
+/// `gpt-5.1` for the model and `gpt-5.5`/`gpt-5.4-mini` for routing — ids the
|
||
+/// Hanzo Cloud answers with `400 model is not available`, so the feature was
|
||
+/// broken out of the box for the default provider rather than merely misnamed.
|
||
+#[test]
|
||
+fn auto_drive_defaults_to_models_our_gateway_serves() {
|
||
+ let settings = code_core::config_types::AutoDriveSettings::default();
|
||
+
|
||
+ assert!(
|
||
+ is_zen_model(&settings.model),
|
||
+ "Auto Drive defaults to `{}`, which the Hanzo Cloud does not serve",
|
||
+ settings.model
|
||
+ );
|
||
+
|
||
+ for entry in code_core::config_types::default_auto_drive_model_routing_entries() {
|
||
+ assert!(
|
||
+ is_zen_model(&entry.model),
|
||
+ "Auto Drive routes to `{}`, which the Hanzo Cloud does not serve",
|
||
+ entry.model
|
||
+ );
|
||
+ }
|
||
+}
|
||
+
|
||
+/// `brew upgrade code` names a formula that does not exist; ours is
|
||
+/// `Formula/hanzo-dev.rb`. This string is not merely displayed — the TUI runs it.
|
||
+#[test]
|
||
+fn the_homebrew_upgrade_command_names_our_formula() {
|
||
+ let updates = repo_file("hanzo-dev/tui/src/updates.rs");
|
||
+
|
||
+ assert!(
|
||
+ updates.contains("brew upgrade hanzo-dev"),
|
||
+ "the guided Homebrew upgrade must name the `hanzo-dev` formula"
|
||
+ );
|
||
+ assert!(
|
||
+ !updates.contains(r#""upgrade".to_string(),
|
||
+ "code".to_string(),"#),
|
||
+ "the Homebrew upgrade must not target a `code` formula"
|
||
+ );
|
||
+}
|
||
+
|
||
+/// The first screen a new user sees. It greeted them as OpenAI's product for
|
||
+/// months after the rest of the identity had been fixed, because nothing here
|
||
+/// looked at rendered text — only at structural defaults.
|
||
+#[test]
|
||
+fn the_onboarding_screen_introduces_our_product() {
|
||
+ let welcome = repo_file("hanzo-dev/tui/src/onboarding/welcome.rs");
|
||
+ assert!(
|
||
+ welcome.contains("Welcome to Hanzo Dev"),
|
||
+ "the welcome screen must greet the user as Hanzo Dev"
|
||
+ );
|
||
+
|
||
+ // The screen serves both providers, so the OpenAI copy — including a link to
|
||
+ // ChatGPT's settings — legitimately survives on that branch. What must hold
|
||
+ // is that the Hanzo path exists and is the one a default install is offered.
|
||
+ let auth = repo_file("hanzo-dev/tui/src/onboarding/auth.rs");
|
||
+ for needle in [
|
||
+ "Sign in with Hanzo",
|
||
+ "Sign in with Hanzo to use your Hanzo Cloud account",
|
||
+ ] {
|
||
+ assert!(
|
||
+ auth.contains(needle),
|
||
+ "the sign-in screen must offer the Hanzo path (`{needle}` is missing)"
|
||
+ );
|
||
+ }
|
||
+}
|
||
diff --git a/core/tests/remote_models_manager.rs b/core/tests/remote_models_manager.rs
|
||
index 0974bdc..cb3ba43 100644
|
||
--- a/core/tests/remote_models_manager.rs
|
||
+++ b/core/tests/remote_models_manager.rs
|
||
@@ -12,7 +12,7 @@ use wiremock::matchers::{header, method, path};
|
||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||
|
||
fn skip_if_no_network() -> bool {
|
||
- std::env::var(code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok()
|
||
+ std::env::var(code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok()
|
||
}
|
||
|
||
fn remote_model(slug: &str, display: &str, priority: i32) -> ModelInfo {
|
||
@@ -45,6 +45,7 @@ fn remote_model(slug: &str, display: &str, priority: i32) -> ModelInfo {
|
||
|
||
fn provider_for(base_url: String) -> ModelProviderInfo {
|
||
ModelProviderInfo {
|
||
+ alt_env_keys: Vec::new(),
|
||
name: "mock".into(),
|
||
base_url: Some(base_url),
|
||
env_key: None,
|
||
diff --git a/core/tests/repeated_tool_loop.rs b/core/tests/repeated_tool_loop.rs
|
||
index 9deed29..70895f5 100644
|
||
--- a/core/tests/repeated_tool_loop.rs
|
||
+++ b/core/tests/repeated_tool_loop.rs
|
||
@@ -120,6 +120,7 @@ async fn repeated_identical_tool_cycle_stops_with_visible_error() {
|
||
config.approval_policy = AskForApproval::Never;
|
||
config.sandbox_policy = SandboxPolicy::DangerFullAccess;
|
||
config.model_provider = ModelProviderInfo {
|
||
+ alt_env_keys: Vec::new(),
|
||
base_url: Some(format!("{}/v1", server.uri())),
|
||
..built_in_model_providers(None)["openai"].clone()
|
||
};
|
||
diff --git a/core/tests/tool_hooks.rs b/core/tests/tool_hooks.rs
|
||
index 1fb2cf6..b203d6d 100644
|
||
--- a/core/tests/tool_hooks.rs
|
||
+++ b/core/tests/tool_hooks.rs
|
||
@@ -78,6 +78,7 @@ fn configure_test(
|
||
config.sandbox_policy = SandboxPolicy::DangerFullAccess;
|
||
config.project_hooks = ProjectHooks::from_configs(&hook_configs, &config.cwd);
|
||
config.model_provider = ModelProviderInfo {
|
||
+ alt_env_keys: Vec::new(),
|
||
base_url: Some(format!("{}/v1", server.uri())),
|
||
..built_in_model_providers(None)["openai"].clone()
|
||
};
|
||
diff --git a/core/tests/tool_selection.rs b/core/tests/tool_selection.rs
|
||
index 54f9727..229e05c 100644
|
||
--- a/core/tests/tool_selection.rs
|
||
+++ b/core/tests/tool_selection.rs
|
||
@@ -48,6 +48,7 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
|
||
let resp_mock = mount_sse_once(&server, sse).await;
|
||
|
||
let model_provider = ModelProviderInfo {
|
||
+ alt_env_keys: Vec::new(),
|
||
base_url: Some(format!("{}/v1", server.uri())),
|
||
..built_in_model_providers(None)["openai"].clone()
|
||
};
|
||
diff --git a/default.nix b/default.nix
|
||
index 90d1570..45a2b01 100644
|
||
--- a/default.nix
|
||
+++ b/default.nix
|
||
@@ -7,7 +7,7 @@ in
|
||
rec {
|
||
package = pkgs.rustPlatform.buildRustPackage {
|
||
inherit env;
|
||
- pname = "code-rs";
|
||
+ pname = "hanzo-dev";
|
||
version = "0.1.0";
|
||
cargoLock = {
|
||
lockFile = ./Cargo.lock;
|
||
@@ -27,20 +27,20 @@ rec {
|
||
openssl
|
||
];
|
||
meta = with pkgs.lib; {
|
||
- description = "OpenAI Codex command‑line interface rust implementation";
|
||
+ description = "Hanzo Dev — the Hanzo coding agent (Rust implementation)";
|
||
license = licenses.asl20;
|
||
- homepage = "https://github.com/openai/codex";
|
||
+ homepage = "https://github.com/hanzoai/dev";
|
||
};
|
||
};
|
||
devShell = pkgs.mkShell {
|
||
inherit env;
|
||
- name = "code-rs-dev";
|
||
+ name = "hanzo-dev-dev";
|
||
packages = monorep-deps ++ [
|
||
pkgs.cargo
|
||
package
|
||
];
|
||
shellHook = ''
|
||
- echo "Entering development shell for code-rs"
|
||
+ echo "Entering development shell for hanzo-dev"
|
||
alias codex="cd ${package.src}/tui; cargo run; cd -"
|
||
${pkgs.rustPlatform.cargoSetupHook}
|
||
'';
|
||
diff --git a/docs/architecture/phase0-baseline.md b/docs/architecture/phase0-baseline.md
|
||
index 2fc77d9..7aa30d3 100644
|
||
--- a/docs/architecture/phase0-baseline.md
|
||
+++ b/docs/architecture/phase0-baseline.md
|
||
@@ -1,6 +1,6 @@
|
||
# Phase 0 Baseline – Architecture Survey (September 20, 2025)
|
||
|
||
-This note captures the current layout of the `code-rs` workspace before any
|
||
+This note captures the current layout of the `hanzo-dev` workspace before any
|
||
phase-one refactors. It should help us reason about the execution pipeline and
|
||
identify the modules that will be split up in later phases.
|
||
|
||
diff --git a/exec/Cargo.toml b/exec/Cargo.toml
|
||
index 57c6baf..71963d1 100644
|
||
--- a/exec/Cargo.toml
|
||
+++ b/exec/Cargo.toml
|
||
@@ -26,6 +26,7 @@ code-common = { workspace = true, features = [
|
||
code-core = { workspace = true }
|
||
code-ollama = { workspace = true }
|
||
code-protocol = { workspace = true }
|
||
+code-version = { path = "../code-version" }
|
||
code-app-server-protocol = { workspace = true }
|
||
code-auto-drive-core = { workspace = true }
|
||
code-git-tooling = { workspace = true }
|
||
@@ -46,6 +47,11 @@ tokio = { workspace = true, features = [
|
||
"sync",
|
||
] }
|
||
tracing = { workspace = true, features = ["log"] }
|
||
+ts-rs = { workspace = true, features = [
|
||
+ "uuid-impl",
|
||
+ "serde-json-impl",
|
||
+ "no-serde-warnings",
|
||
+] }
|
||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||
once_cell = { workspace = true }
|
||
|
||
diff --git a/exec/src/cli.rs b/exec/src/cli.rs
|
||
index f1e38ce..e54b983 100644
|
||
--- a/exec/src/cli.rs
|
||
+++ b/exec/src/cli.rs
|
||
@@ -65,7 +65,7 @@ pub struct Cli {
|
||
#[clap(long = "debug", short = 'd', default_value_t = false)]
|
||
pub debug: bool,
|
||
|
||
- /// Allow running Codex outside a Git repository.
|
||
+ /// Allow running Hanzo Dev outside a Git repository.
|
||
#[arg(long = "skip-git-repo-check", default_value_t = false)]
|
||
pub skip_git_repo_check: bool,
|
||
|
||
@@ -86,7 +86,7 @@ pub struct Cli {
|
||
pub color: Color,
|
||
|
||
/// Print events to stdout as JSONL.
|
||
- #[arg(long = "json", default_value_t = false)]
|
||
+ #[arg(long = "json", alias = "experimental-json", default_value_t = false)]
|
||
pub json: bool,
|
||
|
||
/// Maximum wall-clock time budget (seconds) before aborting the run.
|
||
diff --git a/exec/src/event_processor.rs b/exec/src/event_processor.rs
|
||
index 3bc1158..f35f37f 100644
|
||
--- a/exec/src/event_processor.rs
|
||
+++ b/exec/src/event_processor.rs
|
||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||
|
||
use code_core::config::Config;
|
||
use code_core::protocol::Event;
|
||
+use code_core::protocol::SessionConfiguredEvent;
|
||
|
||
pub(crate) enum CodexStatus {
|
||
Running,
|
||
@@ -13,6 +14,11 @@ pub(crate) trait EventProcessor {
|
||
/// Print summary of effective configuration and user prompt.
|
||
fn print_config_summary(&mut self, config: &Config, prompt: &str);
|
||
|
||
+ /// The session handshake. `exec` consumes `SessionConfigured` before the
|
||
+ /// event loop starts, so a processor that needs it — JSON mode, which
|
||
+ /// derives `thread.started` from it — has to be handed it here.
|
||
+ fn on_session_configured(&mut self, _ev: &SessionConfiguredEvent) {}
|
||
+
|
||
/// Handle a single event emitted by the agent.
|
||
fn process_event(&mut self, event: Event) -> CodexStatus;
|
||
|
||
diff --git a/exec/src/event_processor_with_human_output.rs b/exec/src/event_processor_with_human_output.rs
|
||
index 4c324ea..8005528 100644
|
||
--- a/exec/src/event_processor_with_human_output.rs
|
||
+++ b/exec/src/event_processor_with_human_output.rs
|
||
@@ -160,12 +160,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||
/// for the session. This mirrors the information shown in the TUI welcome
|
||
/// screen.
|
||
fn print_config_summary(&mut self, config: &Config, prompt: &str) {
|
||
- const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||
- ts_println!(
|
||
- self,
|
||
- "OpenAI Codex v{} (research preview)",
|
||
- VERSION
|
||
- );
|
||
+ ts_println!(self, "Hanzo Dev v{}\n--------", code_version::version());
|
||
|
||
// Show which binary is being used to execute sub-agents so users can
|
||
// confirm path mismatches when testing new builds.
|
||
@@ -262,7 +257,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||
EventMsg::AutoContextCheck(_) => {}
|
||
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => {
|
||
if !self.answer_started {
|
||
- ts_println!(self, "{}\n", "codex".style(self.italic).style(self.magenta));
|
||
+ ts_println!(self, "{}\n", "dev".style(self.italic).style(self.magenta));
|
||
self.answer_started = true;
|
||
}
|
||
print!("{delta}");
|
||
@@ -327,7 +322,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||
ts_println!(
|
||
self,
|
||
"{}\n{}",
|
||
- "codex".style(self.italic).style(self.magenta),
|
||
+ "dev".style(self.italic).style(self.magenta),
|
||
message,
|
||
);
|
||
} else {
|
||
@@ -588,7 +583,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||
ts_println!(
|
||
self,
|
||
"{}\n{}",
|
||
- "codex".style(self.italic).style(self.magenta),
|
||
+ "dev".style(self.italic).style(self.magenta),
|
||
agent_reasoning_event.text,
|
||
);
|
||
} else {
|
||
@@ -596,7 +591,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||
ts_println!(
|
||
self,
|
||
"{}\n{}",
|
||
- "codex".style(self.italic).style(self.magenta),
|
||
+ "dev".style(self.italic).style(self.magenta),
|
||
agent_reasoning_event.text,
|
||
);
|
||
}
|
||
@@ -608,7 +603,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||
ts_println!(
|
||
self,
|
||
"{} {}",
|
||
- "codex session".style(self.magenta).style(self.bold),
|
||
+ "dev session".style(self.magenta).style(self.bold),
|
||
conversation_id.to_string().style(self.dimmed)
|
||
);
|
||
|
||
diff --git a/exec/src/event_processor_with_json_output.rs b/exec/src/event_processor_with_json_output.rs
|
||
deleted file mode 100644
|
||
index ac27eea..0000000
|
||
--- a/exec/src/event_processor_with_json_output.rs
|
||
+++ /dev/null
|
||
@@ -1,67 +0,0 @@
|
||
-use std::collections::HashMap;
|
||
-use std::path::PathBuf;
|
||
-
|
||
-use code_core::config::Config;
|
||
-use code_core::protocol::Event;
|
||
-use code_core::protocol::EventMsg;
|
||
-use code_core::protocol::TaskCompleteEvent;
|
||
-use serde_json::json;
|
||
-
|
||
-use crate::event_processor::CodexStatus;
|
||
-use crate::event_processor::EventProcessor;
|
||
-use crate::event_processor::handle_last_message;
|
||
-use code_common::create_config_summary_entries;
|
||
-
|
||
-pub(crate) struct EventProcessorWithJsonOutput {
|
||
- last_message_path: Option<PathBuf>,
|
||
- had_error: bool,
|
||
-}
|
||
-
|
||
-impl EventProcessorWithJsonOutput {
|
||
- pub fn new(last_message_path: Option<PathBuf>) -> Self {
|
||
- Self { last_message_path, had_error: false }
|
||
- }
|
||
-}
|
||
-
|
||
-impl EventProcessor for EventProcessorWithJsonOutput {
|
||
- fn print_config_summary(&mut self, config: &Config, prompt: &str) {
|
||
- let entries = create_config_summary_entries(config)
|
||
- .into_iter()
|
||
- .map(|(key, value)| (key.to_string(), value))
|
||
- .collect::<HashMap<String, String>>();
|
||
- #[expect(clippy::expect_used)]
|
||
- let config_json =
|
||
- serde_json::to_string(&entries).expect("Failed to serialize config summary to JSON");
|
||
- println!("{config_json}");
|
||
-
|
||
- let prompt_json = json!({
|
||
- "prompt": prompt,
|
||
- });
|
||
- println!("{prompt_json}");
|
||
- }
|
||
-
|
||
- fn process_event(&mut self, event: Event) -> CodexStatus {
|
||
- match event.msg {
|
||
- EventMsg::Error(_) => { self.had_error = true; CodexStatus::Running }
|
||
- EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_) => {
|
||
- // Suppress streaming events in JSON mode.
|
||
- CodexStatus::Running
|
||
- }
|
||
- EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
|
||
- if let Some(output_file) = self.last_message_path.as_deref() {
|
||
- handle_last_message(last_agent_message.as_deref(), output_file);
|
||
- }
|
||
- CodexStatus::InitiateShutdown
|
||
- }
|
||
- EventMsg::ShutdownComplete => CodexStatus::Shutdown,
|
||
- _ => {
|
||
- if let Ok(line) = serde_json::to_string(&event) {
|
||
- println!("{line}");
|
||
- }
|
||
- CodexStatus::Running
|
||
- }
|
||
- }
|
||
- }
|
||
-
|
||
- // exit_code handled by CLI; suppress unused warnings by omitting method.
|
||
-}
|
||
diff --git a/exec/src/exec_events.rs b/exec/src/exec_events.rs
|
||
index 606cf91..c184f3c 100644
|
||
--- a/exec/src/exec_events.rs
|
||
+++ b/exec/src/exec_events.rs
|
||
@@ -60,8 +60,14 @@ pub struct Usage {
|
||
pub input_tokens: u64,
|
||
/// The number of cached input tokens used during the turn.
|
||
pub cached_input_tokens: u64,
|
||
+ /// The number of input tokens written to the prompt cache during the turn.
|
||
+ #[serde(default)]
|
||
+ pub cache_write_input_tokens: u64,
|
||
/// The number of output tokens used during the turn.
|
||
pub output_tokens: u64,
|
||
+ /// The number of reasoning output tokens used during the turn.
|
||
+ #[serde(default)]
|
||
+ pub reasoning_output_tokens: u64,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||
diff --git a/exec/src/experimental_event_processor_with_json_output.rs b/exec/src/experimental_event_processor_with_json_output.rs
|
||
index 215af25..2b45054 100644
|
||
--- a/exec/src/experimental_event_processor_with_json_output.rs
|
||
+++ b/exec/src/experimental_event_processor_with_json_output.rs
|
||
@@ -5,7 +5,7 @@ use std::sync::atomic::AtomicU64;
|
||
use crate::event_processor::CodexStatus;
|
||
use crate::event_processor::EventProcessor;
|
||
use crate::event_processor::handle_last_message;
|
||
-use crate::exec_events::AssistantMessageItem;
|
||
+use crate::exec_events::AgentMessageItem;
|
||
use crate::exec_events::CommandExecutionItem;
|
||
use crate::exec_events::CommandExecutionStatus;
|
||
use crate::exec_events::FileChangeItem;
|
||
@@ -46,8 +46,7 @@ use code_core::protocol::PatchApplyBeginEvent;
|
||
use code_core::protocol::PatchApplyEndEvent;
|
||
use code_core::protocol::SessionConfiguredEvent;
|
||
use code_core::protocol::TaskCompleteEvent;
|
||
-use code_core::protocol::TaskStartedEvent;
|
||
-use code_core::protocol::WebSearchEndEvent;
|
||
+use code_core::protocol::WebSearchCompleteEvent;
|
||
use tracing::error;
|
||
use tracing::warn;
|
||
|
||
@@ -109,14 +108,14 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
EventMsg::PatchApplyBegin(ev) => self.handle_patch_apply_begin(ev),
|
||
EventMsg::PatchApplyEnd(ev) => self.handle_patch_apply_end(ev),
|
||
EventMsg::WebSearchBegin(_) => Vec::new(),
|
||
- EventMsg::WebSearchEnd(ev) => self.handle_web_search_end(ev),
|
||
+ EventMsg::WebSearchComplete(ev) => self.handle_web_search_complete(ev),
|
||
EventMsg::TokenCount(ev) => {
|
||
if let Some(info) = &ev.info {
|
||
self.last_total_token_usage = Some(info.total_token_usage.clone());
|
||
}
|
||
Vec::new()
|
||
}
|
||
- EventMsg::TaskStarted(ev) => self.handle_task_started(ev),
|
||
+ EventMsg::TaskStarted => self.handle_task_started(),
|
||
EventMsg::TaskComplete(_) => self.handle_task_complete(),
|
||
EventMsg::Error(ev) => {
|
||
let error = ThreadErrorEvent {
|
||
@@ -125,9 +124,6 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
self.last_critical_error = Some(error.clone());
|
||
vec![ThreadEvent::Error(error)]
|
||
}
|
||
- EventMsg::StreamError(ev) => vec![ThreadEvent::Error(ThreadErrorEvent {
|
||
- message: ev.message.clone(),
|
||
- })],
|
||
EventMsg::PlanUpdate(ev) => self.handle_plan_update(ev),
|
||
_ => Vec::new(),
|
||
}
|
||
@@ -147,11 +143,11 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
})]
|
||
}
|
||
|
||
- fn handle_web_search_end(&self, ev: &WebSearchEndEvent) -> Vec<ThreadEvent> {
|
||
+ fn handle_web_search_complete(&self, ev: &WebSearchCompleteEvent) -> Vec<ThreadEvent> {
|
||
let item = ThreadItem {
|
||
id: self.get_next_item_id(),
|
||
details: ThreadItemDetails::WebSearch(WebSearchItem {
|
||
- query: ev.query.clone(),
|
||
+ query: ev.query.clone().unwrap_or_default(),
|
||
}),
|
||
};
|
||
|
||
@@ -162,7 +158,7 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
let item = ThreadItem {
|
||
id: self.get_next_item_id(),
|
||
|
||
- details: ThreadItemDetails::AssistantMessage(AssistantMessageItem {
|
||
+ details: ThreadItemDetails::AgentMessage(AgentMessageItem {
|
||
text: payload.message.clone(),
|
||
}),
|
||
};
|
||
@@ -339,7 +335,7 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
|
||
details: ThreadItemDetails::CommandExecution(CommandExecutionItem {
|
||
command,
|
||
- aggregated_output: ev.aggregated_output.clone(),
|
||
+ aggregated_output: format!("{}{}", ev.stdout, ev.stderr),
|
||
exit_code: Some(ev.exit_code),
|
||
status,
|
||
}),
|
||
@@ -382,7 +378,7 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
vec![ThreadEvent::ItemStarted(ItemStartedEvent { item })]
|
||
}
|
||
|
||
- fn handle_task_started(&mut self, _: &TaskStartedEvent) -> Vec<ThreadEvent> {
|
||
+ fn handle_task_started(&mut self) -> Vec<ThreadEvent> {
|
||
self.last_critical_error = None;
|
||
vec![ThreadEvent::TurnStarted(TurnStartedEvent {})]
|
||
}
|
||
@@ -392,7 +388,9 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
Usage {
|
||
input_tokens: u.input_tokens,
|
||
cached_input_tokens: u.cached_input_tokens,
|
||
+ cache_write_input_tokens: u.cache_write_input_tokens,
|
||
output_tokens: u.output_tokens,
|
||
+ reasoning_output_tokens: u.reasoning_output_tokens,
|
||
}
|
||
} else {
|
||
Usage::default()
|
||
@@ -420,26 +418,29 @@ impl ExperimentalEventProcessorWithJsonOutput {
|
||
}
|
||
}
|
||
|
||
+impl ExperimentalEventProcessorWithJsonOutput {
|
||
+ fn emit(&self, events: Vec<ThreadEvent>) {
|
||
+ for event in events {
|
||
+ match serde_json::to_string(&event) {
|
||
+ Ok(line) => println!("{line}"),
|
||
+ Err(e) => error!("Failed to serialize event: {e:?}"),
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
impl EventProcessor for ExperimentalEventProcessorWithJsonOutput {
|
||
- fn print_config_summary(&mut self, _: &Config, _: &str, ev: &SessionConfiguredEvent) {
|
||
- self.process_event(Event {
|
||
- id: "".to_string(),
|
||
- msg: EventMsg::SessionConfigured(ev.clone()),
|
||
- });
|
||
+ /// JSON mode emits no human-readable preamble.
|
||
+ fn print_config_summary(&mut self, _: &Config, _: &str) {}
|
||
+
|
||
+ fn on_session_configured(&mut self, ev: &SessionConfiguredEvent) {
|
||
+ let started = self.handle_session_configured(ev);
|
||
+ self.emit(started);
|
||
}
|
||
|
||
fn process_event(&mut self, event: Event) -> CodexStatus {
|
||
let aggregated = self.collect_thread_events(&event);
|
||
- for conv_event in aggregated {
|
||
- match serde_json::to_string(&conv_event) {
|
||
- Ok(line) => {
|
||
- println!("{line}");
|
||
- }
|
||
- Err(e) => {
|
||
- error!("Failed to serialize event: {e:?}");
|
||
- }
|
||
- }
|
||
- }
|
||
+ self.emit(aggregated);
|
||
|
||
let Event { msg, .. } = event;
|
||
|
||
diff --git a/exec/src/lib.rs b/exec/src/lib.rs
|
||
index 5fc3572..80a4875 100644
|
||
--- a/exec/src/lib.rs
|
||
+++ b/exec/src/lib.rs
|
||
@@ -1,7 +1,8 @@
|
||
mod cli;
|
||
mod event_processor;
|
||
mod event_processor_with_human_output;
|
||
-mod event_processor_with_json_output;
|
||
+pub mod exec_events;
|
||
+mod experimental_event_processor_with_json_output;
|
||
mod slash;
|
||
|
||
pub use cli::Cli;
|
||
@@ -48,7 +49,7 @@ use code_protocol::protocol::SessionSource;
|
||
use code_ollama::DEFAULT_OSS_MODEL;
|
||
use code_protocol::config_types::SandboxMode;
|
||
use event_processor_with_human_output::EventProcessorWithHumanOutput;
|
||
-use event_processor_with_json_output::EventProcessorWithJsonOutput;
|
||
+use experimental_event_processor_with_json_output::ExperimentalEventProcessorWithJsonOutput;
|
||
use event_processor::handle_last_message;
|
||
use code_git_tooling::GhostCommit;
|
||
use code_git_tooling::CreateGhostCommitOptions;
|
||
@@ -101,7 +102,7 @@ use code_core::timeboxed_exec_guidance::{
|
||
const AUTO_REVIEW_SHUTDOWN_GRACE_MS: u64 = 1_500;
|
||
const REVIEW_SCOPE_MAX_LISTED_PATHS: usize = 120;
|
||
const REVIEW_SCOPE_EXCLUDED_PREFIXES: [&str; 5] = [
|
||
- "codex-rs/",
|
||
+ "vendor/",
|
||
"node_modules/",
|
||
"target/",
|
||
".git/",
|
||
@@ -477,7 +478,9 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
|
||
}
|
||
let stop_on_task_complete = auto_drive_goal.is_none() && auto_resolve_state.is_none();
|
||
let mut event_processor: Box<dyn EventProcessor> = if json_mode {
|
||
- Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone()))
|
||
+ Box::new(ExperimentalEventProcessorWithJsonOutput::new(
|
||
+ last_message_file.clone(),
|
||
+ ))
|
||
} else {
|
||
Box::new(EventProcessorWithHumanOutput::create_with_ansi(
|
||
stdout_with_ansi,
|
||
@@ -493,6 +496,11 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
|
||
.map_err(|e| anyhow::anyhow!("OSS setup failed: {e}"))?;
|
||
}
|
||
|
||
+ // Say what a free run costs and shares before it starts, not after.
|
||
+ if code_core::free::runs(&config) {
|
||
+ eprintln!("{}", code_core::free::NOTICE);
|
||
+ }
|
||
+
|
||
// Print the effective configuration and prompt so users can see what Codex
|
||
// is using.
|
||
let default_cwd = config.cwd.to_path_buf();
|
||
@@ -542,7 +550,8 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
|
||
} else {
|
||
event_processor.print_config_summary(&config, &summary_prompt);
|
||
}
|
||
- info!("Codex initialized with event: {session_configured:?}");
|
||
+ event_processor.on_session_configured(&session_configured);
|
||
+ info!("Hanzo Dev initialized with event: {session_configured:?}");
|
||
|
||
if let Some(goal) = auto_drive_goal {
|
||
return run_auto_drive_session(
|
||
@@ -2822,7 +2831,7 @@ mod tests {
|
||
fn apply_commit_scope_filters_and_truncates_paths() {
|
||
let mut paths = vec![
|
||
"./src/main.rs".to_string(),
|
||
- "codex-rs/Cargo.lock".to_string(),
|
||
+ "vendor/codex/Cargo.lock".to_string(),
|
||
"./src/main.rs".to_string(),
|
||
];
|
||
for idx in 0..130 {
|
||
@@ -2840,19 +2849,19 @@ mod tests {
|
||
let scoped = apply_commit_scope_to_review_request(request, "abc1234", "def5678", Some(&paths));
|
||
assert!(scoped.prompt.contains("Files changed in this snapshot:"));
|
||
assert!(scoped.prompt.contains("- src/main.rs"));
|
||
- assert!(!scoped.prompt.contains("codex-rs/Cargo.lock"));
|
||
+ assert!(!scoped.prompt.contains("vendor/codex/Cargo.lock"));
|
||
assert!(
|
||
scoped
|
||
.prompt
|
||
.contains("Omitted 12 changed path(s) from this list (1 excluded by path policy; 11 truncated for prompt size).")
|
||
);
|
||
- assert!(scoped.prompt.contains("Excluded prefixes: codex-rs/"));
|
||
+ assert!(scoped.prompt.contains("Excluded prefixes: vendor/"));
|
||
}
|
||
|
||
#[test]
|
||
fn apply_commit_scope_reports_when_all_paths_excluded() {
|
||
let paths = vec![
|
||
- "codex-rs/Cargo.lock".to_string(),
|
||
+ "vendor/codex/Cargo.lock".to_string(),
|
||
"target/debug/code".to_string(),
|
||
"node_modules/pkg/index.js".to_string(),
|
||
];
|
||
@@ -2872,7 +2881,7 @@ mod tests {
|
||
.prompt
|
||
.contains("Omitted 3 changed path(s) from this list (3 excluded by path policy; 0 truncated for prompt size).")
|
||
);
|
||
- assert!(scoped.prompt.contains("Excluded prefixes: codex-rs/"));
|
||
+ assert!(scoped.prompt.contains("Excluded prefixes: vendor/"));
|
||
}
|
||
|
||
#[test]
|
||
diff --git a/git-apply/src/lib.rs b/git-apply/src/lib.rs
|
||
index 5d14631..418919c 100644
|
||
--- a/git-apply/src/lib.rs
|
||
+++ b/git-apply/src/lib.rs
|
||
@@ -45,7 +45,7 @@ pub fn apply_git_patch(req: &ApplyGitRequest) -> io::Result<ApplyGitResult> {
|
||
|
||
// Optional: additional git config via env knob (defaults OFF)
|
||
let mut cfg_parts: Vec<String> = Vec::new();
|
||
- if let Ok(cfg) = std::env::var("CODEX_APPLY_GIT_CFG") {
|
||
+ if let Ok(cfg) = std::env::var("DEV_APPLY_GIT_CFG") {
|
||
for pair in cfg.split(',') {
|
||
let p = pair.trim();
|
||
if p.is_empty() || !p.contains('=') {
|
||
diff --git a/git-tooling/src/ghost_commits.rs b/git-tooling/src/ghost_commits.rs
|
||
index c56ada6..d7c80ac 100644
|
||
--- a/git-tooling/src/ghost_commits.rs
|
||
+++ b/git-tooling/src/ghost_commits.rs
|
||
@@ -482,7 +482,7 @@ mod tests {
|
||
assert_eq!(root_after, "root after\n");
|
||
let nested_after = std::fs::read_to_string(workspace.join("nested.txt"))?;
|
||
assert_eq!(nested_after, "nested modified\n");
|
||
- assert!(!workspace.join("code-rs").exists());
|
||
+ assert!(!workspace.join("hanzo-dev").exists());
|
||
|
||
Ok(())
|
||
}
|
||
@@ -494,7 +494,7 @@ mod tests {
|
||
let repo = temp.path();
|
||
init_test_repo(repo);
|
||
|
||
- let workspace = repo.join("code-rs");
|
||
+ let workspace = repo.join("hanzo-dev");
|
||
std::fs::create_dir_all(&workspace)?;
|
||
std::fs::write(repo.join(".gitignore"), ".vscode/\n")?;
|
||
std::fs::write(workspace.join("tracked.txt"), "snapshot version\n")?;
|
||
diff --git a/login/src/assets/success.html b/login/src/assets/success.html
|
||
index 37c4efa..255a3e9 100644
|
||
--- a/login/src/assets/success.html
|
||
+++ b/login/src/assets/success.html
|
||
@@ -4,7 +4,7 @@
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="color-scheme" content="light dark" />
|
||
- <title>Sign into Codex</title>
|
||
+ <title>Sign in to Hanzo Dev</title>
|
||
<link rel="icon" href='data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"%3E%3Cpath stroke="%23000" stroke-linecap="round" stroke-width="2.484" d="M22.356 19.797H17.17M9.662 12.29l1.979 3.576a.511.511 0 0 1-.005.504l-1.974 3.409M30.758 16c0 8.15-6.607 14.758-14.758 14.758-8.15 0-14.758-6.607-14.758-14.758C1.242 7.85 7.85 1.242 16 1.242c8.15 0 14.758 6.608 14.758 14.758Z"/%3E%3C/svg%3E' type="image/svg+xml">
|
||
<style>
|
||
:root {
|
||
@@ -169,7 +169,7 @@
|
||
<div class="logo">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path stroke="#000" stroke-linecap="round" stroke-width="2.484" d="M22.356 19.797H17.17M9.662 12.29l1.979 3.576a.511.511 0 0 1-.005.504l-1.974 3.409M30.758 16c0 8.15-6.607 14.758-14.758 14.758-8.15 0-14.758-6.607-14.758-14.758C1.242 7.85 7.85 1.242 16 1.242c8.15 0 14.758 6.608 14.758 14.758Z"></path></svg>
|
||
</div>
|
||
- <div class="title">Signed in to Codex</div>
|
||
+ <div class="title">Signed in to Hanzo Dev</div>
|
||
</div>
|
||
<div class="close-box" style="display: none;">
|
||
<div class="setup-description">You may now close this page</div>
|
||
diff --git a/login/src/lib.rs b/login/src/lib.rs
|
||
index 93834ab..2466d26 100644
|
||
--- a/login/src/lib.rs
|
||
+++ b/login/src/lib.rs
|
||
@@ -1,8 +1,10 @@
|
||
mod device_code_auth;
|
||
+mod oidc_device_auth;
|
||
mod pkce;
|
||
mod server;
|
||
|
||
pub use device_code_auth::{run_device_code_login, DeviceCodeSession};
|
||
+pub use oidc_device_auth::DeviceGrant;
|
||
pub use server::LoginServer;
|
||
pub use server::ServerOptions;
|
||
pub use server::ShutdownHandle;
|
||
@@ -14,7 +16,9 @@ pub use code_core::AuthManager;
|
||
pub use code_core::CodexAuth;
|
||
pub use code_core::auth::AuthDotJson;
|
||
pub use code_core::auth::CLIENT_ID;
|
||
-pub use code_core::auth::CODEX_API_KEY_ENV_VAR;
|
||
+pub use code_core::auth::HANZO_CLIENT_ID;
|
||
+pub use code_core::auth::HANZO_ISSUER;
|
||
+pub use code_core::auth::HANZO_API_KEY_ENV_VAR;
|
||
pub use code_core::auth::OPENAI_API_KEY_ENV_VAR;
|
||
pub use code_core::auth::get_auth_file;
|
||
pub use code_core::auth::login_with_api_key;
|
||
diff --git a/login/src/oidc_device_auth.rs b/login/src/oidc_device_auth.rs
|
||
new file mode 100644
|
||
index 0000000..e045b25
|
||
--- /dev/null
|
||
+++ b/login/src/oidc_device_auth.rs
|
||
@@ -0,0 +1,434 @@
|
||
+//! RFC 8628 OAuth 2.0 Device Authorization Grant against a standards OIDC
|
||
+//! issuer — Hanzo IAM (hanzo.id / lux.id / zoolabs.id).
|
||
+//!
|
||
+//! Deliberately separate from [`crate::device_code_auth`], which speaks
|
||
+//! OpenAI's proprietary `/api/accounts/deviceauth` + PKCE scheme. Here it is
|
||
+//! plain RFC 8628:
|
||
+//!
|
||
+//! 1. `POST {issuer}/oauth/device` (form: `client_id`, `scope`)
|
||
+//! → `{ device_code, user_code, verification_uri[_complete], expires_in, interval }`
|
||
+//! 2. `POST {issuer}/oauth/token`
|
||
+//! `grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=…&client_id=…`
|
||
+//! → `{ access_token, id_token, refresh_token }`, or an OAuth error
|
||
+//! (`authorization_pending` / `slow_down` / `access_denied` / `expired_token`).
|
||
+//!
|
||
+//! Issuer and client come from [`ServerOptions`], so one flow serves every
|
||
+//! brand. Tokens are persisted through the store the browser flow already
|
||
+//! writes, so `dev` reads them back unchanged.
|
||
+
|
||
+use std::io;
|
||
+use std::time::Duration;
|
||
+use std::time::Instant;
|
||
+
|
||
+use serde::Deserialize;
|
||
+
|
||
+use crate::server::ServerOptions;
|
||
+use crate::server::persist_tokens_async;
|
||
+use code_core::default_client::create_client;
|
||
+
|
||
+const ANSI_BLUE: &str = "\x1b[94m";
|
||
+const ANSI_GRAY: &str = "\x1b[90m";
|
||
+const ANSI_RESET: &str = "\x1b[0m";
|
||
+
|
||
+/// RFC 8628 §3.5 default polling interval when the issuer omits one.
|
||
+const DEFAULT_INTERVAL_SECS: u64 = 5;
|
||
+const MIN_INTERVAL_SECS: u64 = 1;
|
||
+const MAX_INTERVAL_SECS: u64 = 60;
|
||
+/// RFC 8628 §3.2 default lifetime when the issuer omits `expires_in`.
|
||
+const DEFAULT_EXPIRES_IN_SECS: u64 = 900;
|
||
+/// Ceiling on an issuer-provided `expires_in`: bounds polling and stops a
|
||
+/// hostile or buggy issuer from overflowing `Instant + Duration` or pinning the
|
||
+/// CLI in a poll loop for years.
|
||
+const MAX_EXPIRES_IN_SECS: u64 = 1800;
|
||
+/// `slow_down` (RFC 8628 §3.5) requires increasing the interval by 5s.
|
||
+const SLOW_DOWN_BACKOFF_SECS: u64 = 5;
|
||
+
|
||
+/// `offline_access` earns the refresh token the auth store expects; the rest is
|
||
+/// the identity every brand's IAM advertises under `scopes_supported`.
|
||
+const SCOPE: &str = "openid profile email offline_access";
|
||
+
|
||
+#[derive(Debug, Clone, Deserialize)]
|
||
+struct DeviceAuth {
|
||
+ device_code: String,
|
||
+ user_code: String,
|
||
+ verification_uri: String,
|
||
+ #[serde(default)]
|
||
+ verification_uri_complete: Option<String>,
|
||
+ #[serde(default)]
|
||
+ expires_in: u64,
|
||
+ #[serde(default)]
|
||
+ interval: u64,
|
||
+}
|
||
+
|
||
+impl DeviceAuth {
|
||
+ /// Clamp the issuer's interval into a sane band; fall back to the RFC
|
||
+ /// default when the issuer omits it (0).
|
||
+ fn poll_interval(&self) -> u64 {
|
||
+ if self.interval == 0 {
|
||
+ DEFAULT_INTERVAL_SECS
|
||
+ } else {
|
||
+ self.interval.clamp(MIN_INTERVAL_SECS, MAX_INTERVAL_SECS)
|
||
+ }
|
||
+ }
|
||
+
|
||
+ fn lifetime(&self) -> Duration {
|
||
+ let secs = if self.expires_in == 0 {
|
||
+ DEFAULT_EXPIRES_IN_SECS
|
||
+ } else {
|
||
+ self.expires_in
|
||
+ };
|
||
+ Duration::from_secs(secs.min(MAX_EXPIRES_IN_SECS))
|
||
+ }
|
||
+
|
||
+ /// The link to show: prefer `verification_uri_complete` (it embeds the user
|
||
+ /// code for one-click approval), else the bare `verification_uri`.
|
||
+ fn link(&self) -> &str {
|
||
+ self.verification_uri_complete
|
||
+ .as_deref()
|
||
+ .filter(|s| !s.is_empty())
|
||
+ .unwrap_or(&self.verification_uri)
|
||
+ }
|
||
+}
|
||
+
|
||
+#[derive(Debug, Deserialize)]
|
||
+struct TokenSuccess {
|
||
+ access_token: String,
|
||
+ #[serde(default)]
|
||
+ id_token: Option<String>,
|
||
+ #[serde(default)]
|
||
+ refresh_token: Option<String>,
|
||
+}
|
||
+
|
||
+#[derive(Debug, Deserialize)]
|
||
+struct OAuthError {
|
||
+ error: String,
|
||
+ #[serde(default)]
|
||
+ error_description: Option<String>,
|
||
+}
|
||
+
|
||
+/// The outcome of one poll of the token endpoint.
|
||
+#[derive(Debug)]
|
||
+enum Poll {
|
||
+ Token(TokenSuccess),
|
||
+ /// Keep waiting at the current interval (`authorization_pending`).
|
||
+ Pending,
|
||
+ /// Keep waiting but back off the interval (`slow_down`).
|
||
+ SlowDown,
|
||
+}
|
||
+
|
||
+/// Classify a single token-endpoint response. Pure (no reqwest types) so the
|
||
+/// error mapping is unit-testable. A successful token wins; otherwise the body
|
||
+/// is parsed as an OAuth error and mapped to pending/slow_down (retry) or a
|
||
+/// terminal `io::Error`.
|
||
+fn classify_poll(is_success: bool, body: &str) -> io::Result<Poll> {
|
||
+ if is_success {
|
||
+ if let Ok(tok) = serde_json::from_str::<TokenSuccess>(body) {
|
||
+ if !tok.access_token.is_empty() {
|
||
+ return Ok(Poll::Token(tok));
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+
|
||
+ // A body that is not an OAuth error is the edge answering for the issuer
|
||
+ // — a 502 page mid-wait. The grant is still pending there; keep polling
|
||
+ // and let the device code's own lifetime end it.
|
||
+ let Ok(err) = serde_json::from_str::<OAuthError>(body) else {
|
||
+ return Ok(Poll::Pending);
|
||
+ };
|
||
+
|
||
+ match err.error.as_str() {
|
||
+ "authorization_pending" => Ok(Poll::Pending),
|
||
+ "slow_down" => Ok(Poll::SlowDown),
|
||
+ "access_denied" => Err(io::Error::new(
|
||
+ io::ErrorKind::PermissionDenied,
|
||
+ "device authorization was denied",
|
||
+ )),
|
||
+ "expired_token" => Err(io::Error::other(
|
||
+ "the device code expired before approval; run login again",
|
||
+ )),
|
||
+ other => Err(io::Error::other(match err.error_description {
|
||
+ Some(desc) if !desc.is_empty() => {
|
||
+ format!("device authorization failed: {other} — {desc}")
|
||
+ }
|
||
+ _ => format!("device authorization failed: {other}"),
|
||
+ })),
|
||
+ }
|
||
+}
|
||
+
|
||
+async fn request_device_auth(
|
||
+ client: &reqwest::Client,
|
||
+ issuer: &str,
|
||
+ client_id: &str,
|
||
+) -> io::Result<DeviceAuth> {
|
||
+ let resp = client
|
||
+ .post(format!("{issuer}/oauth/device"))
|
||
+ .form(&[
|
||
+ ("client_id", client_id),
|
||
+ ("scope", SCOPE),
|
||
+ ("response_type", "device_code"),
|
||
+ ])
|
||
+ .send()
|
||
+ .await
|
||
+ .map_err(io::Error::other)?;
|
||
+
|
||
+ let status = resp.status();
|
||
+ let body = resp.text().await.map_err(io::Error::other)?;
|
||
+ if !status.is_success() {
|
||
+ return Err(io::Error::other(format!(
|
||
+ "device authorization request failed ({status}): {body}"
|
||
+ )));
|
||
+ }
|
||
+
|
||
+ let auth: DeviceAuth = serde_json::from_str(&body)
|
||
+ .map_err(|e| io::Error::other(format!("invalid device authorization response: {e}: {body}")))?;
|
||
+ if auth.device_code.is_empty() || auth.user_code.is_empty() {
|
||
+ // A JSON 200 that is really an error envelope (e.g. an invalid client).
|
||
+ return Err(io::Error::other(format!(
|
||
+ "device authorization response missing device_code/user_code: {body}"
|
||
+ )));
|
||
+ }
|
||
+ Ok(auth)
|
||
+}
|
||
+
|
||
+async fn poll_for_token(
|
||
+ client: &reqwest::Client,
|
||
+ issuer: &str,
|
||
+ client_id: &str,
|
||
+ auth: &DeviceAuth,
|
||
+) -> io::Result<TokenSuccess> {
|
||
+ let url = format!("{issuer}/oauth/token");
|
||
+ let deadline = Instant::now() + auth.lifetime();
|
||
+ let mut interval = auth.poll_interval();
|
||
+
|
||
+ loop {
|
||
+ if Instant::now() >= deadline {
|
||
+ return Err(io::Error::other(
|
||
+ "the device code expired before approval; run login again",
|
||
+ ));
|
||
+ }
|
||
+
|
||
+ let resp = client
|
||
+ .post(&url)
|
||
+ .form(&[
|
||
+ ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||
+ ("device_code", auth.device_code.as_str()),
|
||
+ ("client_id", client_id),
|
||
+ ])
|
||
+ .send()
|
||
+ .await
|
||
+ .map_err(io::Error::other)?;
|
||
+
|
||
+ let is_success = resp.status().is_success();
|
||
+ let body = resp.text().await.map_err(io::Error::other)?;
|
||
+
|
||
+ match classify_poll(is_success, &body)? {
|
||
+ Poll::Token(tok) => return Ok(tok),
|
||
+ Poll::Pending => {}
|
||
+ Poll::SlowDown => {
|
||
+ interval = (interval + SLOW_DOWN_BACKOFF_SECS).min(MAX_INTERVAL_SECS);
|
||
+ }
|
||
+ }
|
||
+
|
||
+ let remaining = deadline.saturating_duration_since(Instant::now());
|
||
+ tokio::time::sleep(Duration::from_secs(interval).min(remaining)).await;
|
||
+ }
|
||
+}
|
||
+
|
||
+/// An approved-or-not device authorization, in flight. Split so a terminal
|
||
+/// caller can print the code while a TUI renders it, over one implementation.
|
||
+pub struct DeviceGrant {
|
||
+ client: reqwest::Client,
|
||
+ opts: ServerOptions,
|
||
+ auth: DeviceAuth,
|
||
+}
|
||
+
|
||
+impl DeviceGrant {
|
||
+ /// Ask the issuer for a device code. `opts.issuer` is the issuer base (e.g.
|
||
+ /// `https://hanzo.id/v1/iam`) and `opts.client_id` the public client.
|
||
+ pub async fn start(mut opts: ServerOptions) -> io::Result<Self> {
|
||
+ opts.issuer = opts.issuer.trim_end_matches('/').to_string();
|
||
+ let client = create_client(&opts.originator);
|
||
+ let auth = request_device_auth(&client, &opts.issuer, &opts.client_id).await?;
|
||
+ Ok(Self { client, opts, auth })
|
||
+ }
|
||
+
|
||
+ /// Where the user approves this device.
|
||
+ pub fn link(&self) -> &str {
|
||
+ self.auth.link()
|
||
+ }
|
||
+
|
||
+ /// The code the user confirms against the page.
|
||
+ pub fn user_code(&self) -> &str {
|
||
+ &self.auth.user_code
|
||
+ }
|
||
+
|
||
+ /// Poll until the user approves, then persist the tokens.
|
||
+ pub async fn wait(self) -> io::Result<()> {
|
||
+ let TokenSuccess {
|
||
+ access_token,
|
||
+ id_token,
|
||
+ refresh_token,
|
||
+ } = poll_for_token(
|
||
+ &self.client,
|
||
+ &self.opts.issuer,
|
||
+ &self.opts.client_id,
|
||
+ &self.auth,
|
||
+ )
|
||
+ .await?;
|
||
+
|
||
+ // IAM returns an id_token alongside the access token; fall back to the
|
||
+ // access token (also a JWT) when the issuer omits it.
|
||
+ let id_token = id_token.unwrap_or_else(|| access_token.clone());
|
||
+ persist_tokens_async(
|
||
+ &self.opts.code_home,
|
||
+ None,
|
||
+ id_token,
|
||
+ access_token,
|
||
+ refresh_token.unwrap_or_default(),
|
||
+ )
|
||
+ .await
|
||
+ }
|
||
+
|
||
+ /// The whole flow for a terminal: ask, print, wait.
|
||
+ pub async fn run(opts: ServerOptions) -> io::Result<()> {
|
||
+ let grant = Self::start(opts).await?;
|
||
+ eprintln!(
|
||
+ "\nSign in to authorize this device:\n\
|
||
+\n1. Open this link and sign in:\n {ANSI_BLUE}{link}{ANSI_RESET}\n\
|
||
+\n2. Confirm this one-time code matches what the page shows:\n {ANSI_BLUE}{code}{ANSI_RESET}\n\
|
||
+\n{ANSI_GRAY}Device codes are a common phishing target. Never share this code.{ANSI_RESET}\n",
|
||
+ link = grant.link(),
|
||
+ code = grant.user_code(),
|
||
+ );
|
||
+ grant.wait().await
|
||
+ }
|
||
+}
|
||
+
|
||
+#[cfg(test)]
|
||
+mod tests {
|
||
+ use super::*;
|
||
+
|
||
+ fn auth(interval: u64, expires_in: u64) -> DeviceAuth {
|
||
+ DeviceAuth {
|
||
+ device_code: "dc".into(),
|
||
+ user_code: "WDJB-MJHT".into(),
|
||
+ verification_uri: "https://hanzo.id/login/oauth/device".into(),
|
||
+ verification_uri_complete: None,
|
||
+ expires_in,
|
||
+ interval,
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn poll_interval_defaults_and_clamps() {
|
||
+ assert_eq!(auth(0, 0).poll_interval(), DEFAULT_INTERVAL_SECS);
|
||
+ assert_eq!(auth(3, 0).poll_interval(), 3);
|
||
+ assert_eq!(auth(9999, 0).poll_interval(), MAX_INTERVAL_SECS);
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn lifetime_defaults_and_clamps() {
|
||
+ assert_eq!(
|
||
+ auth(5, 0).lifetime(),
|
||
+ Duration::from_secs(DEFAULT_EXPIRES_IN_SECS)
|
||
+ );
|
||
+ assert_eq!(auth(5, 120).lifetime(), Duration::from_secs(120));
|
||
+ // a hostile or huge expires_in is clamped, never overflowing Instant+Duration.
|
||
+ assert_eq!(
|
||
+ auth(5, 999_999).lifetime(),
|
||
+ Duration::from_secs(MAX_EXPIRES_IN_SECS)
|
||
+ );
|
||
+ assert_eq!(
|
||
+ auth(5, u64::MAX).lifetime(),
|
||
+ Duration::from_secs(MAX_EXPIRES_IN_SECS)
|
||
+ );
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn link_prefers_complete() {
|
||
+ let mut a = auth(5, 900);
|
||
+ assert_eq!(a.link(), "https://hanzo.id/login/oauth/device");
|
||
+ a.verification_uri_complete = Some("https://hanzo.id/login/oauth/device/WDJB-MJHT".into());
|
||
+ assert_eq!(a.link(), "https://hanzo.id/login/oauth/device/WDJB-MJHT");
|
||
+ a.verification_uri_complete = Some(String::new());
|
||
+ assert_eq!(a.link(), "https://hanzo.id/login/oauth/device");
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn deserializes_iam_device_response() {
|
||
+ // Verbatim from hanzo.id/v1/iam/oauth/device.
|
||
+ let body = r#"{"device_code":"CeFNZQ7B6FI7NPfJ2LjkmC6miFdSdtoQVwwzV3gV-ac",
|
||
+ "user_code":"WAAF4ART",
|
||
+ "verification_uri":"https://hanzo.id/login/oauth/device",
|
||
+ "verification_uri_complete":"https://hanzo.id/login/oauth/device/WAAF4ART",
|
||
+ "expires_in":900,"interval":5}"#;
|
||
+ let a: DeviceAuth = serde_json::from_str(body).expect("parse");
|
||
+ assert_eq!(a.device_code, "CeFNZQ7B6FI7NPfJ2LjkmC6miFdSdtoQVwwzV3gV-ac");
|
||
+ assert_eq!(a.user_code, "WAAF4ART");
|
||
+ assert_eq!(a.expires_in, 900);
|
||
+ assert_eq!(a.poll_interval(), 5);
|
||
+ assert_eq!(a.link(), "https://hanzo.id/login/oauth/device/WAAF4ART");
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn classify_success_token() {
|
||
+ let body = r#"{"access_token":"at","id_token":"it","refresh_token":"rt","token_type":"Bearer","expires_in":1209600}"#;
|
||
+ match classify_poll(true, body).expect("ok") {
|
||
+ Poll::Token(t) => {
|
||
+ assert_eq!(t.access_token, "at");
|
||
+ assert_eq!(t.id_token.as_deref(), Some("it"));
|
||
+ assert_eq!(t.refresh_token.as_deref(), Some("rt"));
|
||
+ }
|
||
+ other => panic!("expected token, got {other:?}"),
|
||
+ }
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn classify_pending_and_slow_down_retry() {
|
||
+ assert!(matches!(
|
||
+ classify_poll(false, r#"{"error":"authorization_pending"}"#).unwrap(),
|
||
+ Poll::Pending
|
||
+ ));
|
||
+ assert!(matches!(
|
||
+ classify_poll(false, r#"{"error":"slow_down"}"#).unwrap(),
|
||
+ Poll::SlowDown
|
||
+ ));
|
||
+ }
|
||
+
|
||
+ #[test]
|
||
+ fn classify_terminal_errors() {
|
||
+ let denied = classify_poll(false, r#"{"error":"access_denied"}"#).unwrap_err();
|
||
+ assert_eq!(denied.kind(), io::ErrorKind::PermissionDenied);
|
||
+
|
||
+ // Verbatim from hanzo.id's token endpoint for an unknown device_code.
|
||
+ let expired = classify_poll(
|
||
+ false,
|
||
+ r#"{"error":"expired_token","error_description":"the device code is expired or already redeemed"}"#,
|
||
+ )
|
||
+ .unwrap_err();
|
||
+ assert!(expired.to_string().contains("expired"));
|
||
+
|
||
+ let other = classify_poll(
|
||
+ false,
|
||
+ r#"{"error":"unsupported_grant_type","error_description":"nope"}"#,
|
||
+ )
|
||
+ .unwrap_err();
|
||
+ assert!(other.to_string().contains("unsupported_grant_type"));
|
||
+ assert!(other.to_string().contains("nope"));
|
||
+ }
|
||
+
|
||
+ /// The edge answered instead of the issuer — Cloudflare's 502 page mid-wait
|
||
+ /// killed a login that would have succeeded on the next poll. Anything that
|
||
+ /// is not an OAuth error keeps polling; the device code's lifetime ends it.
|
||
+ #[test]
|
||
+ fn classify_unparseable_keeps_polling() {
|
||
+ assert!(matches!(
|
||
+ classify_poll(false, "<!DOCTYPE html><html>502</html>"),
|
||
+ Ok(Poll::Pending)
|
||
+ ));
|
||
+ assert!(matches!(
|
||
+ classify_poll(true, r#"{"foo":"bar"}"#),
|
||
+ Ok(Poll::Pending)
|
||
+ ));
|
||
+ }
|
||
+}
|
||
diff --git a/login/src/server.rs b/login/src/server.rs
|
||
index b801f19..0a58126 100644
|
||
--- a/login/src/server.rs
|
||
+++ b/login/src/server.rs
|
||
@@ -16,6 +16,8 @@ use base64::Engine;
|
||
use chrono::Utc;
|
||
use code_app_server_protocol::AuthMode;
|
||
use code_core::auth::AuthDotJson;
|
||
+use code_core::auth::HANZO_CLIENT_ID;
|
||
+use code_core::auth::HANZO_ISSUER;
|
||
use code_core::auth::get_auth_file;
|
||
use code_core::token_data::TokenData;
|
||
use code_core::token_data::parse_id_token;
|
||
@@ -52,6 +54,15 @@ impl ServerOptions {
|
||
originator,
|
||
}
|
||
}
|
||
+
|
||
+ /// Hanzo IAM's issuer and public client. Sibling brands reach their own IAM
|
||
+ /// by overriding `issuer` and `client_id` (e.g. `https://lux.id/v1/iam`).
|
||
+ pub fn hanzo(code_home: PathBuf, originator: String) -> Self {
|
||
+ Self {
|
||
+ issuer: HANZO_ISSUER.to_string(),
|
||
+ ..Self::new(code_home, HANZO_CLIENT_ID.to_string(), originator)
|
||
+ }
|
||
+ }
|
||
}
|
||
|
||
pub struct LoginServer {
|
||
diff --git a/mcp-client/src/main.rs b/mcp-client/src/main.rs
|
||
index 980887c..cd2cf59 100644
|
||
--- a/mcp-client/src/main.rs
|
||
+++ b/mcp-client/src/main.rs
|
||
@@ -63,7 +63,7 @@ async fn main() -> Result<()> {
|
||
client_info: Implementation {
|
||
name: "codex-mcp-client".to_owned(),
|
||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||
- title: Some("Codex".to_string()),
|
||
+ title: Some("Hanzo Dev".to_string()),
|
||
// This field is used by Codex when it is an MCP server: it should
|
||
// not be used when Codex is an MCP client.
|
||
user_agent: None,
|
||
diff --git a/mcp-server/src/acp_tool_runner.rs b/mcp-server/src/acp_tool_runner.rs
|
||
index 8886304..d745d61 100644
|
||
--- a/mcp-server/src/acp_tool_runner.rs
|
||
+++ b/mcp-server/src/acp_tool_runner.rs
|
||
@@ -42,7 +42,7 @@ pub async fn new_session(
|
||
let result = CallToolResult {
|
||
content: vec![ContentBlock::TextContent(TextContent {
|
||
r#type: "text".to_string(),
|
||
- text: format!("Failed to start Codex session: {err}"),
|
||
+ text: format!("Failed to start Hanzo Dev session: {err}"),
|
||
annotations: None,
|
||
})],
|
||
is_error: Some(true),
|
||
@@ -78,7 +78,7 @@ pub async fn prompt(
|
||
final_output_json_schema: None,
|
||
})
|
||
.await
|
||
- .context("failed to submit prompt to Codex")?;
|
||
+ .context("failed to submit prompt to Hanzo Dev")?;
|
||
|
||
let mut stop_reason = acp::StopReason::EndTurn;
|
||
|
||
diff --git a/mcp-server/src/code_tool_config.rs b/mcp-server/src/code_tool_config.rs
|
||
index b6a53cf..020b76c 100644
|
||
--- a/mcp-server/src/code_tool_config.rs
|
||
+++ b/mcp-server/src/code_tool_config.rs
|
||
@@ -47,7 +47,7 @@ pub struct CodexToolCallParam {
|
||
pub sandbox: Option<CodexToolCallSandboxMode>,
|
||
|
||
/// Individual config settings that will override what is in
|
||
- /// CODEX_HOME/config.toml.
|
||
+ /// DEV_HOME/config.toml.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||
|
||
@@ -127,12 +127,12 @@ pub(crate) fn create_tool_for_code_tool_call_param() -> Tool {
|
||
|
||
Tool {
|
||
name: "codex".to_string(),
|
||
- title: Some("Codex".to_string()),
|
||
+ title: Some("Hanzo Dev".to_string()),
|
||
input_schema: tool_input_schema,
|
||
// TODO(mbolin): This should be defined.
|
||
output_schema: None,
|
||
description: Some(
|
||
- "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(),
|
||
+ "Run a Hanzo Dev session. Accepts configuration parameters matching the Config struct.".to_string(),
|
||
),
|
||
annotations: None,
|
||
}
|
||
@@ -160,7 +160,7 @@ pub(crate) fn create_tool_for_acp_new_session() -> Tool {
|
||
title: Some(acp::AGENT_METHOD_NAMES.session_new.to_string()),
|
||
input_schema,
|
||
output_schema: Some(output_schema),
|
||
- description: Some("Start a Codex session over ACP.".to_string()),
|
||
+ description: Some("Start a Hanzo Dev session over ACP.".to_string()),
|
||
annotations: None,
|
||
}
|
||
}
|
||
@@ -182,7 +182,7 @@ pub(crate) fn create_tool_for_acp_prompt() -> Tool {
|
||
title: Some(acp::AGENT_METHOD_NAMES.session_prompt.to_string()),
|
||
input_schema,
|
||
output_schema: None,
|
||
- description: Some("Send a prompt to an existing ACP Codex session.".to_string()),
|
||
+ description: Some("Send a prompt to an existing ACP Hanzo Dev session.".to_string()),
|
||
annotations: None,
|
||
}
|
||
}
|
||
@@ -203,7 +203,7 @@ pub(crate) fn create_tool_for_acp_set_model() -> Tool {
|
||
title: Some(acp::AGENT_METHOD_NAMES.session_set_model.to_string()),
|
||
input_schema,
|
||
output_schema: None,
|
||
- description: Some("Select a model for an existing ACP Codex session.".to_string()),
|
||
+ description: Some("Select a model for an existing ACP Hanzo Dev session.".to_string()),
|
||
annotations: None,
|
||
}
|
||
}
|
||
@@ -324,11 +324,11 @@ pub(crate) fn create_tool_for_code_tool_call_reply_param() -> Tool {
|
||
|
||
Tool {
|
||
name: "codex-reply".to_string(),
|
||
- title: Some("Codex Reply".to_string()),
|
||
+ title: Some("Hanzo Dev Reply".to_string()),
|
||
input_schema: tool_input_schema,
|
||
output_schema: None,
|
||
description: Some(
|
||
- "Continue a Codex session by providing the session id and prompt.".to_string(),
|
||
+ "Continue a Hanzo Dev session by providing the session id and prompt.".to_string(),
|
||
),
|
||
annotations: None,
|
||
}
|
||
@@ -357,8 +357,8 @@ mod tests {
|
||
let tool_json = serde_json::to_value(&tool).expect("tool serializes");
|
||
let expected_tool_json = serde_json::json!({
|
||
"name": "codex",
|
||
- "title": "Codex",
|
||
- "description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
|
||
+ "title": "Hanzo Dev",
|
||
+ "description": "Run a Hanzo Dev session. Accepts configuration parameters matching the Config struct.",
|
||
"inputSchema": {
|
||
"type": "object",
|
||
"properties": {
|
||
@@ -382,7 +382,7 @@ mod tests {
|
||
"type": "string"
|
||
},
|
||
"config": {
|
||
- "description": "Individual config settings that will override what is in CODEX_HOME/config.toml.",
|
||
+ "description": "Individual config settings that will override what is in DEV_HOME/config.toml.",
|
||
"additionalProperties": true,
|
||
"type": "object"
|
||
},
|
||
@@ -517,7 +517,7 @@ mod tests {
|
||
#[expect(clippy::expect_used)]
|
||
let tool_json = serde_json::to_value(&tool).expect("tool serializes");
|
||
let expected_tool_json = serde_json::json!({
|
||
- "description": "Continue a Codex session by providing the session id and prompt.",
|
||
+ "description": "Continue a Hanzo Dev session by providing the session id and prompt.",
|
||
"inputSchema": {
|
||
"properties": {
|
||
"prompt": {
|
||
@@ -536,7 +536,7 @@ mod tests {
|
||
"type": "object",
|
||
},
|
||
"name": "codex-reply",
|
||
- "title": "Codex Reply",
|
||
+ "title": "Hanzo Dev Reply",
|
||
});
|
||
assert_eq!(expected_tool_json, tool_json);
|
||
}
|
||
diff --git a/mcp-server/src/code_tool_runner.rs b/mcp-server/src/code_tool_runner.rs
|
||
index 6d06ce4..6b73567 100644
|
||
--- a/mcp-server/src/code_tool_runner.rs
|
||
+++ b/mcp-server/src/code_tool_runner.rs
|
||
@@ -60,7 +60,7 @@ pub async fn run_code_tool_session(
|
||
let result = CallToolResult {
|
||
content: vec![ContentBlock::TextContent(TextContent {
|
||
r#type: "text".to_string(),
|
||
- text: format!("Failed to start Codex session: {e}"),
|
||
+ text: format!("Failed to start Hanzo Dev session: {e}"),
|
||
annotations: None,
|
||
})],
|
||
is_error: Some(true),
|
||
@@ -367,7 +367,7 @@ async fn run_code_tool_session_inner(
|
||
let result = CallToolResult {
|
||
content: vec![ContentBlock::TextContent(TextContent {
|
||
r#type: "text".to_string(),
|
||
- text: format!("Codex runtime error: {e}"),
|
||
+ text: format!("Hanzo Dev runtime error: {e}"),
|
||
annotations: None,
|
||
})],
|
||
is_error: Some(true),
|
||
diff --git a/mcp-server/src/exec_approval.rs b/mcp-server/src/exec_approval.rs
|
||
index da6b618..c09a188 100644
|
||
--- a/mcp-server/src/exec_approval.rs
|
||
+++ b/mcp-server/src/exec_approval.rs
|
||
@@ -61,7 +61,7 @@ pub(crate) async fn handle_exec_approval_request(
|
||
let escaped_command =
|
||
shlex::try_join(command.iter().map(String::as_str)).unwrap_or_else(|_| command.join(" "));
|
||
let message = format!(
|
||
- "Allow Codex to run `{escaped_command}` in `{cwd}`?",
|
||
+ "Allow Hanzo Dev to run `{escaped_command}` in `{cwd}`?",
|
||
cwd = cwd.to_string_lossy()
|
||
);
|
||
|
||
diff --git a/mcp-server/src/message_processor.rs b/mcp-server/src/message_processor.rs
|
||
index cfefad8..0d1be5c 100644
|
||
--- a/mcp-server/src/message_processor.rs
|
||
+++ b/mcp-server/src/message_processor.rs
|
||
@@ -401,7 +401,7 @@ impl MessageProcessor {
|
||
let server_info = serde_json::json!({
|
||
"name": "code-mcp-server",
|
||
"version": env!("CARGO_PKG_VERSION"),
|
||
- "title": "Codex",
|
||
+ "title": "Hanzo Dev",
|
||
"user_agent": get_code_user_agent_default(),
|
||
});
|
||
|
||
@@ -418,9 +418,12 @@ impl MessageProcessor {
|
||
});
|
||
|
||
let auth_methods = serde_json::json!([{
|
||
- "id": "code-login",
|
||
- "name": "Use Code login",
|
||
- "description": "Run `code login` (ChatGPT or API key) before connecting."
|
||
+ "id": "hanzo-login",
|
||
+ "name": "Use Hanzo login",
|
||
+ "description": format!(
|
||
+ "Run `{}` before connecting.",
|
||
+ code_core::auth::LOGIN_COMMAND
|
||
+ ),
|
||
}]);
|
||
|
||
let result = serde_json::json!({
|
||
@@ -575,7 +578,7 @@ impl MessageProcessor {
|
||
content: vec![ContentBlock::TextContent(TextContent {
|
||
r#type: "text".to_owned(),
|
||
text: format!(
|
||
- "Failed to load Codex configuration from overrides: {e}"
|
||
+ "Failed to load configuration from overrides: {e}"
|
||
),
|
||
annotations: None,
|
||
})],
|
||
@@ -591,7 +594,7 @@ impl MessageProcessor {
|
||
let result = CallToolResult {
|
||
content: vec![ContentBlock::TextContent(TextContent {
|
||
r#type: "text".to_owned(),
|
||
- text: format!("Failed to parse configuration for Codex tool: {e}"),
|
||
+ text: format!("Failed to parse configuration for the Hanzo Dev tool: {e}"),
|
||
annotations: None,
|
||
})],
|
||
is_error: Some(true),
|
||
@@ -659,7 +662,7 @@ impl MessageProcessor {
|
||
let result = CallToolResult {
|
||
content: vec![ContentBlock::TextContent(TextContent {
|
||
r#type: "text".to_owned(),
|
||
- text: format!("Failed to parse configuration for Codex tool: {e}"),
|
||
+ text: format!("Failed to parse configuration for the Hanzo Dev tool: {e}"),
|
||
annotations: None,
|
||
})],
|
||
is_error: Some(true),
|
||
@@ -1542,7 +1545,7 @@ fn session_models_from_config(config: &Config) -> Option<acp::SessionModelState>
|
||
available_models.push(acp::ModelInfo {
|
||
model_id: id.clone(),
|
||
name: config.model.clone(),
|
||
- description: Some("Configured via CODEX_HOME/config.toml".to_string()),
|
||
+ description: Some("Configured via DEV_HOME/config.toml".to_string()),
|
||
meta: None,
|
||
});
|
||
return Some(acp::SessionModelState {
|
||
@@ -1558,7 +1561,7 @@ fn session_models_from_config(config: &Config) -> Option<acp::SessionModelState>
|
||
available_models.push(acp::ModelInfo {
|
||
model_id: id.clone(),
|
||
name: config.model.clone(),
|
||
- description: Some("Configured via CODEX_HOME/config.toml".to_string()),
|
||
+ description: Some("Configured via DEV_HOME/config.toml".to_string()),
|
||
meta: None,
|
||
});
|
||
current_model_id = Some(id);
|
||
diff --git a/mcp-server/src/patch_approval.rs b/mcp-server/src/patch_approval.rs
|
||
index a6e7958..0f90d1f 100644
|
||
--- a/mcp-server/src/patch_approval.rs
|
||
+++ b/mcp-server/src/patch_approval.rs
|
||
@@ -56,7 +56,7 @@ pub(crate) async fn handle_patch_approval_request(
|
||
if let Some(r) = &reason {
|
||
message_lines.push(r.clone());
|
||
}
|
||
- message_lines.push("Allow Codex to apply proposed code changes?".to_string());
|
||
+ message_lines.push("Allow Hanzo Dev to apply proposed code changes?".to_string());
|
||
|
||
let params = PatchApprovalElicitRequestParams {
|
||
message: message_lines.join("\n"),
|
||
diff --git a/migrate-crate.sh b/migrate-crate.sh
|
||
index 129791f..0593b50 100755
|
||
--- a/migrate-crate.sh
|
||
+++ b/migrate-crate.sh
|
||
@@ -1,5 +1,5 @@
|
||
#!/bin/bash
|
||
-# migrate-crate.sh - Helper script to migrate a crate from codex-rs to code-rs
|
||
+# migrate-crate.sh - Helper script to migrate a crate from codex-rs to hanzo-dev
|
||
# Usage: ./migrate-crate.sh <crate-name>
|
||
# Example: ./migrate-crate.sh linux-sandbox
|
||
|
||
@@ -12,7 +12,7 @@ if [ $# -ne 1 ]; then
|
||
fi
|
||
|
||
CRATE_NAME="$1"
|
||
-CODEX_CRATE_PATH="../codex-rs/${CRATE_NAME}"
|
||
+DEV_CRATE_PATH="../codex-rs/${CRATE_NAME}"
|
||
CODE_CRATE_PATH="${CRATE_NAME}"
|
||
|
||
# Colors for output
|
||
@@ -21,11 +21,11 @@ GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
NC='\033[0m' # No Color
|
||
|
||
-echo -e "${GREEN}=== Migrating ${CRATE_NAME} from codex-rs to code-rs ===${NC}"
|
||
+echo -e "${GREEN}=== Migrating ${CRATE_NAME} from codex-rs to hanzo-dev ===${NC}"
|
||
|
||
# Step 1: Verify source crate exists
|
||
-if [ ! -d "$CODEX_CRATE_PATH" ]; then
|
||
- echo -e "${RED}Error: Source crate not found at $CODEX_CRATE_PATH${NC}"
|
||
+if [ ! -d "$DEV_CRATE_PATH" ]; then
|
||
+ echo -e "${RED}Error: Source crate not found at $DEV_CRATE_PATH${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
@@ -42,13 +42,13 @@ cp "${CODE_CRATE_PATH}/src/lib.rs" "${CODE_CRATE_PATH}/src/lib.rs.backup"
|
||
# Step 4: Copy source files
|
||
echo -e "${YELLOW}Copying source files from codex-rs...${NC}"
|
||
# Copy all .rs files except main.rs and lib.rs (we'll handle those specially)
|
||
-find "${CODEX_CRATE_PATH}/src" -name "*.rs" -not -name "main.rs" -not -name "lib.rs" -exec cp -v {} "${CODE_CRATE_PATH}/src/" \;
|
||
+find "${DEV_CRATE_PATH}/src" -name "*.rs" -not -name "main.rs" -not -name "lib.rs" -exec cp -v {} "${CODE_CRATE_PATH}/src/" \;
|
||
|
||
# Step 5: Copy tests if they exist
|
||
-if [ -d "${CODEX_CRATE_PATH}/tests" ]; then
|
||
+if [ -d "${DEV_CRATE_PATH}/tests" ]; then
|
||
echo -e "${YELLOW}Copying test files...${NC}"
|
||
mkdir -p "${CODE_CRATE_PATH}/tests"
|
||
- cp -rv "${CODEX_CRATE_PATH}/tests/"* "${CODE_CRATE_PATH}/tests/"
|
||
+ cp -rv "${DEV_CRATE_PATH}/tests/"* "${CODE_CRATE_PATH}/tests/"
|
||
fi
|
||
|
||
# Step 6: Display the original Cargo.toml for reference
|
||
@@ -56,11 +56,11 @@ echo -e "${YELLOW}=== Original codex-rs Cargo.toml dependencies ===${NC}"
|
||
echo "You need to manually update ${CODE_CRATE_PATH}/Cargo.toml with these dependencies:"
|
||
echo "(replacing codex-* with code-* where applicable)"
|
||
echo ""
|
||
-grep -A 100 "^\[dependencies\]" "${CODEX_CRATE_PATH}/Cargo.toml" | grep -B 100 "^\[" | head -n -1 || true
|
||
+grep -A 100 "^\[dependencies\]" "${DEV_CRATE_PATH}/Cargo.toml" | grep -B 100 "^\[" | head -n -1 || true
|
||
echo ""
|
||
-grep -A 100 "^\[dev-dependencies\]" "${CODEX_CRATE_PATH}/Cargo.toml" | grep -B 100 "^\[" | head -n -1 || true
|
||
+grep -A 100 "^\[dev-dependencies\]" "${DEV_CRATE_PATH}/Cargo.toml" | grep -B 100 "^\[" | head -n -1 || true
|
||
echo ""
|
||
-grep -A 100 "^\[target\." "${CODEX_CRATE_PATH}/Cargo.toml" || true
|
||
+grep -A 100 "^\[target\." "${DEV_CRATE_PATH}/Cargo.toml" || true
|
||
|
||
echo -e "${GREEN}Files copied successfully!${NC}"
|
||
echo ""
|
||
diff --git a/ollama/src/client.rs b/ollama/src/client.rs
|
||
index 84c55f6..1ef83a5 100644
|
||
--- a/ollama/src/client.rs
|
||
+++ b/ollama/src/client.rs
|
||
@@ -298,10 +298,10 @@ mod tests {
|
||
// Happy-path tests using a mock HTTP server; skip if sandbox network is disabled.
|
||
#[tokio::test]
|
||
async fn test_fetch_models_happy_path() {
|
||
- if std::env::var(code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
+ if std::env::var(code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
tracing::info!(
|
||
"{} is set; skipping test_fetch_models_happy_path",
|
||
- code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
+ code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
);
|
||
return;
|
||
}
|
||
@@ -329,10 +329,10 @@ mod tests {
|
||
|
||
#[tokio::test]
|
||
async fn test_probe_server_happy_path_openai_compat_and_native() {
|
||
- if std::env::var(code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
+ if std::env::var(code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
tracing::info!(
|
||
"{} set; skipping test_probe_server_happy_path_openai_compat_and_native",
|
||
- code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
+ code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
);
|
||
return;
|
||
}
|
||
@@ -366,10 +366,10 @@ mod tests {
|
||
|
||
#[tokio::test]
|
||
async fn test_try_from_oss_provider_ok_when_server_running() {
|
||
- if std::env::var(code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
+ if std::env::var(code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
tracing::info!(
|
||
"{} set; skipping test_try_from_oss_provider_ok_when_server_running",
|
||
- code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
+ code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
);
|
||
return;
|
||
}
|
||
@@ -390,10 +390,10 @@ mod tests {
|
||
|
||
#[tokio::test]
|
||
async fn test_try_from_oss_provider_err_when_server_missing() {
|
||
- if std::env::var(code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
+ if std::env::var(code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
tracing::info!(
|
||
"{} set; skipping test_try_from_oss_provider_err_when_server_missing",
|
||
- code_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
+ code_core::spawn::DEV_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||
);
|
||
return;
|
||
}
|
||
diff --git a/ollama/src/lib.rs b/ollama/src/lib.rs
|
||
index 336e592..cfe4571 100644
|
||
--- a/ollama/src/lib.rs
|
||
+++ b/ollama/src/lib.rs
|
||
@@ -46,7 +46,7 @@ pub async fn ensure_oss_ready(config: &Config) -> std::io::Result<()> {
|
||
// Avoid exporting obviously tiny defaults (some installs report 2048);
|
||
// still set it so the client can override server defaults.
|
||
// Mutating process env requires `unsafe` on Rust 2024; scoped to process.
|
||
- unsafe { std::env::set_var("CODEX_OLLAMA_NUM_CTX", ctx.to_string()); }
|
||
+ unsafe { std::env::set_var("DEV_OLLAMA_NUM_CTX", ctx.to_string()); }
|
||
tracing::info!("Detected Ollama model context length: {ctx}");
|
||
}
|
||
|
||
diff --git a/protocol/src/mcp_protocol.rs b/protocol/src/mcp_protocol.rs
|
||
index ca8b332..b046449 100644
|
||
--- a/protocol/src/mcp_protocol.rs
|
||
+++ b/protocol/src/mcp_protocol.rs
|
||
@@ -156,6 +156,11 @@ pub enum ClientRequest {
|
||
request_id: RequestId,
|
||
params: SendUserMessageParams,
|
||
},
|
||
+ QueueUserMessage {
|
||
+ #[serde(rename = "id")]
|
||
+ request_id: RequestId,
|
||
+ params: QueueUserMessageParams,
|
||
+ },
|
||
SendUserTurn {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
@@ -317,7 +322,7 @@ pub struct NewConversationParams {
|
||
pub sandbox: Option<SandboxMode>,
|
||
|
||
/// Individual config settings that will override what is in
|
||
- /// CODEX_HOME/config.toml.
|
||
+ /// DEV_HOME/config.toml.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||
|
||
@@ -639,6 +644,23 @@ pub struct SendUserMessageParams {
|
||
pub items: Vec<InputItem>,
|
||
}
|
||
|
||
+/// Steer a turn that is already running instead of replacing it.
|
||
+///
|
||
+/// `sendUserMessage` submits `Op::UserInput`, which calls `sess.abort()` and
|
||
+/// spawns a fresh turn — the caller can interrupt, never redirect. This submits
|
||
+/// `Op::QueueUserInput`, which folds the items into the turn already in flight.
|
||
+/// With no turn running, core spawns one either way.
|
||
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
+#[serde(rename_all = "camelCase")]
|
||
+pub struct QueueUserMessageParams {
|
||
+ pub conversation_id: ConversationId,
|
||
+ pub items: Vec<InputItem>,
|
||
+}
|
||
+
|
||
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
+#[serde(rename_all = "camelCase")]
|
||
+pub struct QueueUserMessageResponse {}
|
||
+
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SendUserTurnParams {
|
||
@@ -945,6 +967,33 @@ mod tests {
|
||
Ok(())
|
||
}
|
||
|
||
+ /// Locks the wire shape of the steering method. `sendUserMessage` submits
|
||
+ /// `Op::UserInput`, which aborts the running turn; this one submits
|
||
+ /// `Op::QueueUserInput`, which does not. They are separate methods
|
||
+ /// precisely so a caller cannot pick the destructive one by accident.
|
||
+ #[test]
|
||
+ fn serialize_queue_user_message() -> Result<()> {
|
||
+ let request = ClientRequest::QueueUserMessage {
|
||
+ request_id: RequestId::Integer(7),
|
||
+ params: QueueUserMessageParams {
|
||
+ conversation_id: ConversationId::default(),
|
||
+ items: vec![InputItem::Text {
|
||
+ text: "steer".to_string(),
|
||
+ }],
|
||
+ },
|
||
+ };
|
||
+ let value = serde_json::to_value(&request)?;
|
||
+ assert_eq!(json!("queueUserMessage"), value["method"]);
|
||
+ assert_eq!(
|
||
+ json!([{ "type": "text", "data": { "text": "steer" } }]),
|
||
+ value["params"]["items"],
|
||
+ );
|
||
+ // And it round-trips back to the same variant.
|
||
+ let back: ClientRequest = serde_json::from_value(value)?;
|
||
+ assert!(matches!(back, ClientRequest::QueueUserMessage { .. }));
|
||
+ Ok(())
|
||
+ }
|
||
+
|
||
#[test]
|
||
fn test_conversation_id_default_is_not_zeroes() {
|
||
let id = ConversationId::default();
|
||
diff --git a/protocol/src/protocol.rs b/protocol/src/protocol.rs
|
||
index fae0c2a..c425958 100644
|
||
--- a/protocol/src/protocol.rs
|
||
+++ b/protocol/src/protocol.rs
|
||
@@ -70,7 +70,7 @@ pub const BROWSER_SNAPSHOT_OPEN_TAG: &str = "<browser_snapshot>";
|
||
pub const BROWSER_SNAPSHOT_CLOSE_TAG: &str = "</browser_snapshot>";
|
||
pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";
|
||
pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
|
||
-pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
|
||
+pub const USER_MESSAGE_BEGIN: &str = "## My request for Hanzo Dev:";
|
||
|
||
/// Submission Queue Entry - requests from user
|
||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
|
||
diff --git a/responses-api-proxy/npm/package.json b/responses-api-proxy/npm/package.json
|
||
index 0d3a793..e245638 100644
|
||
--- a/responses-api-proxy/npm/package.json
|
||
+++ b/responses-api-proxy/npm/package.json
|
||
@@ -1,5 +1,5 @@
|
||
{
|
||
- "name": "@openai/codex-responses-api-proxy",
|
||
+ "name": "@hanzo/dev-responses-api-proxy",
|
||
"version": "0.0.0-dev",
|
||
"license": "Apache-2.0",
|
||
"bin": {
|
||
@@ -15,7 +15,7 @@
|
||
],
|
||
"repository": {
|
||
"type": "git",
|
||
- "url": "git+https://github.com/openai/codex.git",
|
||
- "directory": "code-rs/responses-api-proxy/npm"
|
||
+ "url": "git+https://github.com/hanzoai/dev.git",
|
||
+ "directory": "hanzo-dev/responses-api-proxy/npm"
|
||
}
|
||
}
|
||
diff --git a/scripts/create_github_release b/scripts/create_github_release
|
||
index 3962c63..89db9b6 100755
|
||
--- a/scripts/create_github_release
|
||
+++ b/scripts/create_github_release
|
||
@@ -10,7 +10,7 @@ import sys
|
||
|
||
REPO = "openai/codex"
|
||
BRANCH_REF = "heads/main"
|
||
-CARGO_TOML_PATH = "code-rs/Cargo.toml"
|
||
+CARGO_TOML_PATH = "hanzo-dev/Cargo.toml"
|
||
|
||
|
||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||
diff --git a/tui/build.rs b/tui/build.rs
|
||
index f6b92d1..4c073aa 100644
|
||
--- a/tui/build.rs
|
||
+++ b/tui/build.rs
|
||
@@ -1,6 +1,6 @@
|
||
fn main() {
|
||
if let Ok(profile) = std::env::var("PROFILE") {
|
||
- println!("cargo:rustc-env=CODEX_PROFILE={profile}");
|
||
+ println!("cargo:rustc-env=DEV_PROFILE={profile}");
|
||
}
|
||
use std::collections::BTreeMap;
|
||
use std::fs;
|
||
diff --git a/tui/src/app/events.rs b/tui/src/app/events.rs
|
||
index 04dd254..0704c80 100644
|
||
--- a/tui/src/app/events.rs
|
||
+++ b/tui/src/app/events.rs
|
||
@@ -2095,6 +2095,22 @@ impl App<'_> {
|
||
flow.join_handle.abort();
|
||
}
|
||
|
||
+ // Hanzo IAM registers no loopback redirect for its CLI
|
||
+ // client, so its sign-in is the device grant. The popup
|
||
+ // renders whichever the provider answers with.
|
||
+ if self.config.model_provider_id == code_core::HANZO_PROVIDER_ID {
|
||
+ widget.notify_login_device_code_pending();
|
||
+ let join_handle = spawn_device_grant(
|
||
+ ServerOptions::hanzo(
|
||
+ self.config.code_home.clone(),
|
||
+ self.config.responses_originator_header.clone(),
|
||
+ ),
|
||
+ self.app_event_tx.clone(),
|
||
+ );
|
||
+ self.login_flow = Some(LoginFlowState { shutdown: None, join_handle });
|
||
+ continue 'main;
|
||
+ }
|
||
+
|
||
let opts = ServerOptions::new(
|
||
self.config.code_home.clone(),
|
||
code_login::CLIENT_ID.to_string(),
|
||
@@ -2140,26 +2156,38 @@ impl App<'_> {
|
||
}
|
||
widget.notify_login_device_code_pending();
|
||
|
||
- let opts = ServerOptions::new(
|
||
- self.config.code_home.clone(),
|
||
- code_login::CLIENT_ID.to_string(),
|
||
- self.config.responses_originator_header.clone(),
|
||
- );
|
||
let tx = self.app_event_tx.clone();
|
||
- let join_handle = tokio::spawn(async move {
|
||
- match code_login::DeviceCodeSession::start(opts).await {
|
||
- Ok(session) => {
|
||
- let authorize_url = session.authorize_url();
|
||
- let user_code = session.user_code().to_string();
|
||
- let _ = tx.send(AppEvent::LoginDeviceCodeReady { authorize_url, user_code });
|
||
- let result = session.wait_for_tokens().await.map_err(|e| e.to_string());
|
||
- let _ = tx.send(AppEvent::LoginDeviceCodeComplete { result });
|
||
- }
|
||
- Err(err) => {
|
||
- let _ = tx.send(AppEvent::LoginDeviceCodeFailed { message: err.to_string() });
|
||
+ let join_handle = if self.config.model_provider_id
|
||
+ == code_core::HANZO_PROVIDER_ID
|
||
+ {
|
||
+ spawn_device_grant(
|
||
+ ServerOptions::hanzo(
|
||
+ self.config.code_home.clone(),
|
||
+ self.config.responses_originator_header.clone(),
|
||
+ ),
|
||
+ tx,
|
||
+ )
|
||
+ } else {
|
||
+ let opts = ServerOptions::new(
|
||
+ self.config.code_home.clone(),
|
||
+ code_login::CLIENT_ID.to_string(),
|
||
+ self.config.responses_originator_header.clone(),
|
||
+ );
|
||
+ tokio::spawn(async move {
|
||
+ match code_login::DeviceCodeSession::start(opts).await {
|
||
+ Ok(session) => {
|
||
+ let authorize_url = session.authorize_url();
|
||
+ let user_code = session.user_code().to_string();
|
||
+ let _ = tx.send(AppEvent::LoginDeviceCodeReady { authorize_url, user_code });
|
||
+ let result = session.wait_for_tokens().await.map_err(|e| e.to_string());
|
||
+ let _ = tx.send(AppEvent::LoginDeviceCodeComplete { result });
|
||
+ }
|
||
+ Err(err) => {
|
||
+ let _ = tx.send(AppEvent::LoginDeviceCodeFailed { message: err.to_string() });
|
||
+ }
|
||
}
|
||
- }
|
||
- });
|
||
+ })
|
||
+ };
|
||
self.login_flow = Some(LoginFlowState { shutdown: None, join_handle });
|
||
}
|
||
}
|
||
@@ -2460,6 +2488,29 @@ impl App<'_> {
|
||
|
||
}
|
||
|
||
+/// Run an RFC 8628 device grant, reporting the code to show and then the
|
||
+/// outcome over the same events the OpenAI device flow reports on.
|
||
+fn spawn_device_grant(
|
||
+ opts: ServerOptions,
|
||
+ tx: crate::app_event_sender::AppEventSender,
|
||
+) -> tokio::task::JoinHandle<()> {
|
||
+ tokio::spawn(async move {
|
||
+ match code_login::DeviceGrant::start(opts).await {
|
||
+ Ok(grant) => {
|
||
+ tx.send(AppEvent::LoginDeviceCodeReady {
|
||
+ authorize_url: grant.link().to_string(),
|
||
+ user_code: grant.user_code().to_string(),
|
||
+ });
|
||
+ let result = grant.wait().await.map_err(|e| e.to_string());
|
||
+ tx.send(AppEvent::LoginDeviceCodeComplete { result });
|
||
+ }
|
||
+ Err(err) => {
|
||
+ tx.send(AppEvent::LoginDeviceCodeFailed { message: err.to_string() });
|
||
+ }
|
||
+ }
|
||
+ })
|
||
+}
|
||
+
|
||
fn is_image_clipboard_paste_shortcut(key_event: &KeyEvent) -> bool {
|
||
if !matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
|
||
return false;
|
||
diff --git a/tui/src/app/state.rs b/tui/src/app/state.rs
|
||
index 8fa7a9e..b33b271 100644
|
||
--- a/tui/src/app/state.rs
|
||
+++ b/tui/src/app/state.rs
|
||
@@ -296,7 +296,7 @@ pub(crate) struct ChatWidgetArgs {
|
||
}
|
||
|
||
impl App<'_> {
|
||
- pub(crate) const DEFAULT_TERMINAL_TITLE: &'static str = "Code";
|
||
+ pub(crate) const DEFAULT_TERMINAL_TITLE: &'static str = "Hanzo Dev";
|
||
|
||
#[cfg(unix)]
|
||
pub(crate) fn sigterm_triggered(&self) -> bool {
|
||
diff --git a/tui/src/app/terminal.rs b/tui/src/app/terminal.rs
|
||
index c819751..47e8a3d 100644
|
||
--- a/tui/src/app/terminal.rs
|
||
+++ b/tui/src/app/terminal.rs
|
||
@@ -22,10 +22,18 @@ use super::state::{
|
||
|
||
impl App<'_> {
|
||
pub(super) fn apply_terminal_title(&self) {
|
||
+ let cwd_name = self
|
||
+ .config
|
||
+ .cwd
|
||
+ .file_name()
|
||
+ .and_then(|name| name.to_str())
|
||
+ .filter(|name| !name.is_empty())
|
||
+ .unwrap_or(Self::DEFAULT_TERMINAL_TITLE);
|
||
+ let default_title = format!("{} · {}", Self::DEFAULT_TERMINAL_TITLE, cwd_name);
|
||
let title = self
|
||
.terminal_title_override
|
||
.as_deref()
|
||
- .unwrap_or(Self::DEFAULT_TERMINAL_TITLE);
|
||
+ .unwrap_or(&default_title);
|
||
let _ = crossterm::execute!(
|
||
std::io::stdout(),
|
||
crossterm::terminal::SetTitle(title.to_string())
|
||
diff --git a/tui/src/auto_drive_style.rs b/tui/src/auto_drive_style.rs
|
||
index c2869b9..fd8f7a0 100644
|
||
--- a/tui/src/auto_drive_style.rs
|
||
+++ b/tui/src/auto_drive_style.rs
|
||
@@ -53,7 +53,7 @@ impl AutoDriveVariant {
|
||
}
|
||
|
||
pub fn from_env() -> Self {
|
||
- env::var("CODEX_AUTO_DRIVE_VARIANT")
|
||
+ env::var("DEV_AUTO_DRIVE_VARIANT")
|
||
.ok()
|
||
.and_then(|raw| raw.trim().parse::<usize>().ok())
|
||
.map(Self::from_index)
|
||
diff --git a/tui/src/bottom_pane/account_switch_settings_view.rs b/tui/src/bottom_pane/account_switch_settings_view.rs
|
||
index e484cfe..9730a5f 100644
|
||
--- a/tui/src/bottom_pane/account_switch_settings_view.rs
|
||
+++ b/tui/src/bottom_pane/account_switch_settings_view.rs
|
||
@@ -120,7 +120,7 @@ impl AccountSwitchSettingsView {
|
||
lines.push(Line::from(vec![
|
||
Span::raw(" "),
|
||
Span::styled(
|
||
- "Only used if every connected ChatGPT account is limited.",
|
||
+ "Only used if every connected account is limited.",
|
||
dim,
|
||
),
|
||
]));
|
||
diff --git a/tui/src/bottom_pane/chat_composer.rs b/tui/src/bottom_pane/chat_composer.rs
|
||
index 5df7e68..92ab911 100644
|
||
--- a/tui/src/bottom_pane/chat_composer.rs
|
||
+++ b/tui/src/bottom_pane/chat_composer.rs
|
||
@@ -174,6 +174,7 @@ pub(crate) struct ChatComposer {
|
||
/// Tracks whether the user has typed or pasted any content since startup.
|
||
typed_anything: bool,
|
||
is_task_running: bool,
|
||
+ task_started_at: Option<Instant>,
|
||
// Current status message to display when task is running
|
||
status_message: String,
|
||
// Animation thread for spinning icon when task is running
|
||
@@ -264,6 +265,7 @@ impl ChatComposer {
|
||
typed_anything: false,
|
||
// no double‑Esc handling here; App manages Esc policy
|
||
is_task_running: false,
|
||
+ task_started_at: None,
|
||
status_message: String::from("coding"),
|
||
animation_running: None,
|
||
using_chatgpt_auth,
|
||
@@ -330,6 +332,11 @@ impl ChatComposer {
|
||
}
|
||
|
||
pub fn set_task_running(&mut self, running: bool) {
|
||
+ if running && !self.is_task_running {
|
||
+ self.task_started_at = Some(Instant::now());
|
||
+ } else if !running {
|
||
+ self.task_started_at = None;
|
||
+ }
|
||
self.is_task_running = running;
|
||
|
||
if running {
|
||
@@ -651,9 +658,9 @@ impl ChatComposer {
|
||
])
|
||
.areas(area);
|
||
|
||
- // Get inner area of the bordered input box
|
||
- let input_block = Block::default().borders(Borders::ALL);
|
||
- let textarea_rect = input_block.inner(input_area);
|
||
+ // The row above the text carries the running status; the row below
|
||
+ // is breathing room. Same inset as render, so the cursor lands on the text.
|
||
+ let textarea_rect = input_area.inner(Margin::new(0, 1));
|
||
|
||
// Apply the same inner padding as in render (horizontal only).
|
||
let padded_textarea_rect = textarea_rect.inner(Margin::new(crate::layout_consts::COMPOSER_INNER_HPAD.into(), 0));
|
||
@@ -2906,8 +2913,9 @@ impl WidgetRef for ChatComposer {
|
||
if let Some(area) = footer_area {
|
||
self.render_footer(area, buf);
|
||
}
|
||
- // Draw border around input area with optional variant title when task is running
|
||
- let mut input_block = Block::default().borders(Borders::ALL);
|
||
+ // No frame around the input. The block still carries the running
|
||
+ // status as its title, drawn on the row above the text.
|
||
+ let mut input_block = Block::default().borders(Borders::NONE);
|
||
let mut auto_drive_border_gradient = None;
|
||
if let Some(style) = self
|
||
.auto_drive_style
|
||
@@ -2943,28 +2951,16 @@ impl WidgetRef for ChatComposer {
|
||
}
|
||
}
|
||
} else {
|
||
- use std::time::{SystemTime, UNIX_EPOCH};
|
||
- let now_ms = SystemTime::now()
|
||
- .duration_since(UNIX_EPOCH)
|
||
- .unwrap_or_default()
|
||
- .as_millis();
|
||
- let def = crate::spinner::current_spinner();
|
||
- let spinner_str = crate::spinner::frame_at_time(def, now_ms);
|
||
-
|
||
- let title_line = Line::from(vec![
|
||
- Span::raw(" "),
|
||
- Span::styled(spinner_str, Style::default().fg(crate::colors::info())),
|
||
- Span::styled(
|
||
- format!(" {}... ", self.status_message),
|
||
- Style::default().fg(crate::colors::info()),
|
||
- ),
|
||
- ])
|
||
- .centered();
|
||
- input_block = input_block.title(title_line);
|
||
+ let elapsed = if crate::chatwidget::is_test_mode() {
|
||
+ Duration::ZERO
|
||
+ } else {
|
||
+ self.task_started_at.map(|start| start.elapsed()).unwrap_or_default()
|
||
+ };
|
||
+ input_block = input_block.title(super::composer_status::working_line(elapsed));
|
||
}
|
||
}
|
||
|
||
- let textarea_rect = input_block.inner(input_area);
|
||
+ let textarea_rect = input_area.inner(Margin::new(0, 1));
|
||
input_block.render_ref(input_area, buf);
|
||
if let Some(gradient) = auto_drive_border_gradient {
|
||
apply_auto_drive_border_gradient(buf, input_area, gradient);
|
||
@@ -2983,6 +2979,15 @@ impl WidgetRef for ChatComposer {
|
||
.render_ref(padded_textarea_rect, buf);
|
||
}
|
||
|
||
+ // Keep the working line on the transcript background and give the
|
||
+ // editable surface its own dark bar, including the blank padding row.
|
||
+ let input_bg = crate::colors::input_bg();
|
||
+ for y in input_area.y.saturating_add(1)..input_area.bottom() {
|
||
+ for x in input_area.x..input_area.right() {
|
||
+ buf[(x, y)].set_bg(input_bg);
|
||
+ }
|
||
+ }
|
||
+
|
||
// Draw a high-contrast cursor overlay under the terminal cursor using the theme's
|
||
// `cursor` color. This improves visibility on dark themes where the terminal's own
|
||
// cursor color may be hard to see or user-defined.
|
||
@@ -3298,3 +3303,7 @@ mod tests {
|
||
);
|
||
}
|
||
}
|
||
+
|
||
+#[cfg(test)]
|
||
+#[path = "composer_chrome_tests.rs"]
|
||
+mod chrome_tests;
|
||
diff --git a/tui/src/bottom_pane/composer_chrome_tests.rs b/tui/src/bottom_pane/composer_chrome_tests.rs
|
||
new file mode 100644
|
||
index 0000000..a40350c
|
||
--- /dev/null
|
||
+++ b/tui/src/bottom_pane/composer_chrome_tests.rs
|
||
@@ -0,0 +1,16 @@
|
||
+use super::*;
|
||
+
|
||
+#[test]
|
||
+fn working_line_and_input_surface_are_distinct() {
|
||
+ let (tx, _rx) = std::sync::mpsc::channel();
|
||
+ let mut composer = ChatComposer::new(true, AppEventSender::new(tx), true, false);
|
||
+ composer.is_task_running = true;
|
||
+ composer.status_message = "Working".into();
|
||
+ let area = Rect::new(0, 0, 80, 4);
|
||
+ let mut buffer = Buffer::empty(area);
|
||
+ composer.render_ref(area, &mut buffer);
|
||
+ let row: String = (0..80).map(|x| buffer[(x, 0)].symbol()).collect();
|
||
+ assert!(row.starts_with("• Working (0s · esc to interrupt)"), "{row}");
|
||
+ assert_ne!(buffer[(10, 1)].bg, crate::colors::background());
|
||
+ assert_eq!(buffer[(10, 0)].bg, crate::colors::background());
|
||
+}
|
||
diff --git a/tui/src/bottom_pane/composer_status.rs b/tui/src/bottom_pane/composer_status.rs
|
||
new file mode 100644
|
||
index 0000000..ad7cfa5
|
||
--- /dev/null
|
||
+++ b/tui/src/bottom_pane/composer_status.rs
|
||
@@ -0,0 +1,31 @@
|
||
+use std::time::Duration;
|
||
+
|
||
+use ratatui::style::{Color, Modifier, Style};
|
||
+use ratatui::text::{Line, Span};
|
||
+
|
||
+/// The animated status stays left aligned above the editable surface.
|
||
+pub(super) fn working_line(elapsed: Duration) -> Line<'static> {
|
||
+ let seconds = elapsed.as_secs();
|
||
+ let clock = if seconds < 60 {
|
||
+ format!("{seconds}s")
|
||
+ } else if seconds < 3600 {
|
||
+ format!("{}m {:02}s", seconds / 60, seconds % 60)
|
||
+ } else {
|
||
+ format!("{}h {:02}m", seconds / 3600, seconds / 60 % 60)
|
||
+ };
|
||
+ let base = crate::colors::color_to_rgb(crate::colors::text_dim());
|
||
+ let bright = crate::colors::color_to_rgb(crate::colors::text());
|
||
+ let phase = (elapsed.as_millis() / 90 % 18) as i32 - 3;
|
||
+ let mut spans = vec![Span::styled("• ", Style::default().fg(crate::colors::text_dim()))];
|
||
+ for (index, ch) in "Working".chars().enumerate() {
|
||
+ let strength = (1.0 - (index as i32 - phase).abs() as f32 / 3.0).max(0.0);
|
||
+ let mix = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * strength) as u8;
|
||
+ let color = Color::Rgb(mix(base.0, bright.0), mix(base.1, bright.1), mix(base.2, bright.2));
|
||
+ spans.push(Span::styled(ch.to_string(), Style::default().fg(color).add_modifier(Modifier::BOLD)));
|
||
+ }
|
||
+ spans.push(Span::styled(
|
||
+ format!(" ({clock} · esc to interrupt)"),
|
||
+ Style::default().fg(crate::colors::text_dim()),
|
||
+ ));
|
||
+ Line::from(spans)
|
||
+}
|
||
diff --git a/tui/src/bottom_pane/login_accounts_view.rs b/tui/src/bottom_pane/login_accounts_view.rs
|
||
index 37138d5..a458217 100644
|
||
--- a/tui/src/bottom_pane/login_accounts_view.rs
|
||
+++ b/tui/src/bottom_pane/login_accounts_view.rs
|
||
@@ -219,7 +219,7 @@ impl LoginAccountsState {
|
||
true,
|
||
) {
|
||
self.feedback = Some(Feedback {
|
||
- message: format!("Failed to record ChatGPT login: {err}"),
|
||
+ message: format!("Failed to record login: {err}"),
|
||
is_error: true,
|
||
});
|
||
}
|
||
@@ -333,7 +333,7 @@ impl LoginAccountsState {
|
||
Ok(()) => {
|
||
self.feedback = Some(Feedback {
|
||
message: if mode.is_chatgpt() {
|
||
- "ChatGPT account selected".to_string()
|
||
+ "Account selected".to_string()
|
||
} else {
|
||
"API key selected".to_string()
|
||
},
|
||
@@ -695,7 +695,7 @@ impl LoginAddAccountState {
|
||
KeyCode::Enter => {
|
||
if *selected == 0 {
|
||
self.feedback = Some(Feedback {
|
||
- message: "Opening browser for ChatGPT sign-in…".to_string(),
|
||
+ message: "Starting sign-in…".to_string(),
|
||
is_error: false,
|
||
});
|
||
self.step = AddStep::Waiting { auth_url: None };
|
||
@@ -835,7 +835,7 @@ impl LoginAddAccountState {
|
||
AddStep::Choose { selected } => {
|
||
lines.push(Line::from("Choose how you’d like to add an account:"));
|
||
lines.push(Line::from(""));
|
||
- let options = ["ChatGPT sign-in", "API key"];
|
||
+ let options = ["Sign in", "API key"];
|
||
for (idx, option) in options.iter().enumerate() {
|
||
let mut spans = Vec::new();
|
||
if idx == *selected {
|
||
@@ -860,7 +860,7 @@ impl LoginAddAccountState {
|
||
]));
|
||
}
|
||
AddStep::ApiKey { field } => {
|
||
- lines.push(Line::from("Paste your OpenAI API key:"));
|
||
+ lines.push(Line::from("Paste your API key:"));
|
||
lines.push(Line::from(field.render_line()));
|
||
lines.push(Line::from(""));
|
||
lines.push(Line::from(vec![
|
||
@@ -871,7 +871,7 @@ impl LoginAddAccountState {
|
||
]));
|
||
}
|
||
AddStep::Waiting { auth_url } => {
|
||
- lines.push(Line::from("Finish signing in with ChatGPT in your browser."));
|
||
+ lines.push(Line::from("Finish signing in from your browser."));
|
||
lines.push(Line::from(vec![
|
||
Span::styled("Not seeing a browser? ", Style::default().fg(crate::colors::text_dim())),
|
||
Span::styled(
|
||
@@ -975,8 +975,8 @@ impl LoginAddAccountState {
|
||
pub fn on_chatgpt_complete(&mut self, result: Result<(), String>) {
|
||
match result {
|
||
Ok(()) => {
|
||
- self.feedback = Some(Feedback { message: "ChatGPT account connected".to_string(), is_error: false });
|
||
- self.send_tail("ChatGPT account connected".to_string());
|
||
+ self.feedback = Some(Feedback { message: "Account connected".to_string(), is_error: false });
|
||
+ self.send_tail("Account connected".to_string());
|
||
self.app_event_tx
|
||
.send(AppEvent::LoginUsingChatGptChanged { using_chatgpt_auth: true });
|
||
self.finish_and_show_accounts();
|
||
@@ -991,7 +991,7 @@ impl LoginAddAccountState {
|
||
pub fn cancel_active_flow(&mut self) {
|
||
let message = match self.step {
|
||
AddStep::DeviceCode(_) => "Cancelled code authentication",
|
||
- AddStep::Waiting { .. } => "Cancelled ChatGPT login",
|
||
+ AddStep::Waiting { .. } => "Cancelled sign-in",
|
||
_ => "Cancelled login",
|
||
};
|
||
self.step = AddStep::Choose { selected: 0 };
|
||
diff --git a/tui/src/bottom_pane/mod.rs b/tui/src/bottom_pane/mod.rs
|
||
index f47e19b..8c537f7 100644
|
||
--- a/tui/src/bottom_pane/mod.rs
|
||
+++ b/tui/src/bottom_pane/mod.rs
|
||
@@ -29,6 +29,7 @@ mod account_switch_settings_view;
|
||
mod memories_settings_view;
|
||
mod bottom_pane_view;
|
||
mod chat_composer;
|
||
+mod composer_status;
|
||
mod chat_composer_history;
|
||
mod diff_popup;
|
||
mod custom_prompt_view;
|
||
diff --git a/tui/src/bottom_pane/model_selection_view.rs b/tui/src/bottom_pane/model_selection_view.rs
|
||
index 52e5bc4..73bec57 100644
|
||
--- a/tui/src/bottom_pane/model_selection_view.rs
|
||
+++ b/tui/src/bottom_pane/model_selection_view.rs
|
||
@@ -21,7 +21,12 @@ use std::cmp::Ordering;
|
||
/// Flattened preset entry combining a model with a specific reasoning effort.
|
||
#[derive(Clone, Debug)]
|
||
struct FlatPreset {
|
||
+ /// Position of the model in the catalog. The catalog decides what a user
|
||
+ /// should see first; the view renders that order rather than guessing one
|
||
+ /// from how the slug is spelled.
|
||
+ rank: usize,
|
||
model: String,
|
||
+ model_description: String,
|
||
effort: ReasoningEffort,
|
||
label: String,
|
||
description: String,
|
||
@@ -34,13 +39,27 @@ struct ModelLine {
|
||
}
|
||
|
||
impl FlatPreset {
|
||
- fn from_model_preset(preset: &ModelPreset) -> Vec<Self> {
|
||
+ /// Flatten the catalog into one entry per (model, reasoning effort), keeping
|
||
+ /// catalog order and lifting the default model to the front.
|
||
+ fn flatten(presets: &[ModelPreset]) -> Vec<Self> {
|
||
+ let mut ordered: Vec<&ModelPreset> = presets.iter().collect();
|
||
+ ordered.sort_by_key(|preset| !preset.is_default);
|
||
+ ordered
|
||
+ .into_iter()
|
||
+ .enumerate()
|
||
+ .flat_map(|(rank, preset)| Self::from_model_preset(rank, preset))
|
||
+ .collect()
|
||
+ }
|
||
+
|
||
+ fn from_model_preset(rank: usize, preset: &ModelPreset) -> Vec<Self> {
|
||
preset
|
||
.supported_reasoning_efforts
|
||
.iter()
|
||
.map(|effort_preset| {
|
||
FlatPreset {
|
||
+ rank,
|
||
model: preset.model.to_string(),
|
||
+ model_description: preset.description.to_string(),
|
||
effort: effort_preset.effort.clone().into(),
|
||
label: format!(
|
||
"{} {}",
|
||
@@ -169,10 +188,7 @@ impl ModelSelectionView {
|
||
target: ModelSelectionTarget,
|
||
app_event_tx: AppEventSender,
|
||
) -> Self {
|
||
- let flat_presets: Vec<FlatPreset> = presets
|
||
- .iter()
|
||
- .flat_map(FlatPreset::from_model_preset)
|
||
- .collect();
|
||
+ let flat_presets = FlatPreset::flatten(&presets);
|
||
|
||
let initial_index = Self::initial_selection(
|
||
target.supports_fast_mode(),
|
||
@@ -206,10 +222,7 @@ impl ModelSelectionView {
|
||
let previous_selected = previous_entries.get(self.selected_index).copied();
|
||
let previous_flat = self.flat_presets.clone();
|
||
|
||
- self.flat_presets = presets
|
||
- .iter()
|
||
- .flat_map(FlatPreset::from_model_preset)
|
||
- .collect();
|
||
+ self.flat_presets = FlatPreset::flatten(&presets);
|
||
|
||
let mut next_selected: Option<usize> = None;
|
||
match previous_selected {
|
||
@@ -794,10 +807,10 @@ impl ModelSelectionView {
|
||
)]),
|
||
is_selected: false,
|
||
});
|
||
- if let Some(desc) = Self::model_description(&flat_preset.model) {
|
||
+ if !flat_preset.model_description.is_empty() {
|
||
lines.push(ModelLine {
|
||
line: Line::from(vec![Span::styled(
|
||
- desc.to_string(),
|
||
+ flat_preset.model_description.clone(),
|
||
Style::default().fg(crate::colors::text_dim()),
|
||
)]),
|
||
is_selected: false,
|
||
@@ -883,7 +896,7 @@ impl ModelSelectionView {
|
||
lines = lines.saturating_add(1);
|
||
}
|
||
lines = lines.saturating_add(1);
|
||
- if Self::model_description(&flat_preset.model).is_some() {
|
||
+ if !flat_preset.model_description.is_empty() {
|
||
lines = lines.saturating_add(1);
|
||
}
|
||
previous_model = Some(&flat_preset.model);
|
||
@@ -902,9 +915,9 @@ impl ModelSelectionView {
|
||
}
|
||
|
||
fn compare_presets(a: &FlatPreset, b: &FlatPreset) -> Ordering {
|
||
- let model_name_rank = Self::compare_model_names(&a.model, &b.model);
|
||
- if model_name_rank != Ordering::Equal {
|
||
- return model_name_rank;
|
||
+ let rank = a.rank.cmp(&b.rank);
|
||
+ if rank != Ordering::Equal {
|
||
+ return rank;
|
||
}
|
||
|
||
let effort_rank = Self::effort_rank(a.effort).cmp(&Self::effort_rank(b.effort));
|
||
@@ -915,77 +928,6 @@ impl ModelSelectionView {
|
||
a.label.cmp(&b.label)
|
||
}
|
||
|
||
- fn compare_model_names(a: &str, b: &str) -> Ordering {
|
||
- let a_lower = a.to_ascii_lowercase();
|
||
- let b_lower = b.to_ascii_lowercase();
|
||
- match (
|
||
- Self::parse_model_version_components(&a_lower),
|
||
- Self::parse_model_version_components(&b_lower),
|
||
- ) {
|
||
- (Some(a_components), Some(b_components)) => {
|
||
- let component_rank = b_components.cmp(&a_components);
|
||
- if component_rank != Ordering::Equal {
|
||
- return component_rank;
|
||
- }
|
||
- a_lower.cmp(&b_lower)
|
||
- }
|
||
- (Some(_), None) => Ordering::Less,
|
||
- (None, Some(_)) => Ordering::Greater,
|
||
- (None, None) => a_lower.cmp(&b_lower),
|
||
- }
|
||
- }
|
||
-
|
||
- fn parse_model_version_components(model: &str) -> Option<Vec<u32>> {
|
||
- let canonical = model.rsplit('/').next().unwrap_or(model);
|
||
- let mut components = Vec::new();
|
||
-
|
||
- for segment in canonical.split('-') {
|
||
- let first = segment.chars().next()?;
|
||
- if !first.is_ascii_digit() {
|
||
- continue;
|
||
- }
|
||
-
|
||
- for part in segment.split('.') {
|
||
- if part.is_empty() || !part.chars().all(|ch| ch.is_ascii_digit()) {
|
||
- return None;
|
||
- }
|
||
- components.push(part.parse().ok()?);
|
||
- }
|
||
-
|
||
- return (!components.is_empty()).then_some(components);
|
||
- }
|
||
-
|
||
- None
|
||
- }
|
||
-
|
||
- fn model_description(model: &str) -> Option<&'static str> {
|
||
- if model.eq_ignore_ascii_case("gpt-5.5") {
|
||
- Some("Frontier model for complex coding, research, and real-world work.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.4") {
|
||
- Some("Brings together flagship reasoning, coding, and tool use in a single frontier model.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.4-mini") {
|
||
- Some("Smaller GPT-5.4 variant tuned for faster coding loops.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.3-codex") {
|
||
- Some("Frontier agentic coding, 25% faster than previous models.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
|
||
- Some("Fast codex variant tuned for responsive coding loops and smaller edits.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.2-codex") {
|
||
- Some("Frontier agentic coding model.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.2") {
|
||
- Some("Latest frontier model with improvements across knowledge, reasoning, and coding.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
|
||
- Some("Latest Codex-optimized flagship for deep and fast reasoning.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.1-codex") {
|
||
- Some("Optimized for Code.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.1-codex-mini") {
|
||
- Some("Optimized for Code. Cheaper, faster, but less capable.")
|
||
- } else if model.eq_ignore_ascii_case("gpt-5.1") {
|
||
- Some("Broad world knowledge with strong general reasoning.")
|
||
- } else {
|
||
- None
|
||
- }
|
||
- }
|
||
-
|
||
fn effort_rank(effort: ReasoningEffort) -> u8 {
|
||
match effort {
|
||
ReasoningEffort::XHigh => 0,
|
||
@@ -1291,15 +1233,14 @@ mod tests {
|
||
let lines = buffer_body_lines(&buf, width, height);
|
||
let visible = lines.join("\n");
|
||
let has_header = visible.contains("GPT-5.3-Codex");
|
||
- let has_desc = visible.contains("Frontier agentic coding")
|
||
- && visible.contains("25% faster than previous models.");
|
||
+ let has_desc = visible.contains("gpt-5.3-codex model");
|
||
|
||
assert!(has_header);
|
||
assert!(has_desc);
|
||
}
|
||
|
||
#[test]
|
||
- fn model_selection_prioritizes_higher_versions_and_shows_fast_mode_toggle() {
|
||
+ fn model_selection_keeps_catalog_order_and_shows_fast_mode_toggle() {
|
||
let mut gpt_5_5 = make_preset("gpt-5.5");
|
||
gpt_5_5.supported_reasoning_efforts = vec![
|
||
ReasoningEffortPreset {
|
||
@@ -1336,6 +1277,8 @@ mod tests {
|
||
},
|
||
];
|
||
|
||
+ gpt_5_5.is_default = true;
|
||
+
|
||
let presets = vec![
|
||
make_preset("gpt-5.3-codex"),
|
||
make_preset("gpt-5.3-codex-spark"),
|
||
@@ -1370,15 +1313,16 @@ mod tests {
|
||
height,
|
||
}, &mut buf);
|
||
|
||
+ // The default leads; everything else keeps the order the catalog gave.
|
||
let order = sorted_model_order(&view);
|
||
assert_eq!(
|
||
order,
|
||
vec![
|
||
"gpt-5.5".to_string(),
|
||
- "gpt-5.4".to_string(),
|
||
- "gpt-5.4-mini".to_string(),
|
||
"gpt-5.3-codex".to_string(),
|
||
"gpt-5.3-codex-spark".to_string(),
|
||
+ "gpt-5.4-mini".to_string(),
|
||
+ "gpt-5.4".to_string(),
|
||
]
|
||
);
|
||
|
||
diff --git a/tui/src/bottom_pane/settings_overlay.rs b/tui/src/bottom_pane/settings_overlay.rs
|
||
index 139b118..95669f8 100644
|
||
--- a/tui/src/bottom_pane/settings_overlay.rs
|
||
+++ b/tui/src/bottom_pane/settings_overlay.rs
|
||
@@ -85,7 +85,7 @@ impl SettingsSection {
|
||
SettingsSection::Model => "Model settings coming soon.",
|
||
SettingsSection::Theme => "Theme settings coming soon.",
|
||
SettingsSection::Planning => "Planning settings coming soon.",
|
||
- SettingsSection::Updates => "Upgrade Codex and manage automatic updates.",
|
||
+ SettingsSection::Updates => "Upgrade Hanzo Dev and manage automatic updates.",
|
||
SettingsSection::Accounts => "Account switching settings coming soon.",
|
||
SettingsSection::Agents => "Agents configuration coming soon.",
|
||
SettingsSection::Memories => "Enable or disable cross-session memories.",
|
||
diff --git a/tui/src/bottom_pane/theme_selection_view.rs b/tui/src/bottom_pane/theme_selection_view.rs
|
||
index 05ddadb..815b4a9 100644
|
||
--- a/tui/src/bottom_pane/theme_selection_view.rs
|
||
+++ b/tui/src/bottom_pane/theme_selection_view.rs
|
||
@@ -2731,7 +2731,7 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
|
||
// Blank line between title and content
|
||
form_lines.push(Line::default());
|
||
form_lines.push(Line::from(Span::styled(
|
||
- "Code can generate a custom spinner just for you!",
|
||
+ "Hanzo Dev can generate a custom spinner just for you!",
|
||
Style::default().fg(theme.text),
|
||
)));
|
||
form_lines.push(Line::from(Span::styled(
|
||
@@ -3017,7 +3017,7 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
|
||
}
|
||
}
|
||
form_lines.push(Line::from(Span::styled(
|
||
- "Code can generate a custom theme just for you!",
|
||
+ "Hanzo Dev can generate a custom theme just for you!",
|
||
Style::default().fg(theme.text),
|
||
)));
|
||
form_lines.push(Line::from(Span::styled(
|
||
diff --git a/tui/src/bottom_pane/update_settings_view.rs b/tui/src/bottom_pane/update_settings_view.rs
|
||
index 8bb7261..26d9092 100644
|
||
--- a/tui/src/bottom_pane/update_settings_view.rs
|
||
+++ b/tui/src/bottom_pane/update_settings_view.rs
|
||
@@ -103,7 +103,7 @@ impl UpdateSettingsView {
|
||
let Some(latest) = state.latest_version.clone() else {
|
||
self.app_event_tx.send_background_event_with_ticket(
|
||
&self.ticket,
|
||
- "✅ Code is already up to date.".to_string(),
|
||
+ "✅ Hanzo Dev is already up to date.".to_string(),
|
||
);
|
||
return;
|
||
};
|
||
@@ -129,7 +129,7 @@ impl UpdateSettingsView {
|
||
self.app_event_tx.send_background_event_with_ticket(
|
||
&self.ticket,
|
||
format!(
|
||
- "↻ Complete the guided terminal steps for `{}` then restart Code to finish upgrading to {}.",
|
||
+ "↻ Complete the guided terminal steps for `{}` then restart Hanzo Dev to finish upgrading to {}.",
|
||
display, latest
|
||
),
|
||
);
|
||
diff --git a/tui/src/chatwidget/events_audit.md b/tui/src/chatwidget/events_audit.md
|
||
index f432c4d..e15b1c0 100644
|
||
--- a/tui/src/chatwidget/events_audit.md
|
||
+++ b/tui/src/chatwidget/events_audit.md
|
||
@@ -2,7 +2,7 @@
|
||
|
||
This note summarizes the places where `ChatWidget` still mutates `history_cells`
|
||
directly so we can migrate them onto the `HistoryState::apply_event` pipeline.
|
||
-Line numbers reference `code-rs/tui/src/chatwidget.rs` as of 2025-09-27.
|
||
+Line numbers reference `hanzo-dev/tui/src/chatwidget.rs` as of 2025-09-27.
|
||
|
||
## Structural mutations
|
||
|
||
diff --git a/tui/src/chatwidget/gh_actions.rs b/tui/src/chatwidget/gh_actions.rs
|
||
index d8d5de8..6c89aa7 100644
|
||
--- a/tui/src/chatwidget/gh_actions.rs
|
||
+++ b/tui/src/chatwidget/gh_actions.rs
|
||
@@ -59,7 +59,7 @@ pub(super) fn maybe_watch_after_push(
|
||
let api_base = format!("https://api.github.com/repos/{owner}/{repo}/actions/runs");
|
||
let token = get_github_token().map(|(t, _)| t);
|
||
let client = reqwest::Client::builder()
|
||
- .user_agent("codex-cli-rs/github-monitor")
|
||
+ .user_agent("hanzo-dev/github-monitor")
|
||
.build()
|
||
.unwrap_or_else(|_| reqwest::Client::new());
|
||
|
||
diff --git a/tui/src/chatwidget/layout_scroll.rs b/tui/src/chatwidget/layout_scroll.rs
|
||
index 1c5dc92..5b68478 100644
|
||
--- a/tui/src/chatwidget/layout_scroll.rs
|
||
+++ b/tui/src/chatwidget/layout_scroll.rs
|
||
@@ -193,7 +193,5 @@ pub(super) fn layout_areas(chat: &ChatWidget<'_>, area: Rect) -> Vec<Rect> {
|
||
bottom_desired,
|
||
font_cell,
|
||
None,
|
||
- // Disable status bar when in standard terminal mode
|
||
- !chat.standard_terminal_mode,
|
||
)
|
||
}
|
||
diff --git a/tui/src/chatwidget/limits_overlay.rs b/tui/src/chatwidget/limits_overlay.rs
|
||
index fb955e1..737dec4 100644
|
||
--- a/tui/src/chatwidget/limits_overlay.rs
|
||
+++ b/tui/src/chatwidget/limits_overlay.rs
|
||
@@ -215,7 +215,7 @@ fn placeholder_lines() -> Vec<RtLine<'static>> {
|
||
vec![
|
||
RtLine::from("Usage Limits".bold()),
|
||
RtLine::from(" Real usage data is not available yet."),
|
||
- RtLine::from(" Send a message to Code, then run /limits again.".dim()),
|
||
+ RtLine::from(" Send a message to Hanzo Dev, then run /limits again.".dim()),
|
||
]
|
||
}
|
||
|
||
diff --git a/tui/src/chatwidget/smoke_helpers.rs b/tui/src/chatwidget/smoke_helpers.rs
|
||
index dc99bef..3c06910 100644
|
||
--- a/tui/src/chatwidget/smoke_helpers.rs
|
||
+++ b/tui/src/chatwidget/smoke_helpers.rs
|
||
@@ -33,6 +33,11 @@ pub fn enter_test_runtime_guard() -> tokio::runtime::EnterGuard<'static> {
|
||
TEST_RUNTIME.enter()
|
||
}
|
||
|
||
+// There is deliberately no cwd override here. A literal "/snapshot" cwd does
|
||
+// not exist on disk, and the chatwidget tests that resolve sandbox write
|
||
+// permission against the working directory then decide the workspace is not
|
||
+// writable. Nothing rendered names the cwd, so nothing needs hiding.
|
||
+
|
||
pub struct ChatWidgetHarness {
|
||
chat: ChatWidget<'static>,
|
||
events: Receiver<AppEvent>,
|
||
@@ -70,14 +75,6 @@ impl AutoContinueModeFixture {
|
||
|
||
impl ChatWidgetHarness {
|
||
pub fn new() -> Self {
|
||
- // Stabilize time-of-day dependent greeting so VT100 snapshots remain deterministic.
|
||
- // Safe: tests run single-threaded by design.
|
||
- unsafe { std::env::set_var("CODEX_TUI_FAKE_HOUR", "12"); }
|
||
-
|
||
- unsafe {
|
||
- std::env::set_var("CODEX_TUI_FORCE_MINIMAL_HEADER", "1");
|
||
- }
|
||
-
|
||
unsafe {
|
||
std::env::set_var("CODE_TUI_TEST_MODE", "1");
|
||
}
|
||
diff --git a/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap b/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap
|
||
index b442bb1..e4c650f 100644
|
||
--- a/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap
|
||
+++ b/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap
|
||
@@ -2,7 +2,7 @@
|
||
source: tui/src/chatwidget/tests.rs
|
||
expression: terminal.backend()
|
||
---
|
||
-"? Code wants to run echo hello world "
|
||
+"? Hanzo Dev wants to run echo hello world "
|
||
" "
|
||
"Model wants to run a command "
|
||
" "
|
||
diff --git a/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_placeholder.snap b/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_placeholder.snap
|
||
index 0df4679..37d8aa8 100644
|
||
--- a/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_placeholder.snap
|
||
+++ b/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_placeholder.snap
|
||
@@ -6,4 +6,4 @@ expression: visual
|
||
|
||
[bold]Usage Limits[/]
|
||
Real usage data is not available yet.
|
||
-[dim] Send a message to Codex, then run /limits again.[/]
|
||
+[dim] Send a message to Hanzo Dev, then run /limits again.[/]
|
||
diff --git a/tui/src/chatwidget.rs b/tui/src/chatwidget.rs
|
||
index 26f2c3e..821d93e 100644
|
||
--- a/tui/src/chatwidget.rs
|
||
+++ b/tui/src/chatwidget.rs
|
||
@@ -21,7 +21,6 @@ use std::str::FromStr;
|
||
use base64::prelude::{Engine as _, BASE64_STANDARD};
|
||
|
||
use ratatui::style::{Modifier, Style};
|
||
-use crate::header_wave::HeaderWaveEffect;
|
||
use crate::auto_drive_strings;
|
||
use crate::auto_drive_style::AutoDriveVariant;
|
||
use crate::spinner;
|
||
@@ -828,7 +827,7 @@ use uuid::Uuid;
|
||
|
||
fn history_cell_logging_enabled() -> bool {
|
||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||
- if let Ok(value) = std::env::var("CODEX_TRACE_HISTORY") {
|
||
+ if let Ok(value) = std::env::var("DEV_TRACE_HISTORY") {
|
||
let trimmed = value.trim();
|
||
if !matches!(trimmed, "" | "0") {
|
||
return true;
|
||
@@ -884,7 +883,6 @@ const STATUS_CONTENT_PREFIX: &str = " ";
|
||
const RESUME_PLACEHOLDER_MESSAGE: &str = "Resuming previous session...";
|
||
const RESUME_NO_HISTORY_NOTICE: &str =
|
||
"No saved messages for this session. Start typing to continue.";
|
||
-const ENABLE_WARP_STRIPES: bool = false;
|
||
|
||
fn auto_continue_from_config(mode: AutoDriveContinueMode) -> AutoContinueMode {
|
||
match mode {
|
||
@@ -1345,7 +1343,7 @@ const AUTO_REVIEW_MAX_RAW_DIFF_CHARS: usize = 120_000;
|
||
const AUTO_REVIEW_ESTIMATED_BYTES_PER_TOKEN: u64 = 4;
|
||
const AUTO_REVIEW_PROMPT_TOKEN_LIMIT_DIVISOR: u64 = 2;
|
||
const AUTO_REVIEW_SCOPE_EXCLUDED_PREFIXES: [&str; 5] = [
|
||
- "codex-rs/",
|
||
+ "vendor/",
|
||
"node_modules/",
|
||
"target/",
|
||
".git/",
|
||
@@ -1941,7 +1939,6 @@ pub(crate) struct ChatWidget<'a> {
|
||
idle_spinner_clear_started_at: Option<Instant>,
|
||
idle_spinner_recheck_scheduled: bool,
|
||
live_builder: RowBuilder,
|
||
- header_wave: HeaderWaveEffect,
|
||
browser_overlay_visible: bool,
|
||
browser_overlay_state: BrowserOverlayState,
|
||
// Store pending image paths keyed by their placeholder text
|
||
@@ -1960,7 +1957,6 @@ pub(crate) struct ChatWidget<'a> {
|
||
|
||
// Cached cell size (width,height) in pixels
|
||
cached_cell_size: std::cell::OnceCell<(u16, u16)>,
|
||
- git_branch_cache: RefCell<GitBranchCache>,
|
||
|
||
// Terminal information from startup
|
||
terminal_info: crate::tui::TerminalInfo,
|
||
@@ -2316,13 +2312,6 @@ enum GhostSnapshotJobHandle {
|
||
Skipped,
|
||
}
|
||
|
||
-#[derive(Default)]
|
||
-struct GitBranchCache {
|
||
- value: Option<String>,
|
||
- last_head_mtime: Option<SystemTime>,
|
||
- last_refresh: Option<Instant>,
|
||
-}
|
||
-
|
||
#[derive(Debug, Clone, Default)]
|
||
struct AgentRuntime {
|
||
/// First time this agent entered Running
|
||
@@ -3631,8 +3620,9 @@ impl ChatWidget<'_> {
|
||
app_event_tx_clone.send_background_event_with_ticket(
|
||
&ticket,
|
||
format!(
|
||
- "❌ Failed to initialize model session: {}.\n• Ensure an OpenAI API key is set (CODE_OPENAI_API_KEY / OPENAI_API_KEY) or run `code login`.\n• Also verify config.cwd is an absolute path.",
|
||
- e
|
||
+ "❌ Failed to initialize model session: {}.\n• Sign in with `{}`, or set HANZO_USER_KEY.\n• Also verify config.cwd is an absolute path.",
|
||
+ e,
|
||
+ code_core::auth::LOGIN_COMMAND
|
||
),
|
||
);
|
||
return;
|
||
@@ -5327,17 +5317,6 @@ impl ChatWidget<'_> {
|
||
self.render_request_cache_dirty.set(true);
|
||
}
|
||
|
||
- fn update_welcome_height_hint(&self, height: u16) {
|
||
- for cell in self.history_cells.iter() {
|
||
- if let Some(welcome) = cell
|
||
- .as_any()
|
||
- .downcast_ref::<crate::history_cell::AnimatedWelcomeCell>()
|
||
- {
|
||
- welcome.set_available_height(height);
|
||
- }
|
||
- }
|
||
- }
|
||
-
|
||
fn is_frozen_cell(cell: &dyn HistoryCell) -> bool {
|
||
cell.as_any().downcast_ref::<FrozenHistoryCell>().is_some()
|
||
}
|
||
@@ -7039,15 +7018,6 @@ impl ChatWidget<'_> {
|
||
// Use max width to disable wrapping during streaming
|
||
// Text will be properly wrapped when displayed based on terminal width
|
||
live_builder: RowBuilder::new(usize::MAX),
|
||
- header_wave: {
|
||
- let effect = HeaderWaveEffect::new();
|
||
- if ENABLE_WARP_STRIPES {
|
||
- effect.set_enabled(true, Instant::now());
|
||
- } else {
|
||
- effect.set_enabled(false, Instant::now());
|
||
- }
|
||
- effect
|
||
- },
|
||
browser_overlay_visible: false,
|
||
browser_overlay_state: BrowserOverlayState::default(),
|
||
pending_images: HashMap::new(),
|
||
@@ -7058,7 +7028,6 @@ impl ChatWidget<'_> {
|
||
cached_image_protocol: RefCell::new(None),
|
||
cached_picker: RefCell::new(terminal_info.picker.clone()),
|
||
cached_cell_size: std::cell::OnceCell::new(),
|
||
- git_branch_cache: RefCell::new(GitBranchCache::default()),
|
||
terminal_info,
|
||
active_agents: Vec::new(),
|
||
agents_ready_to_start: false,
|
||
@@ -7281,6 +7250,7 @@ impl ChatWidget<'_> {
|
||
w.welcome_shown = true;
|
||
w.insert_resume_placeholder();
|
||
}
|
||
+ w.announce_free();
|
||
if w.test_mode {
|
||
w.bottom_pane.set_task_running(false);
|
||
w.bottom_pane.update_status_text(String::new());
|
||
@@ -7429,15 +7399,6 @@ impl ChatWidget<'_> {
|
||
idle_spinner_clear_started_at: None,
|
||
idle_spinner_recheck_scheduled: false,
|
||
live_builder: RowBuilder::new(usize::MAX),
|
||
- header_wave: {
|
||
- let effect = HeaderWaveEffect::new();
|
||
- if ENABLE_WARP_STRIPES {
|
||
- effect.set_enabled(true, Instant::now());
|
||
- } else {
|
||
- effect.set_enabled(false, Instant::now());
|
||
- }
|
||
- effect
|
||
- },
|
||
browser_overlay_visible: false,
|
||
browser_overlay_state: BrowserOverlayState::default(),
|
||
pending_images: HashMap::new(),
|
||
@@ -7448,7 +7409,6 @@ impl ChatWidget<'_> {
|
||
cached_image_protocol: RefCell::new(None),
|
||
cached_picker: RefCell::new(terminal_info.picker.clone()),
|
||
cached_cell_size: std::cell::OnceCell::new(),
|
||
- git_branch_cache: RefCell::new(GitBranchCache::default()),
|
||
terminal_info,
|
||
active_agents: Vec::new(),
|
||
agents_ready_to_start: false,
|
||
@@ -7632,6 +7592,7 @@ impl ChatWidget<'_> {
|
||
if show_welcome {
|
||
w.history_push_top_next_req(history_cell::new_animated_welcome());
|
||
}
|
||
+ w.announce_free();
|
||
if w.test_mode {
|
||
w.bottom_pane.set_task_running(false);
|
||
w.bottom_pane.update_status_text(String::new());
|
||
@@ -7642,6 +7603,18 @@ impl ChatWidget<'_> {
|
||
w
|
||
}
|
||
|
||
+ /// Say what a free run costs and shares, above the first turn that uses it.
|
||
+ /// Whether a run is free is read from the environment, so a test render
|
||
+ /// leaves it out — a snapshot must not depend on who is signed in on the
|
||
+ /// machine that renders it.
|
||
+ fn announce_free(&mut self) {
|
||
+ if !self.test_mode && code_core::free::runs(&self.config) {
|
||
+ self.history_push_top_next_req(history_cell::new_background_event(
|
||
+ code_core::free::NOTICE.to_string(),
|
||
+ ));
|
||
+ }
|
||
+ }
|
||
+
|
||
fn auto_drive_lines_to_string(lines: Vec<Line<'static>>) -> String {
|
||
let mut rows: Vec<String> = Vec::new();
|
||
for line in lines {
|
||
@@ -8214,43 +8187,6 @@ impl ChatWidget<'_> {
|
||
}
|
||
}
|
||
|
||
- /// Format model name with proper capitalization (e.g., "gpt-4" -> "GPT-4")
|
||
- fn format_model_name(&self, model_name: &str) -> String {
|
||
- fn format_segment(segment: &str) -> String {
|
||
- if segment.eq_ignore_ascii_case("codex") {
|
||
- return "Codex".to_string();
|
||
- }
|
||
-
|
||
- let mut chars = segment.chars();
|
||
- match chars.next() {
|
||
- Some(first) if first.is_ascii_alphabetic() => {
|
||
- let mut formatted = String::new();
|
||
- formatted.push(first.to_ascii_uppercase());
|
||
- formatted.push_str(chars.as_str());
|
||
- formatted
|
||
- }
|
||
- Some(first) => {
|
||
- let mut formatted = String::new();
|
||
- formatted.push(first);
|
||
- formatted.push_str(chars.as_str());
|
||
- formatted
|
||
- }
|
||
- None => String::new(),
|
||
- }
|
||
- }
|
||
-
|
||
- if let Some(rest) = model_name.strip_prefix("gpt-") {
|
||
- let formatted_rest = rest
|
||
- .split('-')
|
||
- .map(format_segment)
|
||
- .collect::<Vec<_>>()
|
||
- .join("-");
|
||
- format!("GPT-{}", formatted_rest)
|
||
- } else {
|
||
- model_name.to_string()
|
||
- }
|
||
- }
|
||
-
|
||
/// Calculate the maximum scroll offset based on current content size
|
||
#[allow(dead_code)]
|
||
fn calculate_max_scroll_offset(&self, content_area_height: u16) -> u16 {
|
||
@@ -12748,7 +12684,7 @@ impl ChatWidget<'_> {
|
||
_ => (
|
||
format!("Snapshots disabled after Git error: {err}"),
|
||
Some(
|
||
- "Restart Code after resolving the issue to re-enable snapshots.".to_string(),
|
||
+ "Restart Hanzo Dev after resolving the issue to re-enable snapshots.".to_string(),
|
||
),
|
||
),
|
||
};
|
||
@@ -12945,7 +12881,7 @@ impl ChatWidget<'_> {
|
||
}
|
||
} else {
|
||
lines.push(
|
||
- "Snapshots are currently disabled. Resolve the Git issue and restart Code to re-enable them.".to_string(),
|
||
+ "Snapshots are currently disabled. Resolve the Git issue and restart Hanzo Dev to re-enable them.".to_string(),
|
||
);
|
||
}
|
||
|
||
@@ -12965,7 +12901,7 @@ impl ChatWidget<'_> {
|
||
Some(
|
||
"Restores workspace files only. Conversation history remains unchanged.".to_string(),
|
||
),
|
||
- Some("Snapshots appear once Code captures a Git checkpoint.".to_string()),
|
||
+ Some("Snapshots appear once Hanzo Dev captures a Git checkpoint.".to_string()),
|
||
vec![
|
||
"No snapshot is available to restore.".to_string(),
|
||
"Run a command that modifies files to create the first snapshot.".to_string(),
|
||
@@ -13167,7 +13103,7 @@ impl ChatWidget<'_> {
|
||
fn conversation_line(role: UndoPreviewRole, text: &str) -> Line<'static> {
|
||
let (label, color) = match role {
|
||
UndoPreviewRole::User => ("You", crate::colors::text_bright()),
|
||
- UndoPreviewRole::Assistant => ("Code", crate::colors::primary()),
|
||
+ UndoPreviewRole::Assistant => ("Dev", crate::colors::primary()),
|
||
};
|
||
let label_span = Span::styled(
|
||
format!("{label}: "),
|
||
@@ -16205,7 +16141,7 @@ impl ChatWidget<'_> {
|
||
"streaming preview: drafting deployment summary…",
|
||
"streaming preview: cross-checking lint targets…",
|
||
],
|
||
- "**Here's a demo walkthrough:**\n\n1. Run `./build-fast.sh perf` to compile quickly.\n2. Cache artifacts in `code-rs/target/perf`.\n3. Finish by sharing `./build-fast.sh run` output.\n\n```bash\n./build-fast.sh perf run\n```",
|
||
+ "**Here's a demo walkthrough:**\n\n1. Run `./build-fast.sh perf` to compile quickly.\n2. Cache artifacts in `hanzo-dev/target/perf`.\n3. Finish by sharing `./build-fast.sh run` output.\n\n```bash\n./build-fast.sh perf run\n```",
|
||
vec![
|
||
(vec!["git", "status"], "On branch main\nnothing to commit, working tree clean\n"),
|
||
(vec!["rg", "--files"], ""),
|
||
@@ -20102,9 +20038,6 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
self.auto_state.last_completion_explanation.clone(),
|
||
);
|
||
self.auto_turn_review_state = None;
|
||
- if ENABLE_WARP_STRIPES {
|
||
- self.header_wave.set_enabled(false, Instant::now());
|
||
- }
|
||
self.auto_request_session_summary();
|
||
}
|
||
AutoControllerEffect::TransientPause {
|
||
@@ -21706,7 +21639,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
|
||
let launch = TerminalLaunch {
|
||
id,
|
||
- title: "Upgrade Code".to_string(),
|
||
+ title: "Upgrade Hanzo Dev".to_string(),
|
||
command: Vec::new(),
|
||
command_display: display_label,
|
||
controller: Some(controller.clone()),
|
||
@@ -23065,13 +22998,11 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
if cmd_str == "test-approval" {
|
||
continue;
|
||
}
|
||
- // Prefer "Code" branding in the Help panel
|
||
- let desc = cmd.description().replace("Codex", "Code");
|
||
// Render as "/command — description"
|
||
lines.push(RtLine::from(vec![
|
||
RtSpan::styled(format!("/{cmd_str:<12}"), t_fg),
|
||
RtSpan::raw(" — "),
|
||
- RtSpan::styled(desc.to_string(), t_dim),
|
||
+ RtSpan::styled(cmd.description().to_string(), t_dim),
|
||
]));
|
||
}
|
||
|
||
@@ -23088,44 +23019,6 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
self.request_redraw();
|
||
}
|
||
|
||
- fn curated_model_presets(presets: Vec<ModelPreset>) -> Vec<ModelPreset> {
|
||
- const MODEL_PICKER_ORDER: [&str; 5] = [
|
||
- "gpt-5.5",
|
||
- "gpt-5.6-terra",
|
||
- "gpt-5.6-luna",
|
||
- "gpt-5.3-codex",
|
||
- "gpt-5.3-codex-spark",
|
||
- ];
|
||
-
|
||
- let mut curated: Vec<ModelPreset> = MODEL_PICKER_ORDER
|
||
- .into_iter()
|
||
- .filter_map(|model| {
|
||
- presets.iter().find(|preset| {
|
||
- preset.model.eq_ignore_ascii_case(model)
|
||
- || preset.id.eq_ignore_ascii_case(model)
|
||
- })
|
||
- })
|
||
- .cloned()
|
||
- .collect();
|
||
-
|
||
- let curated_slugs: std::collections::HashSet<String> = curated
|
||
- .iter()
|
||
- .flat_map(|preset| {
|
||
- [
|
||
- preset.id.to_ascii_lowercase(),
|
||
- preset.model.to_ascii_lowercase(),
|
||
- ]
|
||
- })
|
||
- .collect();
|
||
-
|
||
- curated.extend(presets.iter().filter(|preset| {
|
||
- !curated_slugs.contains(&preset.id.to_ascii_lowercase())
|
||
- && !curated_slugs.contains(&preset.model.to_ascii_lowercase())
|
||
- }).cloned());
|
||
-
|
||
- if curated.is_empty() { presets } else { curated }
|
||
- }
|
||
-
|
||
fn all_model_presets(&self) -> Vec<ModelPreset> {
|
||
if let Some(presets) = self.remote_model_presets.as_ref() {
|
||
return presets.clone();
|
||
@@ -23142,7 +23035,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
}
|
||
|
||
fn available_model_presets(&self) -> Vec<ModelPreset> {
|
||
- Self::curated_model_presets(self.all_model_presets())
|
||
+ self.all_model_presets()
|
||
}
|
||
|
||
pub(crate) fn update_model_presets(
|
||
@@ -23154,13 +23047,9 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
return;
|
||
}
|
||
|
||
- let curated_presets = Self::curated_model_presets(presets.clone());
|
||
- if curated_presets.is_empty() {
|
||
- return;
|
||
- }
|
||
-
|
||
+ self.bottom_pane
|
||
+ .update_model_selection_presets(presets.clone());
|
||
self.remote_model_presets = Some(presets);
|
||
- self.bottom_pane.update_model_selection_presets(curated_presets);
|
||
|
||
if let Some(default_model) = default_model {
|
||
self.maybe_apply_remote_default_model(default_model);
|
||
@@ -25966,7 +25855,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
// Send ConfigureSession op to backend
|
||
self.submit_configure_session_op();
|
||
|
||
- // Persist selection into CODEX_HOME/config.toml for this project directory so it sticks.
|
||
+ // Persist selection into DEV_HOME/config.toml for this project directory so it sticks.
|
||
let _ = set_project_access_mode(
|
||
&self.config.code_home,
|
||
&self.config.cwd,
|
||
@@ -26121,7 +26010,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
}
|
||
|
||
fn save_theme_to_config(&self, new_theme: code_core::config_types::ThemeName) {
|
||
- // Persist the theme selection to CODE_HOME/CODEX_HOME config.toml
|
||
+ // Persist the theme selection to CODE_HOME/DEV_HOME config.toml
|
||
match code_core::config::find_code_home() {
|
||
Ok(home) => {
|
||
if let Err(e) = code_core::config::set_tui_theme_name(&home, new_theme) {
|
||
@@ -28435,7 +28324,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
);
|
||
|
||
let visible_message = format!(
|
||
- "🤖 Handing /browser failure ({}) to Code. Error: {}",
|
||
+ "🤖 Handing /browser failure ({}) to Hanzo Dev. Error: {}",
|
||
failure_context,
|
||
truncated
|
||
);
|
||
@@ -29329,7 +29218,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
}
|
||
},
|
||
Err(e) => {
|
||
- let msg = format!("Failed to locate CODEX_HOME: {e}");
|
||
+ let msg = format!("Failed to locate DEV_HOME: {e}");
|
||
self.history_push_plain_state(history_cell::new_error_event(msg));
|
||
}
|
||
}
|
||
@@ -29386,7 +29275,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
}
|
||
}
|
||
Err(e) => {
|
||
- let msg = format!("Failed to locate CODEX_HOME: {}", e);
|
||
+ let msg = format!("Failed to locate DEV_HOME: {}", e);
|
||
self.history_push_plain_state(history_cell::new_error_event(msg));
|
||
}
|
||
}
|
||
@@ -29523,7 +29412,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
}
|
||
}
|
||
Err(e) => {
|
||
- let msg = format!("Failed to locate CODEX_HOME: {}", e);
|
||
+ let msg = format!("Failed to locate DEV_HOME: {}", e);
|
||
self.history_push_plain_state(history_cell::new_error_event(msg));
|
||
}
|
||
}
|
||
@@ -30099,7 +29988,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
}
|
||
|
||
/// Export transcript for buffer-mode mirroring: omit internal sentinels
|
||
- /// and include gutter icons and a blank line between items for readability.
|
||
+ /// and put a blank line between items for readability.
|
||
pub(crate) fn export_transcript_lines_for_buffer(&self) -> Vec<ratatui::text::Line<'static>> {
|
||
let mut out: Vec<ratatui::text::Line<'static>> = Vec::new();
|
||
for (idx, cell) in self.history_cells.iter().enumerate() {
|
||
@@ -30113,12 +30002,8 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
.map(|r| ratatui::text::Line::from(r.text))
|
||
.collect::<Vec<_>>();
|
||
if !streaming_lines.is_empty() {
|
||
- // Apply gutter to streaming preview (first line gets " • ", continuations get 3 spaces)
|
||
- if let Some(first) = streaming_lines.first_mut() {
|
||
- first.spans.insert(0, ratatui::text::Span::raw(" • "));
|
||
- }
|
||
- for line in streaming_lines.iter_mut().skip(1) {
|
||
- line.spans.insert(0, ratatui::text::Span::raw(" "));
|
||
+ for line in streaming_lines.iter_mut() {
|
||
+ line.spans.insert(0, ratatui::text::Span::raw(" "));
|
||
}
|
||
out.extend(streaming_lines);
|
||
out.push(ratatui::text::Line::from(""));
|
||
@@ -30126,32 +30011,16 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
out
|
||
}
|
||
|
||
- /// Render a single history cell into terminal-friendly lines:
|
||
- /// - Prepend a gutter icon (symbol + space) to the first line when defined.
|
||
- /// - Add a single blank line after the cell as a separator.
|
||
+ /// Render a single history cell into terminal-friendly lines, one column
|
||
+ /// in from the edge, with a blank line after the cell as a separator.
|
||
fn render_lines_for_terminal(
|
||
&self,
|
||
idx: usize,
|
||
cell: &dyn crate::history_cell::HistoryCell,
|
||
) -> Vec<ratatui::text::Line<'static>> {
|
||
let mut lines = self.cell_lines_for_terminal_index(idx, cell);
|
||
- let _has_icon = cell.gutter_symbol().is_some();
|
||
- let first_prefix = if let Some(sym) = cell.gutter_symbol() {
|
||
- format!(" {} ", sym) // one space, icon, one space
|
||
- } else {
|
||
- " ".to_string() // three spaces when no icon
|
||
- };
|
||
- if let Some(first) = lines.first_mut() {
|
||
- first
|
||
- .spans
|
||
- .insert(0, ratatui::text::Span::raw(first_prefix));
|
||
- }
|
||
- // For wrapped/subsequent lines, use a 3-space gutter to maintain alignment
|
||
- if lines.len() > 1 {
|
||
- for (_idx, line) in lines.iter_mut().enumerate().skip(1) {
|
||
- // Always 3 spaces for continuation lines
|
||
- line.spans.insert(0, ratatui::text::Span::raw(" "));
|
||
- }
|
||
+ for line in lines.iter_mut() {
|
||
+ line.spans.insert(0, ratatui::text::Span::raw(" "));
|
||
}
|
||
lines.push(ratatui::text::Line::from(""));
|
||
lines
|
||
@@ -30181,11 +30050,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
return None;
|
||
}
|
||
let layout_areas = self.layout_areas(area);
|
||
- let bottom_pane_area = if layout_areas.len() == 4 {
|
||
- layout_areas[3]
|
||
- } else {
|
||
- layout_areas[2]
|
||
- };
|
||
+ let bottom_pane_area = *layout_areas.last().unwrap_or(&area);
|
||
self.bottom_pane.cursor_pos(bottom_pane_area)
|
||
}
|
||
|
||
@@ -30213,286 +30078,6 @@ Have we met every part of this goal and is there no further work to do?"#
|
||
})
|
||
}
|
||
|
||
- fn get_git_branch(&self) -> Option<String> {
|
||
- use std::fs;
|
||
- use std::path::Path;
|
||
-
|
||
- let head_path = self.config.cwd.join(".git/HEAD");
|
||
- let mut cache = self.git_branch_cache.borrow_mut();
|
||
- let now = Instant::now();
|
||
-
|
||
- let needs_refresh = match cache.last_refresh {
|
||
- Some(last) => now.duration_since(last) >= Duration::from_millis(500),
|
||
- None => true,
|
||
- };
|
||
-
|
||
- if needs_refresh {
|
||
- let modified = fs::metadata(&head_path)
|
||
- .and_then(|meta| meta.modified())
|
||
- .ok();
|
||
-
|
||
- let metadata_changed = cache.last_head_mtime != modified || cache.last_refresh.is_none();
|
||
-
|
||
- if metadata_changed {
|
||
- cache.value = fs::read_to_string(&head_path)
|
||
- .ok()
|
||
- .and_then(|head_contents| {
|
||
- let head = head_contents.trim();
|
||
-
|
||
- if let Some(rest) = head.strip_prefix("ref: ") {
|
||
- return Path::new(rest)
|
||
- .file_name()
|
||
- .and_then(|s| s.to_str())
|
||
- .filter(|s| !s.is_empty())
|
||
- .map(|name| name.to_string());
|
||
- }
|
||
-
|
||
- if head.len() >= 7
|
||
- && head.as_bytes().iter().all(|byte| byte.is_ascii_hexdigit())
|
||
- {
|
||
- return Some(format!("detached: {}", &head[..7]));
|
||
- }
|
||
-
|
||
- None
|
||
- });
|
||
- cache.last_head_mtime = modified;
|
||
- }
|
||
-
|
||
- cache.last_refresh = Some(now);
|
||
- }
|
||
-
|
||
- cache.value.clone()
|
||
- }
|
||
-
|
||
- fn render_status_bar(&self, area: Rect, buf: &mut Buffer) {
|
||
- use crate::exec_command::relativize_to_home;
|
||
- use ratatui::layout::Margin;
|
||
- use ratatui::style::Modifier;
|
||
- use ratatui::style::Style;
|
||
- use ratatui::text::Line;
|
||
- use ratatui::text::Span;
|
||
- use ratatui::widgets::Block;
|
||
- use ratatui::widgets::Borders;
|
||
- use ratatui::widgets::Paragraph;
|
||
-
|
||
- // Add same horizontal padding as the Message input (2 chars on each side)
|
||
- let horizontal_padding = 1u16;
|
||
- let padded_area = Rect {
|
||
- x: area.x + horizontal_padding,
|
||
- y: area.y,
|
||
- width: area.width.saturating_sub(horizontal_padding * 2),
|
||
- height: area.height,
|
||
- };
|
||
-
|
||
- // Get current working directory string
|
||
- let cwd_str = match relativize_to_home(&self.config.cwd) {
|
||
- Some(rel) if !rel.as_os_str().is_empty() => format!("~/{}", rel.display()),
|
||
- Some(_) => "~".to_string(),
|
||
- None => self.config.cwd.display().to_string(),
|
||
- };
|
||
-
|
||
- let cwd_short_str = cwd_str
|
||
- .rsplit(|c| c == '/' || c == '\\')
|
||
- .find(|segment| !segment.is_empty())
|
||
- .unwrap_or(cwd_str.as_str())
|
||
- .to_string();
|
||
-
|
||
- // Build status line spans with dynamic elision based on width.
|
||
- // Removal priority when space is tight:
|
||
- // 1) Reasoning level
|
||
- // 2) Model
|
||
- // 3) Branch
|
||
- // 4) Directory
|
||
- let branch_opt = self.get_git_branch();
|
||
-
|
||
- // Helper to assemble spans based on include flags
|
||
- let build_spans = |include_reasoning: bool,
|
||
- include_model: bool,
|
||
- include_branch: bool,
|
||
- include_dir: bool,
|
||
- dir_display: &str| {
|
||
- let mut spans: Vec<Span> = Vec::new();
|
||
- let model_display = self.format_model_name(&self.config.model);
|
||
- let mut model_suffix_parts: Vec<String> = Vec::new();
|
||
- if include_reasoning {
|
||
- model_suffix_parts
|
||
- .push(Self::format_reasoning_effort(self.config.model_reasoning_effort).to_string());
|
||
- }
|
||
- if matches!(self.config.service_tier, Some(ServiceTier::Fast)) {
|
||
- model_suffix_parts.push("Fast".to_string());
|
||
- }
|
||
- let model_label = if model_suffix_parts.is_empty() {
|
||
- model_display
|
||
- } else {
|
||
- format!("{} ({})", model_display, model_suffix_parts.join(", "))
|
||
- };
|
||
-
|
||
- // Title follows theme text color
|
||
- spans.push(Span::styled(
|
||
- "Every Code",
|
||
- Style::default()
|
||
- .fg(crate::colors::text())
|
||
- .add_modifier(Modifier::BOLD),
|
||
- ));
|
||
-
|
||
- if include_model {
|
||
- spans.push(Span::styled(
|
||
- " • ",
|
||
- Style::default().fg(crate::colors::text_dim()),
|
||
- ));
|
||
- spans.push(Span::styled(
|
||
- "Model: ",
|
||
- Style::default().fg(crate::colors::text_dim()),
|
||
- ));
|
||
- spans.push(Span::styled(
|
||
- model_label,
|
||
- Style::default().fg(crate::colors::info()),
|
||
- ));
|
||
- }
|
||
-
|
||
- if include_dir {
|
||
- spans.push(Span::styled(
|
||
- " • ",
|
||
- Style::default().fg(crate::colors::text_dim()),
|
||
- ));
|
||
- spans.push(Span::styled(
|
||
- "Directory: ",
|
||
- Style::default().fg(crate::colors::text_dim()),
|
||
- ));
|
||
- spans.push(Span::styled(
|
||
- dir_display.to_string(),
|
||
- Style::default().fg(crate::colors::info()),
|
||
- ));
|
||
- }
|
||
-
|
||
- if include_branch {
|
||
- if let Some(branch) = &branch_opt {
|
||
- spans.push(Span::styled(
|
||
- " • ",
|
||
- Style::default().fg(crate::colors::text_dim()),
|
||
- ));
|
||
- spans.push(Span::styled(
|
||
- "Branch: ",
|
||
- Style::default().fg(crate::colors::text_dim()),
|
||
- ));
|
||
- spans.push(Span::styled(
|
||
- branch.clone(),
|
||
- Style::default().fg(crate::colors::success_green()),
|
||
- ));
|
||
- }
|
||
- }
|
||
-
|
||
- // Footer already shows the Ctrl+R hint; avoid duplicating it here.
|
||
-
|
||
- spans
|
||
- };
|
||
-
|
||
- // Start with all items in production; tests can opt-in to a minimal header via env flag.
|
||
- let minimal_header = std::env::var_os("CODEX_TUI_FORCE_MINIMAL_HEADER").is_some();
|
||
- let demo_mode = self.config.demo_developer_message.is_some();
|
||
- let mut include_reasoning = !minimal_header;
|
||
- let mut include_model = !minimal_header;
|
||
- let mut include_branch = !minimal_header && branch_opt.is_some();
|
||
- let mut include_dir = !minimal_header && !demo_mode;
|
||
- let mut use_short_dir = false;
|
||
- let mut status_spans = build_spans(
|
||
- include_reasoning,
|
||
- include_model,
|
||
- include_branch,
|
||
- include_dir,
|
||
- &cwd_str,
|
||
- );
|
||
-
|
||
- // Now recompute exact available width inside the border + padding before measuring
|
||
- // Render a bordered status block and explicitly fill its background.
|
||
- // Without a background fill, some terminals blend with prior frame
|
||
- // contents, which is especially noticeable on dark themes as dark
|
||
- // "caps" at the edges. Match the app background for consistency.
|
||
- let status_block = Block::default()
|
||
- .borders(Borders::ALL)
|
||
- .border_style(Style::default().fg(crate::colors::border()))
|
||
- .style(Style::default().bg(crate::colors::background()));
|
||
- let inner_area = status_block.inner(padded_area);
|
||
- let padded_inner = inner_area.inner(Margin::new(1, 0));
|
||
- let inner_width = padded_inner.width as usize;
|
||
-
|
||
- // Helper to measure current spans width
|
||
- let measure =
|
||
- |spans: &Vec<Span>| -> usize { spans.iter().map(|s| s.content.chars().count()).sum() };
|
||
-
|
||
- if include_dir && !use_short_dir && measure(&status_spans) > inner_width {
|
||
- use_short_dir = true;
|
||
- status_spans = build_spans(
|
||
- include_reasoning,
|
||
- include_model,
|
||
- include_branch,
|
||
- include_dir,
|
||
- &cwd_short_str,
|
||
- );
|
||
- }
|
||
-
|
||
- // Elide items in priority order until content fits
|
||
- while measure(&status_spans) > inner_width {
|
||
- if include_reasoning {
|
||
- include_reasoning = false;
|
||
- } else if include_model {
|
||
- include_model = false;
|
||
- } else if include_branch {
|
||
- include_branch = false;
|
||
- } else if include_dir {
|
||
- include_dir = false;
|
||
- } else {
|
||
- break;
|
||
- }
|
||
- status_spans = build_spans(
|
||
- include_reasoning,
|
||
- include_model,
|
||
- include_branch,
|
||
- include_dir,
|
||
- if use_short_dir { &cwd_short_str } else { &cwd_str },
|
||
- );
|
||
- }
|
||
-
|
||
- // Note: The reasoning visibility hint is appended inside `build_spans`
|
||
- // so it participates in width measurement and elision. Do not append
|
||
- // it again here to avoid overflow that caused corrupted glyph boxes on
|
||
- // some terminals.
|
||
-
|
||
- let status_line = Line::from(status_spans);
|
||
-
|
||
- let now = Instant::now();
|
||
- let mut frame_needed = false;
|
||
- if ENABLE_WARP_STRIPES && self.header_wave.schedule_if_needed(now) {
|
||
- frame_needed = true;
|
||
- }
|
||
- if frame_needed {
|
||
- self.app_event_tx
|
||
- .send(AppEvent::ScheduleFrameIn(HeaderWaveEffect::FRAME_INTERVAL));
|
||
- }
|
||
-
|
||
- // Render the block first
|
||
- status_block.render(padded_area, buf);
|
||
- let wave_enabled = self.header_wave.is_enabled();
|
||
- if wave_enabled {
|
||
- self.header_wave.render(padded_area, buf, now);
|
||
- }
|
||
-
|
||
- // Then render the text inside with padding, centered
|
||
- let effect_enabled = wave_enabled;
|
||
- let status_style = if effect_enabled {
|
||
- Style::default().fg(crate::colors::text())
|
||
- } else {
|
||
- Style::default()
|
||
- .bg(crate::colors::background())
|
||
- .fg(crate::colors::text())
|
||
- };
|
||
-
|
||
- let status_widget = Paragraph::new(vec![status_line])
|
||
- .alignment(ratatui::layout::Alignment::Center)
|
||
- .style(status_style);
|
||
- ratatui::widgets::Widget::render(status_widget, padded_inner, buf);
|
||
- }
|
||
-
|
||
fn render_screenshot_highlevel(&self, path: &PathBuf, area: Rect, buf: &mut Buffer) {
|
||
use ratatui::widgets::Widget;
|
||
use ratatui_image::Image;
|
||
@@ -30967,7 +30552,6 @@ impl Drop for AutoReviewStubGuard {
|
||
use crate::chatwidget::message::UserMessage;
|
||
use crate::chatwidget::smoke_helpers::{enter_test_runtime_guard, ChatWidgetHarness};
|
||
use crate::history_cell::{self, ExploreAggregationCell, HistoryCellType};
|
||
- use code_common::model_presets::ReasoningEffortPreset;
|
||
use code_auto_drive_core::{
|
||
AutoContinueMode,
|
||
AutoRunPhase,
|
||
@@ -31333,53 +30917,7 @@ use code_core::protocol::OrderMeta;
|
||
assert!(summary_text.chars().count() <= 280);
|
||
}
|
||
|
||
- #[test]
|
||
- fn format_model_name_capitalizes_codex_mini() {
|
||
- let mut harness = ChatWidgetHarness::new();
|
||
- let formatted = harness.chat().format_model_name("gpt-5.1-codex-mini");
|
||
- assert_eq!(formatted, "GPT-5.1-Codex-Mini");
|
||
- }
|
||
-
|
||
- #[test]
|
||
- fn curated_model_presets_falls_back_when_shortlist_missing() {
|
||
- let presets = vec![ModelPreset {
|
||
- id: "custom-model".to_string(),
|
||
- model: "custom-model".to_string(),
|
||
- display_name: "custom-model".to_string(),
|
||
- description: "custom".to_string(),
|
||
- default_reasoning_effort: ReasoningEffort::Medium.into(),
|
||
- supported_reasoning_efforts: vec![ReasoningEffortPreset {
|
||
- effort: ReasoningEffort::Medium.into(),
|
||
- description: "balanced".to_string(),
|
||
- }],
|
||
- supported_text_verbosity: &[TextVerbosity::Medium],
|
||
- is_default: false,
|
||
- upgrade: None,
|
||
- pro_only: false,
|
||
- show_in_picker: true,
|
||
- }];
|
||
-
|
||
- let curated = ChatWidget::curated_model_presets(presets.clone());
|
||
- assert_eq!(curated.len(), 1);
|
||
- assert_eq!(curated[0].id, presets[0].id);
|
||
- }
|
||
|
||
- #[test]
|
||
- fn curated_model_presets_includes_gpt_5_6_variants_when_present() {
|
||
- let presets: Vec<ModelPreset> = builtin_model_presets(Some(AuthMode::ChatGPT), true)
|
||
- .into_iter()
|
||
- .filter(|preset| {
|
||
- matches!(
|
||
- preset.id.as_str(),
|
||
- "gpt-5.6-terra" | "gpt-5.6-luna" | "gpt-5.3-codex"
|
||
- )
|
||
- })
|
||
- .collect();
|
||
-
|
||
- let curated = ChatWidget::curated_model_presets(presets);
|
||
- let ids: Vec<&str> = curated.iter().map(|preset| preset.id.as_str()).collect();
|
||
- assert_eq!(ids, vec!["gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.3-codex"]);
|
||
- }
|
||
|
||
#[test]
|
||
fn builtin_model_presets_include_gpt_5_5() {
|
||
@@ -31387,69 +30925,27 @@ use code_core::protocol::OrderMeta;
|
||
assert!(presets.iter().any(|preset| preset.id == "gpt-5.5"));
|
||
}
|
||
|
||
+ /// The picker used to hoist a hardcoded five-slug OpenAI shortlist to the
|
||
+ /// front, so the model `dev` actually runs was never the one it offered
|
||
+ /// first. Order is the catalog's job now, and this is what that buys.
|
||
#[test]
|
||
- fn curated_model_presets_appends_new_remote_models() {
|
||
- let mut gpt_5_5 = builtin_model_presets(Some(AuthMode::ChatGPT), true)
|
||
- .into_iter()
|
||
- .find(|preset| preset.id == "gpt-5.6-terra")
|
||
- .expect("expected gpt-5.6-terra builtin preset");
|
||
- gpt_5_5.id = "gpt-5.5".to_string();
|
||
- gpt_5_5.model = "gpt-5.5".to_string();
|
||
- gpt_5_5.display_name = "GPT-5.5".to_string();
|
||
-
|
||
- let presets = vec![
|
||
- gpt_5_5,
|
||
- ModelPreset {
|
||
- id: "gpt-5.6-terra".to_string(),
|
||
- model: "gpt-5.6-terra".to_string(),
|
||
- display_name: "GPT-5.6 Terra".to_string(),
|
||
- description: "Frontier flagship model.".to_string(),
|
||
- default_reasoning_effort: ReasoningEffort::Medium.into(),
|
||
- supported_reasoning_efforts: vec![ReasoningEffortPreset {
|
||
- effort: ReasoningEffort::Medium.into(),
|
||
- description: "balanced".to_string(),
|
||
- }],
|
||
- supported_text_verbosity: &[TextVerbosity::Medium],
|
||
- is_default: false,
|
||
- upgrade: None,
|
||
- pro_only: false,
|
||
- show_in_picker: true,
|
||
- },
|
||
- ];
|
||
+ fn the_model_picker_leads_with_our_default() {
|
||
+ let _runtime_guard = enter_test_runtime_guard();
|
||
+ let mut harness = ChatWidgetHarness::new();
|
||
+ let chat = harness.chat();
|
||
+
|
||
+ let presets = chat.available_model_presets();
|
||
+ let first = presets.first().expect("the picker must offer something");
|
||
|
||
- let curated = ChatWidget::curated_model_presets(presets);
|
||
- let ids: Vec<&str> = curated.iter().map(|preset| preset.id.as_str()).collect();
|
||
- assert_eq!(ids, vec!["gpt-5.5", "gpt-5.6-terra"]);
|
||
+ assert_eq!(
|
||
+ first.model, code_core::config::HANZO_DEFAULT_MODEL,
|
||
+ "the picker leads with `{}`, not the default `{}`",
|
||
+ first.model,
|
||
+ code_core::config::HANZO_DEFAULT_MODEL
|
||
+ );
|
||
}
|
||
|
||
- #[test]
|
||
- fn curated_model_presets_deduplicates_alias_slugs() {
|
||
- let canonical = ModelPreset {
|
||
- id: "alias/gpt-5.5".to_string(),
|
||
- model: "gpt-5.5".to_string(),
|
||
- display_name: "GPT-5.5".to_string(),
|
||
- description: "Frontier flagship model.".to_string(),
|
||
- default_reasoning_effort: ReasoningEffort::Medium.into(),
|
||
- supported_reasoning_efforts: vec![ReasoningEffortPreset {
|
||
- effort: ReasoningEffort::Medium.into(),
|
||
- description: "balanced".to_string(),
|
||
- }],
|
||
- supported_text_verbosity: &[TextVerbosity::Medium],
|
||
- is_default: false,
|
||
- upgrade: None,
|
||
- pro_only: false,
|
||
- show_in_picker: true,
|
||
- };
|
||
- let duplicate = ModelPreset {
|
||
- id: "gpt-5.5".to_string(),
|
||
- model: "gpt-5.5".to_string(),
|
||
- ..canonical.clone()
|
||
- };
|
||
|
||
- let curated = ChatWidget::curated_model_presets(vec![canonical, duplicate]);
|
||
- let ids: Vec<&str> = curated.iter().map(|preset| preset.id.as_str()).collect();
|
||
- assert_eq!(ids, vec!["alias/gpt-5.5"]);
|
||
- }
|
||
|
||
#[test]
|
||
fn model_command_accepts_supported_non_curated_builtin_model() {
|
||
@@ -31598,10 +31094,14 @@ use code_core::protocol::OrderMeta;
|
||
|
||
let cfg_text = std::fs::read_to_string(&config_path).expect("read config");
|
||
let cfg = toml::from_str::<ConfigToml>(&cfg_text).expect("parse config");
|
||
+ // 272k is the GPT-5 family's context window. `dev` defaults to
|
||
+ // zen5-coder, which has no fixed window, so name the model this
|
||
+ // assertion is actually about rather than lean on the default.
|
||
let reloaded = Config::load_from_base_config_with_overrides(
|
||
cfg,
|
||
ConfigOverrides {
|
||
cwd: Some(code_home.path().to_path_buf()),
|
||
+ model: Some("gpt-5.5".to_string()),
|
||
..Default::default()
|
||
},
|
||
code_home.path().to_path_buf(),
|
||
@@ -39471,7 +38971,7 @@ impl ChatWidget<'_> {
|
||
pub(crate) fn show_cloud_tasks_loading(&mut self) {
|
||
let loading_item = SelectionItem {
|
||
name: "Loading cloud tasks…".to_string(),
|
||
- description: Some("Fetching latest tasks from Codex Cloud".to_string()),
|
||
+ description: Some("Fetching latest cloud tasks".to_string()),
|
||
is_current: true,
|
||
actions: Vec::new(),
|
||
};
|
||
@@ -39527,7 +39027,7 @@ impl ChatWidget<'_> {
|
||
pub(crate) fn show_cloud_environment_loading(&mut self) {
|
||
let loading_item = SelectionItem {
|
||
name: "Loading environments…".to_string(),
|
||
- description: Some("Fetching available Codex Cloud environments".to_string()),
|
||
+ description: Some("Fetching available cloud environments".to_string()),
|
||
is_current: true,
|
||
actions: Vec::new(),
|
||
};
|
||
@@ -39752,7 +39252,7 @@ impl ChatWidget<'_> {
|
||
|
||
let view = CustomPromptView::new(
|
||
format!("Create cloud task ({env_display})"),
|
||
- "Describe the change you want Codex to implement".to_string(),
|
||
+ "Describe the change you want Hanzo Dev to implement".to_string(),
|
||
Some("Press Enter to submit · Esc cancel".to_string()),
|
||
self.app_event_tx.clone(),
|
||
None,
|
||
@@ -42256,20 +41756,14 @@ impl WidgetRef for &ChatWidget<'_> {
|
||
self.layout.last_frame_width.set(area.width);
|
||
|
||
let layout_areas = self.layout_areas(area);
|
||
- let status_bar_area = layout_areas.get(0).copied().unwrap_or(area);
|
||
- let history_area = layout_areas.get(1).copied().unwrap_or(area);
|
||
- let bottom_pane_area = layout_areas.get(2).copied().unwrap_or(area);
|
||
+ let history_area = layout_areas.get(0).copied().unwrap_or(area);
|
||
+ let bottom_pane_area = layout_areas.get(1).copied().unwrap_or(area);
|
||
|
||
// Record the effective bottom pane height for buffer-mode scrollback inserts.
|
||
self.layout
|
||
.last_bottom_reserved_rows
|
||
.set(bottom_pane_area.height);
|
||
|
||
- // Render status bar and HUD only in full TUI mode
|
||
- if !self.standard_terminal_mode {
|
||
- self.render_status_bar(status_bar_area, buf);
|
||
- }
|
||
-
|
||
// In standard-terminal mode, do not paint the history region: committed
|
||
// content is appended to the terminal's own scrollback via
|
||
// insert_history_lines and repainting here would overwrite it.
|
||
@@ -42292,7 +41786,6 @@ impl WidgetRef for &ChatWidget<'_> {
|
||
height: history_area.height,
|
||
};
|
||
|
||
- self.update_welcome_height_hint(content_area.height);
|
||
|
||
// Reset the full history region to the baseline theme background once per frame.
|
||
// Individual cells only repaint when their visuals differ (e.g., assistant tint),
|
||
@@ -42890,9 +42383,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||
let item = visible
|
||
.cell
|
||
.expect("visible cell missing backing cell for render");
|
||
- // Calculate height with reduced width due to gutter
|
||
- const GUTTER_WIDTH: u16 = 2;
|
||
- let content_width = content_area.width.saturating_sub(GUTTER_WIDTH);
|
||
+ let content_width = content_area.width;
|
||
let maybe_assistant = item
|
||
.as_any()
|
||
.downcast_ref::<crate::history_cell::AssistantMarkdownCell>();
|
||
@@ -42975,21 +42466,10 @@ impl WidgetRef for &ChatWidget<'_> {
|
||
|
||
|
||
if visible_height > 0 {
|
||
- // Define gutter width (2 chars: symbol + space)
|
||
- const GUTTER_WIDTH: u16 = 2;
|
||
-
|
||
- // Split area into gutter and content
|
||
- let gutter_area = Rect {
|
||
- x: content_area.x,
|
||
- y: screen_y,
|
||
- width: GUTTER_WIDTH.min(content_area.width),
|
||
- height: visible_height,
|
||
- };
|
||
-
|
||
let item_area = Rect {
|
||
- x: content_area.x + GUTTER_WIDTH.min(content_area.width),
|
||
+ x: content_area.x,
|
||
y: screen_y,
|
||
- width: content_area.width.saturating_sub(GUTTER_WIDTH),
|
||
+ width: content_area.width,
|
||
height: visible_height,
|
||
};
|
||
|
||
@@ -43020,176 +42500,6 @@ impl WidgetRef for &ChatWidget<'_> {
|
||
);
|
||
}
|
||
|
||
- // Paint gutter background. For Assistant and Auto Review, extend the tint under the
|
||
- // gutter and also one extra column to the left (so the • has color on both sides),
|
||
- // without changing layout or symbol positions.
|
||
- let is_assistant =
|
||
- matches!(item.kind(), crate::history_cell::HistoryCellType::Assistant);
|
||
- let is_auto_review = ChatWidget::is_auto_review_cell(item);
|
||
- let auto_review_bg = crate::history_cell::PlainHistoryCell::auto_review_bg();
|
||
- let gutter_bg = if is_assistant {
|
||
- crate::colors::assistant_bg()
|
||
- } else if is_auto_review {
|
||
- auto_review_bg
|
||
- } else {
|
||
- crate::colors::background()
|
||
- };
|
||
-
|
||
- // Paint gutter background for assistant/auto-review cells so the tinted
|
||
- // strip appears contiguous with the message body. This avoids
|
||
- // the light "hole" seen after we reduced redraws. For other
|
||
- // cell types keep the default background (already painted by
|
||
- // the frame bg fill above).
|
||
- if (is_assistant || is_auto_review) && gutter_area.width > 0 && gutter_area.height > 0 {
|
||
- let _perf_gutter_start = if self.perf_state.enabled {
|
||
- Some(std::time::Instant::now())
|
||
- } else {
|
||
- None
|
||
- };
|
||
- let style = Style::default().bg(gutter_bg);
|
||
- let mut tint_x = gutter_area.x;
|
||
- let mut tint_width = gutter_area.width;
|
||
- if content_area.x > history_area.x {
|
||
- tint_x = content_area.x.saturating_sub(1);
|
||
- tint_width = tint_width.saturating_add(1);
|
||
- }
|
||
- let tint_rect = Rect::new(tint_x, gutter_area.y, tint_width, gutter_area.height);
|
||
- fill_rect(buf, tint_rect, Some(' '), style);
|
||
- // Also tint one column immediately to the right of the content area
|
||
- // so the assistant block is visually bookended. This column lives in the
|
||
- // right padding stripe; when the scrollbar is visible it will draw over
|
||
- // the far-right edge, which is fine.
|
||
- let right_col_x = content_area.x.saturating_add(content_area.width);
|
||
- let history_right = history_area.x.saturating_add(history_area.width);
|
||
- if right_col_x < history_right {
|
||
- let right_rect = Rect::new(right_col_x, item_area.y, 1, item_area.height);
|
||
- fill_rect(buf, right_rect, Some(' '), style);
|
||
- }
|
||
- if let Some(t0) = _perf_gutter_start {
|
||
- let dt = t0.elapsed().as_nanos();
|
||
- let mut p = self.perf_state.stats.borrow_mut();
|
||
- p.ns_gutter_paint = p.ns_gutter_paint.saturating_add(dt);
|
||
- // Rough accounting: area of gutter rectangle (clamped to u64)
|
||
- let area_cells: u64 =
|
||
- (gutter_area.width as u64).saturating_mul(gutter_area.height as u64);
|
||
- p.cells_gutter_paint = p.cells_gutter_paint.saturating_add(area_cells);
|
||
- }
|
||
- }
|
||
-
|
||
- // Render gutter symbol if present
|
||
- if let Some(symbol) = item.gutter_symbol() {
|
||
- // Choose color based on symbol/type
|
||
- let color = if is_auto_review {
|
||
- crate::colors::success()
|
||
- } else if symbol == "❯" {
|
||
- // Executed arrow – color reflects exec state
|
||
- if let Some(exec) = item
|
||
- .as_any()
|
||
- .downcast_ref::<crate::history_cell::ExecCell>()
|
||
- {
|
||
- match &exec.output {
|
||
- None => crate::colors::text(), // Running...
|
||
- // Successful runs use the theme success color so the arrow stays visible on all themes
|
||
- Some(o) if o.exit_code == 0 => crate::colors::text(),
|
||
- Some(_) => crate::colors::error(),
|
||
- }
|
||
- } else {
|
||
- // Handle merged exec cells (multi-block "Ran") the same as single execs
|
||
- match item.kind() {
|
||
- crate::history_cell::HistoryCellType::Exec {
|
||
- kind: crate::history_cell::ExecKind::Run,
|
||
- status: crate::history::state::ExecStatus::Success,
|
||
- } => crate::colors::text(),
|
||
- crate::history_cell::HistoryCellType::Exec {
|
||
- kind: crate::history_cell::ExecKind::Run,
|
||
- status: crate::history::state::ExecStatus::Error,
|
||
- } => crate::colors::error(),
|
||
- crate::history_cell::HistoryCellType::Exec { .. } => {
|
||
- crate::colors::text()
|
||
- }
|
||
- _ => crate::colors::text(),
|
||
- }
|
||
- }
|
||
- } else if symbol == "↯" {
|
||
- // Patch/Updated arrow color – match the header text color
|
||
- match item.kind() {
|
||
- crate::history_cell::HistoryCellType::Patch {
|
||
- kind: crate::history_cell::PatchKind::ApplySuccess,
|
||
- } => crate::colors::success(),
|
||
- crate::history_cell::HistoryCellType::Patch {
|
||
- kind: crate::history_cell::PatchKind::ApplyBegin,
|
||
- } => crate::colors::success(),
|
||
- crate::history_cell::HistoryCellType::Patch {
|
||
- kind: crate::history_cell::PatchKind::Proposed,
|
||
- } => crate::colors::primary(),
|
||
- crate::history_cell::HistoryCellType::Patch {
|
||
- kind: crate::history_cell::PatchKind::ApplyFailure,
|
||
- } => crate::colors::error(),
|
||
- _ => crate::colors::primary(),
|
||
- }
|
||
- } else if matches!(symbol, "◐" | "◓" | "◑" | "◒")
|
||
- && item
|
||
- .as_any()
|
||
- .downcast_ref::<crate::history_cell::RunningToolCallCell>()
|
||
- .map_or(false, |cell| cell.has_title("Waiting"))
|
||
- {
|
||
- crate::colors::text_bright()
|
||
- } else if matches!(symbol, "○" | "◔" | "◑" | "◕" | "●") {
|
||
- if let Some(plan_cell) = item
|
||
- .as_any()
|
||
- .downcast_ref::<crate::history_cell::PlanUpdateCell>()
|
||
- {
|
||
- if plan_cell.is_complete() {
|
||
- crate::colors::success()
|
||
- } else {
|
||
- crate::colors::info()
|
||
- }
|
||
- } else {
|
||
- crate::colors::success()
|
||
- }
|
||
- } else {
|
||
- match symbol {
|
||
- "›" => crate::colors::text(), // user
|
||
- "⋮" => crate::colors::primary(), // thinking
|
||
- "•" => crate::colors::text_bright(), // codex/agent
|
||
- "⚙" => crate::colors::info(), // tool working
|
||
- "✔" => crate::colors::success(), // tool complete
|
||
- "✖" => crate::colors::error(), // error
|
||
- "⚠" => crate::colors::warning(), // warning notice
|
||
- "★" => crate::colors::text_bright(), // notice/popular
|
||
- _ => crate::colors::text_dim(),
|
||
- }
|
||
- };
|
||
-
|
||
- // Draw the symbol anchored to the top of the message (not the viewport).
|
||
- // "Top of the message" accounts for any intentional top padding per cell type.
|
||
- // As you scroll past that anchor, the icon scrolls away with the message.
|
||
- if gutter_area.width >= 2 {
|
||
- // Anchor offset counted from the very start of the item's painted area
|
||
- // to the first line of its content that the icon should align with.
|
||
- let anchor_offset: u16 = match item.kind() {
|
||
- // Assistant messages render with one row of top padding so that
|
||
- // the content visually aligns; anchor to that second row.
|
||
- crate::history_cell::HistoryCellType::Assistant => 1,
|
||
- _ if is_auto_review => {
|
||
- crate::history_cell::PlainHistoryCell::auto_review_padding().0
|
||
- }
|
||
- _ => 0,
|
||
- };
|
||
-
|
||
- // If we've scrolled past the anchor line, don't render the icon.
|
||
- if skip_top <= anchor_offset {
|
||
- let rel = anchor_offset - skip_top; // rows from current viewport top
|
||
- let symbol_y = gutter_area.y.saturating_add(rel);
|
||
- if symbol_y < gutter_area.y.saturating_add(gutter_area.height) {
|
||
- let symbol_style = Style::default().fg(color).bg(gutter_bg);
|
||
- let symbol_x = gutter_area.x;
|
||
- buf.set_string(symbol_x, symbol_y, symbol, symbol_style);
|
||
- }
|
||
- }
|
||
- }
|
||
- }
|
||
-
|
||
// Render only the visible window of the item using vertical skip
|
||
let skip_rows = skip_top;
|
||
|
||
@@ -44818,7 +44128,7 @@ impl ChatWidget<'_> {
|
||
}
|
||
Err(_) => {
|
||
let msg = format!(
|
||
- "✅ {} TUI notifications (not persisted: CODE_HOME/CODEX_HOME not found)",
|
||
+ "✅ {} TUI notifications (not persisted: CODE_HOME/DEV_HOME not found)",
|
||
if enabled { "Enabled" } else { "Disabled" }
|
||
);
|
||
self.push_background_tail(msg);
|
||
@@ -44839,7 +44149,7 @@ impl ChatWidget<'_> {
|
||
.filter(|text| !text.is_empty());
|
||
|
||
self.app_event_tx.send(AppEvent::EmitTuiNotification {
|
||
- title: "Code".to_string(),
|
||
+ title: "Hanzo Dev".to_string(),
|
||
body: snippet,
|
||
});
|
||
}
|
||
@@ -44913,7 +44223,7 @@ impl ChatWidget<'_> {
|
||
}
|
||
},
|
||
Err(e) => {
|
||
- let msg = format!("Failed to locate CODEX_HOME: {}", e);
|
||
+ let msg = format!("Failed to locate DEV_HOME: {}", e);
|
||
self.history_push_plain_state(history_cell::new_error_event(msg));
|
||
}
|
||
}
|
||
diff --git a/tui/src/cloud_tasks_service.rs b/tui/src/cloud_tasks_service.rs
|
||
index 9bb8c17..43d441c 100644
|
||
--- a/tui/src/cloud_tasks_service.rs
|
||
+++ b/tui/src/cloud_tasks_service.rs
|
||
@@ -204,10 +204,10 @@ fn build_backend(config: &CloudTasksConfig) -> Result<Arc<dyn CloudBackend>> {
|
||
}
|
||
|
||
async fn load_config() -> Result<CloudTasksConfig> {
|
||
- let base_url_env = std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||
+ let base_url_env = std::env::var("DEV_CLOUD_TASKS_BASE_URL")
|
||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
|
||
let base_url = normalize_base_url(&base_url_env);
|
||
- let use_mock = std::env::var("CODEX_CLOUD_TASKS_MODE")
|
||
+ let use_mock = std::env::var("DEV_CLOUD_TASKS_MODE")
|
||
.ok()
|
||
.map(|mode| mode.eq_ignore_ascii_case("mock"))
|
||
.unwrap_or(false);
|
||
@@ -230,7 +230,7 @@ async fn load_config() -> Result<CloudTasksConfig> {
|
||
);
|
||
let auth = auth_manager
|
||
.auth()
|
||
- .ok_or_else(|| anyhow!("Not signed in. Run `codex login` to authenticate with ChatGPT."))?;
|
||
+ .ok_or_else(|| anyhow!("Not signed in. Run `dev login` to authenticate."))?;
|
||
let token = auth
|
||
.get_token()
|
||
.await
|
||
diff --git a/tui/src/colors.rs b/tui/src/colors.rs
|
||
index f073240..e15574b 100644
|
||
--- a/tui/src/colors.rs
|
||
+++ b/tui/src/colors.rs
|
||
@@ -115,6 +115,16 @@ pub(crate) fn dim() -> Color {
|
||
text_dim()
|
||
}
|
||
|
||
+/// A restrained surface for user input and submitted user messages.
|
||
+/// Dark themes use charcoal, substantially darker than Codex's gray composer.
|
||
+pub(crate) fn input_bg() -> Color {
|
||
+ if is_dark_background(current_theme().background) {
|
||
+ quantize_color_for_palette(Color::Rgb(24, 24, 28))
|
||
+ } else {
|
||
+ tint_background_toward(text_dim())
|
||
+ }
|
||
+}
|
||
+
|
||
pub(crate) fn background() -> Color {
|
||
current_theme().background
|
||
}
|
||
diff --git a/tui/src/exec_command.rs b/tui/src/exec_command.rs
|
||
index 90cfdf6..0ed85ec 100644
|
||
--- a/tui/src/exec_command.rs
|
||
+++ b/tui/src/exec_command.rs
|
||
@@ -1,5 +1,3 @@
|
||
-use std::path::Path;
|
||
-use std::path::PathBuf;
|
||
|
||
use code_core::util as core_util;
|
||
|
||
@@ -7,28 +5,6 @@ pub(crate) fn strip_bash_lc_and_escape(command: &[String]) -> String {
|
||
core_util::strip_bash_lc_and_escape(command)
|
||
}
|
||
|
||
-/// If `path` is absolute and inside $HOME, return the part *after* the home
|
||
-/// directory; otherwise, return the path as-is. Note if `path` is the homedir,
|
||
-/// this will return and empty path.
|
||
-pub(crate) fn relativize_to_home<P>(path: P) -> Option<PathBuf>
|
||
-where
|
||
- P: AsRef<Path>,
|
||
-{
|
||
- let path = path.as_ref();
|
||
- if !path.is_absolute() {
|
||
- // If the path is not absolute, we can’t do anything with it.
|
||
- return None;
|
||
- }
|
||
-
|
||
- if let Some(home_dir) = std::env::var_os("HOME").map(PathBuf::from) {
|
||
- if let Ok(rel) = path.strip_prefix(&home_dir) {
|
||
- return Some(rel.to_path_buf());
|
||
- }
|
||
- }
|
||
-
|
||
- None
|
||
-}
|
||
-
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::strip_bash_lc_and_escape;
|
||
diff --git a/tui/src/glitch_animation.rs b/tui/src/glitch_animation.rs
|
||
index 7f9932a..bf5d5ff 100644
|
||
--- a/tui/src/glitch_animation.rs
|
||
+++ b/tui/src/glitch_animation.rs
|
||
@@ -1,568 +1,6 @@
|
||
-use ratatui::buffer::Buffer;
|
||
+//! Color ramps shared by the animated surfaces (Auto Drive, the gradient
|
||
+//! background).
|
||
use ratatui::prelude::*;
|
||
-// Paragraph/Widget previously used; manual cell writes now keep static layer intact.
|
||
-
|
||
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||
-pub enum IntroArtSize {
|
||
- Large,
|
||
- Medium,
|
||
- Small,
|
||
- Tiny,
|
||
-}
|
||
-
|
||
-const LARGE_MIN_WIDTH: u16 = 80;
|
||
-const MEDIUM_MIN_WIDTH: u16 = 56;
|
||
-const SMALL_MIN_WIDTH: u16 = 50;
|
||
-const LARGE_MIN_HEIGHT: u16 = 28;
|
||
-const MEDIUM_MIN_HEIGHT: u16 = 21;
|
||
-const SMALL_MIN_HEIGHT: u16 = 19;
|
||
-const LARGE_VERSION_COLUMN: usize = 65;
|
||
-const MEDIUM_VERSION_COLUMN: usize = 43;
|
||
-const ANIMATED_CHARS: &[char] = &['█'];
|
||
-
|
||
-pub fn intro_art_size_for_width(width: u16) -> IntroArtSize {
|
||
- if width >= LARGE_MIN_WIDTH {
|
||
- IntroArtSize::Large
|
||
- } else if width >= MEDIUM_MIN_WIDTH {
|
||
- IntroArtSize::Medium
|
||
- } else if width >= SMALL_MIN_WIDTH {
|
||
- IntroArtSize::Small
|
||
- } else {
|
||
- IntroArtSize::Tiny
|
||
- }
|
||
-}
|
||
-
|
||
-pub(crate) fn intro_art_size_for_area(width: u16, height: u16) -> IntroArtSize {
|
||
- if width >= LARGE_MIN_WIDTH && height >= LARGE_MIN_HEIGHT {
|
||
- IntroArtSize::Large
|
||
- } else if width >= MEDIUM_MIN_WIDTH && height >= MEDIUM_MIN_HEIGHT {
|
||
- IntroArtSize::Medium
|
||
- } else if width >= SMALL_MIN_WIDTH && height >= SMALL_MIN_HEIGHT {
|
||
- IntroArtSize::Small
|
||
- } else {
|
||
- IntroArtSize::Tiny
|
||
- }
|
||
-}
|
||
-
|
||
-pub fn intro_art_height(size: IntroArtSize) -> u16 {
|
||
- match size {
|
||
- IntroArtSize::Large => 28,
|
||
- IntroArtSize::Medium => 21,
|
||
- IntroArtSize::Small => 19,
|
||
- IntroArtSize::Tiny => 7,
|
||
- }
|
||
-}
|
||
-
|
||
-pub(crate) fn render_intro_animation_with_size_and_alpha_offset(
|
||
- area: Rect,
|
||
- buf: &mut Buffer,
|
||
- t: f32,
|
||
- alpha: f32,
|
||
- size: IntroArtSize,
|
||
- version: &str,
|
||
- row_offset: u16,
|
||
-) {
|
||
- if area.width == 0 || area.height == 0 {
|
||
- return;
|
||
- }
|
||
-
|
||
- let t = t.clamp(0.0, 1.0);
|
||
- let alpha = alpha.clamp(0.0, 1.0);
|
||
- let outline_p = smoothstep(0.00, 0.60, t);
|
||
- let fill_p = smoothstep(0.35, 0.95, t);
|
||
- let fade = smoothstep(0.90, 1.00, t);
|
||
- let scan_p = smoothstep(0.55, 0.85, t);
|
||
- let frame = (t * 60.0) as u32;
|
||
-
|
||
- let mut lines = welcome_lines(size, version);
|
||
- let full_width = lines.iter().map(|line| line.chars().count()).max().unwrap_or(0);
|
||
- let start = row_offset as usize;
|
||
- if start >= lines.len() {
|
||
- return;
|
||
- }
|
||
- let end = (start + area.height as usize).min(lines.len());
|
||
- if start > 0 || end < lines.len() {
|
||
- lines = lines[start..end].iter().cloned().collect();
|
||
- if full_width > 0 {
|
||
- for line in &mut lines {
|
||
- let len = line.chars().count();
|
||
- if len < full_width {
|
||
- line.push_str(&" ".repeat(full_width - len));
|
||
- }
|
||
- }
|
||
- }
|
||
- }
|
||
- let (char_mask, anim_mask, shadow_mask, w, h) =
|
||
- lines_masks(&lines, |ch| ANIMATED_CHARS.contains(&ch));
|
||
- if w == 0 || h == 0 {
|
||
- return;
|
||
- }
|
||
- let border = compute_border(&anim_mask);
|
||
-
|
||
- let mut render_area = area;
|
||
- render_area.height = h.min(render_area.height as usize) as u16;
|
||
-
|
||
- let bg = crate::colors::background();
|
||
- for y in render_area.y..render_area.y.saturating_add(render_area.height) {
|
||
- for x in render_area.x..render_area.x.saturating_add(render_area.width) {
|
||
- buf[(x, y)].set_bg(bg);
|
||
- }
|
||
- }
|
||
-
|
||
- let reveal_x_outline = (w as f32 * outline_p).round() as isize;
|
||
- let reveal_x_fill = (w as f32 * fill_p).round() as isize;
|
||
- let reveal_x_shadow = reveal_x_outline;
|
||
-
|
||
- render_static_lines(
|
||
- &lines,
|
||
- &shadow_mask,
|
||
- render_area,
|
||
- buf,
|
||
- alpha,
|
||
- frame,
|
||
- reveal_x_shadow,
|
||
- );
|
||
-
|
||
- let shine_x = (w as f32 * scan_p).round() as isize;
|
||
- let shine_band = 3isize;
|
||
-
|
||
- if alpha >= 1.0 {
|
||
- render_overlay_lines(
|
||
- &char_mask,
|
||
- &anim_mask,
|
||
- &border,
|
||
- reveal_x_outline,
|
||
- reveal_x_fill,
|
||
- shine_x,
|
||
- shine_band,
|
||
- fade,
|
||
- frame,
|
||
- render_area,
|
||
- buf,
|
||
- );
|
||
- } else {
|
||
- render_overlay_lines_with_alpha(
|
||
- &char_mask,
|
||
- &anim_mask,
|
||
- &border,
|
||
- reveal_x_outline,
|
||
- reveal_x_fill,
|
||
- shine_x,
|
||
- shine_band,
|
||
- fade,
|
||
- frame,
|
||
- alpha,
|
||
- render_area,
|
||
- buf,
|
||
- );
|
||
- }
|
||
-}
|
||
-
|
||
-/* ---------------- welcome art ---------------- */
|
||
-
|
||
-fn welcome_lines(size: IntroArtSize, version: &str) -> Vec<String> {
|
||
- match size {
|
||
- IntroArtSize::Large => large_welcome_lines(version),
|
||
- IntroArtSize::Medium => medium_welcome_lines(version),
|
||
- IntroArtSize::Small => small_welcome_lines(version),
|
||
- IntroArtSize::Tiny => tiny_welcome_lines(version),
|
||
- }
|
||
-}
|
||
-
|
||
-const LARGE_VERSION_LINE: &str = " ███████╗ ╚████╔╝ ███████╗██║ ██║ ██║";
|
||
-const LARGE_BODY_TAIL: [&str; 22] = [
|
||
- " █████████╗ █████████╗ ████████████╗ ███████████████╗",
|
||
- " █████████║ █████████║ ████████████║ ███████████████║",
|
||
- " █████████║ █████████║ ████████████║ ███████████████║",
|
||
- " ███╔════════███╗ ███╔════════███╗ ███╔════════███╗ ███╔═══════════╝",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ╚══╝ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███████████████╗",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███████████████║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███████████████║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███╔═══════════╝",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███╗ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ███║ ███║ ███║ ███║ ███║ ███║ ███║",
|
||
- " ╚══█████████╔══╝ ╚══█████████╔══╝ ████████████╔══╝ ███████████████╗",
|
||
- " █████████║ █████████║ ████████████║ ███████████████║",
|
||
- " █████████║ █████████║ ████████████║ ███████████████║",
|
||
- " ╚════════╝ ╚════════╝ ╚══════════╝ ╚═════════════╝",
|
||
-];
|
||
-
|
||
-fn large_welcome_lines(version: &str) -> Vec<String> {
|
||
- let mut animated = vec![
|
||
- " ███████╗██╗ ██╗███████╗██████╗ ██╗ ██╗".to_string(),
|
||
- " ██╔════╝██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝".to_string(),
|
||
- " █████╗ ██║ ██║█████╗ ██████╔╝ ╚████╔╝".to_string(),
|
||
- " ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██╔══██╗ ╚██╔╝".to_string(),
|
||
- ];
|
||
-
|
||
- let base_width = LARGE_VERSION_LINE.chars().count();
|
||
- let padding = LARGE_VERSION_COLUMN.saturating_sub(base_width);
|
||
- let _version_pad = " ".repeat(padding);
|
||
- let footer_line = " ╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝";
|
||
- let footer_len = footer_line.chars().count();
|
||
- let footer_pad = LARGE_VERSION_COLUMN.saturating_sub(footer_len);
|
||
- let footer_version_pad = " ".repeat(footer_pad);
|
||
- animated.push(format!("{LARGE_VERSION_LINE}{footer_version_pad}{version}"));
|
||
- animated.push(footer_line.to_string());
|
||
- animated.extend(LARGE_BODY_TAIL.iter().map(|line| (*line).to_string()));
|
||
-
|
||
- shift_left(animated, 3)
|
||
-}
|
||
-
|
||
-const MEDIUM_VERSION_LINE: &str = " ╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ";
|
||
-const MEDIUM_BODY_TAIL: [&str; 15] = [
|
||
- " ██████╗ ██████╗ ████████╗ ██████████╗",
|
||
- " ██████║ ██████║ ████████║ ██████████║",
|
||
- " ██╔═════██╗ ██╔═════██╗ ██╔═════██╗ ██╔═══════╝",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ╚═╝ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██████████╗",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██████████║",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██╔═══════╝",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ██╗ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██║ ██║",
|
||
- " ╚═██████╔═╝ ╚═██████╔═╝ ████████╔═╝ ██████████╗",
|
||
- " ██████║ ██████║ ████████║ ██████████║",
|
||
- " ╚═════╝ ╚═════╝ ╚═══════╝ ╚═════════╝",
|
||
-];
|
||
-
|
||
-fn medium_welcome_lines(version: &str) -> Vec<String> {
|
||
- let mut animated = vec![
|
||
- " ███████╗██╗ ██╗███████╗██████╗ ██╗ ██╗".to_string(),
|
||
- " ██╔════╝██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝".to_string(),
|
||
- " █████╗ ██║ ██║█████╗ ██████╔╝ ╚████╔╝".to_string(),
|
||
- " ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██╔══██╗ ╚██╔╝".to_string(),
|
||
- " ███████╗ ╚████╔╝ ███████╗██║ ██║ ██║".to_string(),
|
||
- ];
|
||
-
|
||
- let base_width = MEDIUM_VERSION_LINE.chars().count();
|
||
- let padding = MEDIUM_VERSION_COLUMN.saturating_sub(base_width);
|
||
- let _version_pad = " ".repeat(padding);
|
||
- if let Some(first_tail) = MEDIUM_BODY_TAIL.first() {
|
||
- let tail_len = first_tail.chars().count();
|
||
- let tail_pad = MEDIUM_VERSION_COLUMN.saturating_sub(tail_len);
|
||
- let tail_version_pad = " ".repeat(tail_pad);
|
||
- animated.push(format!("{MEDIUM_VERSION_LINE}{tail_version_pad}{version} "));
|
||
- animated.extend(MEDIUM_BODY_TAIL.iter().map(|line| (*line).to_string()));
|
||
- }
|
||
- shift_left(animated, 3)
|
||
-}
|
||
-
|
||
-const SMALL_VERSION_LINE: &str = " ╚═════╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ";
|
||
-
|
||
-fn small_welcome_lines(version: &str) -> Vec<String> {
|
||
- let mut lines = vec![
|
||
- " ██████╗██╗ ██╗██████╗██████╗ ██╗ ██╗".to_string(),
|
||
- " ██╔═══╝██║ ██║██╔═══╝██╔══██╗╚██╗ ██╔╝".to_string(),
|
||
- " █████╗ ██║ ██║█████╗ ██████╔╝ ╚████╔╝".to_string(),
|
||
- " ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██╔══██╗ ╚██╔╝".to_string(),
|
||
- " ██████╗ ╚████╔╝ ██████╗██║ ██║ ██║".to_string(),
|
||
- ];
|
||
-
|
||
- let base_width = SMALL_VERSION_LINE.chars().count();
|
||
- let padding = MEDIUM_VERSION_COLUMN.saturating_sub(base_width);
|
||
- let pad = " ".repeat(padding);
|
||
- lines.push(format!("{SMALL_VERSION_LINE}{pad}{version} "));
|
||
-
|
||
- let tail = [
|
||
- " ██████╗ ██████╗ ████████╗ ██████████╗",
|
||
- " ██████║ ██████║ ████████║ ██████████║",
|
||
- " ██╔═════██╗ ██╔═════██╗ ██╔═════██╗ ██╔═══════╝",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ╚═╝ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██████████╗",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██████████║",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██╔═══════╝",
|
||
- " ██║ ██╗ ██║ ██║ ██║ ██║ ██║",
|
||
- " ██║ ██║ ██║ ██║ ██║ ██║ ██║",
|
||
- " ╚═██████╔═╝ ╚═██████╔═╝ ████████╔═╝ ██████████╗",
|
||
- " ██████║ ██████║ ████████║ ██████████║",
|
||
- " ╚═════╝ ╚═════╝ ╚═══════╝ ╚═════════╝",
|
||
- ];
|
||
- lines.extend(tail.iter().map(|l| (*l).to_string()));
|
||
-
|
||
- shift_left(lines, 3)
|
||
-}
|
||
-
|
||
-fn tiny_welcome_lines(version: &str) -> Vec<String> {
|
||
- vec![
|
||
- format!("EVERY {version} "),
|
||
- " █████╗ █████╗ █████╗ ██████╗ ".to_string(),
|
||
- "██╔═══╝██╔══██╗██╔═██╗██╔═══╝ ".to_string(),
|
||
- "██║ ██║ ██║██║ ██║████╗ ".to_string(),
|
||
- "██║ ██║ ██║██║ ██║██╔═╝ ".to_string(),
|
||
- "╚█████╗╚█████╔╝█████╔╝██████╗ ".to_string(),
|
||
- " ╚════╝ ╚════╝ ╚════╝ ╚═════╝ ".to_string(),
|
||
- ]
|
||
-}
|
||
-
|
||
-fn shift_left(lines: Vec<String>, n: usize) -> Vec<String> {
|
||
- let mut out = Vec::with_capacity(lines.len());
|
||
- for line in lines {
|
||
- let mut drop = n;
|
||
- let mut new_line = String::with_capacity(line.len());
|
||
- for ch in line.chars() {
|
||
- if drop > 0 && ch == ' ' {
|
||
- drop -= 1;
|
||
- continue;
|
||
- }
|
||
- new_line.push(ch);
|
||
- }
|
||
- out.push(new_line);
|
||
- }
|
||
- out
|
||
-}
|
||
-
|
||
-/* ---------------- outline fill renderer ---------------- */
|
||
-
|
||
-fn lines_masks(
|
||
- lines: &[String],
|
||
- is_animated: impl Fn(char) -> bool,
|
||
-) -> (Vec<Vec<char>>, Vec<Vec<bool>>, Vec<Vec<bool>>, usize, usize) {
|
||
- let height = lines.len();
|
||
- let width = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
|
||
-
|
||
- let mut char_mask = vec![vec![' '; width]; height];
|
||
- let mut anim_mask = vec![vec![false; width]; height];
|
||
- let mut shadow_mask = vec![vec![false; width]; height];
|
||
-
|
||
- for (y, line) in lines.iter().enumerate() {
|
||
- for (x, ch) in line.chars().enumerate() {
|
||
- if x >= width {
|
||
- break;
|
||
- }
|
||
- char_mask[y][x] = ch;
|
||
- if is_animated(ch) {
|
||
- anim_mask[y][x] = true;
|
||
- } else if ch != ' ' {
|
||
- shadow_mask[y][x] = true;
|
||
- }
|
||
- }
|
||
- }
|
||
-
|
||
- (char_mask, anim_mask, shadow_mask, width, height)
|
||
-}
|
||
-
|
||
-fn render_static_lines(
|
||
- lines: &[String],
|
||
- shadow_mask: &[Vec<bool>],
|
||
- area: Rect,
|
||
- buf: &mut Buffer,
|
||
- alpha: f32,
|
||
- _frame: u32,
|
||
- reveal_x_shadow: isize,
|
||
-) {
|
||
- let static_target = Color::Rgb(230, 232, 235); // matches CODE/EVERY final color (#e6e8eb)
|
||
- let static_color_base = blend_to_background(static_target, alpha);
|
||
- for (row_idx, line) in lines.iter().enumerate() {
|
||
- let y = area.y + row_idx as u16;
|
||
- if y >= area.y + area.height {
|
||
- break;
|
||
- }
|
||
- for (col_idx, ch) in line.chars().enumerate() {
|
||
- let x = area.x + col_idx as u16;
|
||
- if x >= area.x + area.width {
|
||
- break;
|
||
- }
|
||
- if ch == ' ' || ANIMATED_CHARS.contains(&ch) {
|
||
- continue;
|
||
- }
|
||
- if !shadow_mask[row_idx][col_idx] {
|
||
- continue;
|
||
- }
|
||
- let xi = col_idx as isize;
|
||
- if xi > reveal_x_shadow {
|
||
- continue;
|
||
- }
|
||
- let mut utf8 = [0u8; 4];
|
||
- let sym = ch.encode_utf8(&mut utf8);
|
||
- let cell = &mut buf[(x, y)];
|
||
- cell.set_symbol(sym);
|
||
- cell.set_fg(static_color_base);
|
||
- cell.set_style(Style::default().add_modifier(Modifier::BOLD));
|
||
- }
|
||
- }
|
||
-}
|
||
-fn render_overlay_lines(
|
||
- chars: &[Vec<char>],
|
||
- mask: &[Vec<bool>],
|
||
- border: &[Vec<bool>],
|
||
- reveal_x_outline: isize,
|
||
- reveal_x_fill: isize,
|
||
- shine_x: isize,
|
||
- shine_band: isize,
|
||
- fade: f32,
|
||
- frame: u32,
|
||
- area: Rect,
|
||
- buf: &mut Buffer,
|
||
-) {
|
||
- let h = mask.len();
|
||
- let w = mask[0].len();
|
||
-
|
||
- for y in 0..h {
|
||
- for x in 0..w {
|
||
- let xi = x as isize;
|
||
- let base_char = chars[y][x];
|
||
-
|
||
- let mut draw = false;
|
||
- let mut color = Color::Reset;
|
||
-
|
||
- if mask[y][x] && xi <= reveal_x_fill {
|
||
- let base = gradient_multi(x as f32 / (w.max(1) as f32));
|
||
- let dx = (xi - shine_x).abs();
|
||
- let shine =
|
||
- (1.0 - (dx as f32 / (shine_band as f32 + 0.001)).clamp(0.0, 1.0)).powf(1.6);
|
||
- let bright = bump_rgb(base, shine * 0.30);
|
||
- color = mix_rgb(bright, Color::Rgb(230, 232, 235), fade);
|
||
- draw = true;
|
||
- } else if border[y][x] && xi <= reveal_x_outline.max(reveal_x_fill) {
|
||
- let base = gradient_multi(x as f32 / (w.max(1) as f32));
|
||
- let period = 8usize;
|
||
- let on = ((x + y + (frame as usize)) % period) < (period / 2);
|
||
- let c = if on { bump_rgb(base, 0.22) } else { base };
|
||
- color = mix_rgb(c, Color::Rgb(235, 237, 240), fade * 0.8);
|
||
- draw = true;
|
||
- }
|
||
-
|
||
- if draw {
|
||
- let target_x = area.x + x as u16;
|
||
- let target_y = area.y + y as u16;
|
||
- if target_x < area.x + area.width && target_y < area.y + area.height {
|
||
- let cell = &mut buf[(target_x, target_y)];
|
||
- let mut utf8 = [0u8; 4];
|
||
- let sym = base_char.encode_utf8(&mut utf8);
|
||
- cell.set_symbol(sym);
|
||
- cell.set_fg(color);
|
||
- cell.set_bg(crate::colors::background());
|
||
- cell.set_style(Style::default().add_modifier(Modifier::BOLD));
|
||
- }
|
||
- }
|
||
- }
|
||
- }
|
||
-}
|
||
-
|
||
-fn render_overlay_lines_with_alpha(
|
||
- chars: &[Vec<char>],
|
||
- mask: &[Vec<bool>],
|
||
- border: &[Vec<bool>],
|
||
- reveal_x_outline: isize,
|
||
- reveal_x_fill: isize,
|
||
- shine_x: isize,
|
||
- shine_band: isize,
|
||
- fade: f32,
|
||
- frame: u32,
|
||
- alpha: f32,
|
||
- area: Rect,
|
||
- buf: &mut Buffer,
|
||
-) {
|
||
- let h = mask.len();
|
||
- let w = mask[0].len();
|
||
-
|
||
- for y in 0..h {
|
||
- for x in 0..w {
|
||
- let xi = x as isize;
|
||
- let base_char = chars[y][x];
|
||
-
|
||
- let mut draw = false;
|
||
- let mut color = Color::Reset;
|
||
-
|
||
- if mask[y][x] && xi <= reveal_x_fill {
|
||
- let base = gradient_multi(x as f32 / (w.max(1) as f32));
|
||
- let dx = (xi - shine_x).abs();
|
||
- let shine =
|
||
- (1.0 - (dx as f32 / (shine_band as f32 + 0.001)).clamp(0.0, 1.0)).powf(1.6);
|
||
- let bright = bump_rgb(base, shine * 0.30);
|
||
- color = blend_to_background(mix_rgb(bright, Color::Rgb(230, 232, 235), fade), alpha);
|
||
- draw = true;
|
||
- } else if border[y][x] && xi <= reveal_x_outline.max(reveal_x_fill) {
|
||
- let base = gradient_multi(x as f32 / (w.max(1) as f32));
|
||
- let period = 8usize;
|
||
- let on = ((x + y + (frame as usize)) % period) < (period / 2);
|
||
- let c = if on { bump_rgb(base, 0.22) } else { base };
|
||
- color = blend_to_background(mix_rgb(c, Color::Rgb(235, 237, 240), fade * 0.8), alpha);
|
||
- draw = true;
|
||
- }
|
||
-
|
||
- if draw {
|
||
- let target_x = area.x + x as u16;
|
||
- let target_y = area.y + y as u16;
|
||
- if target_x < area.x + area.width && target_y < area.y + area.height {
|
||
- let cell = &mut buf[(target_x, target_y)];
|
||
- let mut utf8 = [0u8; 4];
|
||
- let sym = base_char.encode_utf8(&mut utf8);
|
||
- cell.set_symbol(sym);
|
||
- cell.set_fg(color);
|
||
- cell.set_bg(crate::colors::background());
|
||
- cell.set_style(Style::default().add_modifier(Modifier::BOLD));
|
||
- }
|
||
- }
|
||
- }
|
||
- }
|
||
-}
|
||
-
|
||
-// Helper function to blend colors towards background
|
||
-pub(crate) fn blend_to_background(color: Color, alpha: f32) -> Color {
|
||
- if alpha >= 1.0 {
|
||
- return color;
|
||
- }
|
||
- if alpha <= 0.0 {
|
||
- return crate::colors::background();
|
||
- }
|
||
-
|
||
- let bg = crate::colors::background();
|
||
-
|
||
- match (color, bg) {
|
||
- (Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
|
||
- let r = (r1 as f32 * alpha + r2 as f32 * (1.0 - alpha)) as u8;
|
||
- let g = (g1 as f32 * alpha + g2 as f32 * (1.0 - alpha)) as u8;
|
||
- let b = (b1 as f32 * alpha + b2 as f32 * (1.0 - alpha)) as u8;
|
||
- Color::Rgb(r, g, b)
|
||
- }
|
||
- _ => {
|
||
- if alpha > 0.5 { color } else { bg }
|
||
- }
|
||
- }
|
||
-}
|
||
-
|
||
-/* ---------------- border computation ---------------- */
|
||
-
|
||
-fn compute_border(mask: &[Vec<bool>]) -> Vec<Vec<bool>> {
|
||
- let h = mask.len();
|
||
- let w = mask[0].len();
|
||
- let mut out = vec![vec![false; w]; h];
|
||
- for y in 0..h {
|
||
- for x in 0..w {
|
||
- if !mask[y][x] {
|
||
- continue;
|
||
- }
|
||
- let up = y == 0 || !mask[y - 1][x];
|
||
- let down = y + 1 >= h || !mask[y + 1][x];
|
||
- let left = x == 0 || !mask[y][x - 1];
|
||
- let right = x + 1 >= w || !mask[y][x + 1];
|
||
- if up || down || left || right {
|
||
- out[y][x] = true;
|
||
- }
|
||
- }
|
||
- }
|
||
- out
|
||
-}
|
||
-
|
||
-/* ================= helpers ================= */
|
||
-
|
||
-fn smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||
- let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0);
|
||
- t * t * (3.0 - 2.0 * t)
|
||
-}
|
||
|
||
fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
|
||
(a as f32 + (b as f32 - a as f32) * t).round() as u8
|
||
@@ -598,174 +36,3 @@ pub(crate) fn gradient_multi(t: f32) -> Color {
|
||
}
|
||
}
|
||
|
||
-fn bump_rgb(c: Color, amt: f32) -> Color {
|
||
- match c {
|
||
- Color::Rgb(r, g, b) => {
|
||
- let add = |x: u8| ((x as f32 + 255.0 * amt).min(255.0)) as u8;
|
||
- Color::Rgb(add(r), add(g), add(b))
|
||
- }
|
||
- _ => c,
|
||
- }
|
||
-}
|
||
-
|
||
-#[cfg(test)]
|
||
-mod tests {
|
||
- use super::*;
|
||
- use indoc::indoc;
|
||
- use ratatui::buffer::Buffer;
|
||
- use ratatui::prelude::Rect;
|
||
-
|
||
- #[test]
|
||
- fn renders_large_art_pixel_perfect() {
|
||
- let version = format!("v{}", code_version::version());
|
||
- let expected = expected_large(&version);
|
||
- let width = expected.iter().map(|l| l.chars().count()).max().unwrap() as u16;
|
||
- let height = expected.len() as u16;
|
||
- let rect = Rect::new(0, 0, width, height);
|
||
- let mut buf = Buffer::empty(rect);
|
||
-
|
||
- render_intro_animation_with_size_and_alpha_offset(
|
||
- rect,
|
||
- &mut buf,
|
||
- 1.0,
|
||
- 1.0,
|
||
- IntroArtSize::Large,
|
||
- &version,
|
||
- 0,
|
||
- );
|
||
-
|
||
- let rendered = buffer_to_strings(&buf, rect);
|
||
- assert_eq!(trim_lines(rendered), trim_lines(expected));
|
||
- }
|
||
-
|
||
- #[test]
|
||
- fn renders_medium_art_pixel_perfect() {
|
||
- let version = format!("v{}", code_version::version());
|
||
- let expected = expected_medium(&version);
|
||
- let width = expected.iter().map(|l| l.chars().count()).max().unwrap() as u16;
|
||
- let height = expected.len() as u16;
|
||
- let rect = Rect::new(0, 0, width, height);
|
||
- let mut buf = Buffer::empty(rect);
|
||
-
|
||
- render_intro_animation_with_size_and_alpha_offset(
|
||
- rect,
|
||
- &mut buf,
|
||
- 1.0,
|
||
- 1.0,
|
||
- IntroArtSize::Medium,
|
||
- &version,
|
||
- 0,
|
||
- );
|
||
-
|
||
- let rendered = buffer_to_strings(&buf, rect);
|
||
- assert_eq!(trim_lines(rendered), trim_lines(expected));
|
||
- }
|
||
-
|
||
- #[test]
|
||
- fn renders_small_art_pixel_perfect() {
|
||
- let version = format!("v{}", code_version::version());
|
||
- let expected = vec!["██████╗██╗ ██╗█".to_string()];
|
||
- let width = expected[0].chars().count() as u16;
|
||
- let rect = Rect::new(0, 0, width, 1);
|
||
- let mut buf = Buffer::empty(rect);
|
||
-
|
||
- render_intro_animation_with_size_and_alpha_offset(
|
||
- rect,
|
||
- &mut buf,
|
||
- 1.0,
|
||
- 1.0,
|
||
- IntroArtSize::Small,
|
||
- &version,
|
||
- 0,
|
||
- );
|
||
-
|
||
- let rendered = buffer_to_strings(&buf, rect);
|
||
- assert_eq!(trim_lines(rendered), trim_lines(expected));
|
||
- }
|
||
-
|
||
- fn buffer_to_strings(buf: &Buffer, area: Rect) -> Vec<String> {
|
||
- let mut lines = Vec::new();
|
||
- for y in area.y..area.y + area.height {
|
||
- let mut line = String::with_capacity(area.width as usize);
|
||
- for x in area.x..area.x + area.width {
|
||
- let symbol = buf[(x, y)].symbol();
|
||
- let ch = symbol.chars().next().unwrap_or(' ');
|
||
- line.push(ch);
|
||
- }
|
||
- lines.push(line);
|
||
- }
|
||
- lines
|
||
- }
|
||
-
|
||
- fn trim_lines(lines: Vec<String>) -> Vec<String> {
|
||
- lines
|
||
- .into_iter()
|
||
- .map(|line| line.trim_end().to_string())
|
||
- .collect()
|
||
- }
|
||
-
|
||
- fn expected_large(version: &str) -> Vec<String> {
|
||
- let art = indoc! {"
|
||
- ███████╗██╗ ██╗███████╗██████╗ ██╗ ██╗
|
||
- ██╔════╝██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝
|
||
- █████╗ ██║ ██║█████╗ ██████╔╝ ╚████╔╝
|
||
- ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██╔══██╗ ╚██╔╝
|
||
- ███████╗ ╚████╔╝ ███████╗██║ ██║ ██║ {VERSION}
|
||
- ╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝
|
||
- █████████╗ █████████╗ ████████████╗ ███████████████╗
|
||
- █████████║ █████████║ ████████████║ ███████████████║
|
||
- █████████║ █████████║ ████████████║ ███████████████║
|
||
- ███╔════════███╗ ███╔════════███╗ ███╔════════███╗ ███╔═══════════╝
|
||
- ███║ ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ╚══╝ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███║ ███║ ███║ ███║ ███████████████╗
|
||
- ███║ ███║ ███║ ███║ ███║ ███████████████║
|
||
- ███║ ███║ ███║ ███║ ███║ ███████████████║
|
||
- ███║ ███║ ███║ ███║ ███║ ███╔═══════════╝
|
||
- ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███╗ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ███║ ███║ ███║ ███║ ███║ ███║ ███║
|
||
- ╚══█████████╔══╝ ╚══█████████╔══╝ ████████████╔══╝ ███████████████╗
|
||
- █████████║ █████████║ ████████████║ ███████████████║
|
||
- █████████║ █████████║ ████████████║ ███████████████║
|
||
- ╚════════╝ ╚════════╝ ╚══════════╝ ╚═════════════╝
|
||
- "}
|
||
- .replace("{VERSION}", version);
|
||
-
|
||
- art.lines().map(|line| line.to_string()).collect()
|
||
- }
|
||
-
|
||
- fn expected_medium(version: &str) -> Vec<String> {
|
||
- let art = indoc! {"
|
||
- ███████╗██╗ ██╗███████╗██████╗ ██╗ ██╗
|
||
- ██╔════╝██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝
|
||
- █████╗ ██║ ██║█████╗ ██████╔╝ ╚████╔╝
|
||
- ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██╔══██╗ ╚██╔╝
|
||
- ███████╗ ╚████╔╝ ███████╗██║ ██║ ██║
|
||
- ╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ {VERSION}
|
||
- ██████╗ ██████╗ ████████╗ ██████████╗
|
||
- ██████║ ██████║ ████████║ ██████████║
|
||
- ██╔═════██╗ ██╔═════██╗ ██╔═════██╗ ██╔═══════╝
|
||
- ██║ ██║ ██║ ██║ ██║ ██║ ██║
|
||
- ██║ ╚═╝ ██║ ██║ ██║ ██║ ██║
|
||
- ██║ ██║ ██║ ██║ ██║ ██║
|
||
- ██║ ██║ ██║ ██║ ██║ ██████████╗
|
||
- ██║ ██║ ██║ ██║ ██║ ██████████║
|
||
- ██║ ██║ ██║ ██║ ██║ ██╔═══════╝
|
||
- ██║ ██║ ██║ ██║ ██║ ██║
|
||
- ██║ ██╗ ██║ ██║ ██║ ██║ ██║
|
||
- ██║ ██║ ██║ ██║ ██║ ██║ ██║
|
||
- ╚═██████╔═╝ ╚═██████╔═╝ ████████╔═╝ ██████████╗
|
||
- ██████║ ██████║ ████████║ ██████████║
|
||
- ╚═════╝ ╚═════╝ ╚═══════╝ ╚═════════╝
|
||
- "}
|
||
- .replace("{VERSION}", version);
|
||
-
|
||
- art.lines().map(|line| line.to_string()).collect()
|
||
- }
|
||
-}
|
||
diff --git a/tui/src/greeting.rs b/tui/src/greeting.rs
|
||
index a49a142..1ff671c 100644
|
||
--- a/tui/src/greeting.rs
|
||
+++ b/tui/src/greeting.rs
|
||
@@ -1,31 +1,4 @@
|
||
-use chrono::Local;
|
||
-use chrono::Timelike;
|
||
-
|
||
-fn current_hour() -> u32 {
|
||
- if let Ok(fake) = std::env::var("CODEX_TUI_FAKE_HOUR") {
|
||
- if let Ok(parsed) = fake.parse::<u32>() {
|
||
- return parsed.min(23);
|
||
- }
|
||
- }
|
||
- Local::now().hour()
|
||
-}
|
||
-
|
||
-/// Build a time-aware placeholder like
|
||
-/// "What can I code for you this morning?".
|
||
+/// Primary empty-composer placeholder.
|
||
pub(crate) fn greeting_placeholder() -> String {
|
||
- let hour = current_hour();
|
||
- // Custom mapping: show "today" for 10:00–13:59 local time.
|
||
- let when = if (10..=13).contains(&hour) {
|
||
- "today"
|
||
- } else if (5..=9).contains(&hour) {
|
||
- "this morning"
|
||
- } else if (14..=16).contains(&hour) {
|
||
- "this afternoon"
|
||
- } else if (17..=20).contains(&hour) {
|
||
- "this evening"
|
||
- } else {
|
||
- // Late night and very early hours
|
||
- "tonight"
|
||
- };
|
||
- format!("What can I code for you {when}?")
|
||
+ "What would you like to work on?".to_string()
|
||
}
|
||
diff --git a/tui/src/header_wave.rs b/tui/src/header_wave.rs
|
||
deleted file mode 100644
|
||
index e35e89a..0000000
|
||
--- a/tui/src/header_wave.rs
|
||
+++ /dev/null
|
||
@@ -1,115 +0,0 @@
|
||
-use std::cell::Cell;
|
||
-use std::f32::consts::TAU;
|
||
-use std::time::{Duration, Instant};
|
||
-
|
||
-use ratatui::buffer::Buffer;
|
||
-use ratatui::layout::{Margin, Rect};
|
||
-use ratatui::style::Color;
|
||
-
|
||
-pub struct HeaderWaveEffect {
|
||
- enabled: Cell<bool>,
|
||
- started_at: Cell<Option<Instant>>,
|
||
- next_frame: Cell<Option<Instant>>,
|
||
-}
|
||
-
|
||
-impl HeaderWaveEffect {
|
||
- /// Animation cadence; keep this in sync with the ScheduleFrameIn duration.
|
||
- pub const FRAME_INTERVAL: Duration = Duration::from_millis(120);
|
||
-
|
||
- pub fn new() -> Self {
|
||
- Self {
|
||
- enabled: Cell::new(false),
|
||
- started_at: Cell::new(None),
|
||
- next_frame: Cell::new(None),
|
||
- }
|
||
- }
|
||
-
|
||
- pub fn set_enabled(&self, enabled: bool, now: Instant) {
|
||
- self.enabled.set(enabled);
|
||
- if enabled {
|
||
- self.started_at.set(Some(now));
|
||
- self.next_frame.set(None);
|
||
- } else {
|
||
- self.started_at.set(None);
|
||
- self.next_frame.set(None);
|
||
- }
|
||
- }
|
||
-
|
||
- pub fn is_enabled(&self) -> bool {
|
||
- self.enabled.get()
|
||
- }
|
||
-
|
||
- /// Returns true if the caller should schedule another frame tick.
|
||
- pub fn schedule_if_needed(&self, now: Instant) -> bool {
|
||
- if !self.enabled.get() {
|
||
- return false;
|
||
- }
|
||
- match self.next_frame.get() {
|
||
- Some(due) if due > now => false,
|
||
- _ => {
|
||
- self.next_frame.set(Some(now + Self::FRAME_INTERVAL));
|
||
- true
|
||
- }
|
||
- }
|
||
- }
|
||
-
|
||
- pub fn render(&self, area: Rect, buf: &mut Buffer, now: Instant) {
|
||
- if !self.enabled.get() {
|
||
- return;
|
||
- }
|
||
- let start = self.started_at.get().unwrap_or_else(|| {
|
||
- self.started_at.set(Some(now));
|
||
- now
|
||
- });
|
||
- let elapsed = now.saturating_duration_since(start).as_secs_f32();
|
||
-
|
||
- if area.width < 2 || area.height < 2 {
|
||
- return;
|
||
- }
|
||
-
|
||
- let inner = area.inner(Margin::new(1, 1));
|
||
- if inner.width < 3 || inner.height == 0 {
|
||
- return;
|
||
- }
|
||
- let stripe_area = inner.inner(Margin::new(1, 0));
|
||
- if stripe_area.width == 0 || stripe_area.height == 0 {
|
||
- return;
|
||
- }
|
||
-
|
||
- let travel = wrap_unit(elapsed * 0.65);
|
||
- let width = stripe_area.width as usize;
|
||
- let base_bg = crate::colors::background();
|
||
-
|
||
- for x_idx in 0..width {
|
||
- let x = stripe_area.x + x_idx as u16;
|
||
- let rel = x_idx as f32 / width.max(1) as f32;
|
||
- let hue = wrap_unit(travel + (rel - 0.5).abs() * 0.45 + rel * 0.3);
|
||
- let tint = crate::colors::mix_toward(base_bg, spectral_color(hue), 0.22);
|
||
- for y in stripe_area.y..stripe_area.y + stripe_area.height {
|
||
- let cell = &mut buf[(x, y)];
|
||
- cell.set_symbol(" ");
|
||
- cell.set_bg(tint);
|
||
- }
|
||
- }
|
||
- }
|
||
-}
|
||
-
|
||
-fn wrap_unit(value: f32) -> f32 {
|
||
- let mut v = value % 1.0;
|
||
- if v < 0.0 {
|
||
- v += 1.0;
|
||
- }
|
||
- v
|
||
-}
|
||
-
|
||
-fn spectral_color(t: f32) -> Color {
|
||
- let angle = wrap_unit(t) * TAU;
|
||
- let r = (angle.sin() * 0.5 + 0.5).powf(0.55);
|
||
- let g = ((angle + TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
|
||
- let b = ((angle + 2.0 * TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
|
||
- Color::Rgb(
|
||
- (r * 255.0).clamp(0.0, 255.0) as u8,
|
||
- (g * 255.0).clamp(0.0, 255.0) as u8,
|
||
- (b * 255.0).clamp(0.0, 255.0) as u8,
|
||
- )
|
||
-}
|
||
diff --git a/tui/src/height_manager.rs b/tui/src/height_manager.rs
|
||
index 25252bd..7d47589 100644
|
||
--- a/tui/src/height_manager.rs
|
||
+++ b/tui/src/height_manager.rs
|
||
@@ -114,8 +114,6 @@ impl HeightManager {
|
||
// Optional target height for HUD computed by caller (e.g., stacked/collapsed layout).
|
||
// When None, a default aspect-based calculation is used.
|
||
hud_target_override: Option<u16>,
|
||
- // When false, do not reserve rows for the status bar.
|
||
- status_enabled: bool,
|
||
) -> Vec<Rect> {
|
||
#[cfg(debug_assertions)]
|
||
{
|
||
@@ -128,9 +126,6 @@ impl HeightManager {
|
||
self.last_area = Some(area);
|
||
}
|
||
|
||
- // Status bar height is fixed at 3 when enabled.
|
||
- let status_h = if status_enabled { 3u16 } else { 0u16 };
|
||
-
|
||
// Cap the bottom pane to a percentage of screen height, with a minimum of 5 rows.
|
||
let percent_cap: u16 = ((area.height as u32).saturating_mul(self.cfg.bottom_percent_cap as u32) / 100) as u16;
|
||
let bottom_cap = percent_cap.max(5);
|
||
@@ -188,10 +183,9 @@ impl HeightManager {
|
||
((number / denom) as u16).saturating_add(1) // include borders budget
|
||
};
|
||
|
||
- // Keep within budget: reserve space for status + bottom + >=1 row history.
|
||
+ // Keep within budget: reserve space for bottom + >=1 row history.
|
||
let vertical_budget = area
|
||
.height
|
||
- .saturating_sub(status_h)
|
||
.saturating_sub(bottom_h)
|
||
.saturating_sub(1);
|
||
let min_height = if override_target { 3 } else { 4 };
|
||
@@ -221,7 +215,7 @@ impl HeightManager {
|
||
|
||
// Ensure the history area has at least one row; reduce HUD first if needed.
|
||
let min_history = 1u16;
|
||
- let total_non_history = status_h + bottom_h + hud_h;
|
||
+ let total_non_history = bottom_h + hud_h;
|
||
if total_non_history.saturating_add(min_history) > area.height {
|
||
let overflow = total_non_history.saturating_add(min_history) - area.height;
|
||
if hud_h > 0 {
|
||
@@ -233,21 +227,16 @@ impl HeightManager {
|
||
// Build rects in the same order used by ChatWidget::layout_areas.
|
||
if hud_h > 0 {
|
||
Layout::vertical([
|
||
- Constraint::Length(status_h),
|
||
Constraint::Length(hud_h),
|
||
Constraint::Fill(1),
|
||
Constraint::Length(bottom_h),
|
||
])
|
||
- .areas::<4>(area)
|
||
- .to_vec()
|
||
- } else {
|
||
- Layout::vertical([
|
||
- Constraint::Length(status_h),
|
||
- Constraint::Fill(1),
|
||
- Constraint::Length(bottom_h),
|
||
- ])
|
||
.areas::<3>(area)
|
||
.to_vec()
|
||
+ } else {
|
||
+ Layout::vertical([Constraint::Fill(1), Constraint::Length(bottom_h)])
|
||
+ .areas::<2>(area)
|
||
+ .to_vec()
|
||
}
|
||
}
|
||
|
||
diff --git a/tui/src/history_cell/agent.rs b/tui/src/history_cell/agent.rs
|
||
index 0ac63ea..5b17c97 100644
|
||
--- a/tui/src/history_cell/agent.rs
|
||
+++ b/tui/src/history_cell/agent.rs
|
||
@@ -1299,11 +1299,6 @@ impl HistoryCell for AgentRunCell {
|
||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||
self
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- None
|
||
- }
|
||
-
|
||
fn kind(&self) -> HistoryCellType {
|
||
let status = if self.completed {
|
||
if self.status_label == "Failed" {
|
||
diff --git a/tui/src/history_cell/animated.rs b/tui/src/history_cell/animated.rs
|
||
index 0b2a58e..131c0bb 100644
|
||
--- a/tui/src/history_cell/animated.rs
|
||
+++ b/tui/src/history_cell/animated.rs
|
||
@@ -1,16 +1,16 @@
|
||
use super::*;
|
||
+use ratatui::style::Stylize;
|
||
use std::cell::{Cell, RefCell};
|
||
use std::time::{Duration, Instant};
|
||
|
||
+/// The first thing on screen: the product name, its version, and the question.
|
||
+/// Plain text, laid down once; it fades when the first turn begins.
|
||
pub(crate) struct AnimatedWelcomeCell {
|
||
start_time: Instant,
|
||
completed: Cell<bool>,
|
||
fade_start: RefCell<Option<Instant>>,
|
||
faded_out: Cell<bool>,
|
||
- available_height: Cell<Option<u16>>,
|
||
- variant: Cell<Option<crate::glitch_animation::IntroArtSize>>,
|
||
version_label: String,
|
||
- hidden: Cell<bool>,
|
||
}
|
||
|
||
impl AnimatedWelcomeCell {
|
||
@@ -20,17 +20,7 @@ impl AnimatedWelcomeCell {
|
||
completed: Cell::new(false),
|
||
fade_start: RefCell::new(None),
|
||
faded_out: Cell::new(false),
|
||
- available_height: Cell::new(None),
|
||
- variant: Cell::new(None),
|
||
version_label: format!("v{}", code_version::version()),
|
||
- hidden: Cell::new(false),
|
||
- }
|
||
- }
|
||
-
|
||
- pub(crate) fn set_available_height(&self, height: u16) {
|
||
- let prev = self.available_height.get();
|
||
- if prev.map_or(true, |current| height > current) {
|
||
- self.available_height.set(Some(height));
|
||
}
|
||
}
|
||
|
||
@@ -38,21 +28,16 @@ impl AnimatedWelcomeCell {
|
||
*self.fade_start.borrow()
|
||
}
|
||
|
||
- fn set_fade_start(&self) {
|
||
+ pub(crate) fn begin_fade(&self) {
|
||
let mut slot = self.fade_start.borrow_mut();
|
||
if slot.is_none() {
|
||
*slot = Some(Instant::now());
|
||
}
|
||
}
|
||
|
||
- pub(crate) fn begin_fade(&self) {
|
||
- self.set_fade_start();
|
||
- }
|
||
-
|
||
pub(crate) fn should_remove(&self) -> bool {
|
||
self.faded_out.get()
|
||
}
|
||
-
|
||
}
|
||
|
||
impl HistoryCell for AnimatedWelcomeCell {
|
||
@@ -69,139 +54,25 @@ impl HistoryCell for AnimatedWelcomeCell {
|
||
}
|
||
|
||
fn display_lines(&self) -> Vec<Line<'static>> {
|
||
+ let title = Line::from(vec![
|
||
+ Span::from(">_ ").dim(),
|
||
+ Span::from("hanzo dev").bold(),
|
||
+ Span::from(format!(" ({})", self.version_label)).dim(),
|
||
+ ]);
|
||
vec![
|
||
+ title,
|
||
Line::from(""),
|
||
- Line::from("Welcome to Code"),
|
||
Line::from(crate::greeting::greeting_placeholder()),
|
||
- Line::from(""),
|
||
]
|
||
}
|
||
|
||
- fn desired_height(&self, width: u16) -> u16 {
|
||
- let variant_for_width = crate::glitch_animation::intro_art_size_for_width(width);
|
||
- let h = crate::glitch_animation::intro_art_height(variant_for_width);
|
||
- h.saturating_add(3)
|
||
- }
|
||
-
|
||
- fn has_custom_render(&self) -> bool {
|
||
- true
|
||
- }
|
||
-
|
||
- fn custom_render(&self, area: Rect, buf: &mut Buffer) {
|
||
- self.custom_render_with_skip(area, buf, 0);
|
||
- }
|
||
-
|
||
- fn custom_render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_rows: u16) {
|
||
- if self.hidden.get() {
|
||
- return;
|
||
- }
|
||
-
|
||
- // Clear the full allocated area first so repositioning the art (e.g.,
|
||
- // bottom-aligning when there's vertical slack) doesn't leave stale
|
||
- // pixels behind in the padding rows.
|
||
- let bg = crate::colors::background();
|
||
- for y in area.y..area.y.saturating_add(area.height) {
|
||
- for x in area.x..area.x.saturating_add(area.width) {
|
||
- let cell = &mut buf[(x, y)];
|
||
- cell.set_bg(bg);
|
||
- cell.set_symbol(" ");
|
||
- }
|
||
- }
|
||
-
|
||
- let height_hint = self.available_height.get().unwrap_or(area.height);
|
||
- let current_variant = crate::glitch_animation::intro_art_size_for_area(
|
||
- area.width,
|
||
- height_hint.saturating_sub(3),
|
||
- );
|
||
- let previous_variant = self.variant.get();
|
||
- let variant_changed = previous_variant.map_or(false, |v| v != current_variant);
|
||
-
|
||
- if variant_changed {
|
||
- self.variant.set(Some(current_variant));
|
||
- // Keep `completed` as-is so the intro animation continues when the
|
||
- // size adjusts mid-run instead of jumping to the final frame.
|
||
- } else if previous_variant.is_none() {
|
||
- // First render: set the variant without suppressing the intro animation.
|
||
- self.variant.set(Some(current_variant));
|
||
- }
|
||
-
|
||
- let variant_for_render = current_variant;
|
||
-
|
||
- let art_height = crate::glitch_animation::intro_art_height(current_variant);
|
||
- let full_height = art_height.saturating_add(3);
|
||
- // Prefer the logo low in the cell so spare lines sit above it. Keep a
|
||
- // small bottom gap (up to 2 rows) when there's room; otherwise center.
|
||
- let slack = full_height.saturating_sub(art_height);
|
||
- let bottom_pad = slack.min(2);
|
||
- let top_pad = slack.saturating_sub(bottom_pad);
|
||
- let art_top = top_pad;
|
||
- let art_bottom = art_top.saturating_add(art_height);
|
||
- let vis_top = skip_rows;
|
||
- let vis_bottom = skip_rows.saturating_add(area.height);
|
||
- let intersection_top = art_top.max(vis_top);
|
||
- let intersection_bottom = art_bottom.min(vis_bottom);
|
||
- if intersection_bottom <= intersection_top {
|
||
- return;
|
||
- }
|
||
- let row_offset = intersection_top.saturating_sub(art_top);
|
||
- let y_offset = intersection_top.saturating_sub(vis_top);
|
||
- let visible_height = intersection_bottom.saturating_sub(intersection_top);
|
||
- let positioned_area = Rect {
|
||
- x: area.x,
|
||
- y: area.y.saturating_add(y_offset),
|
||
- width: area.width,
|
||
- height: visible_height,
|
||
- };
|
||
-
|
||
- let fade_duration = Duration::from_millis(800);
|
||
-
|
||
- if let Some(fade_time) = self.fade_start() {
|
||
- let fade_elapsed = fade_time.elapsed();
|
||
- if fade_elapsed < fade_duration && !self.faded_out.get() {
|
||
- let fade_progress = fade_elapsed.as_secs_f32() / fade_duration.as_secs_f32();
|
||
- let alpha = 1.0 - fade_progress;
|
||
- crate::glitch_animation::render_intro_animation_with_size_and_alpha_offset(
|
||
- positioned_area,
|
||
- buf,
|
||
- 1.0,
|
||
- alpha,
|
||
- current_variant,
|
||
- &self.version_label,
|
||
- row_offset,
|
||
- );
|
||
- } else {
|
||
- self.faded_out.set(true);
|
||
- }
|
||
- return;
|
||
- }
|
||
-
|
||
- let animation_duration = Duration::from_secs(2);
|
||
-
|
||
- let elapsed = self.start_time.elapsed();
|
||
- let progress = if variant_changed {
|
||
- 1.0
|
||
- } else if elapsed < animation_duration && !self.completed.get() {
|
||
- elapsed.as_secs_f32() / animation_duration.as_secs_f32()
|
||
- } else {
|
||
- self.completed.set(true);
|
||
- 1.0
|
||
- };
|
||
-
|
||
- crate::glitch_animation::render_intro_animation_with_size_and_alpha_offset(
|
||
- positioned_area,
|
||
- buf,
|
||
- progress,
|
||
- 1.0,
|
||
- variant_for_render,
|
||
- &self.version_label,
|
||
- row_offset,
|
||
- );
|
||
+ fn desired_height(&self, _width: u16) -> u16 {
|
||
+ 3
|
||
}
|
||
|
||
fn is_animating(&self) -> bool {
|
||
- let animation_duration = Duration::from_secs(2);
|
||
if !self.completed.get() {
|
||
- if self.start_time.elapsed() < animation_duration {
|
||
+ if self.start_time.elapsed() < Duration::from_secs(1) {
|
||
return true;
|
||
}
|
||
self.completed.set(true);
|
||
@@ -209,7 +80,7 @@ impl HistoryCell for AnimatedWelcomeCell {
|
||
|
||
if let Some(fade_time) = self.fade_start() {
|
||
if !self.faded_out.get() {
|
||
- if fade_time.elapsed() < Duration::from_millis(800) {
|
||
+ if fade_time.elapsed() < Duration::from_millis(500) {
|
||
return true;
|
||
}
|
||
self.faded_out.set(true);
|
||
diff --git a/tui/src/history_cell/assistant.rs b/tui/src/history_cell/assistant.rs
|
||
index fe663f2..9e0eb3b 100644
|
||
--- a/tui/src/history_cell/assistant.rs
|
||
+++ b/tui/src/history_cell/assistant.rs
|
||
@@ -269,15 +269,6 @@ impl HistoryCell for AssistantMarkdownCell {
|
||
fn kind(&self) -> HistoryCellType {
|
||
HistoryCellType::Assistant
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- if self.state.mid_turn {
|
||
- None
|
||
- } else {
|
||
- super::gutter_symbol_for_kind(self.kind())
|
||
- }
|
||
- }
|
||
-
|
||
fn display_lines(&self) -> Vec<Line<'static>> {
|
||
assistant_markdown_lines_with_context(&self.state, self.file_opener, &self.cwd)
|
||
}
|
||
diff --git a/tui/src/history_cell/auto_drive.rs b/tui/src/history_cell/auto_drive.rs
|
||
index d68138b..6b455ee 100644
|
||
--- a/tui/src/history_cell/auto_drive.rs
|
||
+++ b/tui/src/history_cell/auto_drive.rs
|
||
@@ -1223,11 +1223,6 @@ impl HistoryCell for AutoDriveCardCell {
|
||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||
self
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- None
|
||
- }
|
||
-
|
||
fn kind(&self) -> HistoryCellType {
|
||
HistoryCellType::Tool {
|
||
status: self.status.tool_status(),
|
||
diff --git a/tui/src/history_cell/browser.rs b/tui/src/history_cell/browser.rs
|
||
index 044337e..784fe64 100644
|
||
--- a/tui/src/history_cell/browser.rs
|
||
+++ b/tui/src/history_cell/browser.rs
|
||
@@ -1013,15 +1013,6 @@ impl HistoryCell for BrowserSessionCell {
|
||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||
self
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- if self.completed {
|
||
- Some("✔")
|
||
- } else {
|
||
- None
|
||
- }
|
||
- }
|
||
-
|
||
fn kind(&self) -> HistoryCellType {
|
||
let status = if self.completed {
|
||
ToolCellStatus::Success
|
||
diff --git a/tui/src/history_cell/core.rs b/tui/src/history_cell/core.rs
|
||
index 73b7fc6..83fb3ce 100644
|
||
--- a/tui/src/history_cell/core.rs
|
||
+++ b/tui/src/history_cell/core.rs
|
||
@@ -42,41 +42,6 @@ pub(crate) enum HistoryCellType {
|
||
Loading,
|
||
}
|
||
|
||
-pub(crate) fn gutter_symbol_for_kind(kind: HistoryCellType) -> Option<&'static str> {
|
||
- match kind {
|
||
- HistoryCellType::Plain => None,
|
||
- HistoryCellType::User => Some("›"),
|
||
- // Restore assistant gutter icon
|
||
- HistoryCellType::Assistant => Some("•"),
|
||
- HistoryCellType::Reasoning => None,
|
||
- HistoryCellType::Error => Some("✖"),
|
||
- HistoryCellType::Tool { status } => Some(match status {
|
||
- ToolCellStatus::Running => "⚙",
|
||
- ToolCellStatus::Success => "✔",
|
||
- ToolCellStatus::Failed => "✖",
|
||
- }),
|
||
- HistoryCellType::Exec { kind, status } => {
|
||
- // Show ❯ only for Run executions; hide for read/search/list summaries
|
||
- match (kind, status) {
|
||
- (ExecKind::Run, ExecStatus::Error) => Some("✖"),
|
||
- (ExecKind::Run, _) => Some("❯"),
|
||
- _ => None,
|
||
- }
|
||
- }
|
||
- HistoryCellType::Patch { .. } => Some("↯"),
|
||
- // Plan updates supply their own gutter glyph dynamically.
|
||
- HistoryCellType::PlanUpdate => None,
|
||
- HistoryCellType::BackgroundEvent => Some("»"),
|
||
- HistoryCellType::Notice => Some("★"),
|
||
- HistoryCellType::CompactionSummary => Some("📍"),
|
||
- HistoryCellType::Diff => Some("↯"),
|
||
- HistoryCellType::Image => None,
|
||
- HistoryCellType::Context => Some("◆"),
|
||
- HistoryCellType::AnimatedWelcome => None,
|
||
- HistoryCellType::Loading => None,
|
||
- }
|
||
-}
|
||
-
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub(crate) enum ExecKind {
|
||
Read,
|
||
@@ -238,12 +203,6 @@ pub(crate) trait HistoryCell {
|
||
fn should_remove(&self) -> bool {
|
||
false // Default: most cells should not be removed
|
||
}
|
||
-
|
||
- /// Returns the gutter symbol for this cell type
|
||
- /// Returns None if no symbol should be displayed
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- gutter_symbol_for_kind(self.kind())
|
||
- }
|
||
}
|
||
|
||
// Allow Box<dyn HistoryCell> to implement HistoryCell
|
||
@@ -297,8 +256,4 @@ impl HistoryCell for Box<dyn HistoryCell> {
|
||
fn should_remove(&self) -> bool {
|
||
self.as_ref().should_remove()
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- self.as_ref().gutter_symbol()
|
||
- }
|
||
}
|
||
diff --git a/tui/src/history_cell/exec.rs b/tui/src/history_cell/exec.rs
|
||
index 6aa831c..a599afe 100644
|
||
--- a/tui/src/history_cell/exec.rs
|
||
+++ b/tui/src/history_cell/exec.rs
|
||
@@ -192,24 +192,6 @@ impl HistoryCell for ExecCell {
|
||
let status = self.record.status;
|
||
HistoryCellType::Exec { kind, status }
|
||
}
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- match self.kind() {
|
||
- HistoryCellType::Exec {
|
||
- kind: ExecKind::Run,
|
||
- status,
|
||
- } => {
|
||
- if matches!(status, ExecStatus::Error) {
|
||
- Some("✖")
|
||
- } else if self.has_bold_command {
|
||
- Some("❯")
|
||
- } else {
|
||
- None
|
||
- }
|
||
- }
|
||
- HistoryCellType::Exec { .. } => None,
|
||
- _ => None,
|
||
- }
|
||
- }
|
||
fn display_lines(&self) -> Vec<Line<'static>> {
|
||
exec_command_lines(
|
||
&self.command,
|
||
diff --git a/tui/src/history_cell/explore.rs b/tui/src/history_cell/explore.rs
|
||
index 96a36fe..34eba87 100644
|
||
--- a/tui/src/history_cell/explore.rs
|
||
+++ b/tui/src/history_cell/explore.rs
|
||
@@ -89,10 +89,6 @@ impl HistoryCell for ExploreAggregationCell {
|
||
.try_into()
|
||
.unwrap_or(0)
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- None
|
||
- }
|
||
}
|
||
|
||
pub(crate) fn explore_record_push_from_parsed(
|
||
diff --git a/tui/src/history_cell/mod.rs b/tui/src/history_cell/mod.rs
|
||
index 88f1d4a..8234b82 100644
|
||
--- a/tui/src/history_cell/mod.rs
|
||
+++ b/tui/src/history_cell/mod.rs
|
||
@@ -36,7 +36,7 @@ pub(crate) use assistant::{
|
||
AssistantLayoutCache,
|
||
AssistantMarkdownCell,
|
||
};
|
||
-pub(crate) use animated::{AnimatedWelcomeCell, new_animated_welcome};
|
||
+pub(crate) use animated::new_animated_welcome;
|
||
pub(crate) use background::{
|
||
new_background_event,
|
||
new_connecting_mcp_status,
|
||
@@ -46,7 +46,6 @@ pub(crate) use context::ContextCell;
|
||
pub(crate) use core::{
|
||
CommandOutput,
|
||
ExecKind,
|
||
- gutter_symbol_for_kind,
|
||
HistoryCell,
|
||
HistoryCellType,
|
||
PatchEventType,
|
||
diff --git a/tui/src/history_cell/plain.rs b/tui/src/history_cell/plain.rs
|
||
index bf14465..2a495b7 100644
|
||
--- a/tui/src/history_cell/plain.rs
|
||
+++ b/tui/src/history_cell/plain.rs
|
||
@@ -170,6 +170,7 @@ impl PlainHistoryCell {
|
||
let is_auto_review = self.is_auto_review_notice();
|
||
let cell_bg = match self.state.kind {
|
||
HistoryCellType::Assistant => crate::colors::assistant_bg(),
|
||
+ HistoryCellType::User => crate::colors::input_bg(),
|
||
HistoryCellType::CompactionSummary => crate::colors::background(),
|
||
_ if is_auto_review => Self::auto_review_bg(),
|
||
_ => crate::colors::background(),
|
||
@@ -281,20 +282,6 @@ impl HistoryCell for PlainHistoryCell {
|
||
fn kind(&self) -> HistoryCellType {
|
||
self.state.kind
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- if self.is_warning_notice() {
|
||
- return Some("⚠");
|
||
- }
|
||
- if let Some(header) = self.state.header() {
|
||
- let label = header.label.trim().to_lowercase();
|
||
- if label == "auto review" {
|
||
- return Some("•");
|
||
- }
|
||
- }
|
||
- super::gutter_symbol_for_kind(self.kind())
|
||
- }
|
||
-
|
||
fn display_lines(&self) -> Vec<Line<'static>> {
|
||
let theme = current_theme();
|
||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||
@@ -348,10 +335,11 @@ impl HistoryCell for PlainHistoryCell {
|
||
let is_auto_review = self.is_auto_review_notice();
|
||
let cell_bg = match self.state.kind {
|
||
HistoryCellType::Assistant => crate::colors::assistant_bg(),
|
||
+ HistoryCellType::User => crate::colors::input_bg(),
|
||
_ if is_auto_review => Self::auto_review_bg(),
|
||
_ => crate::colors::background(),
|
||
};
|
||
- if matches!(self.state.kind, HistoryCellType::Assistant) || is_auto_review {
|
||
+ if matches!(self.state.kind, HistoryCellType::Assistant | HistoryCellType::User) || is_auto_review {
|
||
let bg_style = Style::default().bg(cell_bg).fg(crate::colors::text());
|
||
fill_rect(buf, area, Some(' '), bg_style);
|
||
}
|
||
diff --git a/tui/src/history_cell/plan_update.rs b/tui/src/history_cell/plan_update.rs
|
||
index 50a435a..52c784e 100644
|
||
--- a/tui/src/history_cell/plan_update.rs
|
||
+++ b/tui/src/history_cell/plan_update.rs
|
||
@@ -130,10 +130,6 @@ impl HistoryCell for PlanUpdateCell {
|
||
|
||
lines
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- Some(icon_symbol(&self.state.icon))
|
||
- }
|
||
}
|
||
|
||
struct ProgressMeter {
|
||
@@ -156,22 +152,6 @@ fn progress_meter(progress: &PlanProgress, width: usize) -> ProgressMeter {
|
||
}
|
||
}
|
||
|
||
-fn icon_symbol(icon: &PlanIcon) -> &'static str {
|
||
- match icon {
|
||
- PlanIcon::LightBulb => "💡",
|
||
- PlanIcon::Rocket => "🚀",
|
||
- PlanIcon::Clipboard => "📋",
|
||
- PlanIcon::Custom(kind) => match kind.as_str() {
|
||
- "progress-empty" => "○",
|
||
- "progress-start" => "◔",
|
||
- "progress-mid" => "◑",
|
||
- "progress-late" => "◕",
|
||
- "progress-complete" => "●",
|
||
- _ => "•",
|
||
- },
|
||
- }
|
||
-}
|
||
-
|
||
fn plan_progress_icon(total: usize, completed: usize) -> PlanIcon {
|
||
if total == 0 || completed == 0 {
|
||
PlanIcon::Custom("progress-empty".to_string())
|
||
diff --git a/tui/src/history_cell/reasoning.rs b/tui/src/history_cell/reasoning.rs
|
||
index 28074d7..cda909e 100644
|
||
--- a/tui/src/history_cell/reasoning.rs
|
||
+++ b/tui/src/history_cell/reasoning.rs
|
||
@@ -256,11 +256,6 @@ impl HistoryCell for CollapsibleReasoningCell {
|
||
out
|
||
}
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- None
|
||
- }
|
||
-
|
||
fn has_custom_render(&self) -> bool {
|
||
true
|
||
}
|
||
diff --git a/tui/src/history_cell/tool.rs b/tui/src/history_cell/tool.rs
|
||
index 007ab99..ce22c71 100644
|
||
--- a/tui/src/history_cell/tool.rs
|
||
+++ b/tui/src/history_cell/tool.rs
|
||
@@ -163,12 +163,6 @@ impl RunningToolCallCell {
|
||
Self::strip_zero_seconds_suffix(format_duration(duration)).replace(' ', "")
|
||
}
|
||
|
||
- fn spinner_frame(&self) -> &'static str {
|
||
- const FRAMES: [&str; 4] = ["◐", "◓", "◑", "◒"];
|
||
- let idx = ((self.start_clock.elapsed().as_millis() / 100) as usize) % FRAMES.len();
|
||
- FRAMES[idx]
|
||
- }
|
||
-
|
||
fn is_gh_run_wait(&self) -> bool {
|
||
self.state.title == "Gh Run Wait..."
|
||
}
|
||
@@ -392,19 +386,6 @@ impl HistoryCell for RunningToolCallCell {
|
||
status: super::ToolCellStatus::Running,
|
||
}
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- if self.state.title == "Waiting" {
|
||
- if self.state.wait_has_call_id {
|
||
- None
|
||
- } else {
|
||
- Some(self.spinner_frame())
|
||
- }
|
||
- } else {
|
||
- Some("⚙")
|
||
- }
|
||
- }
|
||
-
|
||
fn is_animating(&self) -> bool {
|
||
true
|
||
}
|
||
diff --git a/tui/src/history_cell/wait_status.rs b/tui/src/history_cell/wait_status.rs
|
||
index 20689f7..fcc9305 100644
|
||
--- a/tui/src/history_cell/wait_status.rs
|
||
+++ b/tui/src/history_cell/wait_status.rs
|
||
@@ -58,10 +58,6 @@ impl HistoryCell for WaitStatusCell {
|
||
lines.push(Line::from(""));
|
||
lines
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- Some("◓")
|
||
- }
|
||
}
|
||
|
||
fn render_header(header: &WaitStatusHeader, theme: &Theme) -> Line<'static> {
|
||
diff --git a/tui/src/history_cell/web_search.rs b/tui/src/history_cell/web_search.rs
|
||
index 616e39e..6d25998 100644
|
||
--- a/tui/src/history_cell/web_search.rs
|
||
+++ b/tui/src/history_cell/web_search.rs
|
||
@@ -508,11 +508,6 @@ impl HistoryCell for WebSearchSessionCell {
|
||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||
self
|
||
}
|
||
-
|
||
- fn gutter_symbol(&self) -> Option<&'static str> {
|
||
- None
|
||
- }
|
||
-
|
||
fn kind(&self) -> HistoryCellType {
|
||
let status = match self.status {
|
||
WebSearchStatus::Running => ToolCellStatus::Running,
|
||
diff --git a/tui/src/lib.rs b/tui/src/lib.rs
|
||
index 1a3d5fd..52e1a91 100644
|
||
--- a/tui/src/lib.rs
|
||
+++ b/tui/src/lib.rs
|
||
@@ -8,7 +8,7 @@ use code_common::model_presets::{
|
||
all_model_presets,
|
||
ModelPreset,
|
||
HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG,
|
||
- HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG,
|
||
+ HIDE_GPT_5_1_DEV_MAX_MIGRATION_PROMPT_CONFIG,
|
||
HIDE_GPT_5_2_MIGRATION_PROMPT_CONFIG,
|
||
};
|
||
use code_core::config_edit::{self, CONFIG_KEY_EFFORT, CONFIG_KEY_MODEL};
|
||
@@ -31,7 +31,7 @@ use code_core::config_types::ThemeName;
|
||
use regex_lite::Regex;
|
||
use code_login::AuthMode;
|
||
use code_login::CodexAuth;
|
||
-use model_migration::{migration_copy_for_key, run_model_migration_prompt, ModelMigrationOutcome};
|
||
+use model_migration::{run_model_migration_prompt, ModelMigrationOutcome};
|
||
use code_ollama::DEFAULT_OSS_MODEL;
|
||
use code_protocol::config_types::SandboxMode;
|
||
use std::fs::OpenOptions;
|
||
@@ -75,7 +75,6 @@ mod get_git_diff;
|
||
mod glitch_animation;
|
||
mod auto_drive_strings;
|
||
mod auto_drive_style;
|
||
-mod header_wave;
|
||
mod history_cell;
|
||
mod history;
|
||
mod insert_history;
|
||
@@ -93,7 +92,6 @@ mod render;
|
||
mod model_migration;
|
||
// mod scroll_view; // Orphaned after trait-based HistoryCell migration
|
||
mod session_log;
|
||
-mod shimmer;
|
||
mod slash_command;
|
||
mod rate_limits_view;
|
||
pub mod resume;
|
||
@@ -481,7 +479,7 @@ pub async fn run_main(
|
||
Ok(code_home) => code_home,
|
||
#[allow(clippy::print_stderr)]
|
||
Err(err) => {
|
||
- eprintln!("Error finding codex home: {err}");
|
||
+ eprintln!("Error finding the Hanzo Dev home directory: {err}");
|
||
std::process::exit(1);
|
||
}
|
||
};
|
||
@@ -554,7 +552,7 @@ pub async fn run_main(
|
||
};
|
||
if let Some(plan) = determine_migration_plan(&config, auth_mode) {
|
||
let should_auto_accept = auth_mode.is_chatgpt()
|
||
- && (plan.hide_key != code_common::model_presets::HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG
|
||
+ && (plan.hide_key != code_common::model_presets::HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG
|
||
|| (plan.current.id.eq_ignore_ascii_case("gpt-5.1-codex")
|
||
&& plan
|
||
.target
|
||
@@ -586,8 +584,7 @@ pub async fn run_main(
|
||
}
|
||
}
|
||
} else {
|
||
- let copy = migration_copy_for_key(plan.hide_key);
|
||
- match run_model_migration_prompt(©)? {
|
||
+ match run_model_migration_prompt()? {
|
||
ModelMigrationOutcome::Accepted => {
|
||
if let Err(err) = persist_migration_acceptance(
|
||
&code_home,
|
||
@@ -1220,11 +1217,11 @@ async fn persist_notice_hide(
|
||
fn set_notice_flag(notices: &mut Notice, key: &str) {
|
||
if key == HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt5_1_migration_prompt = Some(true);
|
||
- } else if key == HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG {
|
||
+ } else if key == HIDE_GPT_5_1_DEV_MAX_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt_5_1_codex_max_migration_prompt = Some(true);
|
||
} else if key == HIDE_GPT_5_2_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt5_2_migration_prompt = Some(true);
|
||
- } else if key == code_common::model_presets::HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG {
|
||
+ } else if key == code_common::model_presets::HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt5_2_codex_migration_prompt = Some(true);
|
||
}
|
||
}
|
||
@@ -1232,11 +1229,11 @@ fn set_notice_flag(notices: &mut Notice, key: &str) {
|
||
fn notice_hidden(notices: &Notice, key: &str) -> bool {
|
||
if key == HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt5_1_migration_prompt.unwrap_or(false)
|
||
- } else if key == HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG {
|
||
+ } else if key == HIDE_GPT_5_1_DEV_MAX_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt_5_1_codex_max_migration_prompt.unwrap_or(false)
|
||
} else if key == HIDE_GPT_5_2_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt5_2_migration_prompt.unwrap_or(false)
|
||
- } else if key == code_common::model_presets::HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG {
|
||
+ } else if key == code_common::model_presets::HIDE_GPT_5_2_DEV_MIGRATION_PROMPT_CONFIG {
|
||
notices.hide_gpt5_2_codex_migration_prompt.unwrap_or(false)
|
||
} else {
|
||
false
|
||
@@ -1298,8 +1295,16 @@ pub enum LoginStatus {
|
||
AuthMode(AuthMode),
|
||
}
|
||
|
||
-/// Determine current login status based on auth.json presence.
|
||
+/// Determine current login status.
|
||
+///
|
||
+/// A Hanzo bearer counts before `auth.json` does. `hanzo code` sets one on the
|
||
+/// child and the free pool answers with a built-in one, so in both cases there
|
||
+/// is a credential and no file — and neither belongs on a screen offering to
|
||
+/// sign the user in to OpenAI.
|
||
pub fn get_login_status(config: &Config) -> LoginStatus {
|
||
+ if code_core::free::signed_in() || code_core::free::runs(config) {
|
||
+ return LoginStatus::AuthMode(AuthMode::ApiKey);
|
||
+ }
|
||
let code_home = config.code_home.clone();
|
||
match CodexAuth::from_code_home(&code_home, AuthMode::ChatGPT, &config.responses_originator_header) {
|
||
Ok(Some(auth)) => LoginStatus::AuthMode(auth.mode),
|
||
diff --git a/tui/src/model_migration.rs b/tui/src/model_migration.rs
|
||
index 0702965..a23579c 100644
|
||
--- a/tui/src/model_migration.rs
|
||
+++ b/tui/src/model_migration.rs
|
||
@@ -4,7 +4,7 @@ use crate::colors;
|
||
use crossterm::cursor::MoveTo;
|
||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||
use crossterm::queue;
|
||
-use crossterm::style::{Attribute as CtAttribute, Color as CtColor, Print, ResetColor, SetAttribute, SetForegroundColor};
|
||
+use crossterm::style::{Color as CtColor, Print, ResetColor, SetForegroundColor};
|
||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
|
||
use crossterm::ExecutableCommand;
|
||
|
||
@@ -14,55 +14,13 @@ pub(crate) enum ModelMigrationOutcome {
|
||
Exit,
|
||
}
|
||
|
||
-pub(crate) struct ModelMigrationCopy {
|
||
- pub heading: &'static str,
|
||
- pub content: &'static [&'static str],
|
||
- pub can_opt_out: bool,
|
||
-}
|
||
+const HEADING: &str = "A new model is available";
|
||
+const CONTENT: &[&str] = &[
|
||
+ "The model you are on has a successor in the catalog.",
|
||
+ "Choose how you'd like Hanzo Dev to proceed.",
|
||
+];
|
||
|
||
-pub(crate) fn migration_copy_for_key(key: &str) -> ModelMigrationCopy {
|
||
- match key {
|
||
- code_common::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG => ModelMigrationCopy {
|
||
- heading: "Introducing our gpt-5.1 models",
|
||
- content: &[
|
||
- "We've upgraded Codex to gpt-5.1, gpt-5.1-codex, and gpt-5.1-codex-mini.",
|
||
- "Legacy gpt-5 models continue to work via -m or config.toml overrides.",
|
||
- "Learn more: www.openai.com/index/gpt-5-1",
|
||
- "Press Enter to continue.",
|
||
- ],
|
||
- can_opt_out: false,
|
||
- },
|
||
- code_common::model_presets::HIDE_GPT_5_2_MIGRATION_PROMPT_CONFIG => ModelMigrationCopy {
|
||
- heading: "Upgrade available: GPT-5.2",
|
||
- content: &[
|
||
- "OpenAI's latest frontier model is here! Improved knowledge, reasoning, and coding.",
|
||
- "Switch now to get better results; you can keep your current model if you prefer.",
|
||
- "Learn more: www.openai.com/index/gpt-5-2",
|
||
- ],
|
||
- can_opt_out: true,
|
||
- },
|
||
- code_common::model_presets::HIDE_GPT_5_2_CODEX_MIGRATION_PROMPT_CONFIG => ModelMigrationCopy {
|
||
- heading: "Upgrade available: GPT-5.3 Codex",
|
||
- content: &[
|
||
- "GPT-5.2 Codex has been superseded by gpt-5.3-codex.",
|
||
- "Switch now for better coding results; you can keep your current model if you prefer.",
|
||
- ],
|
||
- can_opt_out: true,
|
||
- },
|
||
- _ => ModelMigrationCopy {
|
||
- heading: "Codex just got an upgrade: meet gpt-5.1-codex-max",
|
||
- content: &[
|
||
- "Our flagship agentic coding model is smarter, faster, and tuned for long sessions.",
|
||
- "Everyone signed in with ChatGPT gets it automatically.",
|
||
- "Learn more: www.openai.com/index/gpt-5-1-codex-max",
|
||
- "Choose how you'd like Codex to proceed.",
|
||
- ],
|
||
- can_opt_out: true,
|
||
- },
|
||
- }
|
||
-}
|
||
-
|
||
-pub(crate) fn run_model_migration_prompt(copy: &ModelMigrationCopy) -> io::Result<ModelMigrationOutcome> {
|
||
+pub(crate) fn run_model_migration_prompt() -> io::Result<ModelMigrationOutcome> {
|
||
struct RawModeGuard;
|
||
impl RawModeGuard {
|
||
fn new() -> io::Result<Self> {
|
||
@@ -80,7 +38,7 @@ pub(crate) fn run_model_migration_prompt(copy: &ModelMigrationCopy) -> io::Resul
|
||
|
||
let mut stdout = io::stdout();
|
||
let mut highlighted = 0usize;
|
||
- render_prompt(&mut stdout, copy, highlighted)?;
|
||
+ render_prompt(&mut stdout, highlighted)?;
|
||
|
||
loop {
|
||
let event = event::read()?;
|
||
@@ -95,24 +53,14 @@ pub(crate) fn run_model_migration_prompt(copy: &ModelMigrationCopy) -> io::Resul
|
||
return Ok(ModelMigrationOutcome::Exit);
|
||
}
|
||
|
||
- if !copy.can_opt_out {
|
||
- match code {
|
||
- KeyCode::Enter | KeyCode::Esc => {
|
||
- return Ok(ModelMigrationOutcome::Accepted);
|
||
- }
|
||
- _ => {}
|
||
- }
|
||
- continue;
|
||
- }
|
||
-
|
||
match code {
|
||
KeyCode::Up | KeyCode::Char('k') => {
|
||
highlighted = 0;
|
||
- render_prompt(&mut stdout, copy, highlighted)?;
|
||
+ render_prompt(&mut stdout, highlighted)?;
|
||
}
|
||
KeyCode::Down | KeyCode::Char('j') => {
|
||
highlighted = 1;
|
||
- render_prompt(&mut stdout, copy, highlighted)?;
|
||
+ render_prompt(&mut stdout, highlighted)?;
|
||
}
|
||
KeyCode::Char('1') => return Ok(ModelMigrationOutcome::Accepted),
|
||
KeyCode::Char('2') => return Ok(ModelMigrationOutcome::Rejected),
|
||
@@ -131,34 +79,27 @@ pub(crate) fn run_model_migration_prompt(copy: &ModelMigrationCopy) -> io::Resul
|
||
}
|
||
}
|
||
|
||
-fn render_prompt(stdout: &mut io::Stdout, copy: &ModelMigrationCopy, highlighted: usize) -> io::Result<()> {
|
||
+fn render_prompt(stdout: &mut io::Stdout, highlighted: usize) -> io::Result<()> {
|
||
stdout.execute(Clear(ClearType::All))?;
|
||
stdout.execute(MoveTo(0, 0))?;
|
||
|
||
- if copy.heading == "Upgrade available: GPT-5.2 Codex" {
|
||
- let success_fg = CtColor::from(colors::success());
|
||
- write_line_fg_bold(stdout, copy.heading, success_fg)?;
|
||
- } else {
|
||
- write_line(stdout, copy.heading)?;
|
||
- }
|
||
+ write_line(stdout, HEADING)?;
|
||
write_blank(stdout)?;
|
||
- for line in copy.content {
|
||
+ for line in CONTENT {
|
||
write_line(stdout, line)?;
|
||
}
|
||
|
||
- if copy.can_opt_out {
|
||
- write_blank(stdout)?;
|
||
- let primary_fg = CtColor::from(colors::primary());
|
||
- for (idx, label) in ["Try new model (recommended)", "Use existing model"].iter().enumerate() {
|
||
- if idx == highlighted {
|
||
- queue!(stdout, SetForegroundColor(primary_fg), Print("> "), Print(*label), ResetColor, Print("\r\n"))?;
|
||
- } else {
|
||
- queue!(stdout, Print(" "), Print(*label), Print("\r\n"))?;
|
||
- }
|
||
+ write_blank(stdout)?;
|
||
+ let primary_fg = CtColor::from(colors::primary());
|
||
+ for (idx, label) in ["Try new model (recommended)", "Use existing model"].iter().enumerate() {
|
||
+ if idx == highlighted {
|
||
+ queue!(stdout, SetForegroundColor(primary_fg), Print("> "), Print(*label), ResetColor, Print("\r\n"))?;
|
||
+ } else {
|
||
+ queue!(stdout, Print(" "), Print(*label), Print("\r\n"))?;
|
||
}
|
||
- write_blank(stdout)?;
|
||
- write_key_tip_line(stdout)?;
|
||
}
|
||
+ write_blank(stdout)?;
|
||
+ write_key_tip_line(stdout)?;
|
||
|
||
stdout.flush()
|
||
}
|
||
@@ -168,19 +109,6 @@ fn write_line(stdout: &mut io::Stdout, line: &str) -> io::Result<()> {
|
||
stdout.write_all(b"\r\n")
|
||
}
|
||
|
||
-fn write_line_fg_bold(stdout: &mut io::Stdout, line: &str, fg: CtColor) -> io::Result<()> {
|
||
- queue!(
|
||
- stdout,
|
||
- SetForegroundColor(fg),
|
||
- SetAttribute(CtAttribute::Bold),
|
||
- Print(line),
|
||
- SetAttribute(CtAttribute::NormalIntensity),
|
||
- ResetColor,
|
||
- Print("\r\n")
|
||
- )?;
|
||
- Ok(())
|
||
-}
|
||
-
|
||
fn write_key_tip_line(stdout: &mut io::Stdout) -> io::Result<()> {
|
||
let tip_fg = CtColor::from(colors::function());
|
||
queue!(
|
||
diff --git a/tui/src/onboarding/auth.rs b/tui/src/onboarding/auth.rs
|
||
index e60b45e..5ff2e9f 100644
|
||
--- a/tui/src/onboarding/auth.rs
|
||
+++ b/tui/src/onboarding/auth.rs
|
||
@@ -1,4 +1,5 @@
|
||
use code_login::CLIENT_ID;
|
||
+use code_login::DeviceGrant;
|
||
use code_login::ServerOptions;
|
||
use code_login::ShutdownHandle;
|
||
use code_login::run_login_server;
|
||
@@ -18,7 +19,7 @@ use ratatui::widgets::Wrap;
|
||
|
||
use code_login::AuthMode;
|
||
|
||
-use code_core::config::GPT_5_CODEX_MEDIUM_MODEL;
|
||
+use code_core::config::GPT_5_DEV_MEDIUM_MODEL;
|
||
use code_core::model_family::{derive_default_model_family, find_family_for_model};
|
||
|
||
use crate::LoginStatus;
|
||
@@ -27,7 +28,6 @@ use crate::app_event::AppEvent;
|
||
use crate::app_event_sender::AppEventSender;
|
||
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||
-use crate::shimmer::shimmer_spans;
|
||
use std::path::PathBuf;
|
||
use std::sync::Arc;
|
||
use std::sync::Mutex;
|
||
@@ -38,9 +38,11 @@ use super::onboarding_screen::StepState;
|
||
#[derive(Debug)]
|
||
pub(crate) enum SignInState {
|
||
PickMode,
|
||
- ChatGptContinueInBrowser(ContinueInBrowserState),
|
||
- ChatGptSuccessMessage,
|
||
- ChatGptSuccess,
|
||
+ ContinueInBrowser(ContinueInBrowserState),
|
||
+ /// RFC 8628: the issuer named a code the user confirms on its own page.
|
||
+ DeviceCode(DeviceCodeState),
|
||
+ SuccessMessage,
|
||
+ Success,
|
||
EnvVarMissing,
|
||
EnvVarFound,
|
||
}
|
||
@@ -61,6 +63,19 @@ impl Drop for ContinueInBrowserState {
|
||
}
|
||
}
|
||
|
||
+/// The link and code to display, once the issuer has named them.
|
||
+#[derive(Debug)]
|
||
+pub(crate) struct DeviceCodeState {
|
||
+ prompt: Arc<Mutex<Option<(String, String)>>>,
|
||
+ task: tokio::task::JoinHandle<()>,
|
||
+}
|
||
+
|
||
+impl Drop for DeviceCodeState {
|
||
+ fn drop(&mut self) {
|
||
+ self.task.abort();
|
||
+ }
|
||
+}
|
||
+
|
||
impl KeyboardHandler for AuthModeWidget {
|
||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||
match key_event.code {
|
||
@@ -71,24 +86,25 @@ impl KeyboardHandler for AuthModeWidget {
|
||
self.highlighted_mode = AuthMode::ApiKey;
|
||
}
|
||
KeyCode::Char('1') => {
|
||
- self.start_chatgpt_login();
|
||
+ self.start_login();
|
||
}
|
||
KeyCode::Char('2') => self.verify_api_key(),
|
||
KeyCode::Enter => match self.sign_in_state {
|
||
SignInState::PickMode => match self.highlighted_mode {
|
||
- AuthMode::ChatGPT => self.start_chatgpt_login(),
|
||
- AuthMode::ChatgptAuthTokens => self.start_chatgpt_login(),
|
||
- AuthMode::Headers => self.start_chatgpt_login(),
|
||
+ AuthMode::ChatGPT => self.start_login(),
|
||
+ AuthMode::ChatgptAuthTokens => self.start_login(),
|
||
+ AuthMode::Headers => self.start_login(),
|
||
AuthMode::ApiKey => self.verify_api_key(),
|
||
},
|
||
SignInState::EnvVarMissing => self.sign_in_state = SignInState::PickMode,
|
||
- SignInState::ChatGptSuccessMessage => {
|
||
- self.sign_in_state = SignInState::ChatGptSuccess
|
||
- }
|
||
+ SignInState::SuccessMessage => self.sign_in_state = SignInState::Success,
|
||
_ => {}
|
||
},
|
||
KeyCode::Esc => {
|
||
- if matches!(self.sign_in_state, SignInState::ChatGptContinueInBrowser(_)) {
|
||
+ if matches!(
|
||
+ self.sign_in_state,
|
||
+ SignInState::ContinueInBrowser(_) | SignInState::DeviceCode(_)
|
||
+ ) {
|
||
self.sign_in_state = SignInState::PickMode;
|
||
}
|
||
}
|
||
@@ -107,17 +123,28 @@ pub(crate) struct AuthModeWidget {
|
||
pub login_status: LoginStatus,
|
||
pub preferred_auth_method: AuthMode,
|
||
pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
|
||
+ /// The configured provider is Hanzo, so sign-in means Hanzo IAM.
|
||
+ pub hanzo: bool,
|
||
+ /// The variable the configured provider reads its key from.
|
||
+ pub env_key: Option<String>,
|
||
}
|
||
|
||
impl AuthModeWidget {
|
||
+ /// What the account being signed into is called.
|
||
+ fn brand(&self) -> &'static str {
|
||
+ if self.hanzo { "Hanzo" } else { "ChatGPT" }
|
||
+ }
|
||
+
|
||
fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) {
|
||
+ let headline = if self.hanzo {
|
||
+ "Sign in with Hanzo to use your Hanzo Cloud account".to_string()
|
||
+ } else {
|
||
+ "Sign in with ChatGPT to use your paid OpenAI plan".to_string()
|
||
+ };
|
||
let mut lines: Vec<Line> = vec![
|
||
Line::from(vec![
|
||
Span::raw("> "),
|
||
- Span::styled(
|
||
- "Sign in with ChatGPT to use your paid OpenAI plan",
|
||
- Style::default().add_modifier(Modifier::BOLD),
|
||
- ),
|
||
+ Span::styled(headline, Style::default().add_modifier(Modifier::BOLD)),
|
||
]),
|
||
Line::from(vec![
|
||
Span::raw(" "),
|
||
@@ -142,7 +169,7 @@ impl AuthModeWidget {
|
||
let to_label = |mode: AuthMode| match mode {
|
||
AuthMode::ApiKey => "API key",
|
||
AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens | AuthMode::Headers => {
|
||
- "ChatGPT"
|
||
+ self.brand()
|
||
}
|
||
};
|
||
let msg = format!(
|
||
@@ -189,22 +216,22 @@ impl AuthModeWidget {
|
||
|
||
vec![line1, line2]
|
||
};
|
||
- let chatgpt_label = if matches!(
|
||
+ let signed_in = matches!(
|
||
self.login_status,
|
||
LoginStatus::AuthMode(
|
||
AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens | AuthMode::Headers
|
||
)
|
||
- ) {
|
||
- "Continue using ChatGPT"
|
||
- } else {
|
||
- "Sign in with ChatGPT"
|
||
- };
|
||
-
|
||
+ );
|
||
+ let verb = if signed_in { "Continue with" } else { "Sign in with" };
|
||
lines.extend(create_mode_item(
|
||
0,
|
||
AuthMode::ChatGPT,
|
||
- chatgpt_label,
|
||
- "Usage included with Plus, Pro, and Team plans",
|
||
+ &format!("{verb} {}", self.brand()),
|
||
+ if self.hanzo {
|
||
+ "hanzo.id — a one-time code, so it works over SSH too"
|
||
+ } else {
|
||
+ "Usage included with Plus, Pro, and Team plans"
|
||
+ },
|
||
));
|
||
let api_key_label = if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ApiKey))
|
||
{
|
||
@@ -216,7 +243,10 @@ impl AuthModeWidget {
|
||
1,
|
||
AuthMode::ApiKey,
|
||
api_key_label,
|
||
- "Pay for what you use",
|
||
+ &match &self.env_key {
|
||
+ Some(key) => format!("Pay for what you use — reads {key}"),
|
||
+ None => "Pay for what you use".to_string(),
|
||
+ },
|
||
));
|
||
lines.push(Line::from(""));
|
||
lines.push(
|
||
@@ -240,14 +270,14 @@ impl AuthModeWidget {
|
||
|
||
fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) {
|
||
let mut spans = vec![Span::from("> ")];
|
||
- // Schedule a follow-up frame to keep the shimmer animation going.
|
||
- self.event_tx
|
||
- .send(AppEvent::ScheduleFrameIn(std::time::Duration::from_millis(
|
||
- 100,
|
||
- )));
|
||
- spans.extend(shimmer_spans("Finish signing in via your browser"));
|
||
+ // A waiting screen holds still. The shimmer here redrew the whole
|
||
+ // frame ten times a second for as long as the sign-in took.
|
||
+ spans.push(Span::styled(
|
||
+ "Finish signing in via your browser",
|
||
+ Style::default().add_modifier(Modifier::BOLD),
|
||
+ ));
|
||
let mut lines = vec![Line::from(spans), Line::from("")];
|
||
- if let SignInState::ChatGptContinueInBrowser(state) = &self.sign_in_state {
|
||
+ if let SignInState::ContinueInBrowser(state) = &self.sign_in_state {
|
||
if !state.auth_url.is_empty() {
|
||
lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:"));
|
||
lines.push(Line::from(vec![
|
||
@@ -269,36 +299,83 @@ impl AuthModeWidget {
|
||
.render(area, buf);
|
||
}
|
||
|
||
- fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) {
|
||
+ /// Waiting on the user to approve the device code at the issuer.
|
||
+ fn render_device_code(&self, area: Rect, buf: &mut Buffer) {
|
||
+ let mut spans = vec![Span::from("> ")];
|
||
+ spans.push(Span::styled(
|
||
+ format!("Finish signing in with {}", self.brand()),
|
||
+ Style::default().add_modifier(Modifier::BOLD),
|
||
+ ));
|
||
+ let mut lines = vec![Line::from(spans), Line::from("")];
|
||
+
|
||
+ let prompt = match &self.sign_in_state {
|
||
+ SignInState::DeviceCode(state) => {
|
||
+ state.prompt.lock().ok().and_then(|slot| slot.clone())
|
||
+ }
|
||
+ _ => None,
|
||
+ };
|
||
+ if let Some((link, code)) = prompt {
|
||
+ lines.push(Line::from(" Open this link and sign in:"));
|
||
+ lines.push(Line::from(vec![
|
||
+ Span::raw(" "),
|
||
+ link.fg(crate::colors::info()).underlined(),
|
||
+ ]));
|
||
+ lines.push(Line::from(""));
|
||
+ lines.push(Line::from(vec![
|
||
+ Span::raw(" Confirm this one-time code matches the page: "),
|
||
+ code.fg(crate::colors::info()).bold(),
|
||
+ ]));
|
||
+ lines.push(
|
||
+ Line::from(" Device codes are a common phishing target. Never share this code.")
|
||
+ .style(Style::default().fg(crate::colors::text_dim())),
|
||
+ );
|
||
+ lines.push(Line::from(""));
|
||
+ }
|
||
+
|
||
+ lines.push(
|
||
+ Line::from(" Press Esc to cancel").style(Style::default().add_modifier(Modifier::DIM)),
|
||
+ );
|
||
+ Paragraph::new(lines)
|
||
+ .wrap(Wrap { trim: false })
|
||
+ .render(area, buf);
|
||
+ }
|
||
+
|
||
+ fn render_success_message(&self, area: Rect, buf: &mut Buffer) {
|
||
+ // Rate limits and training-data preferences are a ChatGPT plan's terms;
|
||
+ // they say nothing true about a Hanzo account.
|
||
+ let plan_terms = if self.hanzo {
|
||
+ Line::from(" Uses your Hanzo Cloud account")
|
||
+ } else {
|
||
+ Line::from(vec![
|
||
+ Span::raw(" Uses your plan's rate limits and "),
|
||
+ Span::styled(
|
||
+ "\u{1b}]8;;https://chatgpt.com/#settings\u{7}training data preferences\u{1b}]8;;\u{7}",
|
||
+ Style::default().add_modifier(Modifier::UNDERLINED),
|
||
+ ),
|
||
+ ])
|
||
+ };
|
||
let lines = vec![
|
||
- Line::from("✓ Signed in with your ChatGPT account")
|
||
+ Line::from(format!("✓ Signed in with your {} account", self.brand()))
|
||
.fg(crate::colors::success()),
|
||
Line::from(""),
|
||
Line::from("> Before you start:"),
|
||
Line::from(""),
|
||
- Line::from(" Decide how much autonomy you want to grant Code"),
|
||
+ Line::from(" Decide how much autonomy you want to grant Hanzo Dev"),
|
||
Line::from(vec![
|
||
Span::raw(" For more details see the "),
|
||
Span::styled(
|
||
- "\u{1b}]8;;https://github.com/just-every/code\u{7}Code docs\u{1b}]8;;\u{7}",
|
||
+ "\u{1b}]8;;https://github.com/hanzoai/dev\u{7}Hanzo Dev docs\u{1b}]8;;\u{7}",
|
||
Style::default().add_modifier(Modifier::UNDERLINED),
|
||
),
|
||
])
|
||
.style(Style::default().add_modifier(Modifier::DIM)),
|
||
Line::from(""),
|
||
- Line::from(" Code can make mistakes"),
|
||
+ Line::from(" Hanzo Dev can make mistakes"),
|
||
Line::from(" Review the code it writes and commands it runs")
|
||
.style(Style::default().add_modifier(Modifier::DIM)),
|
||
Line::from(""),
|
||
- Line::from(" Powered by your ChatGPT account"),
|
||
- Line::from(vec![
|
||
- Span::raw(" Uses your plan's rate limits and "),
|
||
- Span::styled(
|
||
- "\u{1b}]8;;https://chatgpt.com/#settings\u{7}training data preferences\u{1b}]8;;\u{7}",
|
||
- Style::default().add_modifier(Modifier::UNDERLINED),
|
||
- ),
|
||
- ])
|
||
- .style(Style::default().add_modifier(Modifier::DIM)),
|
||
+ Line::from(format!(" Powered by your {} account", self.brand())),
|
||
+ plan_terms.style(Style::default().add_modifier(Modifier::DIM)),
|
||
Line::from(""),
|
||
Line::from(" Press Enter to continue").fg(crate::colors::info()),
|
||
];
|
||
@@ -308,8 +385,11 @@ impl AuthModeWidget {
|
||
.render(area, buf);
|
||
}
|
||
|
||
- fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) {
|
||
- let lines = vec![Line::from("✓ Signed in with your ChatGPT account").fg(crate::colors::success())];
|
||
+ fn render_success(&self, area: Rect, buf: &mut Buffer) {
|
||
+ let lines = vec![
|
||
+ Line::from(format!("✓ Signed in with your {} account", self.brand()))
|
||
+ .fg(crate::colors::success()),
|
||
+ ];
|
||
|
||
Paragraph::new(lines)
|
||
.wrap(Wrap { trim: false })
|
||
@@ -317,7 +397,13 @@ impl AuthModeWidget {
|
||
}
|
||
|
||
fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) {
|
||
- let lines = vec![Line::from("✓ Using OPENAI_API_KEY").fg(crate::colors::success())];
|
||
+ let lines = vec![
|
||
+ Line::from(match &self.env_key {
|
||
+ Some(key) => format!("✓ Using {key}"),
|
||
+ None => "✓ Using your API key".to_string(),
|
||
+ })
|
||
+ .fg(crate::colors::success()),
|
||
+ ];
|
||
|
||
Paragraph::new(lines)
|
||
.wrap(Wrap { trim: false })
|
||
@@ -325,10 +411,18 @@ impl AuthModeWidget {
|
||
}
|
||
|
||
fn render_env_var_missing(&self, area: Rect, buf: &mut Buffer) {
|
||
+ let api = if self.hanzo {
|
||
+ "the Hanzo Cloud"
|
||
+ } else {
|
||
+ "the OpenAI API"
|
||
+ };
|
||
let lines = vec![
|
||
- Line::from(
|
||
- " To use Code with the OpenAI API, set OPENAI_API_KEY in your environment",
|
||
- )
|
||
+ Line::from(match &self.env_key {
|
||
+ Some(key) => {
|
||
+ format!(" To use Hanzo Dev with {api}, set {key} in your environment")
|
||
+ }
|
||
+ None => format!(" To use Hanzo Dev with {api}, set an API key in your environment"),
|
||
+ })
|
||
.style(Style::default().fg(crate::colors::info())),
|
||
Line::from(""),
|
||
Line::from(" Press Enter to return")
|
||
@@ -340,22 +434,60 @@ impl AuthModeWidget {
|
||
.render(area, buf);
|
||
}
|
||
|
||
- fn start_chatgpt_login(&mut self) {
|
||
- // If we're already authenticated with ChatGPT, don't start a new login –
|
||
- // just proceed to the success message flow.
|
||
+ fn start_login(&mut self) {
|
||
+ // Already signed in — skip straight to the success message.
|
||
if matches!(
|
||
self.login_status,
|
||
LoginStatus::AuthMode(
|
||
AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens | AuthMode::Headers
|
||
)
|
||
) {
|
||
- self.apply_chatgpt_login_side_effects();
|
||
- self.sign_in_state = SignInState::ChatGptSuccess;
|
||
+ self.apply_login_side_effects();
|
||
+ self.sign_in_state = SignInState::Success;
|
||
self.event_tx.send(AppEvent::RequestRedraw);
|
||
return;
|
||
}
|
||
|
||
self.error = None;
|
||
+ if self.hanzo {
|
||
+ self.start_device_grant();
|
||
+ } else {
|
||
+ self.start_browser_login();
|
||
+ }
|
||
+ }
|
||
+
|
||
+ /// Hanzo IAM registers no loopback redirect for its CLI client, so its
|
||
+ /// sign-in is the RFC 8628 device grant — which also survives SSH.
|
||
+ fn start_device_grant(&mut self) {
|
||
+ let opts = ServerOptions::hanzo(
|
||
+ self.code_home.clone(),
|
||
+ code_core::default_client::DEFAULT_ORIGINATOR.to_string(),
|
||
+ );
|
||
+ let prompt = Arc::new(Mutex::new(None));
|
||
+ let slot = prompt.clone();
|
||
+ let event_tx = self.event_tx.clone();
|
||
+ let task = tokio::spawn(async move {
|
||
+ let result = match DeviceGrant::start(opts).await {
|
||
+ Ok(grant) => {
|
||
+ if let Ok(mut slot) = slot.lock() {
|
||
+ *slot = Some((
|
||
+ grant.link().to_string(),
|
||
+ grant.user_code().to_string(),
|
||
+ ));
|
||
+ }
|
||
+ event_tx.send(AppEvent::RequestRedraw);
|
||
+ grant.wait().await.map_err(|e| e.to_string())
|
||
+ }
|
||
+ Err(e) => Err(e.to_string()),
|
||
+ };
|
||
+ event_tx.send(AppEvent::OnboardingAuthComplete(result));
|
||
+ });
|
||
+
|
||
+ self.sign_in_state = SignInState::DeviceCode(DeviceCodeState { prompt, task });
|
||
+ self.event_tx.send(AppEvent::RequestRedraw);
|
||
+ }
|
||
+
|
||
+ fn start_browser_login(&mut self) {
|
||
let opts = ServerOptions::new(
|
||
self.code_home.clone(),
|
||
CLIENT_ID.to_string(),
|
||
@@ -371,12 +503,11 @@ impl AuthModeWidget {
|
||
let join_handle = tokio::spawn(async move {
|
||
spawn_completion_poller(child, event_tx).await;
|
||
});
|
||
- self.sign_in_state =
|
||
- SignInState::ChatGptContinueInBrowser(ContinueInBrowserState {
|
||
- auth_url,
|
||
- shutdown_handle: Some(shutdown_handle),
|
||
- _login_wait_handle: Some(join_handle),
|
||
- });
|
||
+ self.sign_in_state = SignInState::ContinueInBrowser(ContinueInBrowserState {
|
||
+ auth_url,
|
||
+ shutdown_handle: Some(shutdown_handle),
|
||
+ _login_wait_handle: Some(join_handle),
|
||
+ });
|
||
self.event_tx.send(AppEvent::RequestRedraw);
|
||
}
|
||
Err(e) => {
|
||
@@ -400,8 +531,32 @@ impl AuthModeWidget {
|
||
self.event_tx.send(AppEvent::RequestRedraw);
|
||
}
|
||
|
||
- pub(crate) fn apply_chatgpt_login_side_effects(&mut self) {
|
||
+ pub(crate) fn apply_login_side_effects(&mut self) {
|
||
self.login_status = LoginStatus::AuthMode(AuthMode::ChatGPT);
|
||
+ // A Hanzo account is not a ChatGPT plan: neither the flag nor the model
|
||
+ // swap below says anything true about it. What a fresh Hanzo sign-in
|
||
+ // DOES say is which provider this session runs: if the shell's vendor
|
||
+ // key had picked one implicitly, the account the user just created
|
||
+ // wins, model and all.
|
||
+ if self.hanzo {
|
||
+ if let Ok(mut args) = self.chat_widget_args.lock() {
|
||
+ if args.config.model_provider_id != code_core::HANZO_PROVIDER_ID {
|
||
+ if let Some(provider) = code_core::built_in_model_providers(None)
|
||
+ .get(code_core::HANZO_PROVIDER_ID)
|
||
+ .cloned()
|
||
+ {
|
||
+ args.config.model_provider_id = code_core::HANZO_PROVIDER_ID.to_string();
|
||
+ args.config.model_provider = provider;
|
||
+ let model = code_core::config::HANZO_DEFAULT_MODEL.to_string();
|
||
+ let family = find_family_for_model(&model)
|
||
+ .unwrap_or_else(|| derive_default_model_family(&model));
|
||
+ args.config.model = model;
|
||
+ args.config.model_family = family;
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+ return;
|
||
+ }
|
||
if let Ok(mut args) = self.chat_widget_args.lock() {
|
||
args.config.using_chatgpt_auth = true;
|
||
if args
|
||
@@ -409,7 +564,7 @@ impl AuthModeWidget {
|
||
.model
|
||
.eq_ignore_ascii_case("gpt-5.1")
|
||
{
|
||
- let new_model = GPT_5_CODEX_MEDIUM_MODEL.to_string();
|
||
+ let new_model = GPT_5_DEV_MEDIUM_MODEL.to_string();
|
||
args.config.model = new_model.clone();
|
||
|
||
let family = find_family_for_model(&new_model)
|
||
@@ -440,9 +595,10 @@ impl StepStateProvider for AuthModeWidget {
|
||
match &self.sign_in_state {
|
||
SignInState::PickMode
|
||
| SignInState::EnvVarMissing
|
||
- | SignInState::ChatGptContinueInBrowser(_)
|
||
- | SignInState::ChatGptSuccessMessage => StepState::InProgress,
|
||
- SignInState::ChatGptSuccess | SignInState::EnvVarFound => StepState::Complete,
|
||
+ | SignInState::ContinueInBrowser(_)
|
||
+ | SignInState::DeviceCode(_)
|
||
+ | SignInState::SuccessMessage => StepState::InProgress,
|
||
+ SignInState::Success | SignInState::EnvVarFound => StepState::Complete,
|
||
}
|
||
}
|
||
}
|
||
@@ -453,14 +609,17 @@ impl WidgetRef for AuthModeWidget {
|
||
SignInState::PickMode => {
|
||
self.render_pick_mode(area, buf);
|
||
}
|
||
- SignInState::ChatGptContinueInBrowser(_) => {
|
||
+ SignInState::ContinueInBrowser(_) => {
|
||
self.render_continue_in_browser(area, buf);
|
||
}
|
||
- SignInState::ChatGptSuccessMessage => {
|
||
- self.render_chatgpt_success_message(area, buf);
|
||
+ SignInState::DeviceCode(_) => {
|
||
+ self.render_device_code(area, buf);
|
||
+ }
|
||
+ SignInState::SuccessMessage => {
|
||
+ self.render_success_message(area, buf);
|
||
}
|
||
- SignInState::ChatGptSuccess => {
|
||
- self.render_chatgpt_success(area, buf);
|
||
+ SignInState::Success => {
|
||
+ self.render_success(area, buf);
|
||
}
|
||
SignInState::EnvVarMissing => {
|
||
self.render_env_var_missing(area, buf);
|
||
diff --git a/tui/src/onboarding/onboarding_screen.rs b/tui/src/onboarding/onboarding_screen.rs
|
||
index f5df542..7be82be 100644
|
||
--- a/tui/src/onboarding/onboarding_screen.rs
|
||
+++ b/tui/src/onboarding/onboarding_screen.rs
|
||
@@ -69,6 +69,12 @@ impl OnboardingScreen {
|
||
show_login_screen,
|
||
login_status,
|
||
} = args;
|
||
+ // Sign-in leads with Hanzo unless the user NAMED another provider. A
|
||
+ // vendor key exported in the shell picks the run's provider, but it is
|
||
+ // not a decision about whose account this product signs into.
|
||
+ let hanzo = chat_widget_args.config.model_provider_id == code_core::HANZO_PROVIDER_ID
|
||
+ || !chat_widget_args.config.model_provider_explicit;
|
||
+ let env_key = chat_widget_args.config.model_provider.env_key.clone();
|
||
let shared_chat_args = Arc::new(Mutex::new(chat_widget_args));
|
||
|
||
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
||
@@ -77,6 +83,8 @@ impl OnboardingScreen {
|
||
if show_login_screen {
|
||
steps.push(Step::Auth(AuthModeWidget {
|
||
event_tx: event_tx.clone(),
|
||
+ // Row 0 — signing in to the configured provider, which the
|
||
+ // default `hanzo` makes Hanzo IAM.
|
||
highlighted_mode: AuthMode::ChatGPT,
|
||
error: None,
|
||
sign_in_state: SignInState::PickMode,
|
||
@@ -84,6 +92,8 @@ impl OnboardingScreen {
|
||
login_status,
|
||
preferred_auth_method: code_login::AuthMode::ApiKey,
|
||
chat_widget_args: shared_chat_args.clone(),
|
||
+ hanzo,
|
||
+ env_key,
|
||
}))
|
||
}
|
||
let is_git_repo = get_git_repo_root(&cwd).is_some();
|
||
@@ -119,8 +129,8 @@ impl OnboardingScreen {
|
||
if let Some(Step::Auth(state)) = current_step {
|
||
match result {
|
||
Ok(()) => {
|
||
- state.apply_chatgpt_login_side_effects();
|
||
- state.sign_in_state = SignInState::ChatGptSuccessMessage;
|
||
+ state.apply_login_side_effects();
|
||
+ state.sign_in_state = SignInState::SuccessMessage;
|
||
self.event_tx.send(AppEvent::RequestRedraw);
|
||
let tx1 = self.event_tx.clone();
|
||
let tx2 = self.event_tx.clone();
|
||
diff --git a/tui/src/onboarding/trust_directory.rs b/tui/src/onboarding/trust_directory.rs
|
||
index 7d61549..b7cac8e 100644
|
||
--- a/tui/src/onboarding/trust_directory.rs
|
||
+++ b/tui/src/onboarding/trust_directory.rs
|
||
@@ -46,7 +46,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||
.add_modifier(Modifier::BOLD);
|
||
let mut lines: Vec<Line> = vec![
|
||
Line::from(vec![
|
||
- Span::styled("You are running Code in ", success_style),
|
||
+ Span::styled("You are running Hanzo Dev in ", success_style),
|
||
Span::styled(
|
||
self.cwd.to_string_lossy().to_string(),
|
||
Style::default().fg(colors::success()),
|
||
@@ -57,7 +57,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||
|
||
if self.is_git_repo {
|
||
lines.push(Line::from(
|
||
- " Since this folder is version controlled, you may wish to allow Code",
|
||
+ " Since this folder is version controlled, you may wish to allow Hanzo Dev",
|
||
));
|
||
lines.push(Line::from(
|
||
" to work in this folder without asking for approval.",
|
||
@@ -90,7 +90,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||
lines.push(create_option(
|
||
0,
|
||
TrustDirectorySelection::Trust,
|
||
- "Yes, allow Code to work in this folder without asking for approval",
|
||
+ "Yes, allow Hanzo Dev to work in this folder without asking for approval",
|
||
));
|
||
lines.push(create_option(
|
||
1,
|
||
@@ -101,7 +101,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||
lines.push(create_option(
|
||
0,
|
||
TrustDirectorySelection::Trust,
|
||
- "Allow Code to work in this folder without asking for approval",
|
||
+ "Allow Hanzo Dev to work in this folder without asking for approval",
|
||
));
|
||
lines.push(create_option(
|
||
1,
|
||
diff --git a/tui/src/onboarding/welcome.rs b/tui/src/onboarding/welcome.rs
|
||
index 336fc67..1890b70 100644
|
||
--- a/tui/src/onboarding/welcome.rs
|
||
+++ b/tui/src/onboarding/welcome.rs
|
||
@@ -20,7 +20,7 @@ impl WidgetRef for &WelcomeWidget {
|
||
let line1 = Line::from(vec![
|
||
Span::raw(">_ "),
|
||
Span::styled(
|
||
- "Welcome to Code",
|
||
+ "Welcome to Hanzo Dev",
|
||
Style::default().add_modifier(Modifier::BOLD),
|
||
),
|
||
]);
|
||
diff --git a/tui/src/sanitize.rs b/tui/src/sanitize.rs
|
||
index bab9ec6..323c8f5 100644
|
||
--- a/tui/src/sanitize.rs
|
||
+++ b/tui/src/sanitize.rs
|
||
@@ -36,7 +36,7 @@ pub fn sanitize_for_tui(input: &str, mode: Mode, opts: Options) -> String {
|
||
fn ascii_only_enabled() -> bool {
|
||
static ASCII_ONLY: OnceLock<bool> = OnceLock::new();
|
||
*ASCII_ONLY.get_or_init(|| {
|
||
- std::env::var("CODEX_TUI_ASCII")
|
||
+ std::env::var("DEV_TUI_ASCII")
|
||
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||
.unwrap_or(false)
|
||
})
|
||
diff --git a/tui/src/session_log.rs b/tui/src/session_log.rs
|
||
index ef1073e..a9e3afd 100644
|
||
--- a/tui/src/session_log.rs
|
||
+++ b/tui/src/session_log.rs
|
||
@@ -86,14 +86,14 @@ fn now_ts() -> String {
|
||
}
|
||
|
||
pub(crate) fn maybe_init(config: &Config) {
|
||
- let enabled = std::env::var("CODEX_TUI_RECORD_SESSION")
|
||
+ let enabled = std::env::var("DEV_TUI_RECORD_SESSION")
|
||
.map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||
.unwrap_or(false);
|
||
if !enabled {
|
||
return;
|
||
}
|
||
|
||
- let path = if let Ok(path) = std::env::var("CODEX_TUI_SESSION_LOG_PATH") {
|
||
+ let path = if let Ok(path) = std::env::var("DEV_TUI_SESSION_LOG_PATH") {
|
||
PathBuf::from(path)
|
||
} else {
|
||
let mut p = match code_core::config::log_dir(config) {
|
||
diff --git a/tui/src/shimmer.rs b/tui/src/shimmer.rs
|
||
deleted file mode 100644
|
||
index d8c5d9b..0000000
|
||
--- a/tui/src/shimmer.rs
|
||
+++ /dev/null
|
||
@@ -1,77 +0,0 @@
|
||
-use std::sync::OnceLock;
|
||
-use std::time::Duration;
|
||
-use std::time::Instant;
|
||
-
|
||
-use ratatui::style::Color;
|
||
-use ratatui::style::Modifier;
|
||
-use ratatui::style::Style;
|
||
-use ratatui::text::Span;
|
||
-
|
||
-static PROCESS_START: OnceLock<Instant> = OnceLock::new();
|
||
-
|
||
-fn elapsed_since_start() -> Duration {
|
||
- let start = PROCESS_START.get_or_init(Instant::now);
|
||
- start.elapsed()
|
||
-}
|
||
-
|
||
-pub(crate) fn shimmer_spans(text: &str) -> Vec<Span<'static>> {
|
||
- let chars: Vec<char> = text.chars().collect();
|
||
- if chars.is_empty() {
|
||
- return Vec::new();
|
||
- }
|
||
- // Use time-based sweep synchronized to process start.
|
||
- let padding = 10usize;
|
||
- let period = chars.len() + padding * 2;
|
||
- let sweep_seconds = 2.5f32;
|
||
- let pos_f =
|
||
- (elapsed_since_start().as_secs_f32() % sweep_seconds) / sweep_seconds * (period as f32);
|
||
- let pos = pos_f as usize;
|
||
- // Prefer our unified terminal capability detection so Windows Terminal and
|
||
- // other modern emulators that support truecolor aren't mistakenly treated
|
||
- // as 256‑color only (which breaks smooth gradients).
|
||
- let has_true_color = crate::theme::has_truecolor_terminal();
|
||
- let band_half_width = 3.0;
|
||
-
|
||
- let mut spans: Vec<Span<'static>> = Vec::with_capacity(chars.len());
|
||
- for (i, ch) in chars.iter().enumerate() {
|
||
- let i_pos = i as isize + padding as isize;
|
||
- let pos = pos as isize;
|
||
- let dist = (i_pos - pos).abs() as f32;
|
||
-
|
||
- let t = if dist <= band_half_width {
|
||
- let x = std::f32::consts::PI * (dist / band_half_width);
|
||
- 0.5 * (1.0 + x.cos())
|
||
- } else {
|
||
- 0.0
|
||
- };
|
||
- let brightness = 0.4 + 0.6 * t;
|
||
- let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
|
||
- let style = if has_true_color {
|
||
- // Allow custom RGB colors, as the implementation is thoughtfully
|
||
- // adjusting the level of the default foreground color.
|
||
- #[allow(clippy::disallowed_methods)]
|
||
- {
|
||
- Style::default()
|
||
- .fg(Color::Rgb(level, level, level))
|
||
- .add_modifier(Modifier::BOLD)
|
||
- }
|
||
- } else {
|
||
- Style::default()
|
||
- .fg(color_for_level(level))
|
||
- .add_modifier(Modifier::BOLD)
|
||
- };
|
||
- spans.push(Span::styled(ch.to_string(), style));
|
||
- }
|
||
- spans
|
||
-}
|
||
-
|
||
-fn color_for_level(level: u8) -> Color {
|
||
- // For shimmer effect, we map brightness levels to theme colors
|
||
- if level < 128 {
|
||
- crate::colors::text_dim()
|
||
- } else if level < 192 {
|
||
- crate::colors::text()
|
||
- } else {
|
||
- crate::colors::text_bright()
|
||
- }
|
||
-}
|
||
diff --git a/tui/src/slash_command.rs b/tui/src/slash_command.rs
|
||
index c50045d..d735958 100644
|
||
--- a/tui/src/slash_command.rs
|
||
+++ b/tui/src/slash_command.rs
|
||
@@ -6,7 +6,7 @@ use strum_macros::EnumIter;
|
||
use strum_macros::EnumString;
|
||
use strum_macros::IntoStaticStr;
|
||
|
||
-const BUILD_PROFILE: Option<&str> = option_env!("CODEX_PROFILE");
|
||
+const BUILD_PROFILE: Option<&str> = option_env!("DEV_PROFILE");
|
||
|
||
fn demo_command_enabled() -> bool {
|
||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||
@@ -113,12 +113,12 @@ impl SlashCommand {
|
||
SlashCommand::Reasoning => "change reasoning effort (minimal/low/medium/high)",
|
||
SlashCommand::Verbosity => "change text verbosity (high/medium/low)",
|
||
SlashCommand::New => "start a new chat during a conversation",
|
||
- SlashCommand::Init => "create an AGENTS.md file with instructions for Code",
|
||
+ SlashCommand::Init => "create an AGENTS.md file with instructions for Hanzo Dev",
|
||
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
||
- SlashCommand::Undo => "restore the workspace to the last Code snapshot",
|
||
+ SlashCommand::Undo => "restore the workspace to the last Hanzo Dev snapshot",
|
||
SlashCommand::Review => "review your changes for potential issues",
|
||
SlashCommand::Cloud => "browse, apply, and create cloud tasks",
|
||
- SlashCommand::Quit | SlashCommand::Exit => "exit Code",
|
||
+ SlashCommand::Quit | SlashCommand::Exit => "exit Hanzo Dev",
|
||
SlashCommand::Clear => "clear the terminal and start a new chat",
|
||
SlashCommand::Copy => "copy last response as markdown",
|
||
SlashCommand::Diff => "show git diff (including untracked files)",
|
||
@@ -145,8 +145,8 @@ impl SlashCommand {
|
||
SlashCommand::Mcp => "manage MCP servers",
|
||
SlashCommand::Perf => "performance tracing (on/off/show/reset)",
|
||
SlashCommand::Demo => "populate history with demo cells (dev/perf only)",
|
||
- SlashCommand::Login => "manage Code sign-ins (add/select/disconnect)",
|
||
- SlashCommand::Logout => "log out of Code",
|
||
+ SlashCommand::Login => "manage Hanzo Dev sign-ins (add/select/disconnect)",
|
||
+ SlashCommand::Logout => "log out of Hanzo Dev",
|
||
#[cfg(debug_assertions)]
|
||
SlashCommand::TestApproval => "test approval request",
|
||
}
|
||
diff --git a/tui/src/streaming/controller.rs b/tui/src/streaming/controller.rs
|
||
index 17560ef..46b6881 100644
|
||
--- a/tui/src/streaming/controller.rs
|
||
+++ b/tui/src/streaming/controller.rs
|
||
@@ -154,7 +154,7 @@ pub(crate) fn set_last_sequence_number(&mut self, kind: StreamKind, seq: Option<
|
||
/// This avoids mutating the model content so title detection remains intact.
|
||
fn maybe_append_reasoning_debug_marker(&self, kind: StreamKind, lines: &mut Vec<Line<'static>>) {
|
||
// Only when explicitly enabled to avoid noise in normal use.
|
||
- let enabled = match std::env::var("CODEX_DEBUG_REASONING_INDEX") {
|
||
+ let enabled = match std::env::var("DEV_DEBUG_REASONING_INDEX") {
|
||
Ok(val) => !val.is_empty() && val != "0",
|
||
Err(_) => false,
|
||
};
|
||
@@ -599,7 +599,7 @@ pub(crate) fn set_last_sequence_number(&mut self, kind: StreamKind, seq: Option<
|
||
// For finalized Reasoning blocks, include a debug marker as a separate line.
|
||
if matches!(kind, StreamKind::Reasoning) {
|
||
// Append a FINAL marker variant to distinguish from streaming commits
|
||
- let enabled = match std::env::var("CODEX_DEBUG_REASONING_INDEX") {
|
||
+ let enabled = match std::env::var("DEV_DEBUG_REASONING_INDEX") {
|
||
Ok(val) => !val.is_empty() && val != "0",
|
||
Err(_) => false,
|
||
};
|
||
diff --git a/tui/src/tui.rs b/tui/src/tui.rs
|
||
index d8ed738..82e9926 100644
|
||
--- a/tui/src/tui.rs
|
||
+++ b/tui/src/tui.rs
|
||
@@ -135,7 +135,7 @@ pub fn init(config: &Config) -> Result<(Tui, TerminalInfo)> {
|
||
)),
|
||
Clear(ClearType::All),
|
||
MoveTo(0, 0),
|
||
- crossterm::terminal::SetTitle("Code"),
|
||
+ crossterm::terminal::SetTitle("Hanzo Dev"),
|
||
crossterm::terminal::EnableLineWrap
|
||
)?;
|
||
|
||
@@ -293,7 +293,7 @@ pub fn enter_alt_screen_only(theme_fg: ratatui::style::Color, theme_bg: ratatui:
|
||
SetColors(crossterm::style::Colors::new(theme_fg.into(), theme_bg.into())),
|
||
Clear(ClearType::All),
|
||
MoveTo(0, 0),
|
||
- crossterm::terminal::SetTitle("Code"),
|
||
+ crossterm::terminal::SetTitle("Hanzo Dev"),
|
||
crossterm::terminal::EnableLineWrap
|
||
)?;
|
||
Ok(())
|
||
diff --git a/tui/src/updates.rs b/tui/src/updates.rs
|
||
index a9a0f3c..456067d 100644
|
||
--- a/tui/src/updates.rs
|
||
+++ b/tui/src/updates.rs
|
||
@@ -144,10 +144,10 @@ struct ReleaseInfo {
|
||
}
|
||
|
||
const VERSION_FILENAME: &str = "version.json";
|
||
-const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/just-every/code/releases/latest";
|
||
-const CURRENT_RELEASE_REPO: &str = "just-every/code";
|
||
-const LEGACY_RELEASE_REPO: &str = "openai/codex";
|
||
-pub const CODE_RELEASE_URL: &str = "https://github.com/just-every/code/releases/latest";
|
||
+const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/hanzoai/dev/releases/latest";
|
||
+const CURRENT_RELEASE_REPO: &str = "hanzoai/dev";
|
||
+const LEGACY_RELEASE_REPO: &str = "just-every/code";
|
||
+pub const CODE_RELEASE_URL: &str = "https://github.com/hanzoai/dev/releases/latest";
|
||
|
||
const CACHE_TTL_HOURS: i64 = 20;
|
||
const MAX_CLOCK_SKEW_MINUTES: i64 = 5;
|
||
@@ -179,15 +179,15 @@ fn version_filepath(config: &Config) -> PathBuf {
|
||
}
|
||
|
||
pub fn resolve_upgrade_resolution() -> UpgradeResolution {
|
||
- if std::env::var_os("CODEX_MANAGED_BY_NPM").is_some() {
|
||
+ if std::env::var_os("DEV_MANAGED_BY_NPM").is_some() {
|
||
return UpgradeResolution::Command {
|
||
command: vec![
|
||
"npm".to_string(),
|
||
"install".to_string(),
|
||
"-g".to_string(),
|
||
- "@just-every/code@latest".to_string(),
|
||
+ "@hanzo/dev@latest".to_string(),
|
||
],
|
||
- display: "npm install -g @just-every/code@latest".to_string(),
|
||
+ display: "npm install -g @hanzo/dev@latest".to_string(),
|
||
};
|
||
}
|
||
|
||
@@ -199,9 +199,9 @@ pub fn resolve_upgrade_resolution() -> UpgradeResolution {
|
||
command: vec![
|
||
"brew".to_string(),
|
||
"upgrade".to_string(),
|
||
- "code".to_string(),
|
||
+ "hanzo-dev".to_string(),
|
||
],
|
||
- display: "brew upgrade code".to_string(),
|
||
+ display: "brew upgrade hanzo-dev".to_string(),
|
||
};
|
||
}
|
||
}
|
||
diff --git a/tui/src/user_approval_widget.rs b/tui/src/user_approval_widget.rs
|
||
index a46b017..65f27f0 100644
|
||
--- a/tui/src/user_approval_widget.rs
|
||
+++ b/tui/src/user_approval_widget.rs
|
||
@@ -96,14 +96,14 @@ impl UserApprovalWidget<'_> {
|
||
command, reason, ..
|
||
} => {
|
||
let cmd = strip_bash_lc_and_escape(command);
|
||
- // Present a single-line summary without cwd: "codex wants to run: <cmd>"
|
||
+ // Present a single-line summary without cwd: "Hanzo Dev wants to run <cmd>"
|
||
let mut cmd_span: Span = cmd.clone().into();
|
||
cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM);
|
||
let mut contents: Vec<Line> = vec![
|
||
Line::from(""), // extra spacing above the prompt
|
||
Line::from(vec![
|
||
"? ".fg(crate::colors::info()),
|
||
- "Code wants to run ".bold(),
|
||
+ "Hanzo Dev wants to run ".bold(),
|
||
cmd_span,
|
||
]),
|
||
Line::from(""),
|
||
diff --git a/tui/tests/bottom_spacer_clip_regression.rs b/tui/tests/bottom_spacer_clip_regression.rs
|
||
index 96031a1..384d92a 100644
|
||
--- a/tui/tests/bottom_spacer_clip_regression.rs
|
||
+++ b/tui/tests/bottom_spacer_clip_regression.rs
|
||
@@ -65,27 +65,59 @@ fn bottom_spacer_skips_when_history_fits() {
|
||
let _ = render_chat_widget_to_vt100(&mut harness, 120, 40);
|
||
let metrics = layout_metrics(&harness);
|
||
|
||
- assert!(
|
||
- metrics.last_max_scroll >= 4,
|
||
- "with the command header we intentionally reserve at least four spacer rows, got {}",
|
||
- metrics.last_max_scroll
|
||
+ assert_eq!(
|
||
+ metrics.last_max_scroll, 0,
|
||
+ "a transcript that fits the viewport has nothing to scroll"
|
||
+ );
|
||
+ assert_eq!(
|
||
+ harness.bottom_spacer_lines(),
|
||
+ 0,
|
||
+ "the spacer only exists to protect a last row the viewport would clip"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn bottom_spacer_hysteresis_requests_followup_frame() {
|
||
- // First known viewport combo that reliably triggers the hysteresis path
|
||
- // without exhausting file descriptors by brute-force searching.
|
||
- let activate_h = 32;
|
||
- let release_h = 18;
|
||
-
|
||
+ // The spacer shrinks one frame late on purpose. To see that, the first
|
||
+ // viewport must ask for two spacer rows (history height an exact multiple
|
||
+ // of the viewport) and the second for one — so both heights are derived
|
||
+ // from the transcript as rendered rather than assumed.
|
||
let mut harness = ChatWidgetHarness::new();
|
||
seed_overflow_wrapped_transcript(&mut harness);
|
||
|
||
+ // Grow the transcript one line at a time until some viewport height in
|
||
+ // range divides it exactly.
|
||
+ // The multiple is of the history viewport, which is the terminal minus
|
||
+ // the rows the composer keeps.
|
||
+ let probe_h = 24;
|
||
+ let (base, chrome, activate_h) = loop {
|
||
+ let _ = render_chat_widget_to_vt100(&mut harness, 100, probe_h);
|
||
+ let metrics = layout_metrics(&harness);
|
||
+ let chrome = probe_h - metrics.last_viewport_height;
|
||
+ let base = metrics.last_max_scroll + metrics.last_viewport_height
|
||
+ - harness.bottom_spacer_lines();
|
||
+ if let Some(h) = (chrome + 8..=probe_h)
|
||
+ .rev()
|
||
+ .find(|h| base % (h - chrome) == 0 && base > h - chrome)
|
||
+ {
|
||
+ break (base, chrome, h);
|
||
+ }
|
||
+ harness.push_assistant_markdown("One more line.");
|
||
+ };
|
||
+ let release_h = (chrome + 8..activate_h)
|
||
+ .rev()
|
||
+ .find(|h| {
|
||
+ let v = h - chrome;
|
||
+ let r = base % v;
|
||
+ r > 2 && r < v - 2
|
||
+ })
|
||
+ .expect("some viewport height leaves a middling remainder");
|
||
+
|
||
let _ = render_chat_widget_to_vt100(&mut harness, 100, activate_h);
|
||
- assert!(
|
||
- harness.bottom_spacer_lines() > 0,
|
||
- "spacer should activate for the overflowing transcript"
|
||
+ assert_eq!(
|
||
+ harness.bottom_spacer_lines(),
|
||
+ 2,
|
||
+ "an exact multiple asks for two spacer rows"
|
||
);
|
||
|
||
harness.take_scheduled_frame_events();
|
||
diff --git a/tui/tests/mid_turn_assistant_styling.rs b/tui/tests/mid_turn_assistant_styling.rs
|
||
index 72c2cd5..bd15662 100644
|
||
--- a/tui/tests/mid_turn_assistant_styling.rs
|
||
+++ b/tui/tests/mid_turn_assistant_styling.rs
|
||
@@ -1,13 +1,14 @@
|
||
-//! Regression test: mid-turn Answer outputs should not render assistant gutter/bold styling.
|
||
+//! Regression test: mid-turn Answer outputs are plain messages; only the final answer is the assistant message.
|
||
|
||
#![cfg(test)]
|
||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||
|
||
use code_core::protocol::{AgentMessageEvent, Event, EventMsg, OrderMeta};
|
||
+use code_core::history::state::HistoryRecord;
|
||
use code_tui::test_helpers::{render_chat_widget_to_vt100, ChatWidgetHarness};
|
||
|
||
#[test]
|
||
-fn mid_turn_answer_suppresses_bullet_gutter() {
|
||
+fn mid_turn_answer_renders_as_progress() {
|
||
let mut harness = ChatWidgetHarness::new();
|
||
|
||
// Start a turn.
|
||
@@ -60,15 +61,16 @@ fn mid_turn_answer_suppresses_bullet_gutter() {
|
||
});
|
||
|
||
let output = render_chat_widget_to_vt100(&mut harness, 80, 24);
|
||
-
|
||
assert!(output.contains("Progress update"));
|
||
+ assert!(output.contains("Final answer"));
|
||
+
|
||
assert!(
|
||
- !output.contains(" • Progress update"),
|
||
- "mid-turn assistant messages should not show bullet gutter"
|
||
+ is_mid_turn(&mut harness, "Progress update"),
|
||
+ "a mid-turn answer renders as a progress line, not the final word"
|
||
);
|
||
assert!(
|
||
- output.contains(" • Final answer"),
|
||
- "final assistant message should retain bullet gutter"
|
||
+ !is_mid_turn(&mut harness, "Final answer"),
|
||
+ "the final answer renders as the assistant message"
|
||
);
|
||
}
|
||
|
||
@@ -134,7 +136,22 @@ fn missing_task_complete_does_not_stick_mid_turn_across_turns() {
|
||
});
|
||
|
||
let output = render_chat_widget_to_vt100(&mut harness, 80, 24);
|
||
+ assert!(output.contains("First answer"));
|
||
+ assert!(output.contains("Second answer"));
|
||
+ assert!(!is_mid_turn(&mut harness, "First answer"));
|
||
+ assert!(!is_mid_turn(&mut harness, "Second answer"));
|
||
+}
|
||
|
||
- assert!(output.contains(" • First answer"));
|
||
- assert!(output.contains(" • Second answer"));
|
||
+/// Whether the assistant message carrying `text` is a mid-turn one — the
|
||
+/// kind that renders as a plain progress line rather than the answer.
|
||
+fn is_mid_turn(harness: &mut ChatWidgetHarness, text: &str) -> bool {
|
||
+ code_tui::test_helpers::history_records(harness)
|
||
+ .into_iter()
|
||
+ .find_map(|record| match record {
|
||
+ HistoryRecord::AssistantMessage(state) if state.markdown.contains(text) => {
|
||
+ Some(state.mid_turn)
|
||
+ }
|
||
+ _ => None,
|
||
+ })
|
||
+ .unwrap_or_else(|| panic!("no assistant message containing {text:?}"))
|
||
}
|
||
diff --git a/tui/tests/resume_replay.rs b/tui/tests/resume_replay.rs
|
||
index 57d1dd8..45a108c 100644
|
||
--- a/tui/tests/resume_replay.rs
|
||
+++ b/tui/tests/resume_replay.rs
|
||
@@ -12,15 +12,12 @@ use code_protocol::models::{ContentItem, ResponseItem};
|
||
use code_tui::test_helpers::{render_chat_widget_to_vt100, ChatWidgetHarness};
|
||
use serde_json::to_value;
|
||
|
||
+/// Every replayed assistant message in these tests says "Working"; a
|
||
+/// duplicate shows up as a second line that starts with it.
|
||
fn assistant_cell_count(screen: &str) -> usize {
|
||
screen
|
||
.lines()
|
||
- .filter(|line| line.trim_start().starts_with("• "))
|
||
- .filter(|line| {
|
||
- let trimmed = line.trim_start();
|
||
- !(trimmed.contains("How can I help you today?")
|
||
- || trimmed.contains("I can help with various tasks"))
|
||
- })
|
||
+ .filter(|line| line.trim_start().starts_with("Working"))
|
||
.count()
|
||
}
|
||
|
||
diff --git a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_collapsed_reasoning_120x40.snap b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_collapsed_reasoning_120x40.snap
|
||
index efe7cb7..a010210 100644
|
||
--- a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_collapsed_reasoning_120x40.snap
|
||
+++ b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_collapsed_reasoning_120x40.snap
|
||
@@ -2,25 +2,26 @@
|
||
source: tui/tests/bottom_spacer_clip_regression.rs
|
||
expression: collapsed
|
||
---
|
||
- ★ /code - perform a coding task (multiple agents)
|
||
+ /code - perform a coding task (multiple agents)
|
||
|
||
|
||
- • Hello! How can I help you today?
|
||
+ Hello! How can I help you today?
|
||
|
||
|
||
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- › Share your reasoning and then your final answer.
|
||
+ Share your reasoning and then your final answer.
|
||
|
||
- Having reasoned through the constraints, the assistant now presents a final answer summarising why the spacer
|
||
- • preserves the last visible line.
|
||
+ Having reasoned through the constraints, the assistant now presents a final answer summarising why the spacer
|
||
+ preserves the last visible line.
|
||
|
||
- | What can I code for you today? |
|
||
+ ✧ Thinking...
|
||
+ What would you like to work on?
|
||
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_expanded_reasoning_120x40.snap b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_expanded_reasoning_120x40.snap
|
||
index ef13352..acf446e 100644
|
||
--- a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_expanded_reasoning_120x40.snap
|
||
+++ b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_expanded_reasoning_120x40.snap
|
||
@@ -2,25 +2,26 @@
|
||
source: tui/tests/bottom_spacer_clip_regression.rs
|
||
expression: expanded
|
||
---
|
||
- ★ /code - perform a coding task (multiple agents)
|
||
+ /code - perform a coding task (multiple agents)
|
||
|
||
|
||
- • Hello! How can I help you today?
|
||
+ Hello! How can I help you today?
|
||
|
||
|
||
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- › Share your reasoning and then your final answer.
|
||
+ Share your reasoning and then your final answer.
|
||
|
||
- Having reasoned through the constraints, the assistant now presents a final answer summarising why the spacer
|
||
- • preserves the last visible line.
|
||
+ Having reasoned through the constraints, the assistant now presents a final answer summarising why the spacer
|
||
+ preserves the last visible line.
|
||
|
||
- | What can I code for you today? |
|
||
+ ✧ Thinking...
|
||
+ What would you like to work on?
|
||
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_overflow_wrapped_100x30.snap b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_overflow_wrapped_100x30.snap
|
||
index 080ce24..c4fe9e9 100644
|
||
--- a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_overflow_wrapped_100x30.snap
|
||
+++ b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_overflow_wrapped_100x30.snap
|
||
@@ -2,31 +2,32 @@
|
||
source: tui/tests/bottom_spacer_clip_regression.rs
|
||
expression: output
|
||
---
|
||
- | Every Code |
|
||
+ We repeat the insight to guarantee the total height exceeds the viewport.
|
||
|
||
- with scroll offsets.
|
||
+ User follow-up #5: keep adding details about history layout calculations and how they interact
|
||
+ with scroll offsets.
|
||
|
||
+ Assistant reply #6 contains a deliberately verbose paragraph that discusses wrapping,
|
||
+ stream padding, viewport math, and the importance of not clipping the final line.
|
||
|
||
- Assistant reply #6 contains a deliberately verbose paragraph that discusses wrapping,
|
||
- • stream padding, viewport math, and the importance of not clipping the final line.
|
||
+ We repeat the insight to guarantee the total height exceeds the viewport.
|
||
|
||
- We repeat the insight to guarantee the total height exceeds the viewport.
|
||
+ User follow-up #6: keep adding details about history layout calculations and how they interact
|
||
+ with scroll offsets.
|
||
|
||
- › User follow-up #6: keep adding details about history layout calculations and how they interact
|
||
- with scroll offsets.
|
||
+ Assistant reply #7 contains a deliberately verbose paragraph that discusses wrapping,
|
||
+ stream padding, viewport math, and the importance of not clipping the final line.
|
||
|
||
- Assistant reply #7 contains a deliberately verbose paragraph that discusses wrapping,
|
||
- • stream padding, viewport math, and the importance of not clipping the final line.
|
||
+ We repeat the insight to guarantee the total height exceeds the viewport.
|
||
|
||
- We repeat the insight to guarantee the total height exceeds the viewport.
|
||
+ User follow-up #7: keep adding details about history layout calculations and how they interact
|
||
+ with scroll offsets.
|
||
|
||
- › User follow-up #7: keep adding details about history layout calculations and how they interact
|
||
- with scroll offsets.
|
||
+ Final assistant response to seal the deal with yet another wrapped explanation.
|
||
+ It mentions CODE_TUI_ANTICUTOFF repeatedly to highlight the feature’s role in protecting the
|
||
+ last row of history.
|
||
|
||
- Final assistant response to seal the deal with yet another wrapped explanation.
|
||
- • It mentions CODE_TUI_ANTICUTOFF repeatedly to highlight the feature’s role in protecting the
|
||
- last row of history.
|
||
|
||
- | What can I code for you today? |
|
||
+ What would you like to work on?
|
||
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_short_wrapped_80x24.snap b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_short_wrapped_80x24.snap
|
||
index c8cfba7..ec42f58 100644
|
||
--- a/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_short_wrapped_80x24.snap
|
||
+++ b/tui/tests/snapshots/bottom_spacer_clip_regression__bottom_spacer_short_wrapped_80x24.snap
|
||
@@ -2,25 +2,25 @@
|
||
source: tui/tests/bottom_spacer_clip_regression.rs
|
||
expression: output
|
||
---
|
||
- | Every Code |
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
- - Running commands
|
||
|
||
+ Could you provide a detailed overview of how the anti-clipping spacer works?
|
||
|
||
+ Make sure the explanation spans multiple lines so we can exercise wrapping
|
||
+ in the viewport.
|
||
|
||
- › Could you provide a detailed overview of how the anti-clipping spacer
|
||
- works?
|
||
|
||
- Make sure the explanation spans multiple lines so we can exercise wrapping
|
||
- in the viewport.
|
||
+ Absolutely! The spacer is activated whenever the rendered history height
|
||
+ lands directly on or near the viewport boundary.
|
||
|
||
- Absolutely! The spacer is activated whenever the rendered history height
|
||
- • lands directly on or near the viewport boundary.
|
||
+ This explanation intentionally repeats a few phrases so that the text wraps
|
||
+ even on wider terminals, ensuring we cover the regression scenario where the
|
||
+ final row historically vanished.
|
||
|
||
- This explanation intentionally repeats a few phrases so that the text wraps
|
||
- even on wider terminals, ensuring we cover the regression scenario where the
|
||
- final row historically vanished.
|
||
|
||
- | What can I code for you today? |
|
||
+ What would you like to work on?
|
||
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_run_grouped_plain_tool_name.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_run_grouped_plain_tool_name.snap
|
||
index 2c21180..546d84b 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_run_grouped_plain_tool_name.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_run_grouped_plain_tool_name.snap
|
||
@@ -2,34 +2,33 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
|
||
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- › Kick off QA bot regression run
|
||
+ Kick off QA bot regression run
|
||
|
||
- +- qa-bot
|
||
- |
|
||
- | Run targeted regression suite
|
||
- |
|
||
- | Agents
|
||
- | • claude running (Xs)
|
||
- |
|
||
- | Actions
|
||
- | MM:SS Started agent run for qa-bot
|
||
- |
|
||
- +- [Ctrl+A] Expand · [Esc] Stop
|
||
+ +- qa-bot
|
||
+ |
|
||
+ | Run targeted regression suite
|
||
+ |
|
||
+ | Agents
|
||
+ | • claude running (Xs)
|
||
+ |
|
||
+ | Actions
|
||
+ | MM:SS Started agent run for qa-bot
|
||
+ |
|
||
+ +- [Ctrl+A] Expand · [Esc] Stop
|
||
+
|
||
+
|
||
+ ✶ Responding...
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------- ✶ Responding... -----------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_status_missing_batch_displays_error.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_status_missing_batch_displays_error.snap
|
||
index 2b65bed..978aa63 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_status_missing_batch_displays_error.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__agent_status_missing_batch_displays_error.snap
|
||
@@ -2,22 +2,19 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- › /agents
|
||
+ /agents
|
||
|
||
- ✖ ⚠️ status_update: missing agent batch_id. tool=agent_status
|
||
- agents=orphan-agent
|
||
+ ⚠️ status_update: missing agent batch_id. tool=agent_status
|
||
+ agents=orphan-agent
|
||
+
|
||
+ ✶ Using tools...
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------- ✶ Using tools... -----------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__agents_terminal_overlay_full_details.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__agents_terminal_overlay_full_details.snap
|
||
index 68c26b8..e56bcf0 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__agents_terminal_overlay_full_details.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__agents_terminal_overlay_full_details.snap
|
||
@@ -2,9 +2,6 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: "&output"
|
||
---
|
||
- +--------------------------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +--------------------------------------------------------------------------------------------+
|
||
+ Agents - Ctrl+A to close-------------------------------------------------------------------+
|
||
| Filter 1 All - 2 Running - 3 Failed - 4 Done - 5 Review Sort: ( Recent ▼ ) |
|
||
| +----------------------++----------------------------------------------------------------+ |
|
||
@@ -28,6 +25,9 @@ expression: "&output"
|
||
| | ||| Reviewing eng updates | |
|
||
| | ||| STATUS | |
|
||
| | ||| MM:SS Status → Completed | |
|
||
+ | | ||| PROGRESS | |
|
||
+ | | ||| MM:SS Synthesizing highlights | |
|
||
+ | | ||| RESULT | |
|
||
| +----------------------++----------------------------------------------------------------+ |
|
||
| [↑/↓/←/→] Navigate [1-5] Filter [S] Sort [H/A] Toggle Details [X] Stop [Ctrl+A] |
|
||
+--------------------------------------------------------------------------------------------+
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_action_transitions.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_action_transitions.snap
|
||
index db3dede..d9c10f1 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_action_transitions.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_action_transitions.snap
|
||
@@ -2,77 +2,76 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: "frames.join(\"\\n---FRAME---\\n\")"
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Comparing diffs against last run. ✶ Thinking (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
+
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Ready: run cargo test --workspace ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
+
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > waiting for code review... ✶ Awaiting review (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
+
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Queued new command for execution. ✶ Thinking (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_cli_progress_header.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_cli_progress_header.snap
|
||
index b2fee15..6fa1923 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_cli_progress_header.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_cli_progress_header.snap
|
||
@@ -2,40 +2,39 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: "frames.join(\"\\n---FRAME---\\n\")"
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Ready: run cargo check ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- - Running commands
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Running cargo check... · Installed dependencies ✶ Thinking (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- - Running commands
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Running cargo check... · Installed dependencies ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_continue_mode_transitions.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_continue_mode_transitions.snap
|
||
index d0a30fb..120b990 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_continue_mode_transitions.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_continue_mode_transitions.snap
|
||
@@ -2,58 +2,57 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: "frames.join(\"\\n---FRAME---\\n\")"
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Auto Drive ready to run cargo nextest ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
+
|
||
|
||
Auto Drive > Auto Drive ready to run cargo nextest ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
+
|
||
+
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ I can help with various tasks including:
|
||
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Auto Drive ready to run cargo nextest ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_countdown_auto_submit.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_countdown_auto_submit.snap
|
||
index 0d3b9dd..8689713 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_countdown_auto_submit.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_countdown_auto_submit.snap
|
||
@@ -2,58 +2,57 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: "frames.join(\"\\n---FRAME---\\n\")"
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Auto Drive queued cargo fmt --check ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Disabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
+
|
||
|
||
Auto Drive > Auto Drive queued cargo fmt --check ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Disabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
+
|
||
+
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ I can help with various tasks including:
|
||
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Running cargo fmt --check. ✶ Thinking (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Disabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_intro_animation_during_settings_toggle.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_intro_animation_during_settings_toggle.snap
|
||
index bc80938..009b8eb 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_intro_animation_during_settings_toggle.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_intro_animation_during_settings_toggle.snap
|
||
@@ -2,58 +2,57 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: "frames.join(\"\\n---FRAME---\\n\")"
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Mapping strategy... ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
+
|
||
|
||
Auto Drive > Mapping strategy... ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
+
|
||
+
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ I can help with various tasks including:
|
||
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Coordinator is thinking... ✶ Thinking (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_manual_mode_waits.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_manual_mode_waits.snap
|
||
index d7fa15b..b8b0f36 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_manual_mode_waits.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_manual_mode_waits.snap
|
||
@@ -2,20 +2,19 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: frame
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > Auto Drive prepared to run justfile target ✶ Running (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_footer_persists.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_footer_persists.snap
|
||
index 5a255c9..e2783b0 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_footer_persists.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_footer_persists.snap
|
||
@@ -2,20 +2,19 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: frame
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > waiting for code review... ✶ Awaiting review (Xs)
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_resume_returns_to_running.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_resume_returns_to_running.snap
|
||
index bbd5a15..1e28571 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_resume_returns_to_running.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__auto_drive_review_resume_returns_to_running.snap
|
||
@@ -2,38 +2,37 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: "frames.join(\"\\n---FRAME---\\n\")"
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > <status redacted>
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
---FRAME---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
|
||
- • I can help with various tasks including:
|
||
+ Hello! How can I help you today?
|
||
+
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
Auto Drive > <status redacted>
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
• Agents Enabled • Diagnostics Enabled API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_desired_layout.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_desired_layout.snap
|
||
index a75b97b..30e3855 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_desired_layout.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_desired_layout.snap
|
||
@@ -2,34 +2,33 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
|
||
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- › Open docs and find login button
|
||
+ Open docs and find login button
|
||
|
||
- +- Browser (headless): example.com
|
||
- |
|
||
- | Actions
|
||
- | MM:SS Opened https://example.com/docs
|
||
- | MM:SS Clicked Sign in button
|
||
- | MM:SS Scrolled dy=640
|
||
- | MM:SS Typed docs search
|
||
- |
|
||
- | Console: cdp warning: Refused to load script from cdn.example.com
|
||
- |
|
||
- +- [Ctrl+B] View · [Esc] Stop
|
||
+ +- Browser (headless): example.com
|
||
+ |
|
||
+ | Actions
|
||
+ | MM:SS Opened https://example.com/docs
|
||
+ | MM:SS Clicked Sign in button
|
||
+ | MM:SS Scrolled dy=640
|
||
+ | MM:SS Typed docs search
|
||
+ |
|
||
+ | Console: cdp warning: Refused to load script from cdn.example.com
|
||
+ |
|
||
+ +- [Ctrl+B] View · [Esc] Stop
|
||
+
|
||
+
|
||
+ ✶ Browsing...
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------- ✶ Browsing... -----------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_with_unordered_actions.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_with_unordered_actions.snap
|
||
index be90dc3..185dd60 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_with_unordered_actions.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_grouped_with_unordered_actions.snap
|
||
@@ -2,34 +2,34 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- • Hello! How can I help you today?
|
||
+ /code - perform a coding task (multiple agents)
|
||
|
||
|
||
+ Hello! How can I help you today?
|
||
|
||
- • I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
|
||
+ I can help with various tasks including:
|
||
|
||
- › Handle captcha gracefully
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
- +- Browser (headless): example.com
|
||
- |
|
||
- | Actions
|
||
- | MM:SS Opened https://example.com
|
||
- | MM:SS Typed pizza
|
||
- | MM:SS Pressed Enter
|
||
- |
|
||
- | Console: Encountering captcha block
|
||
- |
|
||
- +- [Ctrl+B] View · [Esc] Stop
|
||
|
||
- +----------------------------- ✶ Browsing... -----------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Handle captcha gracefully
|
||
+
|
||
+ +- Browser (headless): example.com
|
||
+ |
|
||
+ | Actions
|
||
+ | MM:SS Opened https://example.com
|
||
+ | MM:SS Typed pizza
|
||
+ | MM:SS Pressed Enter
|
||
+ |
|
||
+ | Console: Encountering captcha block
|
||
+ |
|
||
+ +- [Ctrl+B] View · [Esc] Stop
|
||
+
|
||
+ ✶ Browsing...
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_skips_foreign_background_events.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_skips_foreign_background_events.snap
|
||
index aea8b70..b493701 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_skips_foreign_background_events.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__browser_session_skips_foreign_background_events.snap
|
||
@@ -2,26 +2,25 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- › Keep browsing while checking status
|
||
+ Keep browsing while checking status
|
||
|
||
- » Command guard: Leading cd /tmp/work is redundant
|
||
+ Command guard: Leading cd /tmp/work is redundant
|
||
|
||
- +- Browser (headless): example.com
|
||
- |
|
||
- | Actions
|
||
- | MM:SS Opened https://example.com
|
||
- |
|
||
- | Console: Encountering captcha challenge on checkout
|
||
- |
|
||
- +- [Ctrl+B] View · [Esc] Stop
|
||
+ +- Browser (headless): example.com
|
||
+ |
|
||
+ | Actions
|
||
+ | MM:SS Opened https://example.com
|
||
+ |
|
||
+ | Console: Encountering captcha challenge on checkout
|
||
+ |
|
||
+ +- [Ctrl+B] View · [Esc] Stop
|
||
+
|
||
+ ✶ Browsing...
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------- ✶ Browsing... -----------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__compaction_checkpoint_warning_notice.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__compaction_checkpoint_warning_notice.snap
|
||
index 3f9e2ae..1781b19 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__compaction_checkpoint_warning_notice.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__compaction_checkpoint_warning_notice.snap
|
||
@@ -2,22 +2,21 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: frame
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ Hello! How can I help you today?
|
||
|
||
|
||
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- ★ History checkpoint: earlier conversation compacted.
|
||
+ History checkpoint: earlier conversation compacted.
|
||
+
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_after_delta.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_after_delta.snap
|
||
index 62537d9..476a5af 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_after_delta.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_after_delta.snap
|
||
@@ -2,20 +2,17 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- ✶ 📁 /workspace 2 changes
|
||
- main
|
||
+ 📁 /workspace 2 changes
|
||
+ main
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_baseline.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_baseline.snap
|
||
index de93355..051e071 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_baseline.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_baseline.snap
|
||
@@ -2,20 +2,17 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- ✶ 📁 /workspace
|
||
- feature/context-ui 🧠 medium
|
||
+ 📁 /workspace
|
||
+ feature/context-ui 🧠 medium
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_browser_badge.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_browser_badge.snap
|
||
index c19edde..1dc0c6a 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_browser_badge.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_browser_badge.snap
|
||
@@ -2,20 +2,19 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- ✶ 📁 /workspace 1 change
|
||
- 🖼 Example Domain 1280×720 2025-11-05T12:00:00Z browser_type=chromium,
|
||
- cursor_position=120,340
|
||
+ 📁 /workspace 1 change
|
||
+ 🖼 Example Domain 1280×720 2025-11-05T12:00:00Z browser_type=chromium,
|
||
+ cursor_position=120,340
|
||
+
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_expanded_deltas.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_expanded_deltas.snap
|
||
index 75a2cae..f774d13 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_expanded_deltas.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__context_cell_expanded_deltas.snap
|
||
@@ -2,20 +2,19 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- ✶ 📁 /workspace/subdir 3 changes
|
||
- main 🧠 high
|
||
- Recent changes:
|
||
- • reasoning: medium → high #3
|
||
- • branch: feature/context-ui → main #2
|
||
- • cwd: /workspace → /workspace/subdir #2
|
||
+ 📁 /workspace/subdir 3 changes
|
||
+ main 🧠 high
|
||
+ Recent changes:
|
||
+ • reasoning: medium → high #3
|
||
+ • branch: feature/context-ui → main #2
|
||
+ • cwd: /workspace → /workspace/subdir #2
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__empty_chat.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__empty_chat.snap
|
||
index 34d540e..80aab6b 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__empty_chat.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__empty_chat.snap
|
||
@@ -2,26 +2,25 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ >_ hanzo dev (v0.0.0)
|
||
|
||
+ What would you like to work on?
|
||
|
||
- ★ /code - perform a coding task (multiple agents)
|
||
+ /code - perform a coding task (multiple agents)
|
||
|
||
|
||
- • Hello! How can I help you today?
|
||
+ Hello! How can I help you today?
|
||
|
||
|
||
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__multiline_formatting.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__multiline_formatting.snap
|
||
index 24ca818..7b98b74 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__multiline_formatting.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__multiline_formatting.snap
|
||
@@ -2,26 +2,25 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
|
||
- • Here's a simple function:
|
||
+ Here's a simple function:
|
||
|
||
- + rust ----------------------------+
|
||
- | fn hello() { |
|
||
- | println!("Hello, world!"); |
|
||
- | } |
|
||
- +----------------------------------+
|
||
+ + rust ----------------------------+
|
||
+ | fn hello() { |
|
||
+ | println!("Hello, world!"); |
|
||
+ | } |
|
||
+ +----------------------------------+
|
||
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__scroll_spacing_scrolled_intact.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__scroll_spacing_scrolled_intact.snap
|
||
index b14ead4..ba58f62 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__scroll_spacing_scrolled_intact.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__scroll_spacing_scrolled_intact.snap
|
||
@@ -2,26 +2,26 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: scrolled
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- •
|
||
- Here is another paragraph to expand height.
|
||
+ scroll.
|
||
|
||
- Yet another paragraph for good measure.
|
||
+ Assistant reply number two with multiple paragraphs.
|
||
|
||
- › Third user prompt to push history further.
|
||
+ Here is another paragraph to expand height.
|
||
|
||
- Assistant reply number three, still going strong.
|
||
- •
|
||
- - Bullet one
|
||
- - Bullet two
|
||
- - Bullet three
|
||
+ Yet another paragraph for good measure.
|
||
|
||
- › Fourth user prompt to guarantee overflow beyond the viewport height.
|
||
+ Third user prompt to push history further.
|
||
|
||
+ Assistant reply number three, still going strong.
|
||
+
|
||
+ - Bullet one
|
||
+ - Bullet two
|
||
+ - Bullet three
|
||
+
|
||
+ Fourth user prompt to guarantee overflow beyond the viewport height.
|
||
+
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap
|
||
index e4da351..2f8b6e1 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap
|
||
@@ -2,12 +2,9 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: closed
|
||
---
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+Settings ▸ Overview-----------------------------------------------------------------------------+
|
||
| |
|
||
- | › Model Model: GPT-5.5 (Medium) |
|
||
+ | › Model Model: ZEN5-Coder (Medium) |
|
||
| + Choose the language model used for new completions. |
|
||
| |
|
||
| Theme Theme: Light - Photon (16-color) · Spinner: Diamond |
|
||
@@ -21,11 +18,14 @@ expression: closed
|
||
| Accounts Auto-switch: On · API key fallback: Off |
|
||
| + Configure account switching behavior under rate and usage limits. |
|
||
| |
|
||
+ | Agents Enabled: 10/13 |
|
||
+ | + Configure linked agents and default task permissions. |
|
||
+ | |
|
||
| ↑ ↓ Move Enter Open Esc Close ? Help |
|
||
| |
|
||
+------------------------------------------------------------------------------------------------+
|
||
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap
|
||
index 0437f04..c00d591 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap
|
||
@@ -2,30 +2,30 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: open
|
||
---
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+Settings ▸ Overview-----------------------------------------------------------------------------+
|
||
| |
|
||
- | › Model Model: GPT-5.5 (Medium) |
|
||
+ | › Model Model: ZEN5-Coder (Medium) |
|
||
| + Choose the language model used for new completions. |
|
||
- | +----------------------------------------+ |
|
||
- | Theme Theme|Settings Overview |mond |
|
||
- | + Switch between preset | | |
|
||
- | |• ↑/↓ Move between sections | |
|
||
- | Updates Auto |• Enter Open selected section | |
|
||
- | + Control CLI auto-updat|• Tab Jump forward between sections | |
|
||
- | |• Esc Close settings | |
|
||
- | ------------------------|• ? Toggle this help |-------------------------- |
|
||
+ | |
|
||
+ | Theme Theme+----------------------------------------+mond |
|
||
+ | + Switch between preset |Settings Overview | |
|
||
| | | |
|
||
- | Accounts Auto-|Press Esc to close | |
|
||
- | + Configure account swit+----------------------------------------+. |
|
||
+ | Updates Auto |• ↑/↓ Move between sections | |
|
||
+ | + Control CLI auto-updat|• Enter Open selected section | |
|
||
+ | |• Tab Jump forward between sections | |
|
||
+ | ------------------------|• Esc Close settings |-------------------------- |
|
||
+ | |• ? Toggle this help | |
|
||
+ | Accounts Auto-| | |
|
||
+ | + Configure account swit|Press Esc to close |. |
|
||
+ | +----------------------------------------+ |
|
||
+ | Agents Enabled: 10/13 |
|
||
+ | + Configure linked agents and default task permissions. |
|
||
| |
|
||
| ↑ ↓ Move Enter Open Esc Close ? Help |
|
||
| |
|
||
+------------------------------------------------------------------------------------------------+
|
||
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_closed.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_closed.snap
|
||
index 16cf9a1..497df14 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_closed.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_closed.snap
|
||
@@ -2,14 +2,11 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: section_closed
|
||
---
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+Settings ▸ Model--------------------------------------------------------------------------------+
|
||
| |
|
||
| › Model + Select Model & Reasoning --------------------------------------------+ |
|
||
| Theme | | |
|
||
- | Updates | Current model: GPT-5.5 | |
|
||
+ | Updates | Current model: ZEN5-Coder | |
|
||
| Accounts | Reasoning effort: Medium | |
|
||
| Agents | | |
|
||
| Prompts | Mode Settings | |
|
||
@@ -18,14 +15,17 @@ expression: section_closed
|
||
| Auto Drive | 1M Context: unavailable | |
|
||
| Review | Unavailable for this model. Saved settings apply automatically on s | |
|
||
| Planning | | |
|
||
- | Validation | ↑↓ Navigate Enter Select Esc Cancel | |
|
||
- | Chrome | | |
|
||
- | ↓ MCP +----------------------------------------------------------------------+ |
|
||
+ | Validation | ZEN5-Coder | |
|
||
+ | Chrome | Agentic coding. 1M context. | |
|
||
+ | MCP | | |
|
||
+ | Notifications | ↑↓ Navigate Enter Select Esc Cancel | |
|
||
+ | Limits | | |
|
||
+ | +----------------------------------------------------------------------+ |
|
||
| ↑ ↓ Move Enter Open Esc Close ? Help |
|
||
| |
|
||
+------------------------------------------------------------------------------------------------+
|
||
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_open.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_open.snap
|
||
index 118ce10..d4f09fd 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_open.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_section_open.snap
|
||
@@ -2,30 +2,30 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: section_open
|
||
---
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+Settings ▸ Model--------------------------------------------------------------------------------+
|
||
| |
|
||
| › Model + Select Model & Reasoning --------------------------------------------+ |
|
||
| Theme | | |
|
||
- | Updates | Curren+------------------------------+ | |
|
||
- | Accounts | Reason|Model Shortcuts | | |
|
||
- | Agents | | | | |
|
||
- | Prompts | Mode S|• Esc Return to overview | | |
|
||
- | Memories | Fast m|• Tab Cycle sections |t is available on supported m | |
|
||
- | Skills | › Fast|• Shift+Tab Cycle backwards | | |
|
||
- | Auto Drive | 1M C|• ? Toggle this help | | |
|
||
- | Review | Unavai| |ings apply automatically on s | |
|
||
- | Planning | |Press Esc to close | | |
|
||
- | Validation | ↑↓ Nav+------------------------------+ | |
|
||
- | Chrome | | |
|
||
- | ↓ MCP +----------------------------------------------------------------------+ |
|
||
+ | Updates | Current model: ZEN5-Coder | |
|
||
+ | Accounts | Reasoning effort: Medium | |
|
||
+ | Agents | +------------------------------+ | |
|
||
+ | Prompts | Mode S|Model Shortcuts | | |
|
||
+ | Memories | Fast m| |t is available on supported m | |
|
||
+ | Skills | › Fast|• Esc Return to overview | | |
|
||
+ | Auto Drive | 1M C|• Tab Cycle sections | | |
|
||
+ | Review | Unavai|• Shift+Tab Cycle backwards |ings apply automatically on s | |
|
||
+ | Planning | |• ? Toggle this help | | |
|
||
+ | Validation | ZEN5-C| | | |
|
||
+ | Chrome | Agenti|Press Esc to close | | |
|
||
+ | MCP | +------------------------------+ | |
|
||
+ | Notifications | ↑↓ Navigate Enter Select Esc Cancel | |
|
||
+ | Limits | | |
|
||
+ | +----------------------------------------------------------------------+ |
|
||
| ↑ ↓ Move Enter Open Esc Close ? Help |
|
||
| |
|
||
+------------------------------------------------------------------------------------------------+
|
||
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap
|
||
index 087b620..900d4ea 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap
|
||
@@ -2,12 +2,9 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+Settings ▸ Overview-----------------------------------------------------------------------------+
|
||
| |
|
||
- | › Model Model: GPT-5.5 (Medium) |
|
||
+ | › Model Model: ZEN5-Coder (Medium) |
|
||
| + Choose the language model used for new completions. |
|
||
| |
|
||
| Theme Theme: Light - Photon (16-color) · Spinner: Diamond |
|
||
@@ -21,11 +18,14 @@ expression: output
|
||
| Accounts Auto-switch: On · API key fallback: Off |
|
||
| + Configure account switching behavior under rate and usage limits. |
|
||
| |
|
||
+ | Agents Enabled: 10/13 |
|
||
+ | + Configure linked agents and default task permissions. |
|
||
+ | |
|
||
| ↑ ↓ Move Enter Open Esc Close ? Help |
|
||
| |
|
||
+------------------------------------------------------------------------------------------------+
|
||
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_truncates.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_truncates.snap
|
||
index 22e3916..9d73d92 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_truncates.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_truncates.snap
|
||
@@ -2,22 +2,22 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +--------------------------------------------------------+
|
||
- | Every Code |
|
||
- +--------------------------------------------------------+
|
||
+Settings ▸ Overview-------------------------------------+
|
||
| |
|
||
- | › Model Model: GPT-5.5 (Medium) |
|
||
+ | › Model Model: ZEN5-Coder (Medium) |
|
||
| + Choose the language model used for new comp... |
|
||
| |
|
||
| Theme Theme: Light - Photon (16-colo... |
|
||
| + Switch between preset color palettes and ad... |
|
||
| |
|
||
+ | Updates Auto update: Disabled |
|
||
+ | + Control CLI auto-update cadence and release... |
|
||
+ | |
|
||
| ↑ ↓ Move Enter Open Esc Close ? Help |
|
||
| |
|
||
+--------------------------------------------------------+
|
||
|
||
- +--------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +--------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap
|
||
index b1cebbd..cd76e69 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap
|
||
@@ -2,12 +2,9 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+Settings ▸ Overview-----------------------------------------------------------------------------+
|
||
| |
|
||
- | › Model Model: GPT-5.5 (Medium) |
|
||
+ | › Model Model: ZEN5-Coder (Medium) |
|
||
| + Choose the language model used for new completions. |
|
||
| |
|
||
| Theme Theme: Light - Photon (16-color) · Spinner: Diamond |
|
||
@@ -15,11 +12,14 @@ expression: output
|
||
| |
|
||
| Updates Auto update: Disabled |
|
||
| + Control CLI auto-update cadence and release channels. |
|
||
+ | |
|
||
+ | |
|
||
+
|
||
| ↑ ↓ Move Enter Open Esc Close ? Help |
|
||
| |
|
||
+------------------------------------------------------------------------------------------------+
|
||
|
||
- +------------------------------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +------------------------------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__simple_conversation.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__simple_conversation.snap
|
||
index 6e6c1ed..f507b81 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__simple_conversation.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__simple_conversation.snap
|
||
@@ -2,26 +2,26 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- › Can you help me understand the available commands?
|
||
+ Can you help me understand the available commands?
|
||
|
||
|
||
- Hello! How can I help you today?
|
||
+ Hello! How can I help you today?
|
||
|
||
|
||
|
||
- • I can help with various tasks including:
|
||
+ I can help with various tasks including:
|
||
|
||
- - Writing code
|
||
- - Reading files
|
||
- - Running commands
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
+
|
||
+ What would you like to work on?
|
||
+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__strict_stream_ids_missing_id_warning.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__strict_stream_ids_missing_id_warning.snap
|
||
index 734b4b9..7d6c2da 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__strict_stream_ids_missing_id_warning.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__strict_stream_ids_missing_id_warning.snap
|
||
@@ -2,14 +2,14 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
+ - Writing code
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
|
||
- » Missing stream id for assistant answer delta
|
||
+ Missing stream id for assistant answer delta
|
||
+
|
||
+
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------------------------------------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/snapshots/vt100_chatwidget_snapshot__tool_activity_showcase.snap b/tui/tests/snapshots/vt100_chatwidget_snapshot__tool_activity_showcase.snap
|
||
index 8ecb201..e6c17b3 100644
|
||
--- a/tui/tests/snapshots/vt100_chatwidget_snapshot__tool_activity_showcase.snap
|
||
+++ b/tui/tests/snapshots/vt100_chatwidget_snapshot__tool_activity_showcase.snap
|
||
@@ -2,42 +2,42 @@
|
||
source: tui/tests/vt100_chatwidget_snapshot.rs
|
||
expression: output
|
||
---
|
||
- +----------------------------------------------------------------------------+
|
||
- | Every Code |
|
||
- +----------------------------------------------------------------------------+
|
||
- › Can you gather details from the latest docs update?
|
||
+ - Reading files
|
||
+ - Running commands
|
||
|
||
- +- Web Search · Completed
|
||
- |
|
||
- | Actions
|
||
- | MM:SS • Searching...
|
||
- | MM:SS • Query: "ratatui widget patterns"
|
||
- | MM:SS ✓ Results ready
|
||
- |
|
||
- +- [Ctrl+S] Settings · [Esc] Stop
|
||
|
||
- +- Browser (headless): Browser Session
|
||
- |
|
||
- | Actions
|
||
- | MM:SS Clicked #login-button
|
||
- |
|
||
- +- [Ctrl+B] View · [Esc] Stop
|
||
+ Can you gather details from the latest docs update?
|
||
|
||
- +- deploy-helper
|
||
- |
|
||
- | Run targeted regression suite
|
||
- |
|
||
- | Agents
|
||
- | No agent updates yet
|
||
- |
|
||
- | Actions
|
||
- | MM:SS Started agent run for qa-bot
|
||
- | MM:SS Waiting for agents
|
||
- |
|
||
- +- [Ctrl+A] Expand
|
||
+ +- Web Search · Completed
|
||
+ |
|
||
+ | Actions
|
||
+ | MM:SS • Searching...
|
||
+ | MM:SS • Query: "ratatui widget patterns"
|
||
+ | MM:SS ✓ Results ready
|
||
+ |
|
||
+ +- [Ctrl+S] Settings · [Esc] Stop
|
||
|
||
+ +- Browser (headless): Browser Session
|
||
+ |
|
||
+ | Actions
|
||
+ | MM:SS Clicked #login-button
|
||
+ |
|
||
+ +- [Ctrl+B] View · [Esc] Stop
|
||
+
|
||
+ +- deploy-helper
|
||
+ |
|
||
+ | Run targeted regression suite
|
||
+ |
|
||
+ | Agents
|
||
+ | No agent updates yet
|
||
+ |
|
||
+ | Actions
|
||
+ | MM:SS Started agent run for qa-bot
|
||
+ | MM:SS Waiting for agents
|
||
+ |
|
||
+ +- [Ctrl+A] Expand
|
||
+
|
||
+ ✶ Agents...
|
||
+ What would you like to work on?
|
||
|
||
- +----------------------------- ✶ Agents... -----------------------------+
|
||
- | What can I code for you today? |
|
||
- +----------------------------------------------------------------------------+
|
||
Read Only (Shift+Tab change) Ctrl+G editor • API key
|
||
diff --git a/tui/tests/stuck_exec.rs b/tui/tests/stuck_exec.rs
|
||
index 1629dcc..9824389 100644
|
||
--- a/tui/tests/stuck_exec.rs
|
||
+++ b/tui/tests/stuck_exec.rs
|
||
@@ -90,7 +90,7 @@ fn exec_cell_clears_after_patch_flow() {
|
||
|
||
let output = render_chat_widget_to_vt100(&mut harness, 80, 12);
|
||
assert!(
|
||
- !output.contains("Running"),
|
||
+ !output.contains("Running..."),
|
||
"exec cell should not remain running after patch apply:\n{}",
|
||
output
|
||
);
|
||
@@ -169,7 +169,7 @@ fn exec_cell_clears_after_task_started_final_answer_without_task_complete() {
|
||
|
||
let output = render_chat_widget_to_vt100(&mut harness, 80, 14);
|
||
assert!(
|
||
- !output.contains("Running"),
|
||
+ !output.contains("Running..."),
|
||
"exec should finalize after final answer even without TaskComplete:\n{}",
|
||
output
|
||
);
|
||
@@ -792,7 +792,7 @@ fn running_exec_is_finalized_when_error_event_arrives() {
|
||
|
||
let output = render_chat_widget_to_vt100(&mut harness, 80, 14);
|
||
assert!(
|
||
- !output.contains("Running"),
|
||
+ !output.contains("Running..."),
|
||
"exec cell should not linger after fatal error:\n{}",
|
||
output
|
||
);
|
||
diff --git a/tui/tests/vt100_chatwidget_snapshot.rs b/tui/tests/vt100_chatwidget_snapshot.rs
|
||
index d042c96..aedf603 100644
|
||
--- a/tui/tests/vt100_chatwidget_snapshot.rs
|
||
+++ b/tui/tests/vt100_chatwidget_snapshot.rs
|
||
@@ -2564,12 +2564,12 @@ fn agents_overlay_narrow_terminal_does_not_panic() {
|
||
#[test]
|
||
fn agents_toggle_claude_opus_persists_via_slash_command() {
|
||
let _lock = ENV_LOCK.lock().unwrap();
|
||
- let env = EnvGuard::new(&["HOME", "CODE_HOME", "CODEX_HOME"]);
|
||
+ let env = EnvGuard::new(&["HOME", "CODE_HOME", "DEV_HOME"]);
|
||
let home_dir = TempDir::new().expect("temp home");
|
||
let code_home = TempDir::new().expect("code home");
|
||
env.set_path("HOME", home_dir.path());
|
||
env.set_path("CODE_HOME", code_home.path());
|
||
- env.remove("CODEX_HOME");
|
||
+ env.remove("DEV_HOME");
|
||
|
||
let mut harness = ChatWidgetHarness::new();
|
||
|
||
diff --git a/utils/cargo-bin/BUILD.bazel b/utils/cargo-bin/BUILD.bazel
|
||
index 1743038..abcd267 100644
|
||
--- a/utils/cargo-bin/BUILD.bazel
|
||
+++ b/utils/cargo-bin/BUILD.bazel
|
||
@@ -7,6 +7,6 @@ codex_rust_crate(
|
||
lib_data_extra = ["repo_root.marker"],
|
||
test_data_extra = ["repo_root.marker"],
|
||
rustc_env = {
|
||
- "CODEX_REPO_ROOT_MARKER": "$(rlocationpath :repo_root.marker)",
|
||
+ "DEV_REPO_ROOT_MARKER": "$(rlocationpath :repo_root.marker)",
|
||
},
|
||
)
|
||
diff --git a/utils/cargo-bin/src/lib.rs b/utils/cargo-bin/src/lib.rs
|
||
index 6b620de..e91a4c9 100644
|
||
--- a/utils/cargo-bin/src/lib.rs
|
||
+++ b/utils/cargo-bin/src/lib.rs
|
||
@@ -169,12 +169,12 @@ pub fn repo_root() -> io::Result<PathBuf> {
|
||
let marker = if runfiles_available() {
|
||
let runfiles = runfiles::Runfiles::create()
|
||
.map_err(|err| io::Error::other(format!("failed to create runfiles: {err}")))?;
|
||
- let marker_path = option_env!("CODEX_REPO_ROOT_MARKER")
|
||
+ let marker_path = option_env!("DEV_REPO_ROOT_MARKER")
|
||
.map(PathBuf::from)
|
||
.ok_or_else(|| {
|
||
io::Error::new(
|
||
io::ErrorKind::NotFound,
|
||
- "CODEX_REPO_ROOT_MARKER was not set at compile time",
|
||
+ "DEV_REPO_ROOT_MARKER was not set at compile time",
|
||
)
|
||
})?;
|
||
runfiles::rlocation!(runfiles, &marker_path).ok_or_else(|| {
|