mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 08:15:35 +00:00
fix(agents): count only agents that start under a view, not ones reattached to
A view rebuilt over a running pane - every agent tab after an app restart, or a workspace switched back to - saw its agent appear from nothing and reported it as detected, bumping that agent's frecency once per launch of the app. The terminal now remembers whether its link was an attach, and such a view only reports agents once its shell has been seen back at the prompt with no agent in front. Also pins down that the agents seen in a remote workspace, its quick launch list, persist in views.json with the rest of the workspace.
This commit is contained in:
@@ -1179,4 +1179,29 @@ mod tests {
|
||||
views.views.retain(|w| w.id != b_id);
|
||||
assert_eq!(numbers(&views), vec![a_id, c_id]);
|
||||
}
|
||||
|
||||
/// A remote workspace's quick-launch list is the agents seen running in
|
||||
/// it, so the list has to be in `views.json` with the rest of the
|
||||
/// workspace — an in-memory one would be empty after every restart.
|
||||
#[test]
|
||||
fn agents_seen_in_a_workspace_survive_a_restart() {
|
||||
let _file = lock_session_file();
|
||||
pin_config_dir();
|
||||
let mut entry = remote_view("build-box");
|
||||
assert!(entry.saw_agent("claude"));
|
||||
assert!(entry.saw_agent("codex"));
|
||||
assert!(!entry.saw_agent("codex"), "already the most recent");
|
||||
assert!(entry.saw_agent("claude"), "moves to the end");
|
||||
let id = entry.id;
|
||||
WindowViews {
|
||||
active: Some(id),
|
||||
views: vec![entry, view()],
|
||||
}
|
||||
.save();
|
||||
let loaded = WindowViews::load().expect("a saved views file should load back");
|
||||
assert_eq!(loaded.get(id).unwrap().seen_agents, vec!["codex", "claude"]);
|
||||
// A workspace that has seen nothing writes nothing for it.
|
||||
let text = std::fs::read_to_string(WindowViews::path().unwrap()).unwrap();
|
||||
assert_eq!(text.matches("seen_agents").count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +571,10 @@ pub struct RemoteTerminal {
|
||||
pub exited: bool,
|
||||
size: TermSize,
|
||||
synced_size: bool,
|
||||
/// This link reattached to a pane that was already running rather than
|
||||
/// spawning a fresh one, so whatever it reports first — an agent in the
|
||||
/// foreground included — predates this client.
|
||||
reattached: bool,
|
||||
/// The `(cell_w, cell_h)` last sent to the daemon, in device pixels. Tracked
|
||||
/// alongside `size` so a display-scale change still reaches the child even
|
||||
/// when the grid dimensions are unchanged.
|
||||
@@ -1087,6 +1091,7 @@ impl RemoteTerminal {
|
||||
exited: false,
|
||||
size,
|
||||
synced_size: false,
|
||||
reattached: awaiting_replay,
|
||||
synced_cell: (0, 0),
|
||||
resize_sent_at: None,
|
||||
link,
|
||||
@@ -1878,6 +1883,11 @@ impl RemoteTerminal {
|
||||
self.remote_context.lock().ok().and_then(|g| g.clone())
|
||||
}
|
||||
|
||||
/// See the field: an attach to a running pane, not a fresh spawn.
|
||||
pub fn reattached(&self) -> bool {
|
||||
self.reattached
|
||||
}
|
||||
|
||||
pub fn at_prompt(&self) -> bool {
|
||||
self.shell_state
|
||||
.lock()
|
||||
|
||||
+117
-7
@@ -445,6 +445,12 @@ pub struct TerminalView {
|
||||
/// The foreground agent as of the last poll, so a new one is reported
|
||||
/// once ([`AgentDetected`]) rather than on every frame it keeps running.
|
||||
seen_agent: Option<crate::core::cli_agent::CLIAgent>,
|
||||
/// Whether an agent appearing here is news. A pane this view spawned is
|
||||
/// armed from the start; one it reattached to — an app restart, a
|
||||
/// workspace switched back to — may already be running an agent that was
|
||||
/// counted when it started, so it arms only once its shell is seen back
|
||||
/// at the prompt with no agent in front.
|
||||
agent_detection_armed: bool,
|
||||
last_agent_status: Option<crate::core::cli_agent::AgentStatus>,
|
||||
last_agent_session: (Option<String>, Option<Vec<String>>),
|
||||
agent_turn_started: Option<std::time::Instant>,
|
||||
@@ -1461,6 +1467,7 @@ impl TerminalView {
|
||||
.map(crate::core::config::gpui_font_features);
|
||||
let report_mouse = config.mouse_reporting;
|
||||
let prompt_editor = config.prompt_editor;
|
||||
let agent_detection_armed = !terminal.reattached();
|
||||
let mut font = gpui::font(font_family);
|
||||
font.fallbacks = Some(gpui::FontFallbacks::from_fonts(fallbacks.clone()));
|
||||
if let Some(features) = &font_features {
|
||||
@@ -1676,6 +1683,7 @@ impl TerminalView {
|
||||
running_title: String::new(),
|
||||
running_agent: None,
|
||||
seen_agent: None,
|
||||
agent_detection_armed,
|
||||
last_agent_status: None,
|
||||
last_agent_session: (None, None),
|
||||
agent_turn_started: None,
|
||||
@@ -3784,13 +3792,7 @@ impl TerminalView {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let agent = self.terminal.foreground_agent();
|
||||
if agent != self.seen_agent {
|
||||
self.seen_agent = agent;
|
||||
if let Some(agent) = agent {
|
||||
cx.emit(AgentDetected(agent));
|
||||
}
|
||||
}
|
||||
self.poll_agent_detection(at_prompt, cx);
|
||||
|
||||
let turn_finished = self.poll_agent_status(notify_allowed, window, cx);
|
||||
|
||||
@@ -4052,6 +4054,24 @@ impl TerminalView {
|
||||
);
|
||||
}
|
||||
|
||||
/// Report an agent that has just started in this pane ([`AgentDetected`]),
|
||||
/// once per start, and never one that was already running when this view
|
||||
/// reattached to the pane.
|
||||
fn poll_agent_detection(&mut self, at_prompt: bool, cx: &mut Context<Self>) {
|
||||
let agent = self.terminal.foreground_agent();
|
||||
if agent != self.seen_agent {
|
||||
self.seen_agent = agent;
|
||||
if let Some(agent) = agent
|
||||
&& self.agent_detection_armed
|
||||
{
|
||||
cx.emit(AgentDetected(agent));
|
||||
}
|
||||
}
|
||||
if agent.is_none() && at_prompt {
|
||||
self.agent_detection_armed = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_agent_status(
|
||||
&mut self,
|
||||
notify_allowed: bool,
|
||||
@@ -16301,6 +16321,96 @@ mod gpui_tests {
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Quick launch counts an agent each time one starts. Reattaching to a pane
|
||||
/// whose agent was already running — every agent tab after an app restart
|
||||
/// — is not a start, and counting it bumped every agent once per launch of
|
||||
/// the app. Once that pane's agent exits, the next one is a start again.
|
||||
#[gpui::test]
|
||||
fn only_an_agent_that_starts_under_this_view_counts_as_detected(cx: &mut TestAppContext) {
|
||||
use crate::core::cli_agent::CLIAgent;
|
||||
|
||||
crate::core::config::pin_test_config_dir();
|
||||
let (window, _root_daemon) = harness(cx);
|
||||
let detected = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let pane = |reattached: bool, cx: &mut TestAppContext| {
|
||||
let (pane, daemon) = window
|
||||
.update(cx, |_, window, cx| {
|
||||
if reattached {
|
||||
super::quiet_reattached_test_pane(2, window, cx)
|
||||
} else {
|
||||
super::quiet_test_pane(3, window, cx)
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let seen = detected.clone();
|
||||
cx.update(|cx| {
|
||||
cx.subscribe(&pane, move |_, ev: &AgentDetected, _| {
|
||||
seen.borrow_mut().push(ev.0)
|
||||
})
|
||||
.detach()
|
||||
});
|
||||
(pane, daemon)
|
||||
};
|
||||
let report = |agent: Option<CLIAgent>,
|
||||
at_prompt: bool,
|
||||
pane: &Entity<TerminalView>,
|
||||
daemon: &mut Stream,
|
||||
cx: &mut TestAppContext| {
|
||||
DaemonMsg::Agent(agent).encode(daemon).unwrap();
|
||||
for _ in 0..200 {
|
||||
if pane.read_with(cx, |p, _| p.terminal.foreground_agent()) == agent {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
pane.update(cx, |p, cx| p.poll_agent_detection(at_prompt, cx));
|
||||
cx.run_until_parked();
|
||||
};
|
||||
|
||||
let (restored, mut restored_daemon) = pane(true, cx);
|
||||
report(
|
||||
Some(CLIAgent::Claude),
|
||||
false,
|
||||
&restored,
|
||||
&mut restored_daemon,
|
||||
cx,
|
||||
);
|
||||
report(
|
||||
Some(CLIAgent::Claude),
|
||||
false,
|
||||
&restored,
|
||||
&mut restored_daemon,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
*detected.borrow(),
|
||||
vec![],
|
||||
"already running when reattached"
|
||||
);
|
||||
report(None, true, &restored, &mut restored_daemon, cx);
|
||||
report(
|
||||
Some(CLIAgent::Codex),
|
||||
false,
|
||||
&restored,
|
||||
&mut restored_daemon,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
*detected.borrow(),
|
||||
vec![CLIAgent::Codex],
|
||||
"started after the reattach"
|
||||
);
|
||||
|
||||
let (fresh, mut fresh_daemon) = pane(false, cx);
|
||||
report(Some(CLIAgent::Claude), false, &fresh, &mut fresh_daemon, cx);
|
||||
report(Some(CLIAgent::Claude), false, &fresh, &mut fresh_daemon, cx);
|
||||
assert_eq!(
|
||||
*detected.borrow(),
|
||||
vec![CLIAgent::Codex, CLIAgent::Claude],
|
||||
"a spawned pane counts its first agent, once"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The window between the shell reporting a prompt and its line editor
|
||||
|
||||
Reference in New Issue
Block a user