Unverified 提交 1e56c111 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Increase Basic Agent free quota from 5 to 10 messages (#3147)

## Summary - Raise the Basic Agent (free tier) message quota from 5 to 10 per window. - Add `src/lib/free_agent_quota_limit.ts` as the single source of truth; IPC re-exports the constant. - UI (selector, banner, chat error) uses `messagesLimit` from quota status so copy stays in sync. - Update `e2e-tests/free_agent_quota.spec.ts` for the new limit and strings. ## Test plan - `npm run fmt && npm run lint:fix && npm run ts` - `npm test` (vitest) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Made with [Cursor](https://cursor.com) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/3147" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> Co-authored-by: 's avatarClaude <noreply@anthropic.com>
上级 3a220449
......@@ -4,7 +4,7 @@ import { expect } from "@playwright/test";
/**
* E2E test for Basic Agent mode quota (free users).
*
* Basic Agent mode is available to non-Pro users with a 5-message-per-day limit.
* Basic Agent mode is available to non-Pro users with a 10-message-per-window limit.
* This test verifies mode availability, quota tracking, exceeded banner, and mode switching.
*/
......@@ -24,9 +24,9 @@ testSkipIfWindows(
po.page.getByRole("option", { name: /Agent v2/ }),
).not.toBeVisible();
// 2. Verify quota display is present (may not be 5/5 if AI_RULES.md generation consumed quota)
// 2. Verify quota display is present (may not be 10/10 if AI_RULES.md generation consumed quota)
await expect(
po.page.getByRole("option", { name: /Basic Agent.*\d\/5 remaining/ }),
po.page.getByRole("option", { name: /Basic Agent.*\d+\/10 remaining/ }),
).toBeVisible();
await po.page.keyboard.press("Escape");
......@@ -36,8 +36,8 @@ testSkipIfWindows(
"Basic Agent",
);
// 4. Send 5 messages to exhaust quota (this will exhaust quota even if some was already used)
for (let i = 0; i < 5; i++) {
// 4. Send 10 messages to exhaust quota (this will exhaust quota even if some was already used)
for (let i = 0; i < 10; i++) {
await po.sendPrompt(`tc=local-agent/simple-response message ${i + 1}`);
await po.chatActions.waitForChatCompletion();
}
......@@ -47,7 +47,7 @@ testSkipIfWindows(
timeout: Timeout.MEDIUM,
});
await expect(po.page.getByTestId("free-agent-quota-banner")).toContainText(
"You have used all 5 messages for the free Agent mode today",
"You have used all 10 messages for the free Agent mode today",
);
await expect(
po.page.getByRole("button", { name: "Upgrade to Dyad Pro" }),
......@@ -56,14 +56,14 @@ testSkipIfWindows(
po.page.getByRole("button", { name: "Switch back to Build mode" }),
).toBeVisible();
// 6. Try to send a 6th message - should be blocked with error
await po.sendPrompt("tc=local-agent/simple-response message 6");
// 6. Try to send an 11th message - should be blocked with error
await po.sendPrompt("tc=local-agent/simple-response message 11");
// Verify error message appears indicating quota exceeded
await expect(po.page.getByTestId("chat-error-box")).toBeVisible({
timeout: Timeout.MEDIUM,
});
await expect(po.page.getByTestId("chat-error-box")).toContainText(
"You have used all 5 free Agent messages for today",
"You have used all 10 free Agent messages for today",
);
// 8. Click "Switch back to Build mode" and verify mode changes
......@@ -104,10 +104,11 @@ testSkipIfWindows(
// 2. Verify quota decreased (exact count may vary due to setup messages)
await po.page.getByTestId("chat-mode-selector").click();
// The quota should be less than 5/5 after sending messages
await expect(
po.page.getByRole("option", { name: /Basic Agent.*[0-4]\/5 remaining/ }),
).toBeVisible();
const basicAgentOption = po.page.getByRole("option", {
name: /Basic Agent/,
});
await expect(basicAgentOption).toContainText(/\/10 remaining/);
await expect(basicAgentOption).not.toContainText("10/10");
await po.page.keyboard.press("Escape");
// 3. Simulate 25 hours passing by calling the test-only IPC handler
......@@ -130,10 +131,10 @@ testSkipIfWindows(
timeout: Timeout.MEDIUM,
});
// 5. Verify quota has reset to 5/5 remaining
// 5. Verify quota has reset to 10/10 remaining
await po.page.getByTestId("chat-mode-selector").click();
await expect(
po.page.getByRole("option", { name: /Basic Agent.*5\/5 remaining/ }),
po.page.getByRole("option", { name: /Basic Agent.*10\/10 remaining/ }),
).toBeVisible();
await po.page.keyboard.press("Escape");
......
......@@ -35,7 +35,8 @@ export function ChatModeSelector() {
// Migration happens on read, so selectedChatMode will never be "agent"
const selectedMode = settings?.selectedChatMode || "build";
const isProEnabled = settings ? isDyadProEnabled(settings) : false;
const { messagesRemaining, isQuotaExceeded } = useFreeAgentQuota();
const { messagesRemaining, messagesLimit, isQuotaExceeded } =
useFreeAgentQuota();
const { servers } = useMcp();
const enabledMcpServersCount = servers.filter((s) => s.enabled).length;
......@@ -171,8 +172,7 @@ export function ChatModeSelector() {
<Bot size={14} className="text-muted-foreground" />
<span className="font-medium">Basic Agent</span>
<span className="text-xs text-muted-foreground">
({isQuotaExceeded ? "0" : messagesRemaining}/5 remaining for
today)
{`(${isQuotaExceeded ? "0" : messagesRemaining}/${messagesLimit} remaining for today)`}
</span>
</div>
<span className="text-xs text-muted-foreground ml-[22px]">
......
......@@ -76,7 +76,7 @@ export function DefaultChatModeSelector() {
<span className="text-xs text-muted-foreground">
{isProEnabled
? "Better at bigger tasks"
: "Free tier (5 messages/day)"}
: "Free tier (10 messages/day)"}
</span>
</div>
</SelectItem>
......
import { ipc } from "@/ipc/types";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
import { AI_STREAMING_ERROR_MESSAGE_PREFIX } from "@/shared/texts";
import {
X,
......@@ -25,6 +26,8 @@ export function ChatErrorBox({
isDyadProEnabled: boolean;
onStartNewChat?: () => void;
}) {
const { messagesLimit } = useFreeAgentQuota();
if (error.includes("doesn't have a free quota tier")) {
return (
<ChatErrorContainer onDismiss={onDismiss}>
......@@ -118,8 +121,8 @@ export function ChatErrorBox({
if (error.includes("FREE_AGENT_QUOTA_EXCEEDED")) {
return (
<ChatErrorContainer onDismiss={onDismiss}>
You have used all 5 free Agent messages for today. Please upgrade to
Dyad Pro for unlimited access or switch to Build mode.
You have used all {messagesLimit} free Agent messages for today. Please
upgrade to Dyad Pro for unlimited access or switch to Build mode.
<div className="mt-2 space-y-2 space-x-2">
<ExternalLink
href="https://dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=free-agent-quota-exceeded"
......
......@@ -14,8 +14,13 @@ interface FreeAgentQuotaBannerProps {
export function FreeAgentQuotaBanner({
onSwitchToBuildMode,
}: FreeAgentQuotaBannerProps) {
const { quotaStatus, isQuotaExceeded, hoursUntilReset, resetTime } =
useFreeAgentQuota();
const {
quotaStatus,
isQuotaExceeded,
hoursUntilReset,
resetTime,
messagesLimit,
} = useFreeAgentQuota();
if (!isQuotaExceeded || !quotaStatus) {
return null;
......@@ -51,9 +56,10 @@ export function FreeAgentQuotaBanner({
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
<div className="flex-1 space-y-2">
<p className="text-sm text-amber-700 dark:text-amber-300">
You have used all 5 messages for the free Agent mode today. Check
back in {resetTimeDisplay} ({resetDateTime}). If you don't want to
wait, upgrade to Dyad Pro or switch back to Build mode.
You have used all {messagesLimit} messages for the free Agent mode
today. Check back in {resetTimeDisplay} ({resetDateTime}). If you
don't want to wait, upgrade to Dyad Pro or switch back to Build
mode.
</p>
<div className="flex flex-wrap gap-2">
<Button onClick={handleUpgrade} size="sm" className="gap-1.5">
......
......@@ -3,6 +3,7 @@ import { ipc, type FreeAgentQuotaStatus } from "@/ipc/types";
import { queryKeys } from "@/lib/queryKeys";
import { useSettings } from "./useSettings";
import { isDyadProEnabled } from "@/lib/schemas";
import { FREE_AGENT_QUOTA_LIMIT } from "@/lib/free_agent_quota_limit";
const THIRTY_MINUTES_IN_MS = 30 * 60 * 1000;
// In test mode, use very short staleTime for faster E2E tests
......@@ -53,10 +54,10 @@ export function useFreeAgentQuota() {
// Convenience properties for easier consumption
isQuotaExceeded: quotaStatus?.isQuotaExceeded ?? false,
messagesUsed: quotaStatus?.messagesUsed ?? 0,
messagesLimit: quotaStatus?.messagesLimit ?? 5,
messagesLimit: quotaStatus?.messagesLimit ?? FREE_AGENT_QUOTA_LIMIT,
messagesRemaining: quotaStatus
? Math.max(0, quotaStatus.messagesLimit - quotaStatus.messagesUsed)
: 5,
: FREE_AGENT_QUOTA_LIMIT,
hoursUntilReset: quotaStatus?.hoursUntilReset ?? null,
resetTime: quotaStatus?.resetTime ?? null,
};
......
......@@ -6,6 +6,7 @@ import { freeAgentQuotaContracts } from "../types/free_agent_quota";
import log from "electron-log";
import { ipcMain } from "electron";
import { IS_TEST_BUILD } from "../utils/test_utils";
import { FREE_AGENT_QUOTA_LIMIT } from "@/lib/free_agent_quota_limit";
import fetch from "node-fetch";
const logger = log.scope("free_agent_quota_handlers");
......@@ -61,8 +62,7 @@ async function getServerTime(): Promise<number> {
}
}
/** Maximum number of free agent messages per 24-hour window */
export const FREE_AGENT_QUOTA_LIMIT = 5;
export { FREE_AGENT_QUOTA_LIMIT };
/**
* Duration of the quota window in milliseconds (23 hours).
......@@ -138,7 +138,7 @@ export async function unmarkMessageAsUsingFreeAgentQuota(
* Gets the current free agent quota status.
* Exported for use in chat stream handlers.
*
* Quota behavior: All 5 messages are released at once when 24 hours have passed
* Quota behavior: All quota messages are released at once when 24 hours have passed
* since the oldest message was sent (not a rolling window).
*/
export async function getFreeAgentQuotaStatus() {
......@@ -164,7 +164,7 @@ export async function getFreeAgentQuotaStatus() {
}
// Check if the oldest message is >= 24 hours old
// If so, all 5 messages are released at once (quota resets)
// If so, all quota messages are released at once (quota resets)
// Uses server time to prevent clock manipulation cheating
const oldestMessage = quotaMessages[0];
const windowStartTime = oldestMessage.createdAt.getTime();
......
......@@ -27,7 +27,7 @@ export type FreeAgentQuotaStatus = z.infer<typeof FreeAgentQuotaStatusSchema>;
/**
* Free agent quota contracts define the IPC interface for managing
* the 5-message-per-day quota for non-Pro users using Basic Agent mode.
* the Basic Agent per-window message quota for non-Pro users.
*/
export const freeAgentQuotaContracts = {
/**
......
/** Maximum number of Basic Agent (free tier) messages per quota window */
export const FREE_AGENT_QUOTA_LIMIT = 10;
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论