Skip to content
3 changes: 2 additions & 1 deletion apps/cli/ai/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Spacer,
} from '@earendil-works/pi-tui';
import { stripMediaWidgetPayloadLines } from '@studio/common/ai/chat-artifacts';
import { isUsageCapError } from '@studio/common/ai/json-events';
import { DEFAULT_MODEL, getAiModelLabel, type AiModelId } from '@studio/common/ai/models';
import { findLastAssistant } from '@studio/common/ai/session-events';
import { randomThinkingMessage } from '@studio/common/ai/thinking-messages';
Expand Down Expand Up @@ -2079,7 +2080,7 @@ export class AiChatUI implements AiOutputAdapter {
if (
message.stopReason === 'error' &&
this.currentProvider === 'wpcom' &&
/API Error:\s*429|status code 429|"status":\s*429/i.test( message.errorMessage ?? '' )
isUsageCapError( message.errorMessage )
) {
this.hideLoader();
this.usageCapReached = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface ComposerProps {
busy: boolean;
isInterrupting?: boolean;
error: string | null;
usageCapMessage?: string | null;
model: AiModelId;
onSend: ( prompt: string, attachments: ComposerSendAttachments ) => Promise< void >;
onInterrupt: () => Promise< void >;
Expand Down Expand Up @@ -225,6 +226,7 @@ export function Composer( {
busy,
isInterrupting = false,
error,
usageCapMessage,
model,
onSend,
onInterrupt,
Expand Down Expand Up @@ -458,6 +460,11 @@ export function Composer( {
return (
<>
<div className={ styles.root }>
{ usageCapMessage ? (
<div className={ styles.usageCapBanner } role="alert">
{ usageCapMessage }
</div>
) : null }
Comment on lines 462 to +467

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Merged the suggestion 👍

<div
className={ cx( styles.shell, isDraggingOver && styles.shellDragging ) }
onDragOver={ dragHandlers.onDragOver }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
flex-direction: column;
}

.usageCapBanner {
padding: var(--wpds-dimension-padding-md) var(--wpds-dimension-padding-lg);
margin-bottom: var(--wpds-dimension-padding-sm);
border-radius: 8px;
background-color: var(--color-frame-surface);
border: 1px solid var(--color-frame-error);
color: var(--color-frame-error);
font-size: var(--wpds-typography-font-size-sm);
line-height: 1.4;
}

.shell {
display: flex;
flex-direction: column;
Expand Down
4 changes: 3 additions & 1 deletion apps/studio/src/components/studio-code-session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ function SessionContent( { selectedSite }: { selectedSite: SiteDetails } ) {
isInterrupting,
startedAt,
error: runError,
usageCapReached,
pendingQuestions,
pendingAnswers,
answeredQuestions,
Expand Down Expand Up @@ -403,7 +404,8 @@ function SessionContent( { selectedSite }: { selectedSite: SiteDetails } ) {
<Composer
busy={ composerBusy }
isInterrupting={ isInterrupting }
error={ runError }
error={ usageCapReached ? null : runError }
usageCapMessage={ usageCapReached ? runError : null }
model={ currentModel }
onSend={ sendMessage }
onInterrupt={ interrupt }
Expand Down
34 changes: 28 additions & 6 deletions apps/studio/src/components/studio-code-session/use-agent-run.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { buildChatAttachmentSummaries } from '@studio/common/ai/chat-attachments';
import { isUsageCapError } from '@studio/common/ai/json-events';
import { useQueryClient } from '@tanstack/react-query';
import { __ } from '@wordpress/i18n';
import {
createContext,
useCallback,
Expand Down Expand Up @@ -64,6 +66,7 @@ export interface LiveAgentEvents {
isInterrupting: boolean;
startedAt: number | null;
error: string | null;
usageCapReached: boolean;
pendingQuestions: PendingQuestion[];
// Accumulated answers for the current batch, keyed by question text.
// The user can re-click an option to change their pick until every
Expand Down Expand Up @@ -95,6 +98,7 @@ interface State {
runId: string | null;
startedAt: number | null;
error: string | null;
usageCapReached: boolean;
isInterrupting: boolean;
pendingQuestions: PendingQuestion[];
pendingAnswers: Record< string, string >;
Expand All @@ -107,6 +111,7 @@ const initialState: State = {
runId: null,
startedAt: null,
error: null,
usageCapReached: false,
isInterrupting: false,
pendingQuestions: [],
pendingAnswers: {},
Expand All @@ -118,7 +123,7 @@ type Action =
| { type: 'hydrate_active_run'; runId: string; startedAt: number; interrupting: boolean }
| { type: 'send_pending'; startedAt: number }
| { type: 'send_start'; runId: string; startedAt: number }
| { type: 'error_set'; message: string | null }
| { type: 'error_set'; message: string | null; usageCapReached?: boolean }
| { type: 'turn_completed' }
| { type: 'run_ended' }
| { type: 'interrupt_requested' }
Expand Down Expand Up @@ -160,7 +165,7 @@ function reducer( state: State, action: Action ): State {
isInterrupting: false,
};
case 'error_set':
return { ...state, error: action.message };
return { ...state, error: action.message, usageCapReached: action.usageCapReached ?? false };
case 'turn_completed':
return {
...state,
Expand Down Expand Up @@ -342,9 +347,18 @@ export function AgentRunProvider( { children }: PropsWithChildren ) {
interrupting: false,
} );
return;
case 'error':
dispatchSession( payload.sessionId, { type: 'error_set', message: event.message } );
case 'error': {
const isUsageCap = isUsageCapError( event.message );
const message = isUsageCap
? __( 'You\u2019ve reached your AI usage limit. Try again later.' )
: event.message;
dispatchSession( payload.sessionId, {
type: 'error_set',
message,
usageCapReached: isUsageCap,
} );
return;
}
case 'turn.completed':
dispatchSession( payload.sessionId, { type: 'turn_completed' } );
return;
Expand Down Expand Up @@ -518,8 +532,14 @@ export function AgentRunProvider( { children }: PropsWithChildren ) {
if ( idx === -1 ) return entries;
return [ ...entries.slice( 0, idx ), ...entries.slice( idx + 1 ) ];
} );
const message = err instanceof Error ? err.message : String( err );
dispatchSession( sessionId, { type: 'error_set', message } );
const rawMessage = err instanceof Error ? err.message : String( err );
const isUsageCap = isUsageCapError( rawMessage );
const message = isUsageCap
? __(
'You\u2019ve reached your AI usage limit. Try again later or use your own Anthropic API key via the CLI (/provider).'
)
: rawMessage;
dispatchSession( sessionId, { type: 'error_set', message, usageCapReached: isUsageCap } );
throw err;
}
},
Expand Down Expand Up @@ -622,6 +642,7 @@ export function useAgentRun( sessionId: string | undefined ): LiveAgentEvents {
phase,
startedAt,
error,
usageCapReached,
isInterrupting,
pendingQuestions,
pendingAnswers,
Expand Down Expand Up @@ -722,6 +743,7 @@ export function useAgentRun( sessionId: string | undefined ): LiveAgentEvents {
isInterrupting,
startedAt,
error,
usageCapReached,
pendingQuestions,
pendingAnswers,
answeredQuestions,
Expand Down
10 changes: 10 additions & 0 deletions packages/common/ai/json-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@ export type JsonEvent =
usage?: { numTurns: number; costUsd?: number };
}
| MediaShareEvent;

const USAGE_CAP_PATTERN = /(?:API Error:\s*429\b|status code\s+429\b|"status"\s*:\s*429\b)/i;

/**
* Returns true when an error message indicates the user hit the AI usage cap
* (HTTP 429 from the WordPress.com proxy).
*/
export function isUsageCapError( message: string | undefined | null ): boolean {
return USAGE_CAP_PATTERN.test( message ?? '' );
}