Fix Copilot PR #3 review: timeout race, ANSI stripping, streaming perf (#5)

- Fix timeout race in _exec_bash: check proc.poll() before setting
  timed_out flag; fall back to proc.kill() if process group kill fails
- Remove stderr_thread.join(timeout=5) — after proc.wait() the pipe
  is closed so join completes promptly without risk of truncation
- Fix CSI final byte range in stripAnsi: [A-Za-z] → [@-~] to handle
  sequences like \x1b[1~ (Home key) that end in non-letter bytes
- Use appendChild(createTextNode()) instead of textContent += for O(1)
  chunk appending in streaming output (avoids O(n²) on large output)
- Use var(--accent-dim) in stream-pulse keyframe instead of hardcoded
  rgba() so pulse color adapts to light/dark theme
- Record tool_output_chunk calls in RecordingUI for test assertions
This commit is contained in:
Patrick Buckley
2026-03-02 18:17:05 -08:00
committed by GitHub
parent 5c452a1239
commit 7ebc04d1cc
4 changed files with 13 additions and 6 deletions
+2 -1
View File
@@ -66,6 +66,7 @@ class RecordingUI:
self.content_tokens: list[str] = []
self.reasoning_tokens: list[str] = []
self.tool_results: list[tuple[str, str]] = []
self.tool_chunks: list[tuple[str, str]] = []
self.errors: list[str] = []
self.infos: list[str] = []
@@ -91,7 +92,7 @@ class RecordingUI:
self.tool_results.append((name, output))
def on_tool_output_chunk(self, call_id, chunk):
pass
self.tool_chunks.append((call_id, chunk))
def on_status(self, usage, context_window, effort):
self.events.append(("status",))
+8 -2
View File
@@ -1766,9 +1766,15 @@ class ChatSession:
timed_out = threading.Event()
def _on_timeout() -> None:
if proc.poll() is not None:
return # process already exited
timed_out.set()
with contextlib.suppress(OSError, ProcessLookupError):
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except OSError:
with contextlib.suppress(OSError, ProcessLookupError):
proc.kill()
timer = threading.Timer(self.tool_timeout, _on_timeout)
timer.start()
@@ -1782,7 +1788,7 @@ class ChatSession:
timer.cancel()
proc.wait()
stderr_thread.join(timeout=5)
stderr_thread.join()
finally:
os.unlink(script_path)
+2 -2
View File
@@ -1345,7 +1345,7 @@ function replayHistory(messages) {
function stripAnsi(s) {
// Strip CSI sequences, OSC sequences, and two-byte escapes
return s.replace(
/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[()#][A-Za-z0-9]|.)/g,
/\x1b(?:\[[0-9;?]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[()#][A-Za-z0-9]|.)/g,
"",
);
}
@@ -1529,7 +1529,7 @@ function appendToolOutputChunk(callId, chunk) {
target.after(el);
}
el.textContent += stripped;
el.appendChild(document.createTextNode(stripped));
el.scrollTop = el.scrollHeight;
scrollToBottom();
}
+1 -1
View File
@@ -517,7 +517,7 @@ body {
}
@keyframes stream-pulse {
0%, 100% { border-left-color: var(--accent); }
50% { border-left-color: rgba(229, 160, 66, 0.15); }
50% { border-left-color: var(--accent-dim); }
}
.tool-output.collapsed { max-height: 150px; position: relative; }
.tool-output.collapsed::after {