From cfb03f4c6365f4c41e59f41505717300a97a5ec5 Mon Sep 17 00:00:00 2001 From: moseoh Date: Tue, 14 Jul 2026 10:40:24 +0900 Subject: [PATCH 1/5] fix(rate-limits): keep Codex PTY reset text for weekly-only plans The PTY /status fallback parses '5h limit' and 'Weekly limit' lines by label, but the extracted reset text was only ever attached to the session window. Codex plans without a 5h session bucket (e.g. current Pro) produce a weekly-only parse, so the reset time the CLI printed was silently dropped. Fall back to the weekly window when no session window exists. --- .../codex-fetcher-pty-settle.test.ts | 36 +++++++++++++++++++ src/main/rate-limits/codex-fetcher.ts | 8 +++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts index c5088a2ae776..a193a229acdb 100644 --- a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts +++ b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts @@ -76,4 +76,40 @@ describe('fetchCodexRateLimits PTY settle timers', () => { status: 'ok' }) }) + + it('keeps the reset text on the weekly window for weekly-only plans', async () => { + const ptyHandlers: { onData?: (data: string) => void } = {} + + childSpawnMock.mockImplementation(() => { + throw new Error('rpc unavailable') + }) + ptySpawnMock.mockReturnValue({ + onData: vi.fn((callback) => { + ptyHandlers.onData = callback + return makeDisposable() + }), + onExit: vi.fn(() => makeDisposable()), + write: vi.fn(), + kill: vi.fn() + }) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(0) + + const onPtyData = ptyHandlers.onData + if (!onPtyData) { + throw new Error('PTY data handler was not registered') + } + + onPtyData('>') + onPtyData('Weekly limit: 76%\nResets in 5d 23h\n') + + await vi.advanceTimersByTimeAsync(500) + + await expect(resultPromise).resolves.toMatchObject({ + session: null, + weekly: { usedPercent: 76, resetDescription: '5d 23h' }, + status: 'ok' + }) + }) }) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index d606a12e850f..26019c5e08eb 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -827,10 +827,12 @@ function parsePtyStatus(output: string): { } : null - // Try to extract reset time from surrounding text + // Try to extract reset time from surrounding text. Weekly-only plans have + // no session window, so fall back to the weekly one instead of dropping it. const resetMatch = RESET_TEXT_RE.exec(output) - if (resetMatch && session) { - session.resetDescription = resetMatch[1].trim() + const resetTarget = session ?? weekly + if (resetMatch && resetTarget) { + resetTarget.resetDescription = resetMatch[1].trim() } return { session, weekly } From da004a02ff911610de67fb58f14e3c29cefaea9b Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:33:22 -0700 Subject: [PATCH 2/5] review: parse Codex PTY reset text per window into resetsAt --- .../codex-fetcher-pty-settle.test.ts | 51 ++++++++++++++++++- src/main/rate-limits/codex-fetcher.ts | 35 ++++++++----- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts index a193a229acdb..ffbf17956523 100644 --- a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts +++ b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts @@ -106,9 +106,58 @@ describe('fetchCodexRateLimits PTY settle timers', () => { await vi.advanceTimersByTimeAsync(500) + const fiveDays23h = (5 * 24 + 23) * 60 * 60 * 1000 await expect(resultPromise).resolves.toMatchObject({ session: null, - weekly: { usedPercent: 76, resetDescription: '5d 23h' }, + weekly: { + usedPercent: 76, + resetDescription: '5d 23h', + resetsAt: Date.now() + fiveDays23h + }, + status: 'ok' + }) + }) + + it('keeps each window reset text on its own window for dual-window plans', async () => { + const ptyHandlers: { onData?: (data: string) => void } = {} + + childSpawnMock.mockImplementation(() => { + throw new Error('rpc unavailable') + }) + ptySpawnMock.mockReturnValue({ + onData: vi.fn((callback) => { + ptyHandlers.onData = callback + return makeDisposable() + }), + onExit: vi.fn(() => makeDisposable()), + write: vi.fn(), + kill: vi.fn() + }) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(0) + + const onPtyData = ptyHandlers.onData + if (!onPtyData) { + throw new Error('PTY data handler was not registered') + } + + onPtyData('>') + onPtyData('5h limit: 17% (resets in 2h 30m)\nWeekly limit: 23% (resets in 5d 3h)\n') + + await vi.advanceTimersByTimeAsync(500) + + await expect(resultPromise).resolves.toMatchObject({ + session: { + usedPercent: 17, + resetDescription: '2h 30m', + resetsAt: Date.now() + (2 * 60 + 30) * 60 * 1000 + }, + weekly: { + usedPercent: 23, + resetDescription: '5d 3h', + resetsAt: Date.now() + (5 * 24 + 3) * 60 * 60 * 1000 + }, status: 'ok' }) }) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index 26019c5e08eb..9b9558d87ba2 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -11,6 +11,7 @@ import { readFile } from 'node:fs/promises' import { homedir } from 'node:os' import { join } from 'node:path' import { probeCodexAuthPresence } from './codex-auth-presence' +import { extractClaudePtyResetMetadata } from './claude-pty-reset-parser' import { resolveCodexCommand } from '../codex-cli/command' import { withMacTailscaleDnsHint } from '../network/macos-tailscale-dns-diagnostic' import { getCmdExePath, getSpawnArgsForWindows } from '../win32-utils' @@ -800,7 +801,10 @@ async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise FIVE_HOUR_RE.test(line), + isPtyLimitLabel + ) + const weeklyReset = extractClaudePtyResetMetadata( + lines, + (line) => WEEKLY_RE.test(line), + isPtyLimitLabel + ) const session: RateLimitWindow | null = fiveMatch ? { usedPercent: Math.min(100, Number.parseInt(fiveMatch[1], 10)), windowMinutes: 300, - resetsAt: null, - resetDescription: null + resetsAt: sessionReset.resetsAt, + resetDescription: sessionReset.resetDescription } : null @@ -822,19 +839,11 @@ function parsePtyStatus(output: string): { ? { usedPercent: Math.min(100, Number.parseInt(weeklyMatch[1], 10)), windowMinutes: 10080, - resetsAt: null, - resetDescription: null + resetsAt: weeklyReset.resetsAt, + resetDescription: weeklyReset.resetDescription } : null - // Try to extract reset time from surrounding text. Weekly-only plans have - // no session window, so fall back to the weekly one instead of dropping it. - const resetMatch = RESET_TEXT_RE.exec(output) - const resetTarget = session ?? weekly - if (resetMatch && resetTarget) { - resetTarget.resetDescription = resetMatch[1].trim() - } - return { session, weekly } } From ae8db378818a90456004d787daedc6545326f635 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:49:25 -0700 Subject: [PATCH 3/5] review: make Codex PTY status fallback work on codex >=0.145 --- .../rate-limits/claude-pty-reset-parser.ts | 65 ++++++++++++++++--- .../codex-fetcher-pty-settle.test.ts | 60 ++++++++++++++++- src/main/rate-limits/codex-fetcher.ts | 63 +++++++++++++++--- 3 files changed, 169 insertions(+), 19 deletions(-) diff --git a/src/main/rate-limits/claude-pty-reset-parser.ts b/src/main/rate-limits/claude-pty-reset-parser.ts index 7cadfe3fba4a..43137a8cea54 100644 --- a/src/main/rate-limits/claude-pty-reset-parser.ts +++ b/src/main/rate-limits/claude-pty-reset-parser.ts @@ -15,6 +15,9 @@ const MONTH_DAY_TIME_RE = new RegExp( const WEEKDAY_TIME_RE = /\b(sun(?:day)?|mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?)\.?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i const TIME_ONLY_RE = /\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i +// Why: newer Codex CLIs print 24-hour reset times ("10:21 on 28 Jul") with no am/pm. +const TIME_24H_RE = /\b(\d{1,2}):(\d{2})\b/ +const DAY_MONTH_RE = new RegExp(`\\b(?:on\\s+)?(\\d{1,2})\\s+(${MONTH_PATTERN})\\b`, 'i') const RELATIVE_RESET_RE = /^(?:\s*\d+\s*(?:d(?:ays?)?|h(?:ours?|rs?)?|m(?:in(?:ute)?s?)?)\s*)+$/i const RELATIVE_RESET_TOKEN_RE = /(\d+)\s*(d(?:ays?)?|h(?:ours?|rs?)?|m(?:in(?:ute)?s?)?)/gi const IANA_TIME_ZONE_RE = /\(([^()]*)\)?\s*$/ @@ -94,13 +97,16 @@ export function extractClaudePtyResetMetadata( function normalizeResetDescription(raw: string): string { // Why: Claude's TUI occasionally drops spaces around the Fable reset date // when copied from the PTY buffer, but the value still encodes a real reset. - return raw - .trim() - .replace(/[)]+$/, '') - .replace(/\s+/g, ' ') - .replace(MONTH_DAY_COMPACT_RE, '$1 $2') - .replace(/(\d{1,2})\s*at\s*(\d{1,2}(?::\d{2})?\s*(?:am|pm))/i, '$1 at $2') - .replace(/(\d)(am|pm)\(/gi, '$1$2 (') + return ( + raw + .trim() + // Why: PTY captures keep trailing box-border glyphs from framed status panels. + .replace(/[)\s│]+$/, '') + .replace(/\s+/g, ' ') + .replace(MONTH_DAY_COMPACT_RE, '$1 $2') + .replace(/(\d{1,2})\s*at\s*(\d{1,2}(?::\d{2})?\s*(?:am|pm))/i, '$1 at $2') + .replace(/(\d)(am|pm)\(/gi, '$1$2 (') + ) } function parseResetTimestamp(resetDescription: string | null): number | null { @@ -112,10 +118,53 @@ function parseResetTimestamp(resetDescription: string | null): number | null { parseRelativeResetTimestamp(resetDescription) ?? parseMonthDayResetTimestamp(resetDescription) ?? parseWeekdayResetTimestamp(resetDescription) ?? - parseTimeOnlyResetTimestamp(resetDescription) + parseTimeOnlyResetTimestamp(resetDescription) ?? + parseTwentyFourHourResetTimestamp(resetDescription) ) } +function parseTwentyFourHourResetTimestamp(resetDescription: string): number | null { + const resetText = stripResetTimeZone(resetDescription) + const timeMatch = TIME_24H_RE.exec(resetText) + if (!timeMatch) { + return null + } + const hour = Number(timeMatch[1]) + const minute = Number(timeMatch[2]) + if (!isValidClockTime(hour, minute)) { + return null + } + + const dayMonthMatch = DAY_MONTH_RE.exec(resetText) + if (dayMonthMatch) { + const day = Number(dayMonthMatch[1]) + const monthIndex = MONTH_INDEX_BY_NAME[dayMonthMatch[2].toLowerCase()] + if (monthIndex === undefined || day < 1 || day > 31) { + return null + } + const now = new Date() + const timeZone = extractResetTimeZone(resetDescription) + let timestamp = buildWallClockTimestamp( + { year: now.getFullYear(), monthIndex, day, hour, minute }, + timeZone + ) + if (timestamp !== null && timestamp <= Date.now()) { + timestamp = buildWallClockTimestamp( + { year: now.getFullYear() + 1, monthIndex, day, hour, minute }, + timeZone + ) + } + return timestamp + } + + const candidate = new Date() + candidate.setHours(hour, minute, 0, 0) + if (candidate.getTime() <= Date.now()) { + candidate.setDate(candidate.getDate() + 1) + } + return candidate.getTime() +} + function parseRelativeResetTimestamp(resetDescription: string): number | null { if (!RELATIVE_RESET_RE.test(resetDescription)) { return null diff --git a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts index ffbf17956523..a427a2fdbb3f 100644 --- a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts +++ b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts @@ -61,12 +61,14 @@ describe('fetchCodexRateLimits PTY settle timers', () => { } onPtyData('>') - expect(vi.getTimerCount()).toBe(1) + // Pending: PTY timeout + the delayed /status Enter keypress. + expect(vi.getTimerCount()).toBe(2) onPtyData('5h limit: 17%\n') onPtyData('Weekly limit: 23%\n') onPtyData('still rendering\n') - expect(vi.getTimerCount()).toBe(2) + // One settle timer armed — not one per data chunk. + expect(vi.getTimerCount()).toBe(3) await vi.advanceTimersByTimeAsync(500) @@ -161,4 +163,58 @@ describe('fetchCodexRateLimits PTY settle timers', () => { status: 'ok' }) }) + + it('parses the framed codex 0.145 status panel via the /status nudge', async () => { + const ptyHandlers: { onData?: (data: string) => void } = {} + const write = vi.fn() + + childSpawnMock.mockImplementation(() => { + throw new Error('rpc unavailable') + }) + ptySpawnMock.mockReturnValue({ + onData: vi.fn((callback) => { + ptyHandlers.onData = callback + return makeDisposable() + }), + onExit: vi.fn(() => makeDisposable()), + write, + kill: vi.fn() + }) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(0) + + const onPtyData = ptyHandlers.onData + if (!onPtyData) { + throw new Error('PTY data handler was not registered') + } + + // codex ≥0.145 shows a '›' composer with placeholder text, never a bare '>' prompt. + onPtyData('›Summarize recent commits') + expect(write).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(2500) + expect(write).toHaveBeenCalledWith('/status') + await vi.advanceTimersByTimeAsync(350) + expect(write).toHaveBeenCalledWith('\r') + + onPtyData( + '│ Weekly limit: [█████████░░░░░░░░░░░] 43% left (resets 10:21 on 28 Jul) │\n' + + '│ GPT-5.3-Codex-Spark Weekly limit: [████████████████████] 100% left (resets 17:40 on 29 Jul) │\n' + ) + await vi.advanceTimersByTimeAsync(500) + + const expectedReset = new Date(new Date().getFullYear(), 6, 28, 10, 21) + if (expectedReset.getTime() <= Date.now()) { + expectedReset.setFullYear(expectedReset.getFullYear() + 1) + } + await expect(resultPromise).resolves.toMatchObject({ + session: null, + weekly: { + usedPercent: 57, + resetDescription: '10:21 on 28 Jul', + resetsAt: expectedReset.getTime() + }, + status: 'ok' + }) + }) }) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index 9b9558d87ba2..083ea8d77fa0 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -34,6 +34,12 @@ import { const RPC_TIMEOUT_MS = 10_000 const WSL_RPC_TIMEOUT_MS = 25_000 const PTY_TIMEOUT_MS = 15_000 +// Why: codex ≥0.145 renders a '›' composer with placeholder text after it, so a +// prompt-anchored send can never fire; nudge /status after a short boot grace. +const PTY_STATUS_NUDGE_MS = 2_500 +// Why: '/status\r' in one write coalesces into a paste-like chunk and the TUI +// inserts the newline instead of submitting; Enter must be its own keypress. +const PTY_STATUS_ENTER_DELAY_MS = 350 const BACKEND_TIMEOUT_MS = 10_000 // Why: redeeming a reset credit is an explicit user action, not a background // poll — give it more room before failing so a slow backend can still finish. @@ -797,15 +803,22 @@ async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise void }[] = [registerHiddenRateLimitPty(term)] + let statusEnter: ReturnType | null = null + function sendStatusCommand(): void { + sentStatus = true + if (statusNudge) { + clearTimeout(statusNudge) + statusNudge = null + } + term.write('/status') + statusEnter = setTimeout(() => { + statusEnter = null + term.write('\r') + }, PTY_STATUS_ENTER_DELAY_MS) + } + + let statusNudge: ReturnType | null = setTimeout(() => { + statusNudge = null + if (!resolved && !sentStatus) { + sendStatusCommand() + } + }, PTY_STATUS_NUDGE_MS) + termDisposables.push({ + dispose: () => { + if (statusNudge) { + clearTimeout(statusNudge) + statusNudge = null + } + if (statusEnter) { + clearTimeout(statusEnter) + statusEnter = null + } + } + }) + function settleAborted(): void { if (resolved) { return @@ -945,9 +991,8 @@ async function fetchViaPty(options?: FetchCodexRateLimitsOptions): Promise\s*$/.test(data)) { - sentStatus = true - term.write('/status\r') + if (!sentStatus && /[>›]\s*$/.test(data)) { + sendStatusCommand() return } From a870e14cafe132e741173e5a4a220e6b0237f394 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:59:46 -0700 Subject: [PATCH 4/5] review: harden PTY status parse against model-scoped rows and styled output --- .../codex-fetcher-pty-settle.test.ts | 80 +++++++++++++++++++ src/main/rate-limits/codex-fetcher.ts | 60 ++++++++++---- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts index a427a2fdbb3f..4cb9b4008b37 100644 --- a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts +++ b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts @@ -217,4 +217,84 @@ describe('fetchCodexRateLimits PTY settle timers', () => { status: 'ok' }) }) + + it('never selects a model-scoped weekly row even when it renders first', async () => { + const ptyHandlers: { onData?: (data: string) => void } = {} + + childSpawnMock.mockImplementation(() => { + throw new Error('rpc unavailable') + }) + ptySpawnMock.mockReturnValue({ + onData: vi.fn((callback) => { + ptyHandlers.onData = callback + return makeDisposable() + }), + onExit: vi.fn(() => makeDisposable()), + write: vi.fn(), + kill: vi.fn() + }) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(0) + + const onPtyData = ptyHandlers.onData + if (!onPtyData) { + throw new Error('PTY data handler was not registered') + } + + onPtyData('>') + onPtyData( + '│ GPT-5.3-Codex-Spark Weekly limit: [████████████████████] 100% left (resets 17:40 on 29 Jul) │\n' + + '│ Weekly limit: [█████████░░░░░░░░░░░] 43% left (resets 10:21 on 28 Jul) │\n' + ) + await vi.advanceTimersByTimeAsync(500) + + await expect(resultPromise).resolves.toMatchObject({ + session: null, + weekly: { usedPercent: 57, resetDescription: '10:21 on 28 Jul' }, + status: 'ok' + }) + }) + + it('re-sends Enter once when the panel does not render after the first submit', async () => { + const ptyHandlers: { onData?: (data: string) => void } = {} + const write = vi.fn() + + childSpawnMock.mockImplementation(() => { + throw new Error('rpc unavailable') + }) + ptySpawnMock.mockReturnValue({ + onData: vi.fn((callback) => { + ptyHandlers.onData = callback + return makeDisposable() + }), + onExit: vi.fn(() => makeDisposable()), + write, + kill: vi.fn() + }) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(0) + + const onPtyData = ptyHandlers.onData + if (!onPtyData) { + throw new Error('PTY data handler was not registered') + } + + onPtyData('>') + await vi.advanceTimersByTimeAsync(350) + expect(write.mock.calls.filter((call) => call[0] === '\r')).toHaveLength(1) + + await vi.advanceTimersByTimeAsync(3000) + expect(write.mock.calls.filter((call) => call[0] === '\r')).toHaveLength(2) + + onPtyData('Weekly limit: 76%\nResets in 5d 23h\n') + await vi.advanceTimersByTimeAsync(500) + + await expect(resultPromise).resolves.toMatchObject({ + session: null, + weekly: { usedPercent: 76 }, + status: 'ok' + }) + }) }) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index 083ea8d77fa0..f96b4951ae24 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -40,6 +40,9 @@ const PTY_STATUS_NUDGE_MS = 2_500 // Why: '/status\r' in one write coalesces into a paste-like chunk and the TUI // inserts the newline instead of submitting; Enter must be its own keypress. const PTY_STATUS_ENTER_DELAY_MS = 350 +// Why: slow hosts (WSL/SSH) can drop the first Enter while the TUI is still +// booting; one spare Enter is a no-op on an empty, ready composer. +const PTY_STATUS_ENTER_RETRY_MS = 3_000 const BACKEND_TIMEOUT_MS = 10_000 // Why: redeeming a reset credit is an explicit user action, not a background // poll — give it more room before failing so a slow backend can still finish. @@ -806,11 +809,23 @@ async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise { statusEnter = null term.write('\r') + statusEnter = setTimeout(() => { + statusEnter = null + if (!resolved && !settleTimer) { + term.write('\r') + } + }, PTY_STATUS_ENTER_RETRY_MS) }, PTY_STATUS_ENTER_DELAY_MS) } - let statusNudge: ReturnType | null = setTimeout(() => { - statusNudge = null - if (!resolved && !sentStatus) { - sendStatusCommand() + let statusNudge: ReturnType | null = null + // Why: count the nudge grace from first TUI output, not spawn, so slow + // WSL/SSH boots get the full window before /status is typed. + function armStatusNudge(): void { + if (statusNudge || sentStatus || resolved) { + return } - }, PTY_STATUS_NUDGE_MS) + statusNudge = setTimeout(() => { + statusNudge = null + if (!resolved && !sentStatus) { + sendStatusCommand() + } + }, PTY_STATUS_NUDGE_MS) + } termDisposables.push({ dispose: () => { if (statusNudge) { @@ -990,6 +1019,8 @@ async function fetchViaPty(options?: FetchCodexRateLimitsOptions): Promise›]\s*$/.test(data)) { sendStatusCommand() @@ -997,9 +1028,10 @@ async function fetchViaPty(options?: FetchCodexRateLimitsOptions): Promise { settleTimer = null if (resolved) { @@ -1012,8 +1044,7 @@ async function fetchViaPty(options?: FetchCodexRateLimitsOptions): Promise Date: Wed, 22 Jul 2026 19:53:27 -0700 Subject: [PATCH 5/5] fix(rate-limits): strip private PTY control sequences --- src/main/rate-limits/codex-fetcher-pty-settle.test.ts | 2 +- src/main/rate-limits/codex-fetcher.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts index 4cb9b4008b37..15c338aad89b 100644 --- a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts +++ b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts @@ -198,7 +198,7 @@ describe('fetchCodexRateLimits PTY settle timers', () => { expect(write).toHaveBeenCalledWith('\r') onPtyData( - '│ Weekly limit: [█████████░░░░░░░░░░░] 43% left (resets 10:21 on 28 Jul) │\n' + + '│ Weekly limit: \x1b[?2026h\x1b[0 q[█████████░░░░░░░░░░░] 43% left\x1b[?2026l (resets 10:21 on 28 Jul) │\n' + '│ GPT-5.3-Codex-Spark Weekly limit: [████████████████████] 100% left (resets 17:40 on 29 Jul) │\n' ) await vi.advanceTimersByTimeAsync(500) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index f96b4951ae24..bd8740451175 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -818,7 +818,7 @@ const WEEKLY_RE = /(?