Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
31 changes: 31 additions & 0 deletions apps/cli/src/agent/lody-acp-extension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest';
import { LODY_EXTENSION_METHODS } from 'acp-extension-core';
import { parseRateLimitsSnapshot, parseLodyExtensionMessage } from './lody-acp-extension';

describe('rate-limit window labels', () => {
const window = { usedPercent: 0, windowDurationSeconds: 604_800, resetsAtEpochSeconds: null };
const snapshot = {
rateLimits: [
{
limitId: 'claude',
scope: { providerId: 'claude' },
windows: [window, { ...window, label: 'Fable' }],
},
],
};

it('preserves same-duration labeled windows in query responses', () => {
expect(parseRateLimitsSnapshot(snapshot)).toEqual(snapshot);
});

it('preserves labels in proactive updates', () => {
expect(
parseLodyExtensionMessage({
method: LODY_EXTENSION_METHODS.rateLimitsUpdate,
params: snapshot,
provider: 'claude',
sessionId: 'synthetic-session',
})
).toEqual({ type: 'rateLimits', snapshot });
});
});
1 change: 1 addition & 0 deletions apps/cli/src/agent/lody-acp-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const SessionUsageUpdateSchema = z.object({
});

const RateLimitWindowSchema = z.object({
label: z.string().optional(),
usedPercent: z.number().min(0).max(100),
windowDurationSeconds: z.number().nonnegative().nullable(),
resetsAtEpochSeconds: z.number().int().positive().nullable(),
Expand Down
1 change: 1 addition & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@
"machines.rateLimits.resetUnknown": "Reset time unknown",
"machines.rateLimits.resetsAt": "Resets {{time}}",
"machines.rateLimits.sevenDay": "7 days usage",
"machines.rateLimits.namedWindow": "{{duration}} · {{label}}",
"machines.rateLimits.unlabeled": "Unlabeled limit",
"machines.rateLimits.usedPercent": "{{percent}}% used",
"machines.resourceStatus": "Resource Status",
Expand Down
1 change: 1 addition & 0 deletions locales/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@
"machines.rateLimits.resetUnknown": "重置时间未知",
"machines.rateLimits.resetsAt": "重置于{{time}}",
"machines.rateLimits.sevenDay": "7 天用量",
"machines.rateLimits.namedWindow": "{{duration}} · {{label}}",
"machines.rateLimits.unlabeled": "未命名额度",
"machines.rateLimits.usedPercent": "已使用 {{percent}}%",
"machines.resourceStatus": "资源状态",
Expand Down
2 changes: 1 addition & 1 deletion packages/acp-extension-claude
2 changes: 1 addition & 1 deletion packages/acp-extension-core
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { cn } from '@/lib/utils';
import {
FIVE_HOUR_WINDOW_SECONDS,
SEVEN_DAY_WINDOW_SECONDS,
formatAgentRateLimitWindowLabel,
formatRateLimitWindowShortLabel,
getAgentRateLimitWindows,
getContextWindowUsageData,
Expand Down Expand Up @@ -185,7 +186,11 @@ export const SessionUsagePopover = memo(function SessionUsagePopover({
rateLimitWindows.map((window, index) => (
<UsageMeter
key={`${window.windowDurationSeconds ?? 'unknown'}-${index}`}
label={formatWindowLabel(window.windowDurationSeconds)}
label={formatAgentRateLimitWindowLabel(
window,
formatWindowLabel(window.windowDurationSeconds),
t
)}
value={window.usedPercent}
detail={formatReset(window.resetsAtEpochSeconds)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { cn } from '@/lib/utils';
import {
FIVE_HOUR_WINDOW_SECONDS,
SEVEN_DAY_WINDOW_SECONDS,
formatAgentRateLimitWindowLabel,
formatRateLimitWindowShortLabel,
getAgentRateLimitWindows,
} from '@/lib/session-usage';
Expand Down Expand Up @@ -121,8 +122,8 @@ export function MachineQuotaCompact({ raceLimits, filterCliType }: MachineQuotaC
return (
<UsageQuotaWindow
key={`${window.windowDurationSeconds ?? 'unknown'}-${index}`}
shortLabel={shortLabel}
fullLabel={fullLabel}
shortLabel={formatAgentRateLimitWindowLabel(window, shortLabel, t)}
fullLabel={formatAgentRateLimitWindowLabel(window, fullLabel, t)}
percent={window.usedPercent}
resetText={formatResetDistance(window.resetsAtEpochSeconds)}
disabled={false}
Expand Down
7 changes: 6 additions & 1 deletion packages/components/src/components/settings/provider-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { CodexResetForecastChip } from '@/components/codex-reset/codex-reset-for
import { canShowCodexResetForecast } from '@/lib/codex-reset-forecast';
import {
canShowSubscriptionRateLimits,
formatAgentRateLimitWindowLabel,
formatRateLimitWindowShortLabel,
getAgentRateLimitEntries,
getAgentRateLimitWindows,
Expand Down Expand Up @@ -187,7 +188,11 @@ export function ProviderRow({
{rateLimitWindows.map((window, index) => (
<RateLimitMeter
key={`${window.windowDurationSeconds ?? 'unknown'}-${index}`}
label={formatRateLimitWindowShortLabel(window.windowDurationSeconds)}
label={formatAgentRateLimitWindowLabel(
window,
formatRateLimitWindowShortLabel(window.windowDurationSeconds),
t
)}
Comment on lines +191 to +195

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve quota labels in the narrow provider layout

When the provider card is below the @sm container breakpoint, this changed block is hidden by hidden ... @sm:flex, while the @sm:hidden fallback later in the same component still uses only formatRateLimitWindowShortLabel. Consequently, Claude's shared weekly and Fable weekly meters both appear as 7d on mobile and narrow settings panels, making the new quota indistinguishable; apply formatAgentRateLimitWindowLabel in that fallback as well.

Useful? React with 👍 / 👎.

remainingPercent={window.remainingPercent}
/>
))}
Expand Down
5 changes: 5 additions & 0 deletions packages/components/src/lib/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ the href parser. It needs a third kind that strips roots without decoding.

## ACP dispatch

Rate-limit window labels are provider-supplied scope names, displayed alongside
localized durations through `formatAgentRateLimitWindowLabel`. Preserve every
window even when durations, utilization, and resets match: a model weekly
sub-cap and the shared weekly pool are concurrent constraints, not alternatives.

Before creating a top-level or child session, call
`filterAcpSessionConfigOptionValues()` so cached values outside the current
selector schema are not dispatched or persisted again.
Expand Down
16 changes: 16 additions & 0 deletions packages/components/src/lib/session-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type SessionContextWindowUsage,
} from '@lody/shared';
import { clamp } from './clamp';
import type { TFunction } from 'i18next';

export const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
export const SEVEN_DAY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
Expand All @@ -32,6 +33,7 @@ export type ContextWindowUsageData = {
};

export type AgentRateLimitWindow = {
label?: string;
usedPercent: number;
remainingPercent: number;
windowDurationSeconds: number | null;
Expand All @@ -56,6 +58,7 @@ export function getAgentRateLimitWindows(limits: MachineRateLimitUsage): AgentRa
if (usedPercent === null) return [];
return [
{
...(window.label?.trim() ? { label: window.label.trim() } : {}),
usedPercent,
remainingPercent: clampPercentage(100 - usedPercent),
windowDurationSeconds:
Expand All @@ -79,6 +82,19 @@ export function formatRateLimitWindowShortLabel(windowDurationSeconds: number |
return `${windowDurationSeconds / 60}m`;
}

export function formatAgentRateLimitWindowLabel(
window: AgentRateLimitWindow,
durationLabel: string,
t: TFunction
): string {
return window.label
? t('machines.rateLimits.namedWindow', '{{duration}} · {{label}}', {
duration: durationLabel,
label: window.label,
})
: durationLabel;
}

export function getContextWindowUsageData(
usage: SessionContextWindowUsage | null | undefined
): ContextWindowUsageData | null {
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/stories/ProviderRow.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ export const ClaudeWithRateLimit: Story = {
windowDurationSeconds: 604_800,
resetsAtEpochSeconds: resetIn(86_400),
},
{
label: 'Fable',
usedPercent: 67,
windowDurationSeconds: 604_800,
resetsAtEpochSeconds: resetIn(86_400),
},
],
},
},
Expand Down
30 changes: 30 additions & 0 deletions packages/components/src/stories/SessionUsagePopover.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,33 @@ export const Unavailable: Story = {
rateLimits: {},
},
};

export const ClaudeFableWeekly: Story = {
args: {
agentType: 'claude',
modelId: 'claude-fable-5',
modelLabel: 'Fable',
showRateLimitWithoutContext: true,
rateLimits: {
[getRateLimitEntryKey('claude', 'claude')]: {
limitId: 'claude',
scope: { providerId: 'claude' },
planName: 'Max',
windows: [
{ usedPercent: 12, windowDurationSeconds: 18_000, resetsAtEpochSeconds: resetIn(3600) },
{
usedPercent: 30,
windowDurationSeconds: 604_800,
resetsAtEpochSeconds: resetIn(172800),
},
{
label: 'Fable',
usedPercent: 67,
windowDurationSeconds: 604_800,
resetsAtEpochSeconds: resetIn(172800),
},
],
},
},
},
};
33 changes: 33 additions & 0 deletions packages/components/tests/session-usage-popover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,39 @@ describe('SessionUsagePopover', () => {
expect(container.querySelector('button')).toBeNull();
});

it('shows Fable weekly as a distinct meter beside shared quotas', async () => {
await renderUsage({
agentType: 'claude',
modelId: 'claude-fable-5',
showRateLimitWithoutContext: true,
rateLimits: {
[getRateLimitEntryKey('claude', 'claude')]: {
limitId: 'claude',
scope: { providerId: 'claude' },
windows: [
{ usedPercent: 12, windowDurationSeconds: 18_000, resetsAtEpochSeconds: null },
{ usedPercent: 30, windowDurationSeconds: 604_800, resetsAtEpochSeconds: null },
{
label: 'Fable',
usedPercent: 67,
windowDurationSeconds: 604_800,
resetsAtEpochSeconds: null,
},
],
},
},
});
await act(async () => {
container.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
const popover = document.body.querySelector('[aria-label="Usage"]');
expect(
Array.from(popover?.querySelectorAll('[role="progressbar"]') ?? []).map((meter) =>
meter.getAttribute('aria-label')
)
).toEqual(['Weekly: 30% used', 'Weekly · Fable: 67% used', '5 hours: 12% used']);
});

it('shows a truthful unavailable state when the provider omits utilization', async () => {
const unavailableLimits: MachineRateLimits = {
[getRateLimitEntryKey('grok', 'grok')]: {
Expand Down
10 changes: 10 additions & 0 deletions packages/components/tests/session-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ describe('session usage', () => {
]);
});

it('preserves scoped windows even when their values match the shared quota', () => {
const weekly = { usedPercent: 0, windowDurationSeconds: 604_800, resetsAtEpochSeconds: null };
expect(
getAgentRateLimitWindows(usage({ windows: [weekly, { ...weekly, label: ' Fable ' }] }))
).toEqual([
{ ...weekly, remainingPercent: 100 },
{ ...weekly, remainingPercent: 100, label: 'Fable' },
]);
});

it('keeps sub-one-percent Grok usage on the percentage scale', () => {
expect(
getAgentRateLimitWindows(
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading