fix(ui): keep workspace titles derived from workspace context

This commit is contained in:
l0ng-ai
2026-08-01 08:35:02 +08:00
parent 74f4f1a35a
commit 596f436c31
4 changed files with 11 additions and 93 deletions
+4 -11
View File
@@ -535,17 +535,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Double-click word/smart-select is unaffected; only link click and hover
bridge hard wraps. (by @ayamir in #258)
- **A sidebar title no longer shows "zsh" over your repo name** — pane
titles were allowed to fall back to the foreground process name before
falling back to cwd/repo, but an idle terminal's foreground process is
just the shell itself, so nearly every idle workspace displayed as "zsh"
or "bash" instead of the more useful cwd-derived name — the opposite of
what that fallback chain was for. Bare interactive-shell names (`zsh`,
`bash`, `fish`, `pwsh`, `cmd`, and similar, matched case- and
extension-insensitively) are now skipped when picking a title, so the
process-name fallback only fires for something actually distinctive
(`nvim`, a coding agent) and an idle shell falls all the way through to
the cwd/repo name as intended. (by @ayamir in #285)
- **A workspace title stays tied to the workspace** — foreground process and
agent names no longer replace the workspace title in the sidebar or its
switcher button. An explicit workspace name wins; otherwise the title is
derived from the workspace's repo/cwd, with `Untitled` as the final fallback.
- **Fullwidth CJK punctuation no longer overlaps, and prompt-mark scanning
is faster** — wide glyphs are now shaped independently instead of being
-43
View File
@@ -27,37 +27,6 @@ pub struct ShellInventory {
pub default_name: String,
}
/// Common interactive-shell process names, lowercase and without a
/// platform-specific extension. Anything that lands in here is what a pane
/// looks like at an idle prompt — not distinctive enough to stand in for a
/// pane's cwd/repo name when picking a display title.
const BARE_SHELL_NAMES: &[&str] = &[
"sh",
"bash",
"zsh",
"fish",
"dash",
"ksh",
"tcsh",
"csh",
"nu",
"elvish",
"xonsh",
"pwsh",
"powershell",
"cmd",
"wsl",
];
/// True when `name` is a bare interactive-shell process name (e.g. the
/// foreground process of an idle terminal), so callers that want a
/// *distinctive* title should skip it and fall back to something else.
pub fn is_bare_shell_name(name: &str) -> bool {
let lower = name.trim().to_ascii_lowercase();
let lower = lower.strip_suffix(".exe").unwrap_or(&lower);
BARE_SHELL_NAMES.contains(&lower)
}
pub fn inventory() -> ShellInventory {
let configured = crate::core::config::shell_command();
ShellInventory {
@@ -440,16 +409,4 @@ mod tests {
assert!(!default_shell_name(None).is_empty());
assert!(!default_shell_name(Some(" ")).is_empty());
}
#[test]
fn bare_shell_names_are_recognized_case_and_extension_insensitively() {
assert!(is_bare_shell_name("zsh"));
assert!(is_bare_shell_name("bash"));
assert!(is_bare_shell_name("PowerShell"));
assert!(is_bare_shell_name("pwsh.exe"));
assert!(is_bare_shell_name("CMD.EXE"));
assert!(!is_bare_shell_name("nvim"));
assert!(!is_bare_shell_name("claude"));
assert!(!is_bare_shell_name(""));
}
}
+6 -21
View File
@@ -257,9 +257,6 @@ pub fn display_name_of(ws: &Workspace, panes: &[PaneRecord]) -> String {
if let Some(name) = ws.name.as_deref().map(str::trim).filter(|n| !n.is_empty()) {
return name.to_string();
}
if let Some(title) = pane_title_of(ws, panes) {
return title.to_string();
}
subject_path_of(ws, panes)
.and_then(|path| {
std::path::Path::new(&path)
@@ -270,15 +267,6 @@ pub fn display_name_of(ws: &Workspace, panes: &[PaneRecord]) -> String {
.unwrap_or_else(|| "Untitled".to_string())
}
fn pane_title_of<'a>(ws: &Workspace, panes: &'a [PaneRecord]) -> Option<&'a str> {
ws.tabs
.iter()
.flat_map(|t| t.root.pane_ids())
.filter_map(|id| panes.iter().find(|p| p.id == id))
.map(|p| p.title.trim())
.find(|title| !title.is_empty() && !tty7_core::core::shells::is_bare_shell_name(title))
}
pub fn subject_path_of(ws: &Workspace, panes: &[PaneRecord]) -> Option<String> {
let mut counts: Vec<(&str, usize)> = Vec::new();
for group in ws.tabs.iter().filter_map(|t| t.sidebar_group.as_deref()) {
@@ -521,20 +509,17 @@ mod tests {
assert_eq!(display_name_of(&ws, &panes), "scratch");
panes[0].title = "nvim".into();
assert_eq!(display_name_of(&ws, &panes), "nvim");
assert_eq!(
display_name_of(&ws, &panes),
"scratch",
"a pane's process title must not rename its workspace"
);
ws.tabs[0].sidebar_group = Some("/repo/tty7".into());
assert_eq!(
display_name_of(&ws, &panes),
"nvim",
"a live process name is more distinctive than the cwd group"
);
panes[0].title = "zsh".into();
assert_eq!(
display_name_of(&ws, &panes),
"tty7",
"an idle shell prompt is not distinctive — cwd/repo name should win"
"the repo group wins over the raw cwd"
);
ws.name = Some(" Release prep ".into());
+1 -18
View File
@@ -232,11 +232,7 @@ impl Tty7App {
}
crate::terminal::pane_liveness::sweep(cx);
let current = self
.tabs
.get(self.active)
.and_then(|tab| workspace_osc_title(tab, cx).or_else(|| workspace_agent_title(tab, cx)))
.or_else(|| crate::ui::machine_mirror::display_name_for(cx, self.workspace))
let current = crate::ui::machine_mirror::display_name_for(cx, self.workspace)
.unwrap_or_else(|| "tty7".to_string());
let monogram: String = current
.chars()
@@ -1086,19 +1082,6 @@ impl Tty7App {
}
}
fn workspace_osc_title(tab: &Tab, cx: &App) -> Option<String> {
let title = tab.leaf_title(None, cx);
let title = title.trim();
if title.is_empty() || title == "tty7" || title.starts_with("tty7 — ") {
return None;
}
Some(title.to_string())
}
fn workspace_agent_title(tab: &Tab, cx: &App) -> Option<String> {
tab.agent(cx).map(|agent| agent.display_name().to_string())
}
#[cfg(test)]
mod tests {
use super::*;