Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions lib/tui-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const variantSuffixes: ReasoningVariant[] = [
"none",
];
const STATUS_SEPARATOR = ` ${String.fromCharCode(183)} `;
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const WARNING_LIMIT_LEFT_PERCENT = 25;
const DANGER_LIMIT_LEFT_PERCENT = 10;
const MASKED_EMAIL = "*****";
Expand Down Expand Up @@ -301,12 +302,21 @@ function formatQuota(quota: CompactQuotaStatus): string | undefined {
return undefined;
}

/**
* Return the character budget for the prompt status line at a given terminal
* width. Budgets scale to about 54% of each tier's minimum width so the
* day-context reset labels and typical account hints fit; `undefined` or
* non-finite widths fall back to the 78-column tier budget.
*/
function maxStatusChars(width: number | undefined): number {
if (!width || !Number.isFinite(width)) return 32;
if (width >= 120) return 48;
if (width >= 96) return 40;
if (width >= 78) return 32;
if (width >= 60) return 22;
// Budgets are ~54% of each tier's minimum width (up from ~40%): the
// day-context reset labels and typical account hints no longer fit at
// 40%, which degraded informative candidates on mid-width terminals.
if (!width || !Number.isFinite(width)) return 42;
if (width >= 120) return 64;
if (width >= 96) return 52;
if (width >= 78) return 42;
if (width >= 60) return 32;
return 12;
}

Expand Down Expand Up @@ -395,17 +405,59 @@ function formatReset(resetAtMs: number | undefined): string | undefined {
return `${time} on ${day}`;
}

/**
* Format a reset timestamp for the compact status line. Same-day resets keep
* the time only (`02:25`); resets within the coming week add the weekday
* (`Tue 02:25`); later resets use the absolute date (`Sep 15 02:25`). The
* time is always kept so short windows such as the 5h limit stay meaningful.
*/
function formatResetTime(resetAtMs: number | undefined): string | undefined {
if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0) {
return undefined;
}
const date = new Date(resetAtMs);
if (!Number.isFinite(date.getTime())) return undefined;
return date.toLocaleTimeString(undefined, {
const time = date.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
const now = new Date();
const sameDay =
now.getFullYear() === date.getFullYear() &&
now.getMonth() === date.getMonth() &&
now.getDate() === date.getDate();
if (sameDay) return time;
// Compare calendar dates, not elapsed ms: a DST transition makes a
// seven-calendar-day gap span 167 or 169 hours, which a fixed 24h
// division would misclassify and repeat today's weekday.
const dayDiff = calendarDayDiff(now, date);
// Within a week each weekday occurs exactly once, so the weekday alone
// disambiguates weekly windows; beyond that the absolute date does.
if (dayDiff > 0 && dayDiff < 7) {
const weekday = date.toLocaleDateString(undefined, { weekday: "short" });
return `${weekday} ${time}`;
}
const day = date.toLocaleDateString(undefined, {
month: "short",
day: "2-digit",
});
return `${day} ${time}`;
}

/**
* Count calendar days between two dates, ignoring wall-clock length. Comparing
* UTC-normalized year/month/day makes the count immune to DST transitions,
* which make a seven-day gap span 167 or 169 hours.
*/
function calendarDayDiff(from: Date, to: Date): number {
const fromDay = Date.UTC(
from.getFullYear(),
from.getMonth(),
from.getDate(),
);
const toDay = Date.UTC(to.getFullYear(), to.getMonth(), to.getDate());
return Math.round((toDay - fromDay) / MS_PER_DAY);
}

function formatUpdatedAge(fetchedAt: number | undefined, now: number): string {
Expand Down
108 changes: 98 additions & 10 deletions test/tui-status.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
formatPromptStatusText,
Expand All @@ -10,16 +10,17 @@ import {
type PromptStatusMessage,
} from "../lib/tui-status.js";

const sep = ` ${String.fromCharCode(183)} `;
const quota: CompactQuotaStatus = {
type: "ready",
limits: [
{ label: "5h", leftPercent: 88 },
{ label: "7d", leftPercent: 83 },
],
stale: false,
};

describe("TUI prompt status helpers", () => {
const sep = ` ${String.fromCharCode(183)} `;
const quota: CompactQuotaStatus = {
type: "ready",
limits: [
{ label: "5h", leftPercent: 88 },
{ label: "7d", leftPercent: 83 },
],
stale: false,
};

it("formats prompt status text from supplied quota labels", () => {
expect(
Expand Down Expand Up @@ -335,3 +336,90 @@ describe("TUI prompt status helpers", () => {
expect(resolvePromptReasoningVariant({ config })).toBe("xhigh");
});
});

describe("formatResetTime day context", () => {
// Fake timers freeze both Date.now() and new Date() so the formatter's
// "now" and the fixed reset timestamps land on deterministic dates.
const now = new Date(2026, 8, 5, 12, 0); // 2026-09-05T12:00 local
// Expected labels are derived from the runtime's own Intl formatting so
// the assertions hold under any default locale.
const weekdayLabel = new Date(2026, 8, 8, 2, 25).toLocaleDateString(
undefined,
{ weekday: "short" },
);
const dateLabel = new Date(2026, 8, 15, 2, 25).toLocaleDateString(undefined, {
month: "short",
day: "2-digit",
});
const timeLabel = (h: number, m: number) =>
new Date(2026, 8, 5, h, m)
.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
.replace(/^24/, "00");

beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(now);
});
afterEach(() => {
vi.useRealTimers();
});

it("keeps time-only format for same-day resets", () => {
const out = formatPromptStatusText({
quota: {
...quota,
limits: [
{ label: "5h", leftPercent: 8, resetAtMs: new Date(2026, 8, 5, 18, 30).getTime() },
],
},
width: 120,
});
expect(out).toBe(`5h 8% resets ${timeLabel(18, 30)}`);
});

it("adds weekday for resets within the coming week", () => {
const out = formatPromptStatusText({
quota: {
...quota,
limits: [
{ label: "7d", leftPercent: 0, resetAtMs: new Date(2026, 8, 8, 2, 25).getTime() },
],
},
width: 120,
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
expect(out).toBe(`7d 0% resets ${weekdayLabel} ${timeLabel(2, 25)}`);
});

it("uses absolute date beyond a week", () => {
const out = formatPromptStatusText({
quota: {
...quota,
limits: [
{ label: "7d", leftPercent: 0, resetAtMs: new Date(2026, 8, 15, 2, 25).getTime() },
],
},
width: 120,
});
expect(out).toBe(`7d 0% resets ${dateLabel} ${timeLabel(2, 25)}`);
});

it("uses absolute date at exactly seven calendar days across DST (America/New_York)", () => {
// 2026-03-02 -> 2026-03-09 is seven calendar days but 167 hours in
// America/New_York; a millisecond division would misread it as six.
vi.setSystemTime(new Date(2026, 2, 2, 12, 0));
const reset = new Date(2026, 2, 9, 2, 25);
const expected = `${reset.toLocaleDateString("en-US", { month: "short", day: "2-digit" })} ${reset.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false })}`;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
const out = formatPromptStatusText({
quota: {
...quota,
limits: [{ label: "7d", leftPercent: 0, resetAtMs: reset.getTime() }],
},
width: 120,
});
expect(out).toBe(`7d 0% resets ${expected}`);
});
});