diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index cf964e5ae..7d5921c40 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,7 +12,7 @@ ### Added - Added the bundled `ts-no-local-is-record` TTSR rule, which catches local `isRecord` function and lambda definitions and directs agents to shared guards plus explicit shape validation. -- Added opt-in `tui.codexResetFireworks` celebrations for Codex 5-hour usage resets and newly banked saved resets, shown in a theme-aware top-third modal until Escape ([#6858](https://github.com/can1357/oh-my-pi/pull/6858) by [@joshrzemien](https://github.com/joshrzemien)). +- Added opt-in `tui.codexResetFireworks` celebrations for unscheduled Codex weekly usage resets and newly banked saved resets, shown in a theme-aware top-third modal until Escape ([#6858](https://github.com/can1357/oh-my-pi/pull/6858) by [@joshrzemien](https://github.com/joshrzemien)). - A `tool_call` handler (extension or hook) can now return `input` to revise the arguments a tool executes with, not just `block` it. The returned object is the raw execution input passed to the tool (ignored when `block` is set, and not applied to `computer` tool calls), enabling wrappers that normalize or rewrite a built-in's arguments without reimplementing the tool. For model-issued calls the event fires at arg-prep time in the agent loop, so a revision is revalidated against the tool schema and is what concurrency scheduling, `tool_execution_start`/transcripts, the persisted assistant message, and the approval gate all observe — the user approves exactly what runs, and a revision that changes a tool's functional concurrency (e.g. bash `pty`) schedules correctly. A revised nested `write xd://` device dispatch forfeits the outer write gate's approval and faces the full prompt again ([#6681](https://github.com/can1357/oh-my-pi/pull/6681) by [@psyrendust](https://github.com/psyrendust)). - Added a parser for macOS `sample`(1) call-tree reports to the read tool: `*.sample.txt` reads now return a compact bottleneck summary — per-thread hot paths with on-CPU sample counts (blocked syscall time excluded), demangled Rust v0/legacy symbols, flattened direct recursion, merged call-site siblings, idle-thread classification, and a process-wide top-functions-by-self-samples table. `:raw` still reads the original report, and files that merely carry the extension fall back to plain text. - Added V8 `.cpuprofile` support to the read tool (Node/Bun `--cpu-prof`, Chrome DevTools, CDP `Profiler.stop` output): reads now return a compact bottleneck summary — hot-path call tree with on-CPU milliseconds (`(idle)` time excluded), collapsed pass-through chains, flattened direct recursion, shortened file URLs, and a top-functions-by-self-time table. `:raw` still reads the original JSON, and files that merely carry the extension fall back to plain text. diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 495d7a2bd..44ecfbd93 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -935,7 +935,7 @@ export const SETTINGS_SCHEMA = { group: "Display", label: "Codex Reset Fireworks", description: - "Celebrate Codex 5-hour usage resets and newly banked saved resets with a top-third fireworks overlay that remains until Escape", + "Celebrate unscheduled Codex weekly usage resets and newly banked saved resets with a top-third fireworks overlay that remains until Escape", }, }, diff --git a/packages/coding-agent/src/modes/components/codex-reset-fireworks.ts b/packages/coding-agent/src/modes/components/codex-reset-fireworks.ts index 10907882e..8f99f2b2b 100644 --- a/packages/coding-agent/src/modes/components/codex-reset-fireworks.ts +++ b/packages/coding-agent/src/modes/components/codex-reset-fireworks.ts @@ -3,7 +3,6 @@ import { type ThemeColor, theme } from "../theme/theme"; const FRAME_INTERVAL_MS = 85; const FRAME_COUNT = 34; -const WEEKLY_COUNTDOWN_RESTART_HOURS = 12; const FIREWORK_THEME_COLORS = { cyan: "mdLink", @@ -17,23 +16,18 @@ const FIREWORK_THEME_COLORS = { type FireworkColor = keyof typeof FIREWORK_THEME_COLORS; -/** A Codex usage window retained between status refreshes for reset detection. */ -export interface CodexUsageWindowSnapshot { - percent: number; - resetMinutes?: number; - resetHours?: number; -} - /** The active Codex account fields retained between status refreshes. */ export interface CodexResetUsageSnapshot { - fiveHour?: CodexUsageWindowSnapshot; - sevenDay?: CodexUsageWindowSnapshot; + /** When this usage report was observed. */ + observedAt: number; + /** Weekly usage and its previously scheduled reset deadline. */ + sevenDay?: { percent: number; resetsAt?: number }; savedResets?: number; } /** A detected Codex quota event that can trigger the fireworks presentation. */ export type CodexResetFireworksEvent = - | { kind: "usage-window-reset" } + | { kind: "unscheduled-weekly-reset" } | { kind: "saved-reset-banked"; added: number; available: number }; interface CanvasCell { @@ -74,8 +68,9 @@ const BURSTS: readonly FireworkBurst[] = [ /** * Compare consecutive reports for one Codex account. A saved-reset grant takes - * precedence when both changes arrive in the same report because it carries - * the more specific celebration copy. + * precedence when both changes arrive in the same report. Weekly usage is only + * celebrated when it visibly falls from non-zero to zero before its previously + * scheduled reset deadline. */ export function detectCodexResetFireworks( previous: CodexResetUsageSnapshot, @@ -93,18 +88,21 @@ export function detectCodexResetFireworks( }; } - if (!previous.fiveHour || !current.fiveHour || !previous.sevenDay || !current.sevenDay) return undefined; - const previousFiveHourPercent = Math.round(Math.max(0, Math.min(100, previous.fiveHour.percent))); - const currentFiveHourPercent = Math.round(Math.max(0, Math.min(100, current.fiveHour.percent))); - if (previousFiveHourPercent === 0 || currentFiveHourPercent !== 0) return undefined; + if (!previous.sevenDay || !current.sevenDay) return undefined; + const previousWeeklyPercent = Math.round(Math.max(0, Math.min(100, previous.sevenDay.percent))); + const currentWeeklyPercent = Math.round(Math.max(0, Math.min(100, current.sevenDay.percent))); + if (previousWeeklyPercent === 0 || currentWeeklyPercent !== 0) return undefined; - const weeklyCountdownRestarted = - previous.sevenDay.resetHours !== undefined && - current.sevenDay.resetHours !== undefined && - current.sevenDay.resetHours - previous.sevenDay.resetHours >= WEEKLY_COUNTDOWN_RESTART_HOURS; - const weeklyUsageDropped = current.sevenDay.percent + 0.01 < previous.sevenDay.percent; - if (weeklyCountdownRestarted || weeklyUsageDropped) return undefined; - return { kind: "usage-window-reset" }; + const scheduledResetAt = previous.sevenDay.resetsAt; + if ( + scheduledResetAt === undefined || + !Number.isFinite(scheduledResetAt) || + !Number.isFinite(current.observedAt) || + current.observedAt >= scheduledResetAt + ) { + return undefined; + } + return { kind: "unscheduled-weekly-reset" }; } function setCell( @@ -154,10 +152,11 @@ function drawBanner( const panelLeft = left + Math.floor((artWidth - panelWidth) / 2); const top = height - 3; const innerWidth = panelWidth - 2; - const titleText = event.kind === "usage-window-reset" ? " C O D E X R E S E T " : " S A V E D R E S E T "; + const titleText = + event.kind === "unscheduled-weekly-reset" ? " O P E N A I R E S E T " : " S A V E D R E S E T "; const subtitleText = - event.kind === "usage-window-reset" - ? "5-hour window: 0% used · ESC to return" + event.kind === "unscheduled-weekly-reset" + ? "Weekly usage cleared early · ESC to return" : event.added === 1 ? `New reset banked · ${event.available} available · ESC to return` : `${event.added} resets banked · ${event.available} available · ESC to return`; diff --git a/packages/coding-agent/src/modes/components/status-line/component.ts b/packages/coding-agent/src/modes/components/status-line/component.ts index d0d05c05d..1557fed17 100644 --- a/packages/coding-agent/src/modes/components/status-line/component.ts +++ b/packages/coding-agent/src/modes/components/status-line/component.ts @@ -330,9 +330,10 @@ export class StatusLineComponent implements Component { // Provider usage caching (5-min TTL, OAuth/sub only) #cachedUsage: { + observedAt: number; tier?: string; fiveHour?: { percent: number; resetMinutes?: number }; - sevenDay?: { percent: number; resetHours?: number }; + sevenDay?: { percent: number; resetHours?: number; resetsAt?: number }; savedResets?: number; } | null = null; #cachedUsageContextKey: string | null = null; @@ -1079,18 +1080,20 @@ export class StatusLineComponent implements Component { activeProvider?: string, activeIdentity?: OAuthAccountIdentity, ): { + observedAt: number; tier?: string; fiveHour?: { percent: number; resetMinutes?: number }; - sevenDay?: { percent: number; resetHours?: number }; + sevenDay?: { percent: number; resetHours?: number; resetsAt?: number }; savedResets?: number; } | null { if (!Array.isArray(reports)) return null; let fiveHour: { percent: number; resetMinutes?: number } | undefined; - let sevenDay: { percent: number; resetHours?: number } | undefined; + let sevenDay: { percent: number; resetHours?: number; resetsAt?: number } | undefined; let savedResets: number | undefined; let fiveHourTier: string | undefined; let sevenDayTier: string | undefined; const now = Date.now(); + let observedAt = now; for (const report of reports) { if (!report || typeof report !== "object") continue; const provider = (report as { provider?: unknown }).provider; @@ -1099,6 +1102,7 @@ export class StatusLineComponent implements Component { if (!Array.isArray(limits)) continue; const usageReport = report as UsageReport; if (provider === "openai-codex" && reportMatchesActiveAccount(usageReport, activeIdentity)) { + if (Number.isFinite(usageReport.fetchedAt)) observedAt = usageReport.fetchedAt; const availableCount = usageReport.resetCredits?.availableCount; if (typeof availableCount === "number" && Number.isFinite(availableCount)) { savedResets = Math.max(0, Math.trunc(availableCount)); @@ -1134,6 +1138,7 @@ export class StatusLineComponent implements Component { percent: fraction * 100, resetHours: typeof resetsAt === "number" ? Math.max(0, Math.round((resetsAt - now) / 3_600_000)) : undefined, + resetsAt: typeof resetsAt === "number" ? resetsAt : undefined, }; sevenDayTier = tier || undefined; } @@ -1142,7 +1147,7 @@ export class StatusLineComponent implements Component { if (!fiveHour && !sevenDay && savedResets === undefined) return null; // Single compact label; prefer the five-hour tier if displayed windows ever disagree. const effectiveTier = fiveHourTier ?? sevenDayTier; - return { tier: effectiveTier, fiveHour, sevenDay, savedResets }; + return { observedAt, tier: effectiveTier, fiveHour, sevenDay, savedResets }; } /** diff --git a/packages/coding-agent/test/modes/components/codex-reset-fireworks.test.ts b/packages/coding-agent/test/modes/components/codex-reset-fireworks.test.ts index 2e8120720..988de1278 100644 --- a/packages/coding-agent/test/modes/components/codex-reset-fireworks.test.ts +++ b/packages/coding-agent/test/modes/components/codex-reset-fireworks.test.ts @@ -84,53 +84,53 @@ describe("Codex reset fireworks", () => { vi.useRealTimers(); }); - it("detects a 5-hour reset and prioritizes newly banked resets", () => { + it("detects an unscheduled weekly reset and prioritizes newly banked resets", () => { const previous = { - fiveHour: { percent: 42, resetMinutes: 1 }, - sevenDay: { percent: 18, resetHours: 80 }, + observedAt: 1_000, + sevenDay: { percent: 42, resetsAt: 10_000 }, savedResets: 0, }; expect( detectCodexResetFireworks(previous, { - fiveHour: { percent: 0, resetMinutes: 300 }, - sevenDay: { percent: 18.2, resetHours: 80 }, + observedAt: 2_000, + sevenDay: { percent: 0, resetsAt: 10_000 }, savedResets: 0, }), - ).toEqual({ kind: "usage-window-reset" }); + ).toEqual({ kind: "unscheduled-weekly-reset" }); expect( detectCodexResetFireworks(previous, { - fiveHour: { percent: 0, resetMinutes: 300 }, - sevenDay: { percent: 18.2, resetHours: 80 }, + observedAt: 2_000, + sevenDay: { percent: 0, resetsAt: 10_000 }, savedResets: 2, }), ).toEqual({ kind: "saved-reset-banked", added: 2, available: 2 }); }); - it("suppresses a 5-hour transition when only the weekly countdown restarts", () => { + it("suppresses a weekly transition observed at its scheduled reset deadline", () => { expect( detectCodexResetFireworks( { - fiveHour: { percent: 42, resetMinutes: 1 }, - sevenDay: { percent: 18, resetHours: 80 }, + observedAt: 1_000, + sevenDay: { percent: 42, resetsAt: 2_000 }, }, { - fiveHour: { percent: 0, resetMinutes: 300 }, - sevenDay: { percent: 18.2, resetHours: 168 }, + observedAt: 2_000, + sevenDay: { percent: 0, resetsAt: 10_000 }, }, ), ).toBeUndefined(); }); - it("suppresses a 5-hour transition when only weekly usage drops", () => { + it("requires the prior weekly reset deadline to establish that a reset was unscheduled", () => { expect( detectCodexResetFireworks( { - fiveHour: { percent: 42, resetMinutes: 1 }, - sevenDay: { percent: 18, resetHours: 80 }, + observedAt: 1_000, + sevenDay: { percent: 42 }, }, { - fiveHour: { percent: 0, resetMinutes: 300 }, - sevenDay: { percent: 0, resetHours: 80 }, + observedAt: 2_000, + sevenDay: { percent: 0 }, }, ), ).toBeUndefined(); @@ -139,7 +139,7 @@ describe("Codex reset fireworks", () => { it("renders distinct copy for usage-window and saved-reset celebrations", () => { const usage = makeHost(); const usageController = new CodexResetFireworksController(usage.host); - usageController.show({ kind: "usage-window-reset" }); + usageController.show({ kind: "unscheduled-weekly-reset" }); const usageText = usage.shown[0]?.render(80).map(stripVTControlCharacters).join("\n") ?? ""; const saved = makeHost(); @@ -147,11 +147,11 @@ describe("Codex reset fireworks", () => { savedController.show({ kind: "saved-reset-banked", added: 1, available: 3 }); const savedText = saved.shown[0]?.render(80).map(stripVTControlCharacters).join("\n") ?? ""; - expect(usageText).toContain("C O D E X R E S E T"); - expect(usageText).toContain("5-hour window: 0% used"); + expect(usageText).toContain("O P E N A I R E S E T"); + expect(usageText).toContain("Weekly usage cleared early"); expect(savedText).toContain("S A V E D R E S E T"); expect(savedText).toContain("New reset banked · 3 available"); - expect(savedText).not.toContain("5-hour window"); + expect(savedText).not.toContain("Weekly usage cleared early"); usageController.dispose(); savedController.dispose(); @@ -160,7 +160,7 @@ describe("Codex reset fireworks", () => { it("holds a top-third modal until Escape and ignores overlapping celebrations", async () => { const fake = makeHost(24); const controller = new CodexResetFireworksController(fake.host); - expect(controller.show({ kind: "usage-window-reset" })).toBe(true); + expect(controller.show({ kind: "unscheduled-weekly-reset" })).toBe(true); expect(controller.show({ kind: "saved-reset-banked", added: 1, available: 1 })).toBe(false); expect(fake.shown).toHaveLength(1); expect(fake.focused).toEqual(fake.shown); diff --git a/packages/coding-agent/test/status-line-usage-refresh.test.ts b/packages/coding-agent/test/status-line-usage-refresh.test.ts index 9018ab7b4..5af6e49a4 100644 --- a/packages/coding-agent/test/status-line-usage-refresh.test.ts +++ b/packages/coding-agent/test/status-line-usage-refresh.test.ts @@ -59,10 +59,8 @@ function usageReport(percent: number): unknown[] { } interface CodexUsageState { - fiveHourPercent: number; - fiveHourResetMinutes: number; sevenDayPercent: number; - sevenDayResetHours: number; + sevenDayResetAt: number; savedResets?: number; } @@ -75,17 +73,6 @@ function codexUsageReport(state: CodexUsageState): unknown[] { metadata: { accountId, email: "codex@example.com" }, ...(state.savedResets === undefined ? {} : { resetCredits: { availableCount: state.savedResets } }), limits: [ - { - id: "openai-codex:primary", - label: "Codex 5 Hour", - scope: { provider: "openai-codex", accountId, windowId: "5h" }, - window: { - id: "5h", - label: "5h", - resetsAt: Date.now() + state.fiveHourResetMinutes * 60_000, - }, - amount: { unit: "percent", usedFraction: state.fiveHourPercent / 100 }, - }, { id: "openai-codex:secondary", label: "Codex 7 Day", @@ -93,7 +80,7 @@ function codexUsageReport(state: CodexUsageState): unknown[] { window: { id: "7d", label: "7d", - resetsAt: Date.now() + state.sevenDayResetHours * 3_600_000, + resetsAt: state.sevenDayResetAt, }, amount: { unit: "percent", usedFraction: state.sevenDayPercent / 100 }, }, @@ -273,11 +260,10 @@ describe("StatusLineComponent usage refresh", () => { }); it("keeps reset fireworks opt-in while advancing the disabled baseline", async () => { + const sevenDayResetAt = Date.now() + 80 * 3_600_000; let state: CodexUsageState = { - fiveHourPercent: 42, - fiveHourResetMinutes: 1, - sevenDayPercent: 18, - sevenDayResetHours: 80, + sevenDayPercent: 42, + sevenDayResetAt, savedResets: 0, }; const component = new StatusLineComponent(makeCodexSession(async () => codexUsageReport(state))); @@ -287,10 +273,8 @@ describe("StatusLineComponent usage refresh", () => { expect(Settings.instance.get("tui.codexResetFireworks")).toBe(false); await refreshUsage(component); state = { - fiveHourPercent: 0, - fiveHourResetMinutes: 300, - sevenDayPercent: 18.2, - sevenDayResetHours: 80, + sevenDayPercent: 0, + sevenDayResetAt, savedResets: 0, }; await refreshUsage(component, 5 * 60_000); @@ -298,13 +282,12 @@ describe("StatusLineComponent usage refresh", () => { component.dispose(); }); - it("emits distinct enabled events for a 5-hour reset and a newly banked reset", async () => { + it("emits distinct enabled events for an unscheduled weekly reset and a newly banked reset", async () => { Settings.instance.set("tui.codexResetFireworks", true); + const sevenDayResetAt = Date.now() + 80 * 3_600_000; let state: CodexUsageState = { - fiveHourPercent: 42, - fiveHourResetMinutes: 1, - sevenDayPercent: 18, - sevenDayResetHours: 80, + sevenDayPercent: 42, + sevenDayResetAt, savedResets: 0, }; const component = new StatusLineComponent(makeCodexSession(async () => codexUsageReport(state))); @@ -314,41 +297,37 @@ describe("StatusLineComponent usage refresh", () => { await refreshUsage(component); expect(events).toEqual([]); state = { - fiveHourPercent: 0, - fiveHourResetMinutes: 300, - sevenDayPercent: 18.2, - sevenDayResetHours: 80, + sevenDayPercent: 0, + sevenDayResetAt, savedResets: 0, }; await refreshUsage(component, 5 * 60_000); state = { - fiveHourPercent: 25, - fiveHourResetMinutes: 1, - sevenDayPercent: 18.4, - sevenDayResetHours: 80, + sevenDayPercent: 25, + sevenDayResetAt, savedResets: 0, }; await refreshUsage(component, 5 * 60_000); state = { - fiveHourPercent: 0, - fiveHourResetMinutes: 300, - sevenDayPercent: 18.6, - sevenDayResetHours: 80, + sevenDayPercent: 25.2, + sevenDayResetAt, savedResets: 1, }; await refreshUsage(component, 5 * 60_000); - expect(events).toEqual([{ kind: "usage-window-reset" }, { kind: "saved-reset-banked", added: 1, available: 1 }]); + expect(events).toEqual([ + { kind: "unscheduled-weekly-reset" }, + { kind: "saved-reset-banked", added: 1, available: 1 }, + ]); component.dispose(); }); it("keeps an unavailable saved-reset count unknown across refreshes", async () => { Settings.instance.set("tui.codexResetFireworks", true); + const sevenDayResetAt = Date.now() + 80 * 3_600_000; let state: CodexUsageState = { - fiveHourPercent: 25, - fiveHourResetMinutes: 300, sevenDayPercent: 18, - sevenDayResetHours: 80, + sevenDayResetAt, savedResets: 1, }; const component = new StatusLineComponent(makeCodexSession(async () => codexUsageReport(state))); @@ -357,10 +336,8 @@ describe("StatusLineComponent usage refresh", () => { await refreshUsage(component); state = { - fiveHourPercent: 25, - fiveHourResetMinutes: 295, sevenDayPercent: 18.1, - sevenDayResetHours: 80, + sevenDayResetAt, }; await refreshUsage(component, 5 * 60_000); state = { ...state, savedResets: 1 }; @@ -373,11 +350,10 @@ describe("StatusLineComponent usage refresh", () => { it("discards a timed-out report after a newer refresh applies", async () => { Settings.instance.set("tui.codexResetFireworks", true); const stale = Promise.withResolvers(); + const sevenDayResetAt = Date.now() + 80 * 3_600_000; const current: CodexUsageState = { - fiveHourPercent: 0, - fiveHourResetMinutes: 300, - sevenDayPercent: 18.2, - sevenDayResetHours: 80, + sevenDayPercent: 0, + sevenDayResetAt, savedResets: 0, }; let calls = 0; @@ -400,10 +376,8 @@ describe("StatusLineComponent usage refresh", () => { stale.resolve( codexUsageReport({ - fiveHourPercent: 42, - fiveHourResetMinutes: 1, - sevenDayPercent: 18, - sevenDayResetHours: 80, + sevenDayPercent: 42, + sevenDayResetAt, savedResets: 1, }), );