Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2dc63af
Add Usage tab with preview-site quota and delete-all to agentic settings
bcotrim Jul 21, 2026
a239537
Merge remote-tracking branch 'origin/trunk' into agentic-settings-usa…
bcotrim Jul 22, 2026
6dfc351
Replace nested ternary with helper for delete-preview-sites label
bcotrim Jul 22, 2026
9e7f6ca
Merge branch 'trunk' into agentic-settings-usage-tab
bcotrim Jul 22, 2026
562a4ac
Keep optimistic zero after delete-all instead of racing a stale usage…
bcotrim Jul 22, 2026
b9511ac
Merge branch 'agentic-settings-usage-tab' of github.com:Automattic/st…
bcotrim Jul 22, 2026
f745456
Merge remote-tracking branch 'origin/trunk' into agentic-settings-usa…
bcotrim Jul 22, 2026
daebb08
Show Studio Code AI usage quota in the agentic Usage settings tab (#4…
sejas Jul 22, 2026
d45228e
Warn once when offline in the Usage tab and share the quota formatters
bcotrim Jul 22, 2026
3c26b8f
Reuse the app sign-in and offline banners in the Usage tab
bcotrim Jul 22, 2026
a1260c5
Move the Usage banner above the card and use disabled colors for unav…
bcotrim Jul 22, 2026
c10e9a6
Grey the whole Usage card and hatch unavailable meters
bcotrim Jul 22, 2026
bd3c68b
Soften the hatch on unavailable usage meters
bcotrim Jul 22, 2026
c78c48a
Hide usage figures while offline instead of showing stale numbers
bcotrim Jul 22, 2026
a28f998
Merge remote-tracking branch 'origin/trunk' into agentic-settings-usa…
bcotrim Jul 22, 2026
bf80539
Show a progress bar in both usage sections while loading
bcotrim Jul 22, 2026
e2b0415
Empty the preview sites bar while usage is loading
bcotrim Jul 22, 2026
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 11 additions & 21 deletions apps/studio/src/modules/user-settings/components/prompt-info.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import {
clampQuotaFraction,
formatQuotaPercentage,
formatQuotaResetDate,
} from '@studio/common/lib/studio-assistant-quota';
import { sprintf } from '@wordpress/i18n';
import { useI18n } from '@wordpress/react-i18n';
import ProgressBar from 'src/components/progress-bar';
import { useOffline } from 'src/hooks/use-offline';
import { useI18nLocale } from 'src/stores';
import { useGetStudioAssistantQuota } from 'src/stores/wpcom-api';

function formatPercentage( value: number, maxValue: number, locale: string ) {
const percentage = Math.max( 0, Math.min( 1, value / maxValue ) );

return new Intl.NumberFormat( locale, {
style: 'percent',
maximumFractionDigits: 2,
} ).format( percentage );
}

function formatResetDate( date: string, locale: string ) {
return new Intl.DateTimeFormat( locale, {
day: 'numeric',
month: 'long',
year: 'numeric',
} ).format( new Date( date ) );
}

export function PromptInfo() {
const { __ } = useI18n();
const locale = useI18nLocale();
Expand Down Expand Up @@ -52,12 +40,14 @@ export function PromptInfo() {
sprintf(
/* translators: %1$s: percentage of monthly limit used (e.g. 7.5%). %2$s: date the limit resets (e.g. July 1, 2026). */
__( '%1$s of monthly limit used (resets on %2$s)' ),
formatPercentage(
assistantQuotaWithCostCap.costUsage,
assistantQuotaWithCostCap.costCap,
formatQuotaPercentage(
clampQuotaFraction(
assistantQuotaWithCostCap.costUsage,
assistantQuotaWithCostCap.costCap
),
locale
),
formatResetDate( assistantQuotaWithCostCap.costResetDate, locale )
formatQuotaResetDate( assistantQuotaWithCostCap.costResetDate, locale )
) }
{ ! isLoading &&
! isOffline &&
Expand Down
13 changes: 1 addition & 12 deletions apps/studio/src/stores/wpcom-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createApi, TypedUseQuery, TypedUseMutation } from '@reduxjs/toolkit/query/react';
import * as Sentry from '@sentry/electron/renderer';
import { studioAssistantQuotaSchema } from '@studio/common/lib/studio-assistant-quota';
import { blueprintSchema, type Blueprint } from '@studio/common/lib/studio-blueprints-api';
import wpcomFactory from '@studio/common/lib/wpcom-factory';
import wpcomXhrRequest from '@studio/common/lib/wpcom-xhr-request-factory';
Expand Down Expand Up @@ -35,18 +36,6 @@ const snapshotStatusSchema = z
isDeleted: data.is_deleted === '1',
} ) );

const studioAssistantQuotaSchema = z
.object( {
cost_usage: z.number(),
cost_cap: z.number(),
cost_reset_date: z.string(),
} )
.transform( ( data ) => ( {
costUsage: data.cost_usage,
costCap: data.cost_cap,
costResetDate: data.cost_reset_date,
} ) );

export type { Blueprint };

let wpcomClient: WPCOM | undefined;
Expand Down
9 changes: 8 additions & 1 deletion apps/ui/src/components/agentic-signin-banner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type { AgenticFeatureReason } from '@/data/queries/use-agentic-features';

export function AgenticSigninBanner() {
const { enabled, reason } = useAgenticFeatures();
const login = useLogin();
const navigate = useNavigate();

// `authenticate()` resolves when the browser opens, not when OAuth
Expand All @@ -28,6 +27,14 @@ export function AgenticSigninBanner() {
return null;
}

return <SigninNotice />;
}

// The banner without the route-aware behaviour, for surfaces that must stay
// put once the user signs in (e.g. Settings).
export function SigninNotice() {
const login = useLogin();

return (
<section className={ styles.root } aria-label={ __( 'Sign in to Studio' ) }>
<div className={ styles.text }>
Expand Down
6 changes: 6 additions & 0 deletions apps/ui/src/components/offline-banner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export function OfflineBanner() {
return null;
}

return <OfflineNotice />;
}

// The banner without the route-aware behaviour, for surfaces that must stay
// put when connectivity returns (e.g. Settings).
export function OfflineNotice() {
return (
<section className={ styles.root } role="status">
<h2 className={ styles.heading }>{ __( "You're offline" ) }</h2>
Expand Down
6 changes: 6 additions & 0 deletions apps/ui/src/components/settings-view/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ vi.mock( '@/hooks/use-sidebar-collapsed', () => ( {
useSidebarCollapsed: () => false,
} ) );

// The mocked Tabs render every panel unconditionally; the usage panel has its
// own test file.
vi.mock( './usage-panel', () => ( {
UsagePanel: () => null,
} ) );

vi.mock( '@/hooks/use-traffic-light-space', () => ( {
useTrafficLightSpace: () => false,
} ) );
Expand Down
14 changes: 13 additions & 1 deletion apps/ui/src/components/settings-view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { UNSET, toPreferencesFormData, toPreferencesPatch } from './preferences'
import { SkillsPanel } from './skills-panel';
import { StudioCodePanel } from './studio-code-panel';
import styles from './style.module.css';
import { UsagePanel } from './usage-panel';
import type { PreferencesFormData } from './preferences';
import type {
ColorScheme,
Expand All @@ -31,7 +32,14 @@ import type {
} from '@/data/core';
import type { ReactNode } from 'react';

const SETTINGS_TABS = [ 'preferences', 'keyboard', 'skills', 'mcp', 'studio-code' ] as const;
const SETTINGS_TABS = [
'preferences',
'usage',
'keyboard',
'skills',
'mcp',
'studio-code',
] as const;

type TabId = ( typeof SETTINGS_TABS )[ number ];

Expand Down Expand Up @@ -105,6 +113,7 @@ function SettingsHeader() {
<div className={ styles.headerTabs }>
<Tabs.List className={ styles.headerTabList }>
<Tabs.Tab tabId="preferences">{ __( 'Settings' ) }</Tabs.Tab>
<Tabs.Tab tabId="usage">{ __( 'Usage' ) }</Tabs.Tab>
<Tabs.Tab tabId="keyboard">{ __( 'Keyboard' ) }</Tabs.Tab>
<Tabs.Tab tabId="skills">{ __( 'Skills' ) }</Tabs.Tab>
<Tabs.Tab tabId="mcp">{ __( 'MCP' ) }</Tabs.Tab>
Expand Down Expand Up @@ -415,6 +424,9 @@ export function SettingsView( {
onChange={ handleChange }
/>
</Tabs.Panel>
<Tabs.Panel tabId="usage">
<UsagePanel />
</Tabs.Panel>
<Tabs.Panel tabId="keyboard">
<KeyboardPanel />
</Tabs.Panel>
Expand Down
124 changes: 123 additions & 1 deletion apps/ui/src/components/settings-view/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,8 @@
line-height: var(--wpds-typography-line-height-sm);
}

.skillsPanel {
.skillsPanel,
.usagePanel {
display: flex;
flex-direction: column;
gap: var(--wpds-dimension-padding-xl);
Expand Down Expand Up @@ -644,3 +645,124 @@
}
}


/* Signed out or offline: grey the whole card, since none of it is live. */
.usageDisabled .settingsPanelHeader h2,
.usageDisabled .settingsPanelHeader p,
.usageDisabled .usageSectionHeader h2,
.usageDisabled .previewUsageText,
.usageDisabled .usageSection p {
color: var(--wpds-color-fg-interactive-neutral-disabled);
}

.usageDisabled .progressTrack {
background: var(--wpds-color-bg-interactive-neutral-weak-disabled);
}

.usageDisabled .progressValue {
background: var(--wpds-color-fg-interactive-neutral-disabled);
}

/* Hatched stand-in for a meter with no figure behind it — same 135deg stripe
idiom as the AI credits placeholder, in neutral disabled colors. */
.unavailableBar {
inline-size: 100%;
block-size: 6px;
border-radius: 999px;
background:
repeating-linear-gradient(
135deg,
color-mix(in srgb, var(--wpds-color-fg-interactive-neutral-disabled) 22%, transparent) 0 3px,
transparent 3px 8px
),
var(--wpds-color-bg-interactive-neutral-weak-disabled);
}

.usageSection {
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--wpds-dimension-padding-md);
padding-block: var(--wpds-dimension-padding-lg);
border-top: 1px solid var(--wpds-color-stroke-surface-neutral);
}

.usageSection:last-child {
padding-block-end: 0;
}

.usageSectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--wpds-dimension-padding-md);
min-width: 0;
}

.usageSectionHeader h2 {
margin: 0;
color: var(--wpds-color-fg-content-neutral);
font-size: var(--wpds-typography-font-size-md);
font-weight: 600;
line-height: var(--wpds-typography-line-height-sm);
}

.usageSection p {
margin: 0;
color: var(--wpds-color-fg-content-neutral-weak);
font-size: var(--wpds-typography-font-size-sm);
line-height: var(--wpds-typography-line-height-sm);
}

.previewUsageText {
color: var(--wpds-color-fg-content-neutral-weak);
font-size: var(--wpds-typography-font-size-sm);
line-height: var(--wpds-typography-line-height-sm);
}

.progressTrack {
overflow: hidden;
inline-size: 100%;
block-size: 6px;
border-radius: 999px;
background: var(--wpds-color-stroke-surface-neutral);
}

.progressValue {
block-size: 100%;
border-radius: inherit;
background: var(--wpds-color-fg-interactive-brand);
}

.previewActionsButton {
flex-shrink: 0;
}

.usageSectionAction {
align-self: flex-start;
}

.aiCreditsTrack {
position: relative;
background: color-mix(
in srgb,
var(--wpds-color-fg-interactive-brand) 14%,
var(--wpds-color-stroke-surface-neutral)
);
}

.aiCreditsMeterValue {
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--wpds-color-fg-interactive-brand) 32%, transparent),
color-mix(in srgb, var(--wpds-color-fg-interactive-brand) 14%, transparent)
),
repeating-linear-gradient(
135deg,
color-mix(in srgb, var(--wpds-color-fg-interactive-brand) 18%, transparent) 0 8px,
transparent 8px 16px
);
}
Loading