Merge branch 'show-thinking-timer' into 'master'
show thinking timer See merge request samnasbo/shared/galaxy!3
This commit is contained in:
Generated
+4
@@ -5191,7 +5191,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "galaxy"
|
||||
<<<<<<< HEAD
|
||||
version = "1.5.3"
|
||||
=======
|
||||
version = "1.5.4"
|
||||
>>>>>>> master
|
||||
dependencies = [
|
||||
"addr",
|
||||
"aho-corasick",
|
||||
|
||||
@@ -133,6 +133,13 @@ pub struct BlocklistAIStatusBar {
|
||||
/// the warping indicator while the active block has a recorded LRC snapshot.
|
||||
last_read_refresh_handle: Option<SpawnedFutureHandle>,
|
||||
|
||||
/// The time the warping indicator started showing for the current exchange.
|
||||
warping_start_time: Option<Instant>,
|
||||
/// The random loading message chosen for the current exchange, stable across re-renders.
|
||||
warping_message: Option<&'static str>,
|
||||
/// Handle for the periodic timer that updates the warping elapsed timer UI.
|
||||
warping_timer_handle: Option<SpawnedFutureHandle>,
|
||||
|
||||
latest_response_stream_id: Option<ResponseStreamId>,
|
||||
|
||||
/// Agent tip to display below the warping indicator.
|
||||
@@ -185,6 +192,7 @@ impl BlocklistAIStatusBar {
|
||||
}
|
||||
BlocklistAIHistoryEvent::ClearedConversationsInTerminalView { .. } => {
|
||||
me.active_exchange_model = None;
|
||||
me.stop_warping_timer();
|
||||
ctx.notify();
|
||||
}
|
||||
BlocklistAIHistoryEvent::ClearedActiveConversation {
|
||||
@@ -199,6 +207,7 @@ impl BlocklistAIStatusBar {
|
||||
.is_some_and(|id| id == *conversation_id)
|
||||
}) {
|
||||
me.active_exchange_model = None;
|
||||
me.stop_warping_timer();
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
@@ -394,6 +403,9 @@ impl BlocklistAIStatusBar {
|
||||
summarization_timer_handle: None,
|
||||
summarization_start_time: None,
|
||||
last_read_refresh_handle: None,
|
||||
warping_start_time: None,
|
||||
warping_message: None,
|
||||
warping_timer_handle: None,
|
||||
ambient_agent_view_model,
|
||||
current_tip: None,
|
||||
ephemeral_message_model,
|
||||
@@ -463,6 +475,7 @@ impl BlocklistAIStatusBar {
|
||||
}) {
|
||||
let Some(conversation) = conversation else {
|
||||
self.active_exchange_model = None;
|
||||
self.stop_warping_timer();
|
||||
ctx.notify();
|
||||
return;
|
||||
};
|
||||
@@ -486,6 +499,7 @@ impl BlocklistAIStatusBar {
|
||||
});
|
||||
self.is_summarization_cancel_dialog_open = false;
|
||||
self.stop_summarization_timer();
|
||||
self.start_warping_timer(ctx);
|
||||
|
||||
if FeatureFlag::AgentTips.is_enabled() {
|
||||
self.update_agent_tip(ctx);
|
||||
@@ -499,6 +513,13 @@ impl BlocklistAIStatusBar {
|
||||
};
|
||||
let status = model.status(ctx);
|
||||
|
||||
let is_finished = matches!(
|
||||
status,
|
||||
AIBlockOutputStatus::Complete { .. }
|
||||
| AIBlockOutputStatus::Cancelled { .. }
|
||||
| AIBlockOutputStatus::Failed { .. }
|
||||
);
|
||||
|
||||
// Auto-clear summarization confirmation dialog if summarization is no longer active
|
||||
if self.is_summarization_cancel_dialog_open
|
||||
&& !model.is_conversation_summarization_active(ctx)
|
||||
@@ -525,6 +546,13 @@ impl BlocklistAIStatusBar {
|
||||
AIBlockOutputStatus::Pending | AIBlockOutputStatus::Failed { .. } => (),
|
||||
}
|
||||
|
||||
// Stop the warping timer when the exchange is no longer streaming/pending.
|
||||
// Placed after the match so `model` (which borrows self.active_exchange_model)
|
||||
// is no longer used, avoiding borrow conflicts with &mut self.
|
||||
if is_finished {
|
||||
self.stop_warping_timer();
|
||||
}
|
||||
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
@@ -668,6 +696,58 @@ impl BlocklistAIStatusBar {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a 1-second periodic timer that keeps the warping elapsed-time indicator fresh.
|
||||
fn start_warping_timer(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.warping_start_time = Some(Instant::now());
|
||||
self.warping_message = Some(random_load_output_message());
|
||||
// Don't start a new timer if one is already running
|
||||
if self.warping_timer_handle.is_some() {
|
||||
return;
|
||||
}
|
||||
let handle = ctx.spawn(
|
||||
async move {
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
},
|
||||
|me, _, ctx| {
|
||||
me.warping_timer_handle = None;
|
||||
if me.warping_start_time.is_some() {
|
||||
ctx.notify();
|
||||
me.restart_warping_timer(ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
self.warping_timer_handle = Some(handle);
|
||||
}
|
||||
|
||||
/// Restarts the warping timer for the next tick (called from within the timer callback).
|
||||
fn restart_warping_timer(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
if self.warping_timer_handle.is_some() {
|
||||
return;
|
||||
}
|
||||
let handle = ctx.spawn(
|
||||
async move {
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
},
|
||||
|me, _, ctx| {
|
||||
me.warping_timer_handle = None;
|
||||
if me.warping_start_time.is_some() {
|
||||
ctx.notify();
|
||||
me.restart_warping_timer(ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
self.warping_timer_handle = Some(handle);
|
||||
}
|
||||
|
||||
/// Stops the warping elapsed-time timer.
|
||||
fn stop_warping_timer(&mut self) {
|
||||
self.warping_start_time = None;
|
||||
self.warping_message = None;
|
||||
if let Some(handle) = self.warping_timer_handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
fn should_refresh_last_read_timer(&self, ctx: &ViewContext<Self>) -> bool {
|
||||
let active_block_id = self
|
||||
.terminal_model
|
||||
@@ -813,7 +893,8 @@ impl BlocklistAIStatusBar {
|
||||
);
|
||||
let default_warping_text = fallback_warping_text
|
||||
.as_deref()
|
||||
.unwrap_or(random_load_output_message())
|
||||
.or(self.warping_message)
|
||||
.unwrap_or_else(|| random_load_output_message())
|
||||
.to_owned();
|
||||
let secondary_element = if fallback_warping_text.is_some() {
|
||||
Some(render_fallback_explanation(model.as_ref(), app))
|
||||
@@ -870,6 +951,7 @@ impl BlocklistAIStatusBar {
|
||||
default_warping_text,
|
||||
secondary_element,
|
||||
last_snapshot_at,
|
||||
warping_start_time: self.warping_start_time,
|
||||
},
|
||||
app,
|
||||
))
|
||||
|
||||
@@ -240,6 +240,8 @@ pub struct WarpingProps<'a, V> {
|
||||
pub secondary_element: Option<Box<dyn Element>>,
|
||||
/// When an LRC subagent has sent at least one snapshot, the timestamp of the most recent snapshot.
|
||||
pub last_snapshot_at: Option<instant::Instant>,
|
||||
/// When the warping indicator started showing, used to display an elapsed timer.
|
||||
pub warping_start_time: Option<instant::Instant>,
|
||||
}
|
||||
|
||||
pub struct ButtonProps<'a> {
|
||||
@@ -470,6 +472,11 @@ pub fn render_warping_indicator<V: View>(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show elapsed timer alongside the random Galaxy status message.
|
||||
if let Some(start_time) = props.warping_start_time {
|
||||
non_shimmering_text =
|
||||
Some(format!(" ({})", format_elapsed_compact(start_time.elapsed())));
|
||||
}
|
||||
props.default_warping_text.clone()
|
||||
}
|
||||
}
|
||||
@@ -725,6 +732,24 @@ pub fn format_elapsed_seconds(elapsed: std::time::Duration) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats elapsed time in a compact form for the warping indicator timer.
|
||||
/// - Less than 60 seconds: shows seconds (e.g. "5s", "42s")
|
||||
/// - Less than 120 minutes: shows minutes (e.g. "1m", "45m")
|
||||
/// - 120 minutes or more: shows hours (e.g. "2h", "3h")
|
||||
pub fn format_elapsed_compact(elapsed: std::time::Duration) -> String {
|
||||
let total_seconds = elapsed.as_secs();
|
||||
if total_seconds < 60 {
|
||||
format!("{total_seconds}s")
|
||||
} else {
|
||||
let total_minutes = total_seconds / 60;
|
||||
if total_minutes < 120 {
|
||||
format!("{total_minutes}m")
|
||||
} else {
|
||||
format!("{}h", total_minutes / 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render output text as shown in the "stopped" and "loading" status banners
|
||||
pub fn render_output_status_text(
|
||||
label: MaybeShimmeringText,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use itertools::Itertools;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use ai::skills::{ParsedSkill, SkillProvider, SkillScope};
|
||||
#[cfg(feature = "local_fs")]
|
||||
@@ -10,9 +10,10 @@ use galaxyui::App;
|
||||
use super::{blocklist_image_asset_source, ResolvedBlocklistImageSources};
|
||||
use super::{
|
||||
collect_visual_markdown_lightbox_collection, compute_visual_section_width,
|
||||
inline_image_source_label, lightbox_trigger_for_section, query_prefix_highlight_len,
|
||||
render_scrollable_collapsible_content, text_sections_with_indices, CollapsibleElementState,
|
||||
CollapsibleExpansionState, VisualMarkdownLightboxCollection,
|
||||
format_elapsed_compact, inline_image_source_label, lightbox_trigger_for_section,
|
||||
query_prefix_highlight_len, render_scrollable_collapsible_content,
|
||||
text_sections_with_indices, CollapsibleElementState, CollapsibleExpansionState,
|
||||
VisualMarkdownLightboxCollection,
|
||||
};
|
||||
use crate::{
|
||||
ai::agent::{
|
||||
@@ -292,3 +293,27 @@ fn blocklist_image_asset_source_uses_cached_resolution_when_available() {
|
||||
other => panic!("expected cached local file asset source, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_elapsed_compact_shows_seconds_under_one_minute() {
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(0)), "0s");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(1)), "1s");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(42)), "42s");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(59)), "59s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_elapsed_compact_shows_minutes_under_120_minutes() {
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(60)), "1m");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(90)), "1m");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(120)), "2m");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(45 * 60)), "45m");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(119 * 60 + 59)), "119m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_elapsed_compact_shows_hours_at_120_minutes_and_above() {
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(120 * 60)), "2h");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(180 * 60)), "3h");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(600 * 60)), "10h");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user