aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-23 17:28:14 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-23 17:28:14 +0300
commit8d855cd9ef32e0c3172e038645b4b534f75c16c8 (patch)
treea808d7a3db96cd9af27b72adc5b6bc4dfc5f0c8d /src
parentc4dd318ea3770d9d0245d077c7ed48c2689384c9 (diff)
fix: use live cwd for workspace labels
Diffstat (limited to 'src')
-rw-r--r--src/ui.rs4
-rw-r--r--src/ui/mobile.rs124
-rw-r--r--src/ui/sidebar.rs103
3 files changed, 215 insertions, 16 deletions
diff --git a/src/ui.rs b/src/ui.rs
index 42b861ee..0ddaa71c 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -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();