@sigitcode-old / sigit / commits / d41841f
Fork 1

I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.

fix: preserve terminal status records on teardown

Max Farrell committed Oct 12, 2026 at 11:51 UTC d41841f7731e0dd215839e48aae7a69c47994aa3
3 files changed +31 -23
README.md
+1 -1
@@ -136,7 +136,7 @@ Run `sigit` in a terminal and you get the same model and system prompt as the ed
136
137 Terminal mode currently needs Unix terminal behavior, so it works on macOS and Linux only.
138
139 -The interactive UI emits [OSC 7501 program status](https://www.superlogical.com/rex/docs/build/program-status) reports so supporting terminals can show working, waiting for permission or input, browser sign-in, completion, and errors across tabs. Reports are cleared on exit and are never written to headless/JSON or ACP output.
139 +The interactive UI emits [OSC 7501 program status](https://www.superlogical.com/rex/docs/build/program-status) reports so supporting terminals can show working, waiting for permission or input, browser sign-in, completion, and errors across tabs. User-driven quit reports idle; terminals expire active work on process exit and retain completion or error reports. Status sequences are never written to headless/JSON or ACP output.
140
141 ## Headless and CI mode
142
src/chat.rs
+7 -2
@@ -965,7 +965,9 @@ mod tui {
965 }
966
967 fn status(&self) -> Status {
968 - if self.load_error.is_some() {
968 + if self.quit {
969 + Status::Idle
970 + } else if self.load_error.is_some() {
971 Status::Error
972 } else if self.pending_approval.is_some() {
973 Status::Permission
@@ -3872,7 +3874,8 @@ mod tui {
3874 app.messages.push(ChatMessage::system("(cancelled)"));
3875 }
3876 // A visible stream can have cleared `thinking`;
3875 - // cancellation must still drop its receiver.
3877 + // cancellation must drop its receiver and clear
3878 + // thinking so the tab cannot stay on working.
3879 app.stop_thinking();
3880 if app.switching_model {
3881 // flag before drop so Disconnected handler stays quiet
@@ -4045,6 +4048,8 @@ mod tui {
4048 app.show_model_picker = false;
4049 app.set_load_error("failed".into());
4050 assert_eq!(app.status(), Status::Error);
4051 + app.quit = true;
4052 + assert_eq!(app.status(), Status::Idle);
4053 }
4054
4055 #[test]
src/program_status.rs
+23 -20
@@ -53,16 +53,8 @@ impl Reporter {
53 }
54 }
55
56 -impl Drop for Reporter {
57 - fn drop(&mut self) {
58 - // Records survive the alternate screen. Remove ours on every exit path,
59 - // including unwinding. The TUI uses the protocol's root record.
60 - if self.last.is_some() {
61 - let _ = self.writer.write_all(b"\x1b]7501;state=clear\x1b\\");
62 - let _ = self.writer.flush();
63 - }
64 - }
65 -}
56 +// Do not clear the root record on drop: a bare clear removes every program's
57 +// records. Terminals expire working/blocked on process exit and retain done/error.
58
59 #[cfg(test)]
60 mod tests {
@@ -82,7 +74,7 @@ mod tests {
74 }
75
76 #[test]
85 - fn lifecycle_reports_are_framed_deduplicated_and_cleared() {
77 + fn lifecycle_reports_are_framed_and_deduplicated() {
78 let output = Output::default();
79 {
80 let mut reporter = Reporter::new(output.clone());
@@ -114,13 +106,12 @@ mod tests {
106 "state=idle",
107 ]
108 .map(|pairs| format!("\x1b]7501;{pairs}:app=sigit\x1b\\"))
117 - .join("")
118 - + "\x1b]7501;state=clear\x1b\\";
109 + .join("");
110 assert_eq!(actual, expected);
111 }
112
113 #[test]
123 - fn unwinding_clears_the_terminal_record() {
114 + fn unwinding_does_not_clear_terminal_records() {
115 let output = Output::default();
116 let captured = output.clone();
117 let _ = std::panic::catch_unwind(move || {
@@ -128,15 +119,27 @@ mod tests {
119 reporter.report(Status::Working);
120 panic!("terminal loop failed");
121 });
131 - assert!(
132 - output
133 - .0
134 - .lock()
135 - .unwrap()
136 - .ends_with(b"\x1b]7501;state=clear\x1b\\")
122 + assert_eq!(
123 + *output.0.lock().unwrap(),
124 + b"\x1b]7501;state=working:app=sigit\x1b\\"
125 );
126 }
127
128 + #[test]
129 + fn dropping_preserves_completion_and_error_reports() {
130 + for status in [Status::Done, Status::Error] {
131 + let output = Output::default();
132 + {
133 + let mut reporter = Reporter::new(output.clone());
134 + reporter.report(status);
135 + }
136 + assert_eq!(
137 + *output.0.lock().unwrap(),
138 + format!("\x1b]7501;{}:app=sigit\x1b\\", status.pairs()).as_bytes()
139 + );
140 + }
141 + }
142 +
143 #[test]
144 fn unused_reporter_emits_nothing() {
145 let output = Output::default();