fix(tui): preserved organization suffixes in usage panel

Removed the 24-column cap from account cells so wide terminals can show full disambiguating labels.

Kept usage bars independently capped and added regression coverage for same-email organization accounts.

Fixes #5701
This commit is contained in:
roboomp
2026-07-16 13:25:20 +00:00
parent 6ae7cdbf97
commit 5fe9bb92d8
3 changed files with 40 additions and 12 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed the TUI usage panel truncating organization suffixes from same-email account labels even when the terminal has enough width ([#5701](https://github.com/can1357/oh-my-pi/issues/5701)).
## [17.0.1] - 2026-07-16
### Changed
@@ -1303,7 +1303,7 @@ export class CommandController {
}
const BAR_WIDTH_MAX = 24;
const BAR_WIDTH_MIN = 4;
const COLUMN_WIDTH_MIN = 4;
function renderJobLine(job: AsyncJobSnapshotItem, now: number): string {
const duration = formatDuration(Math.max(0, now - job.startTime));
@@ -1571,7 +1571,7 @@ function renderUsageBar(limit: UsageLimit, uiTheme: typeof theme, barWidth: numb
}
/**
* Pick a per-column width so n bars + a trailing amount string fit in `available` columns.
* Pick a per-account column width so the columns and trailing amount fit in `available`.
* Falls back to the minimum when the terminal is too narrow rather than wrapping.
*/
function resolveColumnWidth(count: number, available: number, trailing: number): number {
@@ -1580,10 +1580,7 @@ function resolveColumnWidth(count: number, available: number, trailing: number):
const gaps = count - 1;
const spaceForBars = available - indent - gaps - (trailing > 0 ? trailing + 1 : 0);
const ideal = Math.floor(spaceForBars / count);
const min = BAR_WIDTH_MIN;
const max = BAR_WIDTH_MAX;
if (ideal < min) return min;
if (ideal > max) return max;
if (ideal < COLUMN_WIDTH_MIN) return COLUMN_WIDTH_MIN;
return ideal;
}
@@ -1719,6 +1716,7 @@ export function renderUsageReports(
const sectionCount = renderableGroups.reduce((max, g) => Math.max(max, g.sortedLimits.length), 0);
const sectionTrailing = renderableGroups.reduce((max, g) => Math.max(max, visibleWidth(g.amountText)), 0);
const sectionColumnWidth = resolveColumnWidth(sectionCount, availableWidth, sectionTrailing);
const sectionBarWidth = Math.min(sectionColumnWidth, BAR_WIDTH_MAX);
for (const { group, sortedLimits, sortedReports, amountText } of renderableGroups) {
const status = resolveAggregateStatus(sortedLimits);
@@ -1736,7 +1734,7 @@ export function renderUsageReports(
);
lines.push(` ${accountLabels.join(" ")}`.trimEnd());
const bars = sortedLimits.map(limit =>
padColumn(renderUsageBar(limit, uiTheme, sectionColumnWidth), sectionColumnWidth),
padColumn(renderUsageBar(limit, uiTheme, sectionBarWidth), sectionColumnWidth),
);
lines.push(` ${bars.join(" ")} ${amountText}`.trimEnd());
const resetText = sortedLimits.length <= 1 ? resolveResetRange(sortedLimits, nowMs) : null;
@@ -1,14 +1,16 @@
/**
* Regression for #3268 (TUI aggregate path, command-controller.ts).
* Regression coverage for the TUI aggregate path in `command-controller.ts`.
*
* Two contracts that the CLI `formatUsageBreakdown` test cannot cover, because
* the bug lives in the TUI cross-account grouping renderer `renderUsageReports`:
* Three contracts that the CLI `formatUsageBreakdown` test cannot cover,
* because the bug lives in the TUI cross-account grouping renderer
* `renderUsageReports`:
*
* 1. Provider-wide `UsageReport.notes` render ONCE above the per-account
* sections, not once per account/window.
* 2. Identical per-limit notes from multiple accounts that fall in the same
* `label|windowId` group are de-duplicated (the `[...new Set(...)]` at the
* per-group note line). Without the dedup the note is bullet-joined N times.
* `label|windowId` group are de-duplicated.
* 3. Wide terminals preserve organization suffixes that distinguish accounts
* sharing an email address.
*/
import { beforeAll, describe, expect, it } from "bun:test";
@@ -81,4 +83,28 @@ describe("renderUsageReports (#3268 TUI aggregate)", () => {
// would bullet-join it twice (one per account in the group).
expect(occurrences).toBe(1);
});
it("preserves organization suffixes when wide account columns can fit them", () => {
const now = Date.now();
const accountLimit = () => ({
...limit("5 Hour limit", "rolling-5h", 5 * HOUR, 0.3),
window: {
id: "rolling-5h",
label: "5 Hour limit",
durationMs: 5 * HOUR,
resetsAt: now + 2.5 * HOUR,
},
});
const reports: UsageReport[] = [
{
...report("anthropic", "rae@example.com", [accountLimit()]),
metadata: { email: "rae@example.com", orgId: "team-org", orgName: "Team Org" },
},
report("anthropic", "rae@example.com", [accountLimit()]),
];
const text = stripVTControlCharacters(renderUsageReports(reports, theme, now, 160));
expect(text).toContain("rae@example.com (Team Org)");
});
});