Bug
On Windows, codex-auth switch --live (and other live TUI screens: list --live, remove --live) can freeze/hang when the user switches away from the terminal window and then clicks back into it. After that, the picker stops updating and does not respond to input until a real key press or a window resize event happens to arrive.
Root Cause
In src/cli/tui.zig, the live loop uses pollTuiInput, which on Windows calls:
pub const pollTuiInput = if (builtin.os.tag == .windows)
struct {
fn call(file: std.Io.File, timeout_ms: i32, _: i16) !TuiPollResult {
const wait_ms: win.DWORD = if (timeout_ms < 0) win.INFINITE else @intCast(timeout_ms);
return switch (win.WaitForSingleObject(file.handle, wait_ms)) {
win.WAIT_OBJECT_0 => .ready,
win.WAIT_TIMEOUT => .timeout,
else => .closed,
};
}
}.call
...
This correctly waits with a timeout so the live refresh timer can keep firing. However, once WaitForSingleObject reports the input handle is signaled (.ready), control passes to readWindowsKey():
pub fn readWindowsKey(self: *@This()) !TuiInputKey {
if (comptime builtin.os.tag != .windows) unreachable;
if (self.pending_windows_key) |pending| {
...
return pending;
}
while (true) {
var record: win.INPUT_RECORD = undefined;
var events_read: win.DWORD = 0;
if (win.ReadConsoleInputW(self.input.handle, &record, 1, &events_read) == .FALSE) {
return error.EndOfStream;
}
if (events_read == 0) continue;
if (record.EventType == win.WINDOW_BUFFER_SIZE_EVENT) {
self.pending_windows_key = null;
self.pending_windows_repeat_count = 0;
return .redraw;
}
if (record.EventType != win.KEY_EVENT) continue;
...
}
}
This inner loop calls ReadConsoleInputW with no timeout. Since the console input mode has ENABLE_WINDOW_INPUT set (see windowsTuiInputMode), the console queues non-key input records — most notably FOCUS_EVENT_RECORD (event type 0x0010) — whenever the console window loses or gains focus (e.g., the user clicks away from the terminal, or clicks back into it).
When such a record is read, the current code just does continue and blocks again on ReadConsoleInputW with no timeout. This means:
- The user clicks away from the terminal window → the console queues a focus-lost event →
WaitForSingleObject (which does have a timeout) wakes up and returns .ready.
- Execution moves into
readWindowsKey's inner loop, reads the non-key focus record, discards it via continue, and re-blocks indefinitely on ReadConsoleInputW.
- Because this call has no timeout, the periodic live-refresh logic that depends on returning control to the outer live loop (via
.timeout) never runs again.
- When the user clicks back into the terminal, another
FOCUS_EVENT_RECORD (focus gained) is queued, which is likewise silently discarded, and the code blocks again — so the live view appears completely frozen until a real key press or a window resize (WINDOW_BUFFER_SIZE_EVENT) happens to arrive.
Expected Behavior
The Windows console-input reading loop should not block indefinitely once it starts discarding non-key records (such as focus events). It should respect the same effective timeout budget that the outer live-refresh loop relies on, so that:
- Focus-lost / focus-gained events do not cause
switch --live, list --live, or remove --live to become unresponsive.
- The live refresh timer keeps firing on schedule even if unrelated console input records (like focus change events) are queued.
- Real key presses and window resize events continue to work exactly as before.
Suggested Fix
In src/cli/tui.zig:
- Make the Windows key-reading path (
readWindowsKey, and/or the TuiSession.pollKeys/live-loop caller in the same file) timeout-aware, so that when ReadConsoleInputW returns a non-key, non-resize record (e.g., FOCUS_EVENT_RECORD = 0x0010, MENU_EVENT_RECORD = 0x0008), the function does not simply loop forever on a blocking read.
- One approach: track a remaining timeout budget across the inner loop iterations (e.g., using
WaitForSingleObject with a decreasing timeout before each ReadConsoleInputW call, similar to what pollTuiInput already does), and return a .timeout (or equivalent no-op/redraw) result up to the caller so the live loop can proceed to its next scheduled refresh instead of being stuck.
- Ensure any relevant timeout value used for the live refresh is threaded through consistently.
- Do not change the non-Windows (
posix) code path, which already uses std.posix.poll with a timeout correctly.
- Filter out/skip non-key, non-resize console input records (
FOCUS_EVENT_RECORD, MENU_EVENT_RECORD, MOUSE_EVENT_RECORD if not already handled) without blocking indefinitely.
Tests
- Add/extend tests in
tests/tui_session_test.zig that simulate a sequence of non-key console input records (e.g., a focus event) followed by a delay, and verify that the key-reading logic returns control within the expected time budget rather than blocking indefinitely.
- Follow existing project conventions: Zig,
zig build test, BDD-style test names ("Scenario: Given ... when ... then ...").
Additional Notes
- This affects
codex-auth switch --live, codex-auth list --live, and codex-auth remove --live on Windows only. POSIX/macOS/Linux paths are unaffected.
- Keep code style consistent with
src/cli/tui.zig.
Bug
On Windows,
codex-auth switch --live(and other live TUI screens:list --live,remove --live) can freeze/hang when the user switches away from the terminal window and then clicks back into it. After that, the picker stops updating and does not respond to input until a real key press or a window resize event happens to arrive.Root Cause
In
src/cli/tui.zig, the live loop usespollTuiInput, which on Windows calls:This correctly waits with a timeout so the live refresh timer can keep firing. However, once
WaitForSingleObjectreports the input handle is signaled (.ready), control passes toreadWindowsKey():This inner loop calls
ReadConsoleInputWwith no timeout. Since the console input mode hasENABLE_WINDOW_INPUTset (seewindowsTuiInputMode), the console queues non-key input records — most notablyFOCUS_EVENT_RECORD(event type0x0010) — whenever the console window loses or gains focus (e.g., the user clicks away from the terminal, or clicks back into it).When such a record is read, the current code just does
continueand blocks again onReadConsoleInputWwith no timeout. This means:WaitForSingleObject(which does have a timeout) wakes up and returns.ready.readWindowsKey's inner loop, reads the non-key focus record, discards it viacontinue, and re-blocks indefinitely onReadConsoleInputW..timeout) never runs again.FOCUS_EVENT_RECORD(focus gained) is queued, which is likewise silently discarded, and the code blocks again — so the live view appears completely frozen until a real key press or a window resize (WINDOW_BUFFER_SIZE_EVENT) happens to arrive.Expected Behavior
The Windows console-input reading loop should not block indefinitely once it starts discarding non-key records (such as focus events). It should respect the same effective timeout budget that the outer live-refresh loop relies on, so that:
switch --live,list --live, orremove --liveto become unresponsive.Suggested Fix
In
src/cli/tui.zig:readWindowsKey, and/or theTuiSession.pollKeys/live-loop caller in the same file) timeout-aware, so that whenReadConsoleInputWreturns a non-key, non-resize record (e.g.,FOCUS_EVENT_RECORD=0x0010,MENU_EVENT_RECORD=0x0008), the function does not simply loop forever on a blocking read.WaitForSingleObjectwith a decreasing timeout before eachReadConsoleInputWcall, similar to whatpollTuiInputalready does), and return a.timeout(or equivalent no-op/redraw) result up to the caller so the live loop can proceed to its next scheduled refresh instead of being stuck.posix) code path, which already usesstd.posix.pollwith a timeout correctly.FOCUS_EVENT_RECORD,MENU_EVENT_RECORD,MOUSE_EVENT_RECORDif not already handled) without blocking indefinitely.Tests
tests/tui_session_test.zigthat simulate a sequence of non-key console input records (e.g., a focus event) followed by a delay, and verify that the key-reading logic returns control within the expected time budget rather than blocking indefinitely.zig build test, BDD-style test names ("Scenario: Given ... when ... then ...").Additional Notes
codex-auth switch --live,codex-auth list --live, andcodex-auth remove --liveon Windows only. POSIX/macOS/Linux paths are unaffected.src/cli/tui.zig.