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
15 changes: 15 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,21 @@ function runtimeEventToActivities(
];
}

case "account.rate-limits.updated": {
return [
{
id: event.eventId,
createdAt: event.createdAt,
tone: "info",
kind: "account.rate-limits.updated",
summary: "Account rate limits updated",
payload: event.payload.rateLimits,
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
},
];
}

case "item.updated": {
if (!isToolLifecycleItemType(event.payload.itemType)) {
return [];
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
renderProviderTraitsPicker,
} from "./composerProviderState";
import { ContextWindowMeter } from "./ContextWindowMeter";
import { ProviderUsageMeter } from "./ProviderUsageMeter";
import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";
import { basenameOfPath } from "../../vscode-icons";
import { cn, randomUUID } from "~/lib/utils";
Expand Down Expand Up @@ -105,6 +106,7 @@ import type { SessionPhase, Thread } from "../../types";
import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";
import type { PendingApproval, PendingUserInput } from "../../session-logic";
import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow";
import { deriveLatestProviderUsageSnapshot } from "../../lib/providerUsage";
import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";
import { searchProviderSkills } from "../../providerSkillSearch";

Expand Down Expand Up @@ -273,6 +275,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: {
compact: boolean;
activeContextWindow: ReturnType<typeof deriveLatestContextWindowSnapshot>;
activeProviderUsage: ReturnType<typeof deriveLatestProviderUsageSnapshot>;
isPreparingWorktree: boolean;
pendingAction: {
questionIndex: number;
Expand All @@ -293,6 +296,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(
}) {
return (
<>
{props.activeProviderUsage ? <ProviderUsageMeter usage={props.activeProviderUsage} /> : null}
{props.activeContextWindow ? <ContextWindowMeter usage={props.activeContextWindow} /> : null}
{props.isPreparingWorktree ? (
<span className="text-muted-foreground/70 text-xs">Preparing worktree...</span>
Expand Down Expand Up @@ -648,6 +652,14 @@ export const ChatComposer = memo(
[activeThreadActivities],
);

// ------------------------------------------------------------------
// Provider usage (rate limits / session %)
// ------------------------------------------------------------------
const activeProviderUsage = useMemo(
() => deriveLatestProviderUsageSnapshot(activeThreadActivities ?? []),
[activeThreadActivities],
);

// ------------------------------------------------------------------
// Composer-local state
// ------------------------------------------------------------------
Expand Down Expand Up @@ -1962,6 +1974,7 @@ export const ChatComposer = memo(
<ComposerFooterPrimaryActions
compact={isComposerPrimaryActionsCompact}
activeContextWindow={activeContextWindow}
activeProviderUsage={activeProviderUsage}
pendingAction={pendingPrimaryAction}
isRunning={phase === "running"}
showPlanFollowUpPrompt={
Expand Down
101 changes: 101 additions & 0 deletions apps/web/src/components/chat/ProviderUsageMeter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { type ProviderUsageSnapshot } from "~/lib/providerUsage";
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";

function formatResetTime(resetsAt: number | null): string | null {
if (resetsAt === null) {
return null;
}
const date = new Date(resetsAt * 1000);
return `Resets ${date.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
})}, ${date.toLocaleTimeString(undefined, {
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
})}`;
}

function UsageBar(props: { percent: number; status: "ok" | "warning" | "rejected" }) {
const barColor =
props.status === "rejected" || props.percent >= 90
? "bg-red-500"
: props.status === "warning" || props.percent >= 70
? "bg-amber-500"
: "bg-rose-500";

return (
<div className="relative h-1.5 w-full overflow-hidden rounded-full bg-muted/50">
<div
className={`absolute inset-y-0 left-0 rounded-full transition-[width] duration-500 ease-out ${barColor}`}
style={{ width: `${Math.min(100, Math.max(0, props.percent))}%` }}
/>
</div>
);
}

function BarGraphIcon(props: { status: "ok" | "warning" | "rejected" }) {
const barColor =
props.status === "rejected"
? "var(--color-destructive)"
: props.status === "warning"
? "var(--color-warning, #f59e0b)"
: "var(--color-muted-foreground)";

return (
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
<rect x="4" y="13" width="4" height="7" rx="1" fill={barColor} opacity={0.6} />
<rect x="10" y="8" width="4" height="12" rx="1" fill={barColor} opacity={0.8} />
<rect x="16" y="4" width="4" height="16" rx="1" fill={barColor} />
</svg>
);
}

export function ProviderUsageMeter(props: { usage: ProviderUsageSnapshot }) {
const { usage } = props;
const maxPercent = Math.max(...usage.windows.map((w) => w.usedPercent), 0);

return (
<Popover>
<PopoverTrigger
openOnHover
delay={150}
closeDelay={0}
render={
<button
type="button"
className="group inline-flex items-center justify-center rounded-full p-0.5 transition-opacity hover:opacity-85"
aria-label={`${usage.providerLabel} usage: ${Math.round(maxPercent)}%`}
>
<BarGraphIcon status={usage.status} />
</button>
}
/>
<PopoverPopup tooltipStyle side="top" align="end" className="w-64 max-w-none px-4 py-3">
<div className="space-y-3">
<div className="text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
{usage.providerLabel}
</div>

{usage.windows.map((window) => {
const resetText = formatResetTime(window.resetsAt);
return (
<div key={window.label} className="space-y-1.5">
<div className="flex items-baseline justify-between">
<span className="text-xs font-semibold text-foreground">{window.label}</span>
<span className="text-xs font-semibold text-foreground">
{Math.round(window.usedPercent)}%
</span>
</div>
<UsageBar percent={window.usedPercent} status={usage.status} />
{resetText ? (
<div className="text-[11px] text-muted-foreground">{resetText}</div>
) : null}
</div>
);
})}
</div>
</PopoverPopup>
</Popover>
);
}
Loading
Loading