mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
Merge branch 'main' into fix/ime-candidate-anchor
This commit is contained in:
@@ -44,6 +44,7 @@ pub fn two_workspace_machine() -> Machine {
|
||||
let record = |id: u64, cwd: &str| PaneRecord {
|
||||
id,
|
||||
cwd: Some(cwd.to_string()),
|
||||
title: String::new(),
|
||||
ssh_spec: None,
|
||||
agent: None,
|
||||
live: true,
|
||||
|
||||
@@ -258,6 +258,8 @@ pub struct PaneRecord {
|
||||
#[serde(default)]
|
||||
pub cwd: Option<String>,
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub ssh_spec: Option<Box<NativeSshSpec>>,
|
||||
#[serde(default)]
|
||||
pub agent: Option<AgentFacts>,
|
||||
@@ -270,6 +272,7 @@ impl PaneRecord {
|
||||
PaneRecord {
|
||||
id,
|
||||
cwd: None,
|
||||
title: String::new(),
|
||||
ssh_spec: None,
|
||||
agent: None,
|
||||
live: false,
|
||||
@@ -313,6 +316,7 @@ impl PaneSeed {
|
||||
PaneRecord {
|
||||
id: self.pane,
|
||||
cwd: self.cwd,
|
||||
title: String::new(),
|
||||
ssh_spec: self.ssh_spec.map(|s| Box::new(s.without_secrets())),
|
||||
agent: self.agent,
|
||||
live,
|
||||
|
||||
@@ -27,6 +27,37 @@ 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 {
|
||||
@@ -409,4 +440,16 @@ 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(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1571,7 +1571,10 @@ fn agent_facts_changed(
|
||||
match (before, after) {
|
||||
(None, None) => false,
|
||||
(Some(a), Some(b)) => {
|
||||
a.agent != b.agent || a.session_id != b.session_id || a.launch_argv != b.launch_argv
|
||||
a.agent != b.agent
|
||||
|| a.session_id != b.session_id
|
||||
|| a.launch_argv != b.launch_argv
|
||||
|| a.status != b.status
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ impl crate::host::server::PaneDirectory for Registry {
|
||||
self.panes.lock().unwrap().len() as u64
|
||||
}
|
||||
|
||||
fn panes(&self) -> Vec<crate::daemon::protocol::PaneInfo> {
|
||||
self.list()
|
||||
}
|
||||
|
||||
fn agent_states(&self) -> Vec<crate::daemon::control::PaneAgentState> {
|
||||
let panes: Vec<Arc<DaemonPane>> = self.panes.lock().unwrap().values().cloned().collect();
|
||||
let mut states: Vec<_> = panes.iter().filter_map(|p| p.agent_state()).collect();
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::daemon::control::{
|
||||
WireErrorKind, feature, server_started,
|
||||
};
|
||||
use crate::daemon::duplex::{Duplex, Halves};
|
||||
use crate::daemon::protocol::PaneInfo;
|
||||
use crate::host::{Host, SearchHit, SharedHost, WatchSub};
|
||||
|
||||
pub const MAX_WORKERS: usize = 64;
|
||||
@@ -25,6 +26,7 @@ pub const LAYOUT_EVENT_QUEUE: usize = 1024;
|
||||
|
||||
pub trait PaneDirectory: Send + Sync {
|
||||
fn pane_count(&self) -> u64;
|
||||
fn panes(&self) -> Vec<PaneInfo>;
|
||||
fn agent_states(&self) -> Vec<PaneAgentState>;
|
||||
}
|
||||
|
||||
@@ -476,6 +478,26 @@ fn drop_unsendable_hits(hits: &mut Vec<SearchHit>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn machine_with_live_panes(conn: &Conn) -> io::Result<machine::Machine> {
|
||||
let mut machine = conn.machine()?.machine();
|
||||
let Some(panes) = conn.panes.as_ref().map(|p| p.panes()) else {
|
||||
return Ok(machine);
|
||||
};
|
||||
for info in panes {
|
||||
let record = match machine.panes.iter_mut().find(|p| p.id == info.pane_id) {
|
||||
Some(record) => record,
|
||||
None => {
|
||||
machine.panes.push(machine::PaneRecord::new(info.pane_id));
|
||||
machine.panes.last_mut().expect("record was just inserted")
|
||||
}
|
||||
};
|
||||
record.cwd = info.cwd.map(|p| p.to_string_lossy().into_owned());
|
||||
record.title = info.title;
|
||||
record.live = info.alive;
|
||||
}
|
||||
Ok(machine)
|
||||
}
|
||||
|
||||
fn run_request(
|
||||
conn: &Arc<Conn>,
|
||||
req_id: u64,
|
||||
@@ -590,7 +612,7 @@ fn run_request(
|
||||
}
|
||||
|
||||
ControlRequest::MachineGet => (
|
||||
ReplyOk::MachineTree(Box::new(conn.machine()?.machine())),
|
||||
ReplyOk::MachineTree(Box::new(machine_with_live_panes(conn)?)),
|
||||
Vec::new(),
|
||||
),
|
||||
ControlRequest::WorkspaceTree { workspace } => (
|
||||
@@ -1535,13 +1557,19 @@ mod aggregate_tests {
|
||||
use crate::host::local::LocalHost;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
struct ThreePanesOneAgent;
|
||||
struct ThreePanesOneAgent {
|
||||
panes: Vec<PaneInfo>,
|
||||
}
|
||||
|
||||
impl PaneDirectory for ThreePanesOneAgent {
|
||||
fn pane_count(&self) -> u64 {
|
||||
3
|
||||
}
|
||||
|
||||
fn panes(&self) -> Vec<PaneInfo> {
|
||||
self.panes.clone()
|
||||
}
|
||||
|
||||
fn agent_states(&self) -> Vec<PaneAgentState> {
|
||||
vec![PaneAgentState {
|
||||
pane_id: 7,
|
||||
@@ -1574,7 +1602,7 @@ mod aggregate_tests {
|
||||
#[test]
|
||||
fn status_answers_with_this_servers_facts() {
|
||||
let services = Services {
|
||||
panes: Some(Arc::new(ThreePanesOneAgent)),
|
||||
panes: Some(Arc::new(ThreePanesOneAgent { panes: Vec::new() })),
|
||||
..Services::none()
|
||||
};
|
||||
let client = client_with(services);
|
||||
@@ -1601,7 +1629,7 @@ mod aggregate_tests {
|
||||
#[test]
|
||||
fn agent_states_are_the_pane_directorys_snapshot() {
|
||||
let services = Services {
|
||||
panes: Some(Arc::new(ThreePanesOneAgent)),
|
||||
panes: Some(Arc::new(ThreePanesOneAgent { panes: Vec::new() })),
|
||||
..Services::none()
|
||||
};
|
||||
let client = client_with(services);
|
||||
@@ -1616,6 +1644,48 @@ mod aggregate_tests {
|
||||
assert_eq!(states[0].state.session_id.as_deref(), Some("sess-7"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_get_overlays_live_pane_titles() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let store = MachineStore::open(dir.path().join(machine::MACHINE_FILE));
|
||||
let ws = store.workspace_create(None, None, None).unwrap();
|
||||
store
|
||||
.tab_create(
|
||||
ws.id,
|
||||
None,
|
||||
machine::PaneSeed {
|
||||
pane: 7,
|
||||
cwd: Some("/repo/tty7".into()),
|
||||
ssh_spec: None,
|
||||
agent: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let services = Services {
|
||||
machine: Some(store),
|
||||
attachments: Arc::new(AttachRegistry::default()),
|
||||
panes: Some(Arc::new(ThreePanesOneAgent {
|
||||
panes: vec![PaneInfo {
|
||||
pane_id: 7,
|
||||
cwd: Some(PathBuf::from("/repo/tty7")),
|
||||
title: "nvim".into(),
|
||||
alive: true,
|
||||
owner: None,
|
||||
}],
|
||||
})),
|
||||
};
|
||||
let client = client_with(services);
|
||||
let ReplyOk::MachineTree(machine) = client.call(ControlRequest::MachineGet).unwrap() else {
|
||||
panic!("MachineGet must answer with a machine tree");
|
||||
};
|
||||
let pane = machine.panes.iter().find(|p| p.id == 7).unwrap();
|
||||
assert_eq!(pane.title, "nvim");
|
||||
assert!(pane.live);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_still_answer_when_this_process_serves_no_panes() {
|
||||
let client = client_with(Services::none());
|
||||
|
||||
@@ -257,6 +257,9 @@ 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)
|
||||
@@ -267,6 +270,15 @@ 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()) {
|
||||
@@ -501,15 +513,29 @@ mod tests {
|
||||
#[test]
|
||||
fn display_names_derive_from_the_tree_with_the_session_precedence() {
|
||||
let mut ws = Workspace::default();
|
||||
let panes = vec![PaneRecord {
|
||||
let mut panes = vec![PaneRecord {
|
||||
cwd: Some("/home/me/scratch".into()),
|
||||
..PaneRecord::new(1)
|
||||
}];
|
||||
ws.tabs = vec![leaf_tab(1)];
|
||||
assert_eq!(display_name_of(&ws, &panes), "scratch");
|
||||
|
||||
panes[0].title = "nvim".into();
|
||||
assert_eq!(display_name_of(&ws, &panes), "nvim");
|
||||
|
||||
ws.tabs[0].sidebar_group = Some("/repo/tty7".into());
|
||||
assert_eq!(display_name_of(&ws, &panes), "tty7");
|
||||
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"
|
||||
);
|
||||
|
||||
ws.name = Some(" Release prep ".into());
|
||||
assert_eq!(display_name_of(&ws, &panes), "Release prep");
|
||||
|
||||
+18
-1
@@ -232,7 +232,11 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
crate::terminal::pane_liveness::sweep(cx);
|
||||
let current = crate::ui::machine_mirror::display_name_for(cx, self.workspace)
|
||||
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))
|
||||
.unwrap_or_else(|| "tty7".to_string());
|
||||
let monogram: String = current
|
||||
.chars()
|
||||
@@ -1082,6 +1086,19 @@ 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::*;
|
||||
|
||||
Reference in New Issue
Block a user