diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-23 17:28:14 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-23 17:28:14 +0300 |
| commit | 8d855cd9ef32e0c3172e038645b4b534f75c16c8 (patch) | |
| tree | a808d7a3db96cd9af27b72adc5b6bc4dfc5f0c8d /src | |
| parent | c4dd318ea3770d9d0245d077c7ed48c2689384c9 (diff) | |
fix: use live cwd for workspace labels
Diffstat (limited to 'src')
| -rw-r--r-- | src/ui.rs | 4 | ||||
| -rw-r--r-- | src/ui/mobile.rs | 124 | ||||
| -rw-r--r-- | src/ui/sidebar.rs | 103 |
3 files changed, 215 insertions, 16 deletions
@@ -354,7 +354,7 @@ pub fn render_with_runtime_registry( let terminal_area = app.view.terminal_area; if app.view.layout == ViewLayout::Mobile { - render_mobile_header(app, frame, app.view.mobile_header_rect); + render_mobile_header(app, terminal_runtimes, frame, app.view.mobile_header_rect); } else if app.sidebar_collapsed { render_sidebar_collapsed(app, frame, sidebar_area); } else { @@ -373,7 +373,7 @@ pub fn render_with_runtime_registry( Mode::ReleaseNotes => render_release_notes_overlay(app, frame, frame.area()), Mode::ProductAnnouncement => render_product_announcement_overlay(app, frame, frame.area()), Mode::Navigate if app.view.layout == ViewLayout::Mobile => { - render_mobile_panel(app, frame, frame.area()) + render_mobile_panel(app, terminal_runtimes, frame, frame.area()) } Mode::Navigate => render_navigate_overlay(app, frame, terminal_area), Mode::Prefix => render_prefix_overlay(app, frame, terminal_area), diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs index 8bcdf184..89557642 100644 --- a/src/ui/mobile.rs +++ b/src/ui/mobile.rs @@ -6,12 +6,13 @@ use ratatui::{ Frame, }; -use super::sidebar::{agent_panel_entries, AgentPanelEntry}; +use super::sidebar::{agent_panel_entries, agent_panel_entries_from, AgentPanelEntry}; use super::status::{agent_icon, state_dot}; use crate::app::state::{Palette, ToastKind, ToastNotification}; use crate::app::AppState; use crate::detect::AgentState; use crate::layout::PaneId; +use crate::terminal::TerminalRuntimeRegistry; pub(crate) const MOBILE_WIDTH_THRESHOLD: u16 = 64; const SWITCH_BUTTON_WIDTH: u16 = 10; @@ -156,7 +157,12 @@ pub(crate) fn mobile_switcher_target_at( (menu_idx < app.global_menu_labels().len()).then_some(MobileSwitcherTarget::Menu(menu_idx)) } -pub(crate) fn render_mobile_header(app: &AppState, frame: &mut Frame, area: Rect) { +pub(crate) fn render_mobile_header( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + frame: &mut Frame, + area: Rect, +) { if area.width == 0 || area.height == 0 { return; } @@ -168,7 +174,7 @@ pub(crate) fn render_mobile_header(app: &AppState, frame: &mut Frame, area: Rect let status_w = switch.x.saturating_sub(area.x).saturating_sub(1); let status = Rect::new(area.x, area.y, status_w, area.height); - render_header_status(app, frame, status); + render_header_status(app, terminal_runtimes, frame, status); render_switch_button(app, frame, switch); } @@ -224,7 +230,12 @@ pub(crate) fn render_mobile_toast_banner( ); } -pub(crate) fn render_mobile_panel(app: &AppState, frame: &mut Frame, area: Rect) { +pub(crate) fn render_mobile_panel( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + frame: &mut Frame, + area: Rect, +) { if area.width == 0 || area.height == 0 { return; } @@ -253,10 +264,15 @@ pub(crate) fn render_mobile_panel(app: &AppState, frame: &mut Frame, area: Rect) ); } - render_mobile_switcher_content(app, frame, areas.viewport); + render_mobile_switcher_content(app, terminal_runtimes, frame, areas.viewport); } -fn render_header_status(app: &AppState, frame: &mut Frame, area: Rect) { +fn render_header_status( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + frame: &mut Frame, + area: Rect, +) { if area.width == 0 || area.height == 0 { return; } @@ -286,7 +302,10 @@ fn render_header_status(app: &AppState, frame: &mut Frame, area: Rect) { Span::styled(dot, dot_style.bg(p.panel_bg)), Span::raw(" "), Span::styled( - truncate(&ws.display_name(), name_w.saturating_sub(4) as usize), + truncate( + &ws.display_name_from(&app.terminals, terminal_runtimes), + name_w.saturating_sub(4) as usize, + ), Style::default() .fg(p.text) .bg(p.panel_bg) @@ -385,7 +404,12 @@ fn mobile_switcher_content_height(app: &AppState) -> usize { spaces_h + tabs_h + agents_h + menu_h } -fn render_mobile_switcher_content(app: &AppState, frame: &mut Frame, viewport: Rect) { +fn render_mobile_switcher_content( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + frame: &mut Frame, + viewport: Rect, +) { if viewport.width == 0 || viewport.height == 0 { return; } @@ -437,7 +461,10 @@ fn render_mobile_switcher_content(app: &AppState, frame: &mut Frame, viewport: R Span::styled(dot, dot_style.bg(bg)), Span::styled(" ", Style::default().bg(bg)), Span::styled( - truncate(&ws.display_name(), content.width.saturating_sub(5) as usize), + truncate( + &ws.display_name_from(&app.terminals, terminal_runtimes), + content.width.saturating_sub(5) as usize, + ), Style::default() .fg(p.text) .bg(bg) @@ -521,7 +548,7 @@ fn render_mobile_switcher_content(app: &AppState, frame: &mut Frame, viewport: R ws.focused_pane_id() .map(|pane_id| (ws_idx, ws.active_tab, pane_id)) }); - let entries = agent_panel_entries(app); + let entries = agent_panel_entries_from(app, terminal_runtimes); render_section_title_at( frame, viewport, @@ -929,4 +956,81 @@ mod tests { assert_eq!(mobile_agent_detail(&entry), " idle ยท pi"); } + + #[tokio::test] + async fn mobile_header_uses_live_root_runtime_cwd_for_workspace_label() { + let unique = format!( + "herdr-mobile-header-runtime-cwd-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let root = std::env::temp_dir().join(unique); + let stale_cwd = root.join("issue-264-nix-support"); + let live_cwd = root.join("herdr"); + std::fs::create_dir_all(stale_cwd.join(".git")).unwrap(); + std::fs::create_dir_all(live_cwd.join(".git")).unwrap(); + + let mut app = crate::app::state::AppState::test_new(); + let mut workspace = crate::workspace::Workspace::test_new("stale-name"); + workspace.custom_name = None; + workspace.identity_cwd = stale_cwd.clone(); + let pane = workspace.tabs[0].root_pane; + + app.workspaces = vec![workspace]; + app.ensure_test_terminals(); + let terminal_id = app.workspaces[0].tabs[0].panes[&pane] + .attached_terminal_id + .clone(); + app.terminals.get_mut(&terminal_id).unwrap().cwd = stale_cwd; + app.active = Some(0); + app.selected = 0; + app.view.mobile_menu_hit_area = Rect::new(30, 0, 10, 2); + + let (events, _) = tokio::sync::mpsc::channel(4); + let runtime = crate::terminal::TerminalRuntime::spawn( + pane, + 24, + 80, + live_cwd.clone(), + 0, + crate::terminal_theme::TerminalTheme::default(), + "/bin/sh", + events, + std::sync::Arc::new(tokio::sync::Notify::new()), + std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), + ) + .unwrap(); + + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); + while runtime.cwd() != Some(live_cwd.clone()) && std::time::Instant::now() < deadline { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + let mut runtime_registry = TerminalRuntimeRegistry::new(); + runtime_registry.insert(terminal_id, runtime); + let backend = ratatui::backend::TestBackend::new(40, 2); + let mut terminal = ratatui::Terminal::new(backend).unwrap(); + terminal + .draw(|frame| { + render_mobile_header(&app, &runtime_registry, frame, Rect::new(0, 0, 40, 2)) + }) + .unwrap(); + let row = (0..40) + .map(|x| terminal.backend().buffer()[(x, 0)].symbol()) + .collect::<String>(); + + for (_, runtime) in runtime_registry.drain() { + runtime.shutdown(); + } + let _ = std::fs::remove_dir_all(root); + + assert!(row.contains("herdr"), "header row: {row:?}"); + assert!( + !row.contains("issue-264-nix-support"), + "header row: {row:?}" + ); + } } diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 509c400b..1e932503 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -110,6 +110,29 @@ pub(crate) fn agent_panel_toggle_rect(area: Rect, scope: AgentPanelScope) -> Rec } pub(crate) fn agent_panel_entries(app: &AppState) -> Vec<AgentPanelEntry> { + agent_panel_entries_with_runtimes(app, None) +} + +pub(crate) fn agent_panel_entries_from( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, +) -> Vec<AgentPanelEntry> { + agent_panel_entries_with_runtimes(app, Some(terminal_runtimes)) +} + +fn agent_panel_entries_with_runtimes( + app: &AppState, + terminal_runtimes: Option<&TerminalRuntimeRegistry>, +) -> Vec<AgentPanelEntry> { + let empty_runtimes; + let terminal_runtimes = match terminal_runtimes { + Some(terminal_runtimes) => terminal_runtimes, + None => { + empty_runtimes = TerminalRuntimeRegistry::new(); + &empty_runtimes + } + }; + match app.agent_panel_scope { AgentPanelScope::CurrentWorkspace => { let Some(ws_idx) = agent_panel_current_workspace_idx(app) else { @@ -139,7 +162,7 @@ pub(crate) fn agent_panel_entries(app: &AppState) -> Vec<AgentPanelEntry> { .enumerate() .flat_map(|(ws_idx, ws)| { let multi_tab = ws.tabs.len() > 1; - let workspace_label = ws.display_name(); + let workspace_label = ws.display_name_from(&app.terminals, terminal_runtimes); ws.pane_details(&app.terminals) .into_iter() .map(move |detail| AgentPanelEntry { @@ -797,7 +820,7 @@ pub(super) fn render_sidebar( let (ws_area, detail_area) = expanded_sidebar_sections(area, app.sidebar_section_split); render_workspace_list(app, terminal_runtimes, frame, ws_area, is_navigating); - render_agent_detail(app, frame, detail_area); + render_agent_detail(app, terminal_runtimes, frame, detail_area); render_sidebar_toggle(app, frame, area, false, p); } @@ -1010,7 +1033,12 @@ fn render_workspace_list( } } -fn render_agent_detail(app: &AppState, frame: &mut Frame, area: Rect) { +fn render_agent_detail( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + frame: &mut Frame, + area: Rect, +) { let p = &app.palette; if area.height < 3 { @@ -1042,7 +1070,7 @@ fn render_agent_detail(app: &AppState, frame: &mut Frame, area: Rect) { ); } - let details = agent_panel_entries(app); + let details = agent_panel_entries_from(app, terminal_runtimes); let metrics = agent_panel_scroll_metrics(app, area); let scrollbar_rect = agent_panel_scrollbar_rect(app, area); let body = agent_panel_body_rect(area, should_show_scrollbar(metrics)); @@ -1199,6 +1227,73 @@ mod tests { assert_eq!(entries[1].agent_label.as_deref(), Some("claude")); } + #[tokio::test] + async fn all_workspaces_agent_panel_entries_use_live_root_runtime_cwd_for_workspace_label() { + let unique = format!( + "herdr-agent-panel-runtime-cwd-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let root = std::env::temp_dir().join(unique); + let stale_cwd = root.join("issue-264-nix-support"); + let live_cwd = root.join("herdr"); + std::fs::create_dir_all(stale_cwd.join(".git")).unwrap(); + std::fs::create_dir_all(live_cwd.join(".git")).unwrap(); + + let mut app = crate::app::state::AppState::test_new(); + let mut workspace = Workspace::test_new("stale-name"); + workspace.custom_name = None; + workspace.identity_cwd = stale_cwd.clone(); + let pane = workspace.tabs[0].root_pane; + + app.workspaces = vec![workspace]; + app.ensure_test_terminals(); + let terminal_id = app.workspaces[0].tabs[0].panes[&pane] + .attached_terminal_id + .clone(); + let terminal = app.terminals.get_mut(&terminal_id).unwrap(); + terminal.cwd = stale_cwd; + terminal.detected_agent = Some(Agent::Pi); + app.active = Some(0); + app.selected = 0; + app.agent_panel_scope = AgentPanelScope::AllWorkspaces; + + let (events, _) = tokio::sync::mpsc::channel(4); + let runtime = crate::terminal::TerminalRuntime::spawn( + pane, + 24, + 80, + live_cwd.clone(), + 0, + crate::terminal_theme::TerminalTheme::default(), + "/bin/sh", + events, + std::sync::Arc::new(tokio::sync::Notify::new()), + std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), + ) + .unwrap(); + + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); + while runtime.cwd() != Some(live_cwd.clone()) && std::time::Instant::now() < deadline { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + let mut runtime_registry = TerminalRuntimeRegistry::new(); + runtime_registry.insert(terminal_id, runtime); + let entries = agent_panel_entries_from(&app, &runtime_registry); + let primary_label = entries[0].primary_label.clone(); + + for (_, runtime) in runtime_registry.drain() { + runtime.shutdown(); + } + let _ = std::fs::remove_dir_all(root); + + assert_eq!(primary_label, "herdr"); + } + #[test] fn all_workspaces_agent_panel_entries_prefer_agent_names_for_agent_identity() { let mut app = crate::app::state::AppState::test_new(); |
