fix: keep auto tab labels compact
This commit is contained in:
parent
efe7c55769
commit
4ffd99c2ec
|
|
@ -31,6 +31,7 @@
|
|||
### Fixed
|
||||
- Config startup and reload now warn about unknown top-level table sections, including a `[toast]` hint that points to `[ui.toast]`, instead of silently ignoring them.
|
||||
- Claude Code session restore now accepts real `/clear`, `/resume`, and compacted session identity changes while still ignoring nested `claude -p` startup sessions that inherit the pane environment. (#620)
|
||||
- Auto-named tab labels now stay compact after closing, moving, or creating tabs while public tab ids remain stable.
|
||||
- F1-F4 key presses sent as `ESC[11~` through `ESC[14~` now reach pane apps instead of being dropped. (#574)
|
||||
|
||||
## [0.6.10] - 2026-06-11
|
||||
|
|
|
|||
|
|
@ -219,8 +219,9 @@ pub fn notification_context(
|
|||
let mut context = format!("{} · {}", workspace_label, ws_idx + 1);
|
||||
if ws.tabs.len() > 1 {
|
||||
if let Some(tab_idx) = ws.find_tab_index_for_pane(pane_id) {
|
||||
let tab = &ws.tabs[tab_idx];
|
||||
context.push_str(&format!(" · {}", tab.display_name()));
|
||||
if let Some(label) = ws.tab_display_name(tab_idx) {
|
||||
context.push_str(&format!(" · {label}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
context
|
||||
|
|
@ -450,7 +451,9 @@ impl AppState {
|
|||
fn navigator_tab_row(&self, ws_idx: usize, tab_idx: usize) -> NavigatorRow {
|
||||
let ws = &self.workspaces[ws_idx];
|
||||
let tab = &ws.tabs[tab_idx];
|
||||
let label = tab.display_name();
|
||||
let label = ws
|
||||
.tab_display_name(tab_idx)
|
||||
.unwrap_or_else(|| (tab_idx + 1).to_string());
|
||||
let (status, seen) = tab_aggregate_state(tab, &self.terminals);
|
||||
let activity = tab_activity_summary(tab, &self.terminals);
|
||||
let pane_count = tab.panes.len();
|
||||
|
|
|
|||
|
|
@ -677,7 +677,10 @@ mod tests {
|
|||
panic!("expected layout apply response");
|
||||
};
|
||||
assert_eq!(app.state.workspaces[0].tabs.len(), 1);
|
||||
assert_eq!(app.state.workspaces[0].tabs[0].display_name(), "dev");
|
||||
assert_eq!(
|
||||
app.state.workspaces[0].tab_display_name(0).as_deref(),
|
||||
Some("dev")
|
||||
);
|
||||
let LayoutNode::Split {
|
||||
direction,
|
||||
ratio,
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ impl App {
|
|||
ws_idx,
|
||||
workspace,
|
||||
self.public_tab_id(ws_idx, tab_idx),
|
||||
Some(tab.display_name()),
|
||||
ws.tab_display_name(tab_idx),
|
||||
focused_pane,
|
||||
correlation_id,
|
||||
))
|
||||
|
|
@ -257,7 +257,7 @@ impl App {
|
|||
let workspace = self.workspace_info(ws_idx);
|
||||
let tab_idx = ws.active_tab_index();
|
||||
let tab_id = self.public_tab_id(ws_idx, tab_idx);
|
||||
let tab_label = ws.tabs.get(tab_idx).map(|tab| tab.display_name());
|
||||
let tab_label = ws.tab_display_name(tab_idx);
|
||||
let focused_pane = ws
|
||||
.focused_pane_id()
|
||||
.and_then(|pane_id| self.pane_info(ws_idx, pane_id));
|
||||
|
|
@ -283,7 +283,7 @@ impl App {
|
|||
.find_tab_index_for_pane(pane_id)
|
||||
.unwrap_or_else(|| ws.active_tab_index());
|
||||
let tab_id = self.public_tab_id(ws_idx, tab_idx);
|
||||
let tab_label = ws.tabs.get(tab_idx).map(|tab| tab.display_name());
|
||||
let tab_label = ws.tab_display_name(tab_idx);
|
||||
let focused_pane = self.pane_info(ws_idx, pane_id);
|
||||
self.plugin_context_from_parts(
|
||||
ws_idx,
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ impl App {
|
|||
tab_id: self.public_tab_id(ws_idx, tab_idx)?,
|
||||
workspace_id: self.public_workspace_id(ws_idx),
|
||||
number: tab.number,
|
||||
label: tab.display_name(),
|
||||
label: ws.tab_display_name(tab_idx)?,
|
||||
focused: self.state.active == Some(ws_idx) && ws.active_tab == tab_idx,
|
||||
pane_count: tab.panes.len(),
|
||||
agent_status: pane_agent_status(agg_state, seen),
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ fn next_new_tab_default_name(state: &AppState) -> String {
|
|||
state
|
||||
.active
|
||||
.and_then(|i| state.workspaces.get(i))
|
||||
.map(|ws| ws.next_public_tab_number.to_string())
|
||||
.map(|ws| (ws.tabs.len() + 1).to_string())
|
||||
.unwrap_or_else(|| "1".to_string())
|
||||
}
|
||||
|
||||
|
|
@ -453,9 +453,14 @@ pub(super) fn apply_rename_action(state: &mut AppState, action: ModalAction) {
|
|||
if let Some(ws) = state.workspaces.get_mut(ws_idx) {
|
||||
let workspace_id = ws.id.clone();
|
||||
let active_tab = ws.active_tab;
|
||||
let keep_auto_name = ws
|
||||
.tabs
|
||||
.get(active_tab)
|
||||
.is_some_and(|tab| tab.is_auto_named())
|
||||
&& ws
|
||||
.tab_display_name(active_tab)
|
||||
.is_some_and(|name| new_name == name);
|
||||
if let Some(tab) = ws.active_tab_mut() {
|
||||
let keep_auto_name =
|
||||
tab.is_auto_named() && new_name == tab.number.to_string();
|
||||
if !new_name.is_empty() && !keep_auto_name {
|
||||
tab.set_custom_name(new_name);
|
||||
let tab_id = ws
|
||||
|
|
@ -1296,7 +1301,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn closing_first_auto_tab_keeps_remaining_auto_tab_number_and_next_prompt() {
|
||||
fn closing_first_auto_tab_compacts_remaining_auto_tab_label_and_next_prompt() {
|
||||
let mut state = state_with_workspaces(&["test"]);
|
||||
open_new_tab_dialog(&mut state);
|
||||
handle_rename_key(
|
||||
|
|
@ -1311,11 +1316,14 @@ mod tests {
|
|||
state.workspaces[0].close_tab(0);
|
||||
state.workspaces[0].switch_tab(0);
|
||||
|
||||
assert_eq!(state.workspaces[0].tabs[0].display_name(), "2");
|
||||
assert_eq!(
|
||||
state.workspaces[0].tab_display_name(0).as_deref(),
|
||||
Some("1")
|
||||
);
|
||||
assert!(state.workspaces[0].tabs[0].custom_name.is_none());
|
||||
|
||||
open_new_tab_dialog(&mut state);
|
||||
assert_eq!(state.name_input, "3");
|
||||
assert_eq!(state.name_input, "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1332,7 +1340,10 @@ mod tests {
|
|||
|
||||
assert_eq!(state.mode, Mode::Terminal);
|
||||
assert!(state.workspaces[0].tabs[1].custom_name.is_none());
|
||||
assert_eq!(state.workspaces[0].tabs[1].display_name(), "2");
|
||||
assert_eq!(
|
||||
state.workspaces[0].tab_display_name(1).as_deref(),
|
||||
Some("2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1298,9 +1298,10 @@ mod tests {
|
|||
let labels: Vec<_> = app.state.workspaces[0]
|
||||
.tabs
|
||||
.iter()
|
||||
.map(|tab| tab.display_name())
|
||||
.enumerate()
|
||||
.map(|(tab_idx, _)| app.state.workspaces[0].tab_display_name(tab_idx).unwrap())
|
||||
.collect();
|
||||
assert_eq!(labels, vec!["foo", "3", "1"]);
|
||||
assert_eq!(labels, vec!["foo", "2", "3"]);
|
||||
assert_eq!(
|
||||
app.state.workspaces[0].tabs[0].custom_name.as_deref(),
|
||||
Some("foo")
|
||||
|
|
|
|||
|
|
@ -2951,7 +2951,7 @@ mod tests {
|
|||
|
||||
assert_eq!(tab.tab_id, format!("{}:t3", app.state.workspaces[0].id));
|
||||
assert_eq!(tab.number, 3);
|
||||
assert_eq!(tab.label, "3");
|
||||
assert_eq!(tab.label, "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -330,13 +330,13 @@ fn render_header_status(
|
|||
}
|
||||
|
||||
fn mobile_tab_status(ws: &crate::workspace::Workspace) -> String {
|
||||
let tab_number = ws
|
||||
.public_tab_number(ws.active_tab)
|
||||
.unwrap_or(ws.active_tab + 1);
|
||||
let tab_label = ws
|
||||
.tab_display_name(ws.active_tab)
|
||||
.unwrap_or_else(|| (ws.active_tab + 1).to_string());
|
||||
if ws.tabs.len() <= 1 {
|
||||
format!("tab {tab_number}")
|
||||
format!("tab {tab_label}")
|
||||
} else {
|
||||
format!("tab {tab_number} · {}/{}", ws.active_tab + 1, ws.tabs.len())
|
||||
format!("tab {tab_label} · {}/{}", ws.active_tab + 1, ws.tabs.len())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -524,10 +524,13 @@ fn render_mobile_switcher_content(
|
|||
for (idx, tab) in ws.tabs.iter().enumerate() {
|
||||
let active = idx == ws.active_tab;
|
||||
let bg = mobile_item_bg(false, active, p);
|
||||
let display_name = ws
|
||||
.tab_display_name(idx)
|
||||
.unwrap_or_else(|| (idx + 1).to_string());
|
||||
let label = if tab.is_auto_named() {
|
||||
format!("tab {}", tab.display_name())
|
||||
format!("tab {display_name}")
|
||||
} else {
|
||||
format!("{} · {}", tab.number, tab.display_name())
|
||||
format!("{} · {display_name}", idx + 1)
|
||||
};
|
||||
let title = Line::from(vec![
|
||||
Span::styled(" ", Style::default().bg(bg)),
|
||||
|
|
@ -976,18 +979,18 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_tab_status_uses_public_tab_number_and_position() {
|
||||
fn mobile_tab_status_uses_compact_tab_label_and_position() {
|
||||
let mut workspace = crate::workspace::Workspace::test_new("mobile-tabs");
|
||||
let removed_tab = workspace.test_add_tab(None);
|
||||
workspace.test_add_tab(None);
|
||||
assert!(workspace.close_tab(removed_tab));
|
||||
workspace.active_tab = 1;
|
||||
|
||||
assert_eq!(mobile_tab_status(&workspace), "tab 3 · 2/2");
|
||||
assert_eq!(mobile_tab_status(&workspace), "tab 2 · 2/2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_switcher_uses_stable_public_tab_number_for_auto_tab_labels() {
|
||||
fn mobile_switcher_uses_compact_tab_label_for_auto_tab_labels() {
|
||||
let mut app = crate::app::state::AppState::test_new();
|
||||
let mut workspace = crate::workspace::Workspace::test_new("mobile-tabs");
|
||||
let removed_tab = workspace.test_add_tab(None);
|
||||
|
|
@ -1017,8 +1020,8 @@ mod tests {
|
|||
.map(|x| terminal.backend().buffer()[(x, 10)].symbol())
|
||||
.collect::<String>();
|
||||
|
||||
assert!(row.contains("tab 3"), "mobile tab row: {row:?}");
|
||||
assert!(!row.contains("tab 2"), "mobile tab row: {row:?}");
|
||||
assert!(row.contains("tab 2"), "mobile tab row: {row:?}");
|
||||
assert!(!row.contains("tab 3"), "mobile tab row: {row:?}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
|
|
|||
|
|
@ -354,7 +354,11 @@ fn tab_detail(
|
|||
};
|
||||
let mut parts = vec![
|
||||
ws.display_name_from(&app.terminals, terminal_runtimes),
|
||||
format!("tab: {}", tab.display_name()),
|
||||
format!(
|
||||
"tab: {}",
|
||||
ws.tab_display_name(tab_idx)
|
||||
.unwrap_or_else(|| (tab_idx + 1).to_string())
|
||||
),
|
||||
format!("{} panes", tab.panes.len()),
|
||||
];
|
||||
let rows = app.navigator_rows_from(terminal_runtimes);
|
||||
|
|
@ -384,7 +388,11 @@ fn pane_detail(
|
|||
};
|
||||
let mut parts = vec![ws.display_name_from(&app.terminals, terminal_runtimes)];
|
||||
if ws.tabs.len() > 1 {
|
||||
parts.push(format!("tab: {}", tab.display_name()));
|
||||
parts.push(format!(
|
||||
"tab: {}",
|
||||
ws.tab_display_name(tab_idx)
|
||||
.unwrap_or_else(|| (tab_idx + 1).to_string())
|
||||
));
|
||||
}
|
||||
if let Some(pane_number) = ws.public_pane_number(pane_id) {
|
||||
parts.push(format!("pane {pane_number}"));
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ pub(crate) struct TabBarView {
|
|||
pub new_tab_hit_area: Rect,
|
||||
}
|
||||
|
||||
fn tab_width(tab: &crate::workspace::Tab) -> u16 {
|
||||
(tab_chrome_label(tab).chars().count() as u16 + 4).max(MIN_TAB_WIDTH)
|
||||
fn tab_width(ws: &crate::workspace::Workspace, tab_idx: usize) -> u16 {
|
||||
(tab_chrome_label(ws, tab_idx).chars().count() as u16 + 4).max(MIN_TAB_WIDTH)
|
||||
}
|
||||
|
||||
fn tab_chrome_label(tab: &crate::workspace::Tab) -> String {
|
||||
let name = tab.display_name();
|
||||
if tab.zoomed {
|
||||
fn tab_chrome_label(ws: &crate::workspace::Workspace, tab_idx: usize) -> String {
|
||||
let name = ws
|
||||
.tab_display_name(tab_idx)
|
||||
.unwrap_or_else(|| (tab_idx + 1).to_string());
|
||||
if ws.tabs.get(tab_idx).is_some_and(|tab| tab.zoomed) {
|
||||
format!("{name} Z")
|
||||
} else {
|
||||
name
|
||||
|
|
@ -46,7 +48,7 @@ fn layout_tab_hit_areas(ws: &crate::workspace::Workspace, area: Rect, scroll: us
|
|||
if x >= right {
|
||||
break;
|
||||
}
|
||||
let desired = tab_width(&ws.tabs[idx]);
|
||||
let desired = tab_width(ws, idx);
|
||||
let remaining = right.saturating_sub(x);
|
||||
let width = desired.min(remaining).max(1);
|
||||
*rect = Rect::new(x, area.y, width, 1);
|
||||
|
|
@ -333,7 +335,7 @@ pub(super) fn render_tab_bar(app: &AppState, frame: &mut Frame, area: Rect) {
|
|||
Style::default().fg(p.overlay1).bg(p.surface0)
|
||||
};
|
||||
let width = rect.width as usize;
|
||||
let name = tab_chrome_label(tab);
|
||||
let name = tab_chrome_label(ws, idx);
|
||||
let text = format!(" {:width$}", name, width = width.saturating_sub(1));
|
||||
frame.render_widget(Paragraph::new(text).style(style), rect);
|
||||
}
|
||||
|
|
@ -427,8 +429,11 @@ mod tests {
|
|||
let row = buffer_row_text(terminal.backend().buffer(), app.view.tab_bar_rect, 0);
|
||||
assert!(row.contains(" 1 Z"), "tab row: {row:?}");
|
||||
assert!(row.contains(" test Z"), "tab row: {row:?}");
|
||||
assert_eq!(app.workspaces[0].tabs[0].display_name(), "1");
|
||||
assert_eq!(app.workspaces[0].tabs[custom_tab].display_name(), "test");
|
||||
assert_eq!(app.workspaces[0].tab_display_name(0).as_deref(), Some("1"));
|
||||
assert_eq!(
|
||||
app.workspaces[0].tab_display_name(custom_tab).as_deref(),
|
||||
Some("test")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -437,6 +442,6 @@ mod tests {
|
|||
ws.tabs[0].set_custom_name("abcdefgh".into());
|
||||
ws.tabs[0].zoomed = true;
|
||||
|
||||
assert_eq!(tab_width(&ws.tabs[0]), 14);
|
||||
assert_eq!(tab_width(&ws, 0), 14);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -423,7 +423,16 @@ impl Workspace {
|
|||
}
|
||||
|
||||
pub fn active_tab_display_name(&self) -> Option<String> {
|
||||
self.active_tab().map(Tab::display_name)
|
||||
self.tab_display_name(self.active_tab)
|
||||
}
|
||||
|
||||
pub fn tab_display_name(&self, tab_idx: usize) -> Option<String> {
|
||||
let tab = self.tabs.get(tab_idx)?;
|
||||
Some(
|
||||
tab.custom_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| (tab_idx + 1).to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn switch_tab(&mut self, idx: usize) {
|
||||
|
|
@ -1581,8 +1590,10 @@ mod tests {
|
|||
|
||||
assert!(ws.move_tab(0, ws.tabs.len()));
|
||||
|
||||
let labels: Vec<_> = ws.tabs.iter().map(|tab| tab.display_name()).collect();
|
||||
assert_eq!(labels, vec!["foo", "3", "1"]);
|
||||
let labels: Vec<_> = (0..ws.tabs.len())
|
||||
.map(|tab_idx| ws.tab_display_name(tab_idx).unwrap())
|
||||
.collect();
|
||||
assert_eq!(labels, vec!["foo", "2", "3"]);
|
||||
assert_eq!(ws.tabs[0].custom_name.as_deref(), Some("foo"));
|
||||
assert!(ws.tabs[1].custom_name.is_none());
|
||||
assert!(ws.tabs[2].custom_name.is_none());
|
||||
|
|
|
|||
|
|
@ -30,7 +30,12 @@ impl Tab {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn pane_details(&self, terminals: &HashMap<TerminalId, TerminalState>) -> Vec<PaneDetail> {
|
||||
fn pane_details(
|
||||
&self,
|
||||
terminals: &HashMap<TerminalId, TerminalState>,
|
||||
tab_idx: usize,
|
||||
tab_label: &str,
|
||||
) -> Vec<PaneDetail> {
|
||||
self.layout
|
||||
.pane_ids()
|
||||
.iter()
|
||||
|
|
@ -48,8 +53,8 @@ impl Tab {
|
|||
let presentation = terminal.effective_presentation();
|
||||
Some(PaneDetail {
|
||||
pane_id: *id,
|
||||
tab_idx: 0,
|
||||
tab_label: self.display_name(),
|
||||
tab_idx,
|
||||
tab_label: tab_label.to_string(),
|
||||
label: agent_label.clone(),
|
||||
agent_label,
|
||||
agent: terminal.effective_known_agent(),
|
||||
|
|
@ -100,12 +105,10 @@ impl Workspace {
|
|||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(tab_idx, tab)| {
|
||||
tab.pane_details(terminals)
|
||||
.into_iter()
|
||||
.map(move |mut detail| {
|
||||
detail.tab_idx = tab_idx;
|
||||
detail
|
||||
})
|
||||
let tab_label = self
|
||||
.tab_display_name(tab_idx)
|
||||
.unwrap_or_else(|| (tab_idx + 1).to_string());
|
||||
tab.pane_details(terminals, tab_idx, &tab_label).into_iter()
|
||||
})
|
||||
.map(|mut detail| {
|
||||
if multi_tab {
|
||||
|
|
|
|||
|
|
@ -185,12 +185,6 @@ impl Tab {
|
|||
))
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> String {
|
||||
self.custom_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.number.to_string())
|
||||
}
|
||||
|
||||
pub fn is_auto_named(&self) -> bool {
|
||||
self.custom_name.is_none()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue