Skip to content

Commit 0ab95fd

Browse files
committed
feat(tui): add auto-update notification system
- Add UpdateNotificationStatus enum for tracking update states - Add update_status, update_progress, update_version fields to AppState - Spawn background update check task on TUI startup - Add update check result handling in EventLoop - Render update banner above input field with status-specific messages - Add cortex-update dependency to cortex-tui
1 parent e6658f8 commit 0ab95fd

File tree

7 files changed

+144
-4
lines changed

7 files changed

+144
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cortex-tui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ cortex-protocol = { workspace = true }
2828
cortex-common = { workspace = true }
2929
cortex-login = { workspace = true }
3030
cortex-agents = { workspace = true }
31+
cortex-update = { path = "../cortex-update" }
3132

3233
# TUI framework
3334
ratatui = { workspace = true }

src/cortex-tui/src/app/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ mod types;
1515
pub use approval::{ApprovalState, PendingToolResult};
1616
pub use autocomplete::{AutocompleteItem, AutocompleteState};
1717
pub use session::{ActiveModal, SessionSummary};
18-
pub use state::AppState;
18+
pub use state::{AppState, UpdateNotificationStatus};
1919
pub use streaming::StreamingState;
2020
pub use subagent::{
2121
SubagentDisplayStatus, SubagentTaskDisplay, SubagentTodoItem, SubagentTodoStatus,

src/cortex-tui/src/app/state.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ use super::streaming::StreamingState;
2222
use super::subagent::SubagentTaskDisplay;
2323
use super::types::{AppView, FocusTarget, OperationMode};
2424

25+
/// Status of the auto-update notification system
26+
#[derive(Debug, Clone, PartialEq, Eq)]
27+
pub enum UpdateNotificationStatus {
28+
/// Update check in progress
29+
Checking,
30+
/// A new version is available
31+
Available,
32+
/// Currently downloading the update
33+
Downloading,
34+
/// Download complete, restart required
35+
RestartRequired,
36+
}
37+
2538
/// Main application state
2639
pub struct AppState {
2740
pub view: AppView,
@@ -172,6 +185,12 @@ pub struct AppState {
172185
pub user_email: Option<String>,
173186
/// Organization name for welcome screen
174187
pub org_name: Option<String>,
188+
/// Auto-update notification state
189+
pub update_status: Option<UpdateNotificationStatus>,
190+
/// Download progress percentage (0-100)
191+
pub update_progress: Option<f32>,
192+
/// New version string when available
193+
pub update_version: Option<String>,
175194
}
176195

177196
impl AppState {
@@ -272,6 +291,9 @@ impl AppState {
272291
user_name: None,
273292
user_email: None,
274293
org_name: None,
294+
update_status: None,
295+
update_progress: None,
296+
update_version: None,
275297
}
276298
}
277299

src/cortex-tui/src/runner/app_runner/runner.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,11 +731,42 @@ impl AppRunner {
731731
Arc::new(registry)
732732
};
733733

734+
// ====================================================================
735+
// BACKGROUND TASK: Check for updates (non-blocking)
736+
// ====================================================================
737+
let update_check_task = tokio::spawn(async move {
738+
match cortex_update::UpdateManager::new() {
739+
Ok(manager) => match manager.check_update().await {
740+
Ok(Some(info)) => {
741+
tracing::info!(
742+
"Update available: {} -> {}",
743+
info.current_version,
744+
info.latest_version
745+
);
746+
Some((info.latest_version, info.asset.sha256))
747+
}
748+
Ok(None) => {
749+
tracing::debug!("No updates available");
750+
None
751+
}
752+
Err(e) => {
753+
tracing::debug!("Failed to check for updates: {}", e);
754+
None
755+
}
756+
},
757+
Err(e) => {
758+
tracing::debug!("Failed to create update manager: {}", e);
759+
None
760+
}
761+
}
762+
});
763+
734764
// Create event loop with provider manager, cortex session, and tool registry
735765
let mut event_loop = EventLoop::new(app_state)
736766
.with_provider_manager(provider_manager)
737767
.with_cortex_session(cortex_session)
738-
.with_tool_registry(tool_registry);
768+
.with_tool_registry(tool_registry)
769+
.with_update_check_task(update_check_task);
739770

740771
// Add unified executor if available
741772
if let Some(executor) = unified_executor {

src/cortex-tui/src/runner/event_loop/core.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ pub struct EventLoop {
165165

166166
/// TUI capture manager for debugging (enabled via CORTEX_TUI_CAPTURE=1).
167167
pub(super) tui_capture: TuiCapture,
168+
169+
/// Handle for background update check task
170+
pub(super) update_check_task: Option<JoinHandle<Option<(String, String)>>>,
168171
}
169172

170173
impl EventLoop {
@@ -211,6 +214,7 @@ impl EventLoop {
211214
is_continuation: false,
212215
_undo_stack: Vec::new(),
213216
tui_capture,
217+
update_check_task: None,
214218
}
215219
}
216220

@@ -244,6 +248,12 @@ impl EventLoop {
244248
self
245249
}
246250

251+
/// Sets the update check background task.
252+
pub fn with_update_check_task(mut self, task: JoinHandle<Option<(String, String)>>) -> Self {
253+
self.update_check_task = Some(task);
254+
self
255+
}
256+
247257
/// Runs the main event loop.
248258
///
249259
/// This method initializes the FrameEngine to poll keyboard, mouse, and
@@ -365,6 +375,29 @@ impl EventLoop {
365375
tracing::error!("Error rendering after tool event: {}", e);
366376
}
367377
}
378+
379+
// Branch 4: Background update check result
380+
result = async {
381+
match self.update_check_task.as_mut() {
382+
Some(task) => task.await,
383+
None => std::future::pending().await,
384+
}
385+
} => {
386+
// Remove the task handle since it's complete
387+
self.update_check_task = None;
388+
389+
// Update app state with result
390+
if let Ok(Some((version, _sha256))) = result {
391+
self.app_state.update_status = Some(crate::app::UpdateNotificationStatus::Available);
392+
self.app_state.update_version = Some(version);
393+
self.app_state.toasts.info("A new version of Cortex is available!");
394+
}
395+
396+
// Re-render to show update banner
397+
if let Err(e) = self.render(terminal) {
398+
tracing::error!("Error rendering after update check: {}", e);
399+
}
400+
}
368401
}
369402
}
370403

src/cortex-tui/src/views/minimal_session/view.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,47 @@ impl<'a> MinimalSessionView<'a> {
378378
}
379379
}
380380

381+
/// Renders an update notification banner above the input area.
382+
fn render_update_banner(
383+
&self,
384+
area: Rect,
385+
buf: &mut Buffer,
386+
status: &crate::app::UpdateNotificationStatus,
387+
) {
388+
use crate::app::UpdateNotificationStatus;
389+
390+
// Determine the banner text and color based on status
391+
let (banner_text, color) = match status {
392+
UpdateNotificationStatus::Checking => {
393+
("🔍 Checking for updates...".to_string(), self.colors.text_dim)
394+
}
395+
UpdateNotificationStatus::Available => {
396+
let text = if let Some(ref version) = self.app_state.update_version {
397+
format!("🔄 A new version is available ({})", version)
398+
} else {
399+
"🔄 A new version is available".to_string()
400+
};
401+
(text, self.colors.accent)
402+
}
403+
UpdateNotificationStatus::Downloading => {
404+
let text = if let Some(progress) = self.app_state.update_progress {
405+
format!("⬇️ Downloading... {:.0}%", progress)
406+
} else {
407+
"⬇️ Downloading...".to_string()
408+
};
409+
(text, self.colors.warning)
410+
}
411+
UpdateNotificationStatus::RestartRequired => (
412+
"✅ You must restart to run the latest version".to_string(),
413+
self.colors.success,
414+
),
415+
};
416+
417+
// Render the banner at the provided area
418+
let banner_style = Style::default().fg(color);
419+
buf.set_string(area.x + 2, area.y, &banner_text, banner_style);
420+
}
421+
381422
/// Returns whether a task is currently running.
382423
fn is_task_running(&self) -> bool {
383424
self.app_state.streaming.is_streaming
@@ -571,6 +612,9 @@ impl<'a> Widget for MinimalSessionView<'a> {
571612
let status_height: u16 = if is_task_running { 1 } else { 0 };
572613
let input_height: u16 = 3;
573614
let hints_height: u16 = 1;
615+
// Add 1 row for update banner if update is available
616+
let has_update_banner = self.app_state.update_status.is_some();
617+
let update_banner_height: u16 = if has_update_banner { 1 } else { 0 };
574618

575619
// Calculate welcome card heights from render_motd constants
576620
let welcome_card_height = 11_u16;
@@ -584,7 +628,8 @@ impl<'a> Widget for MinimalSessionView<'a> {
584628
layout.gap(1);
585629

586630
// Calculate available height for scrollable content (before input/hints)
587-
let bottom_reserved = status_height + input_height + autocomplete_height + hints_height + 2; // +2 for gaps
631+
let bottom_reserved =
632+
status_height + update_banner_height + input_height + autocomplete_height + hints_height + 2; // +2 for gaps
588633
let available_height = area.height.saturating_sub(1 + bottom_reserved); // 1 for top margin
589634

590635
// Render scrollable content area (welcome cards + messages together)
@@ -608,7 +653,14 @@ impl<'a> Widget for MinimalSessionView<'a> {
608653
next_y += status_height;
609654
}
610655

611-
// 6. Input area - follows status (or content if no status)
656+
// 5.5. Update notification banner (above input, after status)
657+
if let Some(ref status) = self.app_state.update_status {
658+
let banner_area = Rect::new(area.x, next_y, area.width, update_banner_height);
659+
self.render_update_banner(banner_area, buf, status);
660+
next_y += update_banner_height;
661+
}
662+
663+
// 6. Input area - follows status/banner (or content if no status)
612664
let input_y = next_y;
613665
let input_area = Rect::new(area.x, input_y, area.width, input_height);
614666

0 commit comments

Comments
 (0)