mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
fix(daemon): a pane no window shows still answers DA1 (#1176)
* fix(daemon): a pane no window shows still answers DA1 The client's emulator answers a pane's queries, so a pane with no client attached answered nothing, and the replay on attach drops replies. Claude Code ends its OSC 11 theme query with DA1 and waits for it; started in such a pane it hung on that query and skipped every later theme check. The daemon now answers DA1 itself while no client is attached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(daemon): write the DA1 answer after the state lock is released A pty whose program is not reading its input blocks the write, and holding the pane's state lock across it would stall everything else that needs that pane. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
l0ng-ai
parent
8c5ea523b3
commit
37745afaee
@@ -178,6 +178,9 @@ pub struct TerminalModes {
|
||||
/// `CSI ? 996 n` queries seen and not yet taken — see
|
||||
/// [`Self::take_color_scheme_queries`].
|
||||
color_scheme_queries: usize,
|
||||
/// `CSI c` (primary device attributes) queries seen and not yet taken —
|
||||
/// see [`Self::take_device_attribute_queries`].
|
||||
device_attribute_queries: usize,
|
||||
keyboard: KeyboardStacks,
|
||||
}
|
||||
|
||||
@@ -222,6 +225,12 @@ impl TerminalModes {
|
||||
std::mem::take(&mut self.color_scheme_queries)
|
||||
}
|
||||
|
||||
/// How many `CSI c` / `CSI 0 c` (primary device attributes) queries were
|
||||
/// fed since the last call. Counted only, like the 996 ones.
|
||||
pub fn take_device_attribute_queries(&mut self) -> usize {
|
||||
std::mem::take(&mut self.device_attribute_queries)
|
||||
}
|
||||
|
||||
/// The bytes that put a freshly reset terminal back into these modes, or
|
||||
/// `None` when there is nothing to restore.
|
||||
pub fn restore_bytes(&self) -> Option<Vec<u8>> {
|
||||
@@ -364,6 +373,12 @@ impl TerminalModes {
|
||||
}
|
||||
self.state = State::Text;
|
||||
}
|
||||
b'c' => {
|
||||
if marker == 0 && matches!(self.params.as_slice(), b"" | b"0") {
|
||||
self.device_attribute_queries += 1;
|
||||
}
|
||||
self.state = State::Text;
|
||||
}
|
||||
// Any other final byte — or an intermediate such as the `$`
|
||||
// of a DECRQM query — ends a sequence that is not a mode
|
||||
// set. Intermediates are lumped in with finals on purpose:
|
||||
@@ -696,6 +711,19 @@ mod tests {
|
||||
assert!(!modes.is_on(BRACKETED_PASTE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_primary_device_attribute_queries() {
|
||||
let mut modes = TerminalModes::new();
|
||||
modes.feed(b"\x1b]11;?\x07\x1b[");
|
||||
modes.feed(b"c\x1b[0c\x1b[>c\x1b[=c\x1b[?6c");
|
||||
assert_eq!(
|
||||
modes.take_device_attribute_queries(),
|
||||
2,
|
||||
"a split DA1 counts, DA2, DA3 and a DA1 reply do not"
|
||||
);
|
||||
assert_eq!(modes.take_device_attribute_queries(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follows_colour_scheme_updates_and_counts_its_queries() {
|
||||
let mut modes = TerminalModes::new();
|
||||
|
||||
@@ -2423,6 +2423,7 @@ impl DaemonPane {
|
||||
let facts_before = may_change_facts.then(|| observed_facts(&st));
|
||||
record_output(&mut st, bytes);
|
||||
fan_out_output(&mut st, bytes, frames, &gate);
|
||||
let da1_owed = detached_device_attribute_queries(&mut st);
|
||||
apply_signals(&mut st, signals);
|
||||
if let Some(remote) = remote {
|
||||
apply_remote_context(&mut st, remote);
|
||||
@@ -2444,6 +2445,7 @@ impl DaemonPane {
|
||||
let alive = st.alive;
|
||||
let facts_after = may_change_facts.then(|| observed_facts(&st));
|
||||
drop(st);
|
||||
answer_device_attributes(&writer, da1_owed);
|
||||
if !shutting_down.load(Ordering::SeqCst)
|
||||
&& let (Some(before), Some(after)) = (facts_before, facts_after)
|
||||
&& facts_changed(&before, &after)
|
||||
@@ -3338,6 +3340,38 @@ fn record_output(st: &mut PaneState, bytes: &[u8]) {
|
||||
st.modes.feed(bytes);
|
||||
}
|
||||
|
||||
/// The primary device attributes a client's emulator answers with.
|
||||
const DA1_REPLY: &[u8] = b"\x1b[?6c";
|
||||
|
||||
/// A pane with no client attached has nobody to answer its queries, and a
|
||||
/// replay drops replies, so one asked then is never answered. Programs that
|
||||
/// end a batch of queries with DA1 wait on that answer: Claude Code's OSC 11
|
||||
/// theme check hangs on it and then skips every later check, so the theme
|
||||
/// flips tty7 reports never reach it. Answering DA1 here lets the batch end;
|
||||
/// the colour report a client sends on attach then asks again, and is answered.
|
||||
///
|
||||
/// Returns how many DA1 queries the daemon owes an answer to. With a client
|
||||
/// attached the client answers, so none are owed. Read under the state lock;
|
||||
/// the answers are written by [`answer_device_attributes`] after it is
|
||||
/// released, because a pty whose program is not reading its input blocks the
|
||||
/// write, and nothing else that needs this pane's state should wait on that.
|
||||
fn detached_device_attribute_queries(st: &mut PaneState) -> usize {
|
||||
let asked = st.modes.take_device_attribute_queries();
|
||||
if st.subscriber.is_some() { 0 } else { asked }
|
||||
}
|
||||
|
||||
fn answer_device_attributes(writer: &Mutex<Box<dyn Write + Send>>, owed: usize) {
|
||||
if owed == 0 {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut writer) = writer.lock() {
|
||||
for _ in 0..owed {
|
||||
let _ = writer.write_all(DA1_REPLY);
|
||||
}
|
||||
let _ = writer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything a client needs to rebuild the pane's screen and status, in the
|
||||
/// order it has to be applied.
|
||||
///
|
||||
@@ -6554,6 +6588,41 @@ mod tests {
|
||||
withdraw_observations();
|
||||
}
|
||||
|
||||
/// Claude Code ends its OSC 11 theme query with DA1 and waits for both.
|
||||
/// Started in a pane no window shows (an agent woken at launch), it got
|
||||
/// neither and never checked the theme again.
|
||||
#[test]
|
||||
fn a_detached_pane_answers_device_attributes_and_an_attached_one_leaves_it_to_the_client() {
|
||||
#[derive(Clone, Default)]
|
||||
struct Sink(Arc<Mutex<Vec<u8>>>);
|
||||
impl Write for Sink {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.0.lock().unwrap().extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let sink = Sink::default();
|
||||
let writer: Mutex<Box<dyn Write + Send>> = Mutex::new(Box::new(sink.clone()));
|
||||
let mut st = test_state(true);
|
||||
|
||||
record_output(&mut st, b"\x1b]11;?\x07\x1b[c");
|
||||
answer_device_attributes(&writer, detached_device_attribute_queries(&mut st));
|
||||
assert_eq!(sink.0.lock().unwrap().as_slice(), DA1_REPLY);
|
||||
|
||||
let (tx, _rx) = mpsc::channel();
|
||||
st.subscriber = Some(tx);
|
||||
record_output(&mut st, b"\x1b[c");
|
||||
answer_device_attributes(&writer, detached_device_attribute_queries(&mut st));
|
||||
assert_eq!(
|
||||
sink.0.lock().unwrap().as_slice(),
|
||||
DA1_REPLY,
|
||||
"the client answers"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sentinel_events_drive_agent_session_state() {
|
||||
use crate::core::cli_agent::{AgentStatus, CLIAgent};
|
||||
|
||||
Reference in New Issue
Block a user