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();