From 7dd23278a1a953f57256b3dd9e90da58ba1daf0c Mon Sep 17 00:00:00 2001 From: Aasheesh Date: Wed, 25 Mar 2026 18:46:27 +0530 Subject: [PATCH 1/5] perf: fast path for zsh rprompt without conversation ID Bypasses heavy initialization (ForgeInfra, ForgeRepo, ForgeServices) when _FORGE_CONVERSATION_ID is not set. --- crates/forge_main/src/main.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index c9b14c9d1c..2179c9c979 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -33,6 +33,17 @@ async fn main() -> Result<()> { // Initialize and run the UI let mut cli = Cli::parse(); + // Fast path: zsh rprompt without conversation ID doesn't need any infrastructure + let args: Vec = std::env::args().collect(); + let is_zsh_rprompt = args.iter().any(|a| a == "zsh") && args.iter().any(|a| a == "rprompt"); + let conversation_id = std::env::var("_FORGE_CONVERSATION_ID").ok(); + let has_conversation = conversation_id.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false); + + if is_zsh_rprompt && !has_conversation { + println!(" %B%F{{240}}󱙺 FORGE%f%b"); + return Ok(()); + } + // Check if there's piped input if !atty::is(atty::Stream::Stdin) { let mut stdin_content = String::new(); From 93c67e9863c2911c4863368e3728478dfb3ffef6 Mon Sep 17 00:00:00 2001 From: Aasheesh Date: Wed, 25 Mar 2026 18:57:54 +0530 Subject: [PATCH 2/5] perf: fast path for zsh rprompt without conversation ID - Check exact argument positions to avoid false positives - Check BEFORE Cli::parse() to avoid parsing overhead - Falls back to normal path when _FORGE_CONVERSATION_ID is set --- crates/forge_main/src/main.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index 2179c9c979..47ef847911 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -31,19 +31,17 @@ async fn main() -> Result<()> { })); // Initialize and run the UI - let mut cli = Cli::parse(); - - // Fast path: zsh rprompt without conversation ID doesn't need any infrastructure + + // Fast path: zsh rprompt without conversation ID - check BEFORE Cli::parse() let args: Vec = std::env::args().collect(); - let is_zsh_rprompt = args.iter().any(|a| a == "zsh") && args.iter().any(|a| a == "rprompt"); - let conversation_id = std::env::var("_FORGE_CONVERSATION_ID").ok(); - let has_conversation = conversation_id.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false); - - if is_zsh_rprompt && !has_conversation { + let has_conv = std::env::var("_FORGE_CONVERSATION_ID").ok().filter(|s| !s.trim().is_empty()).is_some(); + if args.len() >= 3 && args[1] == "zsh" && args[2] == "rprompt" && !has_conv { println!(" %B%F{{240}}󱙺 FORGE%f%b"); return Ok(()); } + let mut cli = Cli::parse(); + // Check if there's piped input if !atty::is(atty::Stream::Stdin) { let mut stdin_content = String::new(); From b8c50c336fb825c0b203db20076a23f14d9e2df4 Mon Sep 17 00:00:00 2001 From: Aasheesh Date: Thu, 26 Mar 2026 10:18:11 +0530 Subject: [PATCH 3/5] perf: second fast path for zsh rprompt with conversation ID - Add direct SQLite query via rusqlite to fetch rprompt data - Bypass full Forge infrastructure stack for ~7ms performance - Uses json_extract to pull usage.{total_tokens,cost,model} from context - Falls back to normal path if DB/table doesn't exist - Fix edge case: single quote string in model extraction Co-Authored-By: ForgeCode --- Cargo.lock | 75 ++++++++++- Cargo.toml | 1 + crates/forge_main/Cargo.toml | 2 + crates/forge_main/src/lib.rs | 3 +- crates/forge_main/src/main.rs | 54 +++++++- crates/forge_main/src/rprompt_fast.rs | 185 ++++++++++++++++++++++++++ 6 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 crates/forge_main/src/rprompt_fast.rs diff --git a/Cargo.lock b/Cargo.lock index 0267389343..e0f523772c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1393,7 +1393,7 @@ dependencies = [ "downcast-rs", "libsqlite3-sys", "r2d2", - "sqlite-wasm-rs", + "sqlite-wasm-rs 0.4.8", "time", ] @@ -1697,6 +1697,18 @@ dependencies = [ "rand 0.10.0", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1788,6 +1800,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "forge_api" version = "0.1.0" @@ -2020,6 +2038,7 @@ dependencies = [ "console 0.16.3", "convert_case 0.11.0", "derive_setters", + "dirs", "fake", "forge_api", "forge_app", @@ -2045,6 +2064,7 @@ dependencies = [ "open", "pretty_assertions", "reedline", + "rusqlite", "rustls 0.23.37", "serde", "serde_json", @@ -2765,7 +2785,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -2773,6 +2793,9 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -2783,6 +2806,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "heck" version = "0.5.0" @@ -5119,6 +5151,31 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" +dependencies = [ + "bitflags 2.10.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.11.0", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs 0.5.2", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -5757,6 +5814,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "sse-stream" version = "0.2.1" @@ -7854,7 +7923,7 @@ checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", - "hashlink", + "hashlink 0.10.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e461963898..728769cac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ uuid = { version = "1.22.0", features = [ whoami = "2.1.0" fnv_rs = "0.4.3" merge = { version = "0.2", features = ["derive"] } +rusqlite = { version = "0.38", features = ["bundled"] } rmcp = { version = "0.10.0", features = [ "client", "transport-sse-client-reqwest", diff --git a/crates/forge_main/Cargo.toml b/crates/forge_main/Cargo.toml index 27305a1526..ea8b4c3759 100644 --- a/crates/forge_main/Cargo.toml +++ b/crates/forge_main/Cargo.toml @@ -54,6 +54,8 @@ open.workspace = true humantime.workspace = true num-format.workspace = true atty = "0.2" +dirs = "6.0.0" +rusqlite.workspace = true url.workspace = true forge_embed.workspace = true include_dir.workspace = true diff --git a/crates/forge_main/src/lib.rs b/crates/forge_main/src/lib.rs index c5b342df7c..812c1fe549 100644 --- a/crates/forge_main/src/lib.rs +++ b/crates/forge_main/src/lib.rs @@ -9,6 +9,7 @@ mod input; mod model; mod porcelain; mod prompt; +pub mod rprompt_fast; mod sandbox; mod state; mod stream_renderer; @@ -17,7 +18,7 @@ mod title_display; mod tools_display; pub mod tracker; mod ui; -mod utils; +pub mod utils; mod vscode; mod zsh; diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index 47ef847911..9a5a9cb583 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -6,7 +6,8 @@ use anyhow::Result; use clap::Parser; use forge_api::ForgeAPI; use forge_domain::TitleFormat; -use forge_main::{Cli, Sandbox, TitleDisplayExt, UI, tracker}; +use forge_main::{utils, Cli, Sandbox, TitleDisplayExt, UI, tracker}; +use forge_main::rprompt_fast; #[tokio::main] async fn main() -> Result<()> { @@ -40,6 +41,57 @@ async fn main() -> Result<()> { return Ok(()); } + // Fast path: zsh rprompt WITH conversation ID - direct SQLite query + if args.len() >= 3 && args[1] == "zsh" && args[2] == "rprompt" && has_conv { + if let Ok(conv_id) = std::env::var("_FORGE_CONVERSATION_ID") { + let conv_id = conv_id.trim(); + if !conv_id.is_empty() { + // Try fast path - if it fails, fall through to normal path + if let Some(data) = rprompt_fast::fetch_rprompt_data(conv_id) { + let use_nerd_font = std::env::var("NERD_FONT") + .or_else(|_| std::env::var("USE_NERD_FONT")) + .map(|v| v == "1") + .unwrap_or(true); + + // Check if we have token count (active state) or just show inactive + if let Some(token_count) = data.token_count { + let icon = if use_nerd_font { "󱙺" } else { "" }; + let count_str = utils::humanize_number(token_count); + + // Active state: bright colors + print!(" %B%F{{15}}{} FORGE%f%b %B%F{{15}}{}%f%b", icon, count_str); + + if let Some(cost) = data.cost { + let currency = std::env::var("FORGE_CURRENCY_SYMBOL").unwrap_or_else(|_| "$".to_string()); + let ratio: f64 = std::env::var("FORGE_CURRENCY_CONVERSION_RATE") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1.0); + print!(" %B%F{{2}}{}{:.2}%f%b", currency, cost * ratio); + } + + if let Some(ref model) = data.model { + let model_icon = if use_nerd_font { "󰑙" } else { "" }; + print!(" %F{{134}}{}{}", model_icon, model); + } + + println!(); + return Ok(()); + } else { + // No token count - show inactive/dimmed state + let icon = if use_nerd_font { "󱙺" } else { "" }; + let model_str = data.model.as_deref().unwrap_or("forge"); + let model_icon = if use_nerd_font { "󰑙" } else { "" }; + + print!(" %B%F{{240}}{} FORGE%f%b %F{{240}}{}{}%f", icon, model_icon, model_str); + println!(); + return Ok(()); + } + } + } + } + } + let mut cli = Cli::parse(); // Check if there's piped input diff --git a/crates/forge_main/src/rprompt_fast.rs b/crates/forge_main/src/rprompt_fast.rs new file mode 100644 index 0000000000..22492ec125 --- /dev/null +++ b/crates/forge_main/src/rprompt_fast.rs @@ -0,0 +1,185 @@ +//! Fast rprompt data fetcher using direct SQLite access. +//! +//! This module provides a lightweight way to fetch rprompt data (token count, cost, model) +//! directly from the SQLite database without loading the full Forge infrastructure stack. + +use std::path::PathBuf; + +/// Data fetched from the database for rprompt display +#[derive(Debug, Default)] +pub struct RpromptData { + pub token_count: Option, + pub cost: Option, + pub model: Option, +} + +/// Fetches rprompt data from the SQLite database directly. +/// +/// This is a fast path that bypasses the full Forge infrastructure stack. +/// Returns None on any error (DB not found, locked, invalid ID, etc.). +pub fn fetch_rprompt_data(conversation_id: &str) -> Option { + let db_path = get_database_path()?; + let conn = rusqlite::Connection::open(&db_path).ok()?; + + let context: String = conn + .query_row( + "SELECT context FROM conversations WHERE conversation_id = ?1", + [conversation_id], + |row| row.get(0), + ) + .ok()?; + + // Use in-memory SQLite for JSON extraction + let mem_conn = rusqlite::Connection::open_in_memory().ok()?; + + let token_count = extract_token_count(&mem_conn, &context); + let cost = extract_cost(&mem_conn, &context); + let model = extract_model(&mem_conn, &context); + + Some(RpromptData { token_count, cost, model }) +} + +fn get_database_path() -> Option { + // Use current working directory, matching how forge resolves the DB path + // The DB is at .forge/forge.db relative to the project directory + let cwd = std::env::current_dir().ok()?; + Some(cwd.join(".forge").join("forge.db")) +} + +fn extract_token_count(conn: &rusqlite::Connection, context: &str) -> Option { + // Try top-level usage.total_tokens + let result: Option = conn + .query_row( + "SELECT json_extract(?1, '$.usage.total_tokens')", + [context], + |row| row.get(0), + ) + .ok(); + + if let Some(val) = result { + return parse_token_value(&val); + } + + // Fallback: last message's usage.total_tokens + let messages: Option = conn + .query_row("SELECT json_extract(?1, '$.messages')", [context], |row| { + row.get(0) + }) + .ok()?; + + let last_message: Option = conn + .query_row("SELECT json_extract(?1, '$[-1]')", [&messages], |row| { + row.get(0) + }) + .ok(); + + if let Some(msg) = last_message { + let result: Option = conn + .query_row( + "SELECT json_extract(?1, '$.usage.total_tokens')", + [&msg], + |row| row.get(0), + ) + .ok(); + if let Some(val) = result { + return parse_token_value(&val); + } + } + + None +} + +fn parse_token_value(val: &str) -> Option { + let val = val.trim(); + + if let Some(inner) = val + .strip_prefix("Actual(") + .or_else(|| val.strip_prefix("Approx(")) + { + if let Some(num) = inner.strip_suffix(')') { + return num.parse().ok(); + } + } + + val.parse().ok() +} + +fn extract_cost(conn: &rusqlite::Connection, context: &str) -> Option { + let result: Option = conn + .query_row( + "SELECT json_extract(?1, '$.usage.cost')", + [context], + |row| row.get(0), + ) + .ok()?; + + result.and_then(|s| s.parse().ok()) +} + +fn extract_model(conn: &rusqlite::Connection, context: &str) -> Option { + let result: Option = conn + .query_row( + "SELECT json_extract(?1, '$.usage.model')", + [context], + |row| row.get(0), + ) + .ok()?; + + let result = result?; + if result.len() >= 2 && result.starts_with('"') && result.ends_with('"') { + Some(result[1..result.len() - 1].to_string()) + } else { + Some(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_mem_conn() -> Option { + rusqlite::Connection::open_in_memory().ok() + } + + #[test] + fn test_extract_token_count_actual() { + let conn = create_mem_conn().unwrap(); + let context = r#"{"usage": {"total_tokens": "Actual(1500)"}}"#; + assert_eq!(extract_token_count(&conn, context), Some(1500)); + } + + #[test] + fn test_extract_token_count_approx() { + let conn = create_mem_conn().unwrap(); + let context = r#"{"usage": {"total_tokens": "Approx(100)"}}"#; + assert_eq!(extract_token_count(&conn, context), Some(100)); + } + + #[test] + fn test_extract_token_count_raw() { + let conn = create_mem_conn().unwrap(); + let context = r#"{"usage": {"total_tokens": "2000"}}"#; + assert_eq!(extract_token_count(&conn, context), Some(2000)); + } + + #[test] + fn test_extract_cost() { + let conn = create_mem_conn().unwrap(); + let context = r#"{"usage": {"cost": "0.0123"}}"#; + assert_eq!(extract_cost(&conn, context), Some(0.0123)); + } + + #[test] + fn test_extract_model() { + let conn = create_mem_conn().unwrap(); + let context = r#"{"usage": {"model": "gpt-4"}}"#; + assert_eq!(extract_model(&conn, context), Some("gpt-4".to_string())); + } + + #[test] + fn test_extract_model_single_quote_edge_case() { + let conn = create_mem_conn().unwrap(); + let context = r#"{"usage": {"model": "\""}}"#; + assert_eq!(extract_model(&conn, context), Some("\"".to_string())); + } +} From 10dc274d4900b0d226d7407130684c25ce30271e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:45:48 +0000 Subject: [PATCH 4/5] [autofix.ci] apply automated fixes --- crates/forge_main/src/main.rs | 23 ++++++++++++++--------- crates/forge_main/src/rprompt_fast.rs | 9 ++++----- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index 9f5eff954e..f24bab61e7 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -6,8 +6,7 @@ use anyhow::Result; use clap::Parser; use forge_api::ForgeAPI; use forge_domain::TitleFormat; -use forge_main::{utils, Cli, Sandbox, TitleDisplayExt, UI, tracker}; -use forge_main::rprompt_fast; +use forge_main::{Cli, Sandbox, TitleDisplayExt, UI, rprompt_fast, tracker, utils}; #[tokio::main] async fn main() -> Result<()> { @@ -36,18 +35,21 @@ async fn main() -> Result<()> { })); // Initialize and run the UI - + // Fast path: zsh rprompt without conversation ID - check BEFORE Cli::parse() let args: Vec = std::env::args().collect(); - let has_conv = std::env::var("_FORGE_CONVERSATION_ID").ok().filter(|s| !s.trim().is_empty()).is_some(); + let has_conv = std::env::var("_FORGE_CONVERSATION_ID") + .ok() + .filter(|s| !s.trim().is_empty()) + .is_some(); if args.len() >= 3 && args[1] == "zsh" && args[2] == "rprompt" && !has_conv { println!(" %B%F{{240}}󱙺 FORGE%f%b"); return Ok(()); } // Fast path: zsh rprompt WITH conversation ID - direct SQLite query - if args.len() >= 3 && args[1] == "zsh" && args[2] == "rprompt" && has_conv { - if let Ok(conv_id) = std::env::var("_FORGE_CONVERSATION_ID") { + if args.len() >= 3 && args[1] == "zsh" && args[2] == "rprompt" && has_conv + && let Ok(conv_id) = std::env::var("_FORGE_CONVERSATION_ID") { let conv_id = conv_id.trim(); if !conv_id.is_empty() { // Try fast path - if it fails, fall through to normal path @@ -66,7 +68,8 @@ async fn main() -> Result<()> { print!(" %B%F{{15}}{} FORGE%f%b %B%F{{15}}{}%f%b", icon, count_str); if let Some(cost) = data.cost { - let currency = std::env::var("FORGE_CURRENCY_SYMBOL").unwrap_or_else(|_| "$".to_string()); + let currency = std::env::var("FORGE_CURRENCY_SYMBOL") + .unwrap_or_else(|_| "$".to_string()); let ratio: f64 = std::env::var("FORGE_CURRENCY_CONVERSION_RATE") .ok() .and_then(|v| v.parse().ok()) @@ -87,14 +90,16 @@ async fn main() -> Result<()> { let model_str = data.model.as_deref().unwrap_or("forge"); let model_icon = if use_nerd_font { "󰑙" } else { "" }; - print!(" %B%F{{240}}{} FORGE%f%b %F{{240}}{}{}%f", icon, model_icon, model_str); + print!( + " %B%F{{240}}{} FORGE%f%b %F{{240}}{}{}%f", + icon, model_icon, model_str + ); println!(); return Ok(()); } } } } - } let mut cli = Cli::parse(); diff --git a/crates/forge_main/src/rprompt_fast.rs b/crates/forge_main/src/rprompt_fast.rs index 22492ec125..2da7749358 100644 --- a/crates/forge_main/src/rprompt_fast.rs +++ b/crates/forge_main/src/rprompt_fast.rs @@ -1,7 +1,8 @@ //! Fast rprompt data fetcher using direct SQLite access. //! -//! This module provides a lightweight way to fetch rprompt data (token count, cost, model) -//! directly from the SQLite database without loading the full Forge infrastructure stack. +//! This module provides a lightweight way to fetch rprompt data (token count, +//! cost, model) directly from the SQLite database without loading the full +//! Forge infrastructure stack. use std::path::PathBuf; @@ -95,11 +96,9 @@ fn parse_token_value(val: &str) -> Option { if let Some(inner) = val .strip_prefix("Actual(") .or_else(|| val.strip_prefix("Approx(")) - { - if let Some(num) = inner.strip_suffix(')') { + && let Some(num) = inner.strip_suffix(')') { return num.parse().ok(); } - } val.parse().ok() } From 3d2466cb69f44afebca455132f83d561497b1ef9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:04:07 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- crates/forge_main/src/main.rs | 98 ++++++++++++++------------- crates/forge_main/src/rprompt_fast.rs | 7 +- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index 500efb9af2..4cd11621ad 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -48,58 +48,62 @@ async fn main() -> Result<()> { } // Fast path: zsh rprompt WITH conversation ID - direct SQLite query - if args.len() >= 3 && args[1] == "zsh" && args[2] == "rprompt" && has_conv - && let Ok(conv_id) = std::env::var("_FORGE_CONVERSATION_ID") { - let conv_id = conv_id.trim(); - if !conv_id.is_empty() { - // Try fast path - if it fails, fall through to normal path - if let Some(data) = rprompt_fast::fetch_rprompt_data(conv_id) { - let use_nerd_font = std::env::var("NERD_FONT") - .or_else(|_| std::env::var("USE_NERD_FONT")) - .map(|v| v == "1") - .unwrap_or(true); - - // Check if we have token count (active state) or just show inactive - if let Some(token_count) = data.token_count { - let icon = if use_nerd_font { "󱙺" } else { "" }; - let count_str = utils::humanize_number(token_count); - - // Active state: bright colors - print!(" %B%F{{15}}{} FORGE%f%b %B%F{{15}}{}%f%b", icon, count_str); - - if let Some(cost) = data.cost { - let currency = std::env::var("FORGE_CURRENCY_SYMBOL") - .unwrap_or_else(|_| "$".to_string()); - let ratio: f64 = std::env::var("FORGE_CURRENCY_CONVERSION_RATE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1.0); - print!(" %B%F{{2}}{}{:.2}%f%b", currency, cost * ratio); - } - - if let Some(ref model) = data.model { - let model_icon = if use_nerd_font { "󰑙" } else { "" }; - print!(" %F{{134}}{}{}", model_icon, model); - } - - println!(); - return Ok(()); - } else { - // No token count - show inactive/dimmed state - let icon = if use_nerd_font { "󱙺" } else { "" }; - let model_str = data.model.as_deref().unwrap_or("forge"); - let model_icon = if use_nerd_font { "󰑙" } else { "" }; + if args.len() >= 3 + && args[1] == "zsh" + && args[2] == "rprompt" + && has_conv + && let Ok(conv_id) = std::env::var("_FORGE_CONVERSATION_ID") + { + let conv_id = conv_id.trim(); + if !conv_id.is_empty() { + // Try fast path - if it fails, fall through to normal path + if let Some(data) = rprompt_fast::fetch_rprompt_data(conv_id) { + let use_nerd_font = std::env::var("NERD_FONT") + .or_else(|_| std::env::var("USE_NERD_FONT")) + .map(|v| v == "1") + .unwrap_or(true); + + // Check if we have token count (active state) or just show inactive + if let Some(token_count) = data.token_count { + let icon = if use_nerd_font { "󱙺" } else { "" }; + let count_str = utils::humanize_number(token_count); + + // Active state: bright colors + print!(" %B%F{{15}}{} FORGE%f%b %B%F{{15}}{}%f%b", icon, count_str); + + if let Some(cost) = data.cost { + let currency = std::env::var("FORGE_CURRENCY_SYMBOL") + .unwrap_or_else(|_| "$".to_string()); + let ratio: f64 = std::env::var("FORGE_CURRENCY_CONVERSION_RATE") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1.0); + print!(" %B%F{{2}}{}{:.2}%f%b", currency, cost * ratio); + } - print!( - " %B%F{{240}}{} FORGE%f%b %F{{240}}{}{}%f", - icon, model_icon, model_str - ); - println!(); - return Ok(()); + if let Some(ref model) = data.model { + let model_icon = if use_nerd_font { "󰑙" } else { "" }; + print!(" %F{{134}}{}{}", model_icon, model); } + + println!(); + return Ok(()); + } else { + // No token count - show inactive/dimmed state + let icon = if use_nerd_font { "󱙺" } else { "" }; + let model_str = data.model.as_deref().unwrap_or("forge"); + let model_icon = if use_nerd_font { "󰑙" } else { "" }; + + print!( + " %B%F{{240}}{} FORGE%f%b %F{{240}}{}{}%f", + icon, model_icon, model_str + ); + println!(); + return Ok(()); } } } + } let mut cli = Cli::parse(); diff --git a/crates/forge_main/src/rprompt_fast.rs b/crates/forge_main/src/rprompt_fast.rs index 2da7749358..d94ba194aa 100644 --- a/crates/forge_main/src/rprompt_fast.rs +++ b/crates/forge_main/src/rprompt_fast.rs @@ -96,9 +96,10 @@ fn parse_token_value(val: &str) -> Option { if let Some(inner) = val .strip_prefix("Actual(") .or_else(|| val.strip_prefix("Approx(")) - && let Some(num) = inner.strip_suffix(')') { - return num.parse().ok(); - } + && let Some(num) = inner.strip_suffix(')') + { + return num.parse().ok(); + } val.parse().ok() }