diff --git a/app/assets/bundled/svg/conversation-context-100.svg b/app/assets/bundled/svg/context-remaining-0.svg similarity index 100% rename from app/assets/bundled/svg/conversation-context-100.svg rename to app/assets/bundled/svg/context-remaining-0.svg index 37d0ef1277..9511432b12 100644 --- a/app/assets/bundled/svg/conversation-context-100.svg +++ b/app/assets/bundled/svg/context-remaining-0.svg @@ -1,37 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -63,4 +31,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/bundled/svg/conversation-context-90.svg b/app/assets/bundled/svg/context-remaining-10.svg similarity index 97% rename from app/assets/bundled/svg/conversation-context-90.svg rename to app/assets/bundled/svg/context-remaining-10.svg index 747ffa68bb..81d37c7daa 100644 --- a/app/assets/bundled/svg/conversation-context-90.svg +++ b/app/assets/bundled/svg/context-remaining-10.svg @@ -1,62 +1,62 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-0.svg b/app/assets/bundled/svg/context-remaining-100.svg similarity index 91% rename from app/assets/bundled/svg/conversation-context-0.svg rename to app/assets/bundled/svg/context-remaining-100.svg index 7f941b1d5a..71e747d501 100644 --- a/app/assets/bundled/svg/conversation-context-0.svg +++ b/app/assets/bundled/svg/context-remaining-100.svg @@ -1,34 +1,34 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-80.svg b/app/assets/bundled/svg/context-remaining-20.svg similarity index 97% rename from app/assets/bundled/svg/conversation-context-80.svg rename to app/assets/bundled/svg/context-remaining-20.svg index 06260a6a37..2b17d71b10 100644 --- a/app/assets/bundled/svg/conversation-context-80.svg +++ b/app/assets/bundled/svg/context-remaining-20.svg @@ -1,60 +1,60 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-70.svg b/app/assets/bundled/svg/context-remaining-30.svg similarity index 96% rename from app/assets/bundled/svg/conversation-context-70.svg rename to app/assets/bundled/svg/context-remaining-30.svg index 513db5a6d8..7d099372ad 100644 --- a/app/assets/bundled/svg/conversation-context-70.svg +++ b/app/assets/bundled/svg/context-remaining-30.svg @@ -1,56 +1,56 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-60.svg b/app/assets/bundled/svg/context-remaining-40.svg similarity index 95% rename from app/assets/bundled/svg/conversation-context-60.svg rename to app/assets/bundled/svg/context-remaining-40.svg index adbd499c3c..0a0b130a77 100644 --- a/app/assets/bundled/svg/conversation-context-60.svg +++ b/app/assets/bundled/svg/context-remaining-40.svg @@ -1,54 +1,54 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-50.svg b/app/assets/bundled/svg/context-remaining-50.svg similarity index 95% rename from app/assets/bundled/svg/conversation-context-50.svg rename to app/assets/bundled/svg/context-remaining-50.svg index 7e730a66e2..6eccc18877 100644 --- a/app/assets/bundled/svg/conversation-context-50.svg +++ b/app/assets/bundled/svg/context-remaining-50.svg @@ -1,50 +1,50 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-40.svg b/app/assets/bundled/svg/context-remaining-60.svg similarity index 95% rename from app/assets/bundled/svg/conversation-context-40.svg rename to app/assets/bundled/svg/context-remaining-60.svg index eec0598c32..bce954852b 100644 --- a/app/assets/bundled/svg/conversation-context-40.svg +++ b/app/assets/bundled/svg/context-remaining-60.svg @@ -1,46 +1,46 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-30.svg b/app/assets/bundled/svg/context-remaining-70.svg similarity index 94% rename from app/assets/bundled/svg/conversation-context-30.svg rename to app/assets/bundled/svg/context-remaining-70.svg index 89fd8a7254..4f3b1bde95 100644 --- a/app/assets/bundled/svg/conversation-context-30.svg +++ b/app/assets/bundled/svg/context-remaining-70.svg @@ -1,44 +1,44 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-20.svg b/app/assets/bundled/svg/context-remaining-80.svg similarity index 93% rename from app/assets/bundled/svg/conversation-context-20.svg rename to app/assets/bundled/svg/context-remaining-80.svg index fe16834842..6c2e644512 100644 --- a/app/assets/bundled/svg/conversation-context-20.svg +++ b/app/assets/bundled/svg/context-remaining-80.svg @@ -1,40 +1,40 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/assets/bundled/svg/conversation-context-10.svg b/app/assets/bundled/svg/context-remaining-90.svg similarity index 93% rename from app/assets/bundled/svg/conversation-context-10.svg rename to app/assets/bundled/svg/context-remaining-90.svg index 22a088599d..5fa5d801e6 100644 --- a/app/assets/bundled/svg/conversation-context-10.svg +++ b/app/assets/bundled/svg/context-remaining-90.svg @@ -1,38 +1,38 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs index cf8ee68873..679dbb7b1b 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs @@ -609,7 +609,7 @@ impl AgentInputFooter { let context_window_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", AgentInputButtonTheme) - .with_icon(Icon::ConversationContext0) + .with_icon(Icon::ContextRemaining100) .with_tooltip("Context window usage") .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left) diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs index 4d813f620d..601769ffda 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs @@ -138,7 +138,7 @@ impl AgentToolbarItemKind { Self::NLDToggle => Some(Icon::NLD), Self::VoiceInput => Some(Icon::Microphone), Self::FileAttach => Some(Icon::Plus), - Self::ContextWindowUsage => Some(Icon::ConversationContext0), + Self::ContextWindowUsage => Some(Icon::ContextRemaining100), Self::FileExplorer => Some(Icon::FileCopy), Self::RichInput => Some(Icon::TextInput), Self::ShareSession => Some(Icon::Phone01), diff --git a/app/src/ai/blocklist/usage/mod.rs b/app/src/ai/blocklist/usage/mod.rs index c066b12051..6e1ad33e0c 100644 --- a/app/src/ai/blocklist/usage/mod.rs +++ b/app/src/ai/blocklist/usage/mod.rs @@ -6,29 +6,35 @@ pub mod conversation_usage_view; pub mod rollup; pub fn icon_for_context_window_usage(context_window_usage: f32) -> Icon { - // Match the context window usage to the nearest 10% icon. - if context_window_usage >= 0.95 { - Icon::ConversationContext100 - } else if context_window_usage >= 0.85 { - Icon::ConversationContext90 - } else if context_window_usage >= 0.75 { - Icon::ConversationContext80 - } else if context_window_usage >= 0.65 { - Icon::ConversationContext70 - } else if context_window_usage >= 0.55 { - Icon::ConversationContext60 - } else if context_window_usage >= 0.45 { - Icon::ConversationContext50 - } else if context_window_usage >= 0.35 { - Icon::ConversationContext40 - } else if context_window_usage >= 0.25 { - Icon::ConversationContext30 - } else if context_window_usage >= 0.15 { - Icon::ConversationContext20 - } else if context_window_usage >= 0.05 { - Icon::ConversationContext10 + // The circle's solid (white) marks represent the context *remaining*, not + // the amount used: an empty conversation shows an all-white circle (100% + // remaining) and counts down to an all-grey circle as the context window + // fills up (0% remaining). So match the *remaining* fraction + // (`1 - usage`) to the nearest 10% icon, where `ContextRemainingN` + // brightens N% of the ring. + let context_window_remaining = 1.0 - context_window_usage; + if context_window_remaining >= 0.95 { + Icon::ContextRemaining100 + } else if context_window_remaining >= 0.85 { + Icon::ContextRemaining90 + } else if context_window_remaining >= 0.75 { + Icon::ContextRemaining80 + } else if context_window_remaining >= 0.65 { + Icon::ContextRemaining70 + } else if context_window_remaining >= 0.55 { + Icon::ContextRemaining60 + } else if context_window_remaining >= 0.45 { + Icon::ContextRemaining50 + } else if context_window_remaining >= 0.35 { + Icon::ContextRemaining40 + } else if context_window_remaining >= 0.25 { + Icon::ContextRemaining30 + } else if context_window_remaining >= 0.15 { + Icon::ContextRemaining20 + } else if context_window_remaining >= 0.05 { + Icon::ContextRemaining10 } else { - Icon::ConversationContext0 + Icon::ContextRemaining0 } } @@ -47,3 +53,7 @@ pub fn render_context_window_usage_icon( icon.to_warpui_icon(fill).finish() } + +#[cfg(test)] +#[path = "mod_tests.rs"] +mod tests; diff --git a/app/src/ai/blocklist/usage/mod_tests.rs b/app/src/ai/blocklist/usage/mod_tests.rs new file mode 100644 index 0000000000..dbe71cb31e --- /dev/null +++ b/app/src/ai/blocklist/usage/mod_tests.rs @@ -0,0 +1,71 @@ +//! Tests for the context-window usage circle icon mapping. +//! +//! Regression guard for the color semantics of the context-window circle: +//! the solid (white) marks represent the context *remaining*, not the amount +//! used. An empty conversation (0% used → 100% remaining) shows a full white +//! circle and it counts down to an all-grey circle as the window fills up +//! (100% used → 0% remaining). + +use warp_core::ui::Icon; + +use super::icon_for_context_window_usage; + +#[test] +fn empty_conversation_shows_full_white_circle() { + // 0% used == 100% remaining -> all-white circle. + assert_eq!( + icon_for_context_window_usage(0.0), + Icon::ContextRemaining100 + ); +} + +#[test] +fn full_context_window_shows_all_grey_circle() { + // 100% used == 0% remaining -> all-grey circle. + assert_eq!(icon_for_context_window_usage(1.0), Icon::ContextRemaining0); +} + +#[test] +fn icon_brightness_tracks_remaining_not_used() { + // Lightly-used conversation: lots of context remaining -> mostly white. + assert_eq!(icon_for_context_window_usage(0.1), Icon::ContextRemaining90); + // Half used -> half white. + assert_eq!(icon_for_context_window_usage(0.5), Icon::ContextRemaining50); + // Heavily used (the original report's 88%): little remaining -> mostly grey. + assert_eq!( + icon_for_context_window_usage(0.88), + Icon::ContextRemaining10 + ); +} + +#[test] +fn mapping_is_monotonic_more_usage_never_brightens_the_circle() { + // As usage increases, the number of bright (remaining) marks must be + // non-increasing — the circle only ever empties as context fills. + let icon_rank = |usage: f32| match icon_for_context_window_usage(usage) { + Icon::ContextRemaining0 => 0, + Icon::ContextRemaining10 => 10, + Icon::ContextRemaining20 => 20, + Icon::ContextRemaining30 => 30, + Icon::ContextRemaining40 => 40, + Icon::ContextRemaining50 => 50, + Icon::ContextRemaining60 => 60, + Icon::ContextRemaining70 => 70, + Icon::ContextRemaining80 => 80, + Icon::ContextRemaining90 => 90, + Icon::ContextRemaining100 => 100, + other => panic!("unexpected icon: {other:?}"), + }; + + let mut usage = 0.0; + let mut previous = icon_rank(usage); + while usage <= 1.0 { + let current = icon_rank(usage); + assert!( + current <= previous, + "icon brightness increased as usage rose to {usage}: {previous} -> {current}" + ); + previous = current; + usage += 0.05; + } +} diff --git a/crates/warp_core/src/ui/icons.rs b/crates/warp_core/src/ui/icons.rs index 1d4eebd2b2..21dce9d97f 100644 --- a/crates/warp_core/src/ui/icons.rs +++ b/crates/warp_core/src/ui/icons.rs @@ -240,17 +240,17 @@ pub enum Icon { ContextWindowSixtyPct, ContextWindowWarning, ContextWindowSummarized, - ConversationContext0, - ConversationContext10, - ConversationContext20, - ConversationContext30, - ConversationContext40, - ConversationContext50, - ConversationContext60, - ConversationContext70, - ConversationContext80, - ConversationContext90, - ConversationContext100, + ContextRemaining0, + ContextRemaining10, + ContextRemaining20, + ContextRemaining30, + ContextRemaining40, + ContextRemaining50, + ContextRemaining60, + ContextRemaining70, + ContextRemaining80, + ContextRemaining90, + ContextRemaining100, LeftSidebarOpen, LeftSidebarClose, Diff, @@ -577,17 +577,17 @@ impl From for &'static str { Icon::ContextWindowSixtyPct => "bundled/svg/context-window-60-pct.svg", Icon::ContextWindowWarning => "bundled/svg/context-window-warning.svg", Icon::ContextWindowSummarized => "bundled/svg/context-window-summarized.svg", - Icon::ConversationContext0 => "bundled/svg/conversation-context-0.svg", - Icon::ConversationContext10 => "bundled/svg/conversation-context-10.svg", - Icon::ConversationContext20 => "bundled/svg/conversation-context-20.svg", - Icon::ConversationContext30 => "bundled/svg/conversation-context-30.svg", - Icon::ConversationContext40 => "bundled/svg/conversation-context-40.svg", - Icon::ConversationContext50 => "bundled/svg/conversation-context-50.svg", - Icon::ConversationContext60 => "bundled/svg/conversation-context-60.svg", - Icon::ConversationContext70 => "bundled/svg/conversation-context-70.svg", - Icon::ConversationContext80 => "bundled/svg/conversation-context-80.svg", - Icon::ConversationContext90 => "bundled/svg/conversation-context-90.svg", - Icon::ConversationContext100 => "bundled/svg/conversation-context-100.svg", + Icon::ContextRemaining0 => "bundled/svg/context-remaining-0.svg", + Icon::ContextRemaining10 => "bundled/svg/context-remaining-10.svg", + Icon::ContextRemaining20 => "bundled/svg/context-remaining-20.svg", + Icon::ContextRemaining30 => "bundled/svg/context-remaining-30.svg", + Icon::ContextRemaining40 => "bundled/svg/context-remaining-40.svg", + Icon::ContextRemaining50 => "bundled/svg/context-remaining-50.svg", + Icon::ContextRemaining60 => "bundled/svg/context-remaining-60.svg", + Icon::ContextRemaining70 => "bundled/svg/context-remaining-70.svg", + Icon::ContextRemaining80 => "bundled/svg/context-remaining-80.svg", + Icon::ContextRemaining90 => "bundled/svg/context-remaining-90.svg", + Icon::ContextRemaining100 => "bundled/svg/context-remaining-100.svg", Icon::LeftSidebarOpen => "bundled/svg/left-panel-open.svg", Icon::LeftSidebarClose => "bundled/svg/left-panel-close.svg", Icon::Diff => "bundled/svg/diff.svg",