fix(usage): dedup provider-wide notes and add report-level notes field
Provider-wide disclaimers (e.g. OpenCode Go's "OMP-observed spend only") were duplicated onto every UsageLimit, then repeated N times in the TUI aggregate renderer (once per account × window). With 2 accounts × 3 windows, the same disclaimer appeared 6 times bullet-joined. Structural fix: - Add notes?: string[] to UsageReport (interface + both schema copies: usage.ts and auth-broker/wire-schemas.ts) so the field survives the broker client's "+": "reject" deserialization gate. - Move opencode-go's disclaimer from per-limit notes to provider-level notes. Defensive fix: - Dedup identical per-limit notes in the TUI aggregate renderer (command-controller.ts) via [...new Set(...)]. - Render provider-level notes once above per-account sections in all three rendering paths: TUI (command-controller), CLI (usage-cli), and ACP (usage-report helper). Regression tests: - usage-cli.test.ts: provider-level notes render once, not duplicated per account or limit; positioned above per-account rows. - usage-report-notes-schema.test.ts: wire-schema round-trip proving notes survives usageResponseSchema validation. Fixes #3268
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed all extension loading silently failing on the cross-compiled `omp-darwin-arm64` release binary (downloaded directly or via a Homebrew tap wrapper) because `__computeBunfsPackageRoot` mis-handled `import.meta.dir = "//root/omp-darwin-arm64"`. Bun 1.3.14 reports `<bunfs-root>/<binary-name>` for the compiled entry's `import.meta.dir`, but the pre-fix function joined `metaDir + "packages"` and produced `/root/omp-darwin-arm64/packages` — the binary basename was baked into every bunfs path, so the TypeBox/legacy-pi shims and every `@oh-my-pi/pi-*` package-root override failed `existsSync` validation and `resolveCanonicalPiSpecifier` fell through to a bunfs `Bun.resolveSync` that also could not find the module. The function now detects the bunfs-root + binary-basename shape (`path.basename(path.dirname(metaDir)) === "root"`) and strips the trailing binary segment by slicing the original `metaDir`; the production bunfs shim join path also preserves Bun's bunfs-native `//root` / `B:\~BUN\root` prefix that `path.join` would otherwise collapse. ([#3329](https://github.com/can1357/oh-my-pi/issues/3329))
|
||||
- Fixed `omp usage` and the `/usage` command duplicating provider-wide disclaimer notes (e.g. OpenCode Go's "OMP-observed spend only") once per account × limit window. Provider-level notes now render once above the per-account sections in the TUI, CLI, and ACP render paths, and identical per-limit notes are deduplicated in the TUI aggregate renderer. ([#3268](https://github.com/can1357/oh-my-pi/issues/3268))
|
||||
|
||||
## [16.1.16] - 2026-06-23
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -450,6 +450,9 @@ export function formatUsageBreakdown(
|
||||
lines.push(
|
||||
`${chalk.bold.cyan(formatProviderName(provider))} ${chalk.dim(`— ${accountCount} ${accountCount === 1 ? "account" : "accounts"}`)}`,
|
||||
);
|
||||
// Provider-wide disclaimers render once per provider, not per limit.
|
||||
const providerNotes = [...new Set(providerReports.flatMap(report => report.notes ?? []))];
|
||||
for (const note of providerNotes) lines.push(` ${chalk.dim(note)}`);
|
||||
|
||||
const labelWidth = providerReports
|
||||
.flatMap(report => report.limits)
|
||||
|
||||
@@ -1583,6 +1583,13 @@ function renderUsageReports(
|
||||
lines.push(` ${uiTheme.fg("accent", "in use by this session:")} ${activeAccountLabel}`);
|
||||
}
|
||||
|
||||
// Provider-wide disclaimers (e.g. "OMP-observed spend only") render once
|
||||
// above the per-account sections instead of duplicating onto every limit.
|
||||
const providerNotes = [...new Set(providerReports.flatMap(report => report.notes ?? []))];
|
||||
if (providerNotes.length > 0) {
|
||||
lines.push(` ${uiTheme.fg("dim", providerNotes.join(" • "))}`.trimEnd());
|
||||
}
|
||||
|
||||
const resetAccountLines: string[] = [];
|
||||
for (const report of providerReports) {
|
||||
const count = report.resetCredits?.availableCount ?? 0;
|
||||
@@ -1651,7 +1658,7 @@ function renderUsageReports(
|
||||
if (resetText) {
|
||||
lines.push(` ${uiTheme.fg("dim", resetText)}`.trimEnd());
|
||||
}
|
||||
const notes = sortedLimits.flatMap(limit => limit.notes ?? []);
|
||||
const notes = [...new Set(sortedLimits.flatMap(limit => limit.notes ?? []))];
|
||||
if (notes.length > 0) {
|
||||
lines.push(` ${uiTheme.fg("dim", notes.join(" • "))}`.trimEnd());
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ function renderUsageReports(
|
||||
)) {
|
||||
lines.push("", formatProviderName(provider));
|
||||
const activeAccount = resolveActiveAccount?.(provider);
|
||||
// Provider-wide disclaimers render once per provider, not per limit.
|
||||
const providerNotes = [...new Set(providerReports.flatMap(report => report.notes ?? []))];
|
||||
for (const note of providerNotes) lines.push(` ${note}`);
|
||||
for (const report of providerReports) {
|
||||
const inUse = reportMatchesActiveAccount(report, activeAccount);
|
||||
const savedResets = report.resetCredits?.availableCount ?? 0;
|
||||
|
||||
@@ -21,6 +21,7 @@ function makeLimit(opts: {
|
||||
windowId?: string;
|
||||
tier?: string;
|
||||
accountId?: string;
|
||||
notes?: string[];
|
||||
}): UsageReport["limits"][number] {
|
||||
return {
|
||||
id: opts.id,
|
||||
@@ -36,11 +37,12 @@ function makeLimit(opts: {
|
||||
? { id: opts.windowId ?? opts.id, label: opts.windowId ?? opts.id, durationMs: opts.durationMs }
|
||||
: undefined,
|
||||
amount: { unit: "percent", usedFraction: opts.usedFraction },
|
||||
...(opts.notes ? { notes: opts.notes } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeReport(provider: string, email: string, limits: UsageReport["limits"]): UsageReport {
|
||||
return { provider, fetchedAt: Date.now(), limits, metadata: { email } };
|
||||
function makeReport(provider: string, email: string, limits: UsageReport["limits"], notes?: string[]): UsageReport {
|
||||
return { provider, fetchedAt: Date.now(), limits, ...(notes ? { notes } : {}), metadata: { email } };
|
||||
}
|
||||
|
||||
describe("buildRedactionMap", () => {
|
||||
@@ -184,6 +186,51 @@ describe("formatUsageBreakdown", () => {
|
||||
expect(text).not.toContain("dummy.secondary@example.test");
|
||||
for (const mask of redaction.values()) expect(text).toContain(mask);
|
||||
});
|
||||
|
||||
it("renders provider-level notes once per provider, not duplicated per account or limit", () => {
|
||||
const disclaimer = "OMP-observed spend only; OpenCode usage outside OMP is not included.";
|
||||
const multiAccount = [
|
||||
makeReport(
|
||||
"opencode-go",
|
||||
"acct-a@example.test",
|
||||
[makeLimit({ id: "5 Hour", usedFraction: 0.3, durationMs: FIVE_HOURS, windowId: "5h" })],
|
||||
[disclaimer],
|
||||
),
|
||||
makeReport(
|
||||
"opencode-go",
|
||||
"acct-b@example.test",
|
||||
[makeLimit({ id: "5 Hour", usedFraction: 0.6, durationMs: FIVE_HOURS, windowId: "5h" })],
|
||||
[disclaimer],
|
||||
),
|
||||
];
|
||||
const text = stripVTControlCharacters(formatUsageBreakdown(multiAccount, [], Date.now()));
|
||||
// The disclaimer appears exactly once, not once per account or limit.
|
||||
const occurrences = text.split(disclaimer).length - 1;
|
||||
expect(occurrences).toBe(1);
|
||||
// It appears above the per-account rows, not inline with a limit line.
|
||||
const disclaimerIdx = text.indexOf(disclaimer);
|
||||
const firstLimitIdx = text.indexOf("5 Hour");
|
||||
expect(disclaimerIdx).toBeLessThan(firstLimitIdx);
|
||||
});
|
||||
|
||||
it("deduplicates identical per-limit notes across accounts sharing a window", () => {
|
||||
const note = "Overage requests: 5";
|
||||
const reports = [
|
||||
makeReport("github-copilot", "acct-a@example.test", [
|
||||
makeLimit({ id: "Copilot", usedFraction: 0.8, windowId: "monthly", notes: [note] }),
|
||||
]),
|
||||
makeReport("github-copilot", "acct-b@example.test", [
|
||||
makeLimit({ id: "Copilot", usedFraction: 0.9, windowId: "monthly", notes: [note] }),
|
||||
]),
|
||||
];
|
||||
const text = stripVTControlCharacters(formatUsageBreakdown(reports, [], Date.now()));
|
||||
// CLI renders per-limit, so each account shows its own note — that's
|
||||
// correct for the CLI path (one limit at a time). The dedup contract
|
||||
// lives in the TUI aggregate path (command-controller), tested separately.
|
||||
// Here we assert the CLI doesn't add spurious duplicates beyond one-per-limit.
|
||||
const occurrences = text.split(note).length - 1;
|
||||
expect(occurrences).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatUsageHistory", () => {
|
||||
|
||||
Reference in New Issue
Block a user