Unverified 提交 207c9195 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add default chat mode setting (#2244)

- Add `defaultChatMode` field to UserSettings schema - Create `getEffectiveDefaultChatMode()` helper that returns the appropriate default based on settings and pro status: - If defaultChatMode is explicitly set, use it - If not set but user has Dyad Pro enabled, default to "local-agent" - Otherwise default to "build" - Create DefaultChatModeSelector component in Workflow Settings - When user upgrades to Dyad Pro, automatically set default to "local-agent" - Apply default chat mode when navigating to home page <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a default chat mode setting with sane defaults and a selector in Workflow Settings. It applies the effective default on the home page and for new chats, and sets it to Agent when a user upgrades to Dyad Pro. - **New Features** - Added defaultChatMode to UserSettings. - Added getEffectiveDefaultChatMode with rules: explicit > Dyad Pro → local-agent > build. - Added DefaultChatModeSelector in Workflow Settings (shows Agent option when Dyad Pro is enabled). - Home page and new chats sync selectedChatMode to the effective default. - On first Dyad Pro setup, default mode is set to local-agent automatically. <sup>Written for commit c265968422be75cc8b44760a684204fec198ccd0. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds a user-configurable default chat mode and ensures it’s applied consistently across the app. > > - **Core/Schema**: Add `defaultChatMode` to `UserSettings` and `getEffectiveDefaultChatMode()` helper (falls back to `local-agent` for Pro, otherwise `build`) > - **Settings UI**: New `DefaultChatModeSelector` in Workflow Settings; shows `Agent` option only when Pro is enabled > - **Behavior changes**: Apply effective default on home (`home.tsx`) and set `selectedChatMode` for new chats (`ChatList.tsx`) > - **Provider setup**: On first Dyad Pro key save, set `defaultChatMode` to `local-agent` (`ProviderSettingsPage.tsx`) > - **E2E/tests**: New and updated tests for defaults and mentions; stabilize dumps by normalizing `item_reference` IDs; updated chat mode option mapping > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 00d92922ec6cab870c9ff50fdd3912a58d97f0f1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude <noreply@anthropic.com>
上级 458b43d9
import { expect } from "@playwright/test";
import { test } from "./helpers/test_helper";
test("default chat mode - pro user defaults and setting change applies to new chat", async ({
po,
}) => {
await po.setUpDyadPro({ localAgent: true, autoApprove: true });
// Pro users should default to local-agent mode
await expect(
po.getHomeChatInputContainer().getByTestId("chat-mode-selector"),
).toHaveText("Agent");
// Change default chat mode to "agent" (Build with MCP) in settings
await po.goToSettingsTab();
const beforeSettings = po.recordSettings();
await po.page.getByLabel("Default Chat Mode").click();
await po.page.getByRole("option", { name: "Build with MCP" }).click();
po.snapshotSettingsDelta(beforeSettings);
// Import an app and create a new chat to verify the default is applied
await po.goToAppsTab();
await po.importApp("minimal");
await po.clickNewChat();
// Verify the chat mode selector shows the new default mode
await expect(po.page.getByTestId("chat-mode-selector")).toContainText(
"Build (MCP)",
);
});
test("default chat mode - non-pro user defaults to build", async ({ po }) => {
await po.setUp();
// Non-pro users should default to build mode
await expect(
po.getHomeChatInputContainer().getByTestId("chat-mode-selector"),
).toHaveText("Build");
});
......@@ -20,6 +20,26 @@ export const Timeout = {
MEDIUM: process.env.CI ? 30_000 : 15_000,
};
/**
* Normalizes item_reference IDs in the input array to be deterministic.
* item_reference objects have the shape { type: "item_reference", id: "msg_..." }
* where the ID is a timestamp-based value that changes between test runs.
*/
function normalizeItemReferences(dump: any): void {
const input = dump?.body?.input;
if (!Array.isArray(input)) {
return;
}
let refIndex = 0;
for (const item of input) {
if (item?.type === "item_reference" && item?.id) {
item.id = `[[ITEM_REF_${refIndex}]]`;
refIndex++;
}
}
}
/**
* Normalizes fileId hashes in versioned_files to be deterministic.
* FileIds are SHA-256 hashes that may include non-deterministic components
......@@ -328,6 +348,9 @@ export class PageObject {
}
await this.setUpDyadProvider();
await this.goToAppsTab();
if (!localAgent) {
await this.selectChatMode("build");
}
// Select a non-openAI model for local agent mode,
// since openAI models go to the responses API.
if (localAgent && !localAgentUseAutoModel) {
......@@ -411,13 +434,13 @@ export class PageObject {
async selectChatMode(mode: "build" | "ask" | "agent" | "local-agent") {
await this.page.getByTestId("chat-mode-selector").click();
// local-agent appears as "Agent v2" in the UI
const optionName =
mode === "local-agent"
? "Agent v2"
: mode === "agent"
? "Build with MCP"
: mode;
const mapping = {
build: "Build Generate and edit code",
ask: "Ask",
agent: "Build with MCP",
"local-agent": "Agent v2",
};
const optionName = mapping[mode];
await this.page
.getByRole("option", {
name: optionName,
......@@ -831,6 +854,8 @@ export class PageObject {
}
// Normalize fileIds to be deterministic based on content
normalizeVersionedFiles(parsedDump);
// Normalize item_reference IDs (e.g., msg_1234567890) to be deterministic
normalizeItemReferences(parsedDump);
expect(
JSON.stringify(parsedDump, null, 2).replace(/\\r\\n/g, "\\n"),
).toMatchSnapshot(name);
......
......@@ -3,7 +3,6 @@ import { testSkipIfWindows } from "./helpers/test_helper";
testSkipIfWindows("local-agent - auto model", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true, localAgentUseAutoModel: true });
await po.importApp("minimal");
await po.selectLocalAgentMode();
await po.sendPrompt("[dump]");
await po.snapshotServerDump("request");
......
......@@ -15,6 +15,7 @@ test("mention app (with pro)", async ({ po }) => {
await po.importApp("minimal-with-ai-rules");
await po.goToAppsTab();
await po.selectChatMode("build");
await po.sendPrompt("[dump] @app:minimal-with-ai-rules hi");
await po.snapshotServerDump("request");
......
......@@ -26,6 +26,7 @@ testSkipIfWindows(
await po.importApp("minimal-with-ai-rules");
await po.goToAppsTab();
await po.selectChatMode("build");
const proModesDialog = await po.openProModesDialog({
location: "home-chat-input-container",
});
......
- "defaultChatMode": "local-agent"
+ "defaultChatMode": "agent"
\ No newline at end of file
......@@ -16,13 +16,8 @@
]
},
{
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "\n <dyad-write path=\"file1.txt\">\n A file (2)\n </dyad-write>\n More\n EOM"
}
]
"type": "item_reference",
"id": "[[ITEM_REF_0]]"
},
{
"role": "user",
......
......@@ -9,6 +9,8 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { dropdownOpenAtom } from "@/atoms/uiAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import { showError, showSuccess } from "@/lib/toast";
import { useSettings } from "@/hooks/useSettings";
import { getEffectiveDefaultChatMode } from "@/lib/schemas";
import {
SidebarGroup,
SidebarGroupContent,
......@@ -35,6 +37,7 @@ export function ChatList({ show }: { show?: boolean }) {
const [selectedChatId, setSelectedChatId] = useAtom(selectedChatIdAtom);
const [selectedAppId] = useAtom(selectedAppIdAtom);
const [, setIsDropdownOpen] = useAtom(dropdownOpenAtom);
const { settings, updateSettings } = useSettings();
const { chats, loading, invalidateChats } = useChats(selectedAppId);
const routerState = useRouterState();
......@@ -87,6 +90,12 @@ export function ChatList({ show }: { show?: boolean }) {
// Create a new chat with an empty title for now
const chatId = await IpcClient.getInstance().createChat(selectedAppId);
// Set the default chat mode for the new chat
if (settings) {
const effectiveDefaultMode = getEffectiveDefaultChatMode(settings);
updateSettings({ selectedChatMode: effectiveDefaultMode });
}
// Navigate to the new chat
setSelectedChatId(chatId);
navigate({
......
import { useSettings } from "@/hooks/useSettings";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { ChatMode } from "@/lib/schemas";
import { isDyadProEnabled, getEffectiveDefaultChatMode } from "@/lib/schemas";
export function DefaultChatModeSelector() {
const { settings, updateSettings } = useSettings();
if (!settings) {
return null;
}
const isProEnabled = isDyadProEnabled(settings);
const effectiveDefault = getEffectiveDefaultChatMode(settings);
const handleDefaultChatModeChange = (value: ChatMode) => {
updateSettings({ defaultChatMode: value });
};
const getModeDisplayName = (mode: ChatMode) => {
switch (mode) {
case "build":
return "Build";
case "agent":
return "Build (MCP)";
case "local-agent":
return "Agent";
case "ask":
default:
throw new Error(`Unknown chat mode: ${mode}`);
}
};
return (
<div className="space-y-1">
<div className="flex items-center space-x-2">
<label
htmlFor="default-chat-mode"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Default Chat Mode
</label>
<Select
value={effectiveDefault}
onValueChange={handleDefaultChatModeChange}
>
<SelectTrigger className="w-40" id="default-chat-mode">
<SelectValue>{getModeDisplayName(effectiveDefault)}</SelectValue>
</SelectTrigger>
<SelectContent>
{isProEnabled && (
<SelectItem value="local-agent">
<div className="flex flex-col items-start">
<span className="font-medium">Agent</span>
<span className="text-xs text-muted-foreground">
Better at bigger tasks
</span>
</div>
</SelectItem>
)}
<SelectItem value="build">
<div className="flex flex-col items-start">
<span className="font-medium">Build</span>
<span className="text-xs text-muted-foreground">
Generate and edit code
</span>
</div>
</SelectItem>
<SelectItem value="agent">
<div className="flex flex-col items-start">
<span className="font-medium">Build with MCP</span>
<span className="text-xs text-muted-foreground">
Build with tools (MCP)
</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
The chat mode used when creating new chats.
</div>
</div>
);
}
......@@ -15,6 +15,7 @@ import {
UserSettings,
AzureProviderSetting,
VertexProviderSetting,
hasDyadProKey,
} from "@/lib/schemas";
import { ProviderSettingsHeader } from "./ProviderSettingsHeader";
......@@ -124,6 +125,9 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
setIsSaving(true);
setSaveError(null);
try {
// Check if this is the first time user is setting up Dyad Pro
const isNewDyadProSetup = isDyad && settings && !hasDyadProKey(settings);
const settingsUpdate: Partial<UserSettings> = {
providerSettings: {
...settings?.providerSettings,
......@@ -137,6 +141,10 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
};
if (isDyad) {
settingsUpdate.enableDyadPro = true;
// Set default chat mode to local-agent when user upgrades to pro
if (isNewDyadProSetup) {
settingsUpdate.defaultChatMode = "local-agent";
}
}
await updateSettings(settingsUpdate);
setApiKeyInput(""); // Clear input on success
......
......@@ -291,6 +291,7 @@ export const UserSettingsSchema = z
selectedThemeId: z.string().optional(),
enableSupabaseWriteSqlMigration: z.boolean().optional(),
selectedChatMode: ChatModeSchema.optional(),
defaultChatMode: ChatModeSchema.optional(),
acceptedCommunityCode: z.boolean().optional(),
zoomLevel: ZoomLevelSchema.optional(),
......@@ -330,6 +331,27 @@ export function hasDyadProKey(settings: UserSettings): boolean {
return !!settings.providerSettings?.auto?.apiKey?.value;
}
/**
* Gets the effective default chat mode based on settings and pro status.
* - If defaultChatMode is set and valid for the user's Pro status, use it
* - If defaultChatMode is "local-agent" but user doesn't have Pro, fall back to "build"
* - If defaultChatMode is NOT set but user has Dyad Pro enabled, treat as "local-agent"
* - If not pro, treat as "build"
*/
export function getEffectiveDefaultChatMode(settings: UserSettings): ChatMode {
if (settings.defaultChatMode) {
// "local-agent" requires Pro - fall back to "build" if user lost Pro access
if (
settings.defaultChatMode === "local-agent" &&
!isDyadProEnabled(settings)
) {
return "build";
}
return settings.defaultChatMode;
}
return isDyadProEnabled(settings) ? "local-agent" : "build";
}
export function isSupabaseConnected(settings: UserSettings | null): boolean {
if (!settings) {
return false;
......
......@@ -8,7 +8,7 @@ import { useLoadApps } from "@/hooks/useLoadApps";
import { useSettings } from "@/hooks/useSettings";
import { SetupBanner } from "@/components/SetupBanner";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { useStreamChat } from "@/hooks/useStreamChat";
import { HomeChatInput } from "@/components/chat/HomeChatInput";
import { usePostHog } from "posthog-js/react";
......@@ -39,7 +39,7 @@ import {
ManageDyadProButton,
SetupDyadProButton,
} from "@/components/ProBanner";
import { hasDyadProKey } from "@/lib/schemas";
import { hasDyadProKey, getEffectiveDefaultChatMode } from "@/lib/schemas";
// Adding an export for attachments
export interface HomeSubmitOptions {
......@@ -139,6 +139,18 @@ export default function HomePage() {
}
}, [appId, navigate]);
// Apply default chat mode when navigating to home page
const hasAppliedDefaultChatMode = useRef(false);
useEffect(() => {
if (settings && !hasAppliedDefaultChatMode.current) {
hasAppliedDefaultChatMode.current = true;
const effectiveDefaultMode = getEffectiveDefaultChatMode(settings);
if (settings.selectedChatMode !== effectiveDefaultMode) {
updateSettings({ selectedChatMode: effectiveDefaultMode });
}
}
}, [settings, updateSettings]);
const handleSubmit = async (options?: HomeSubmitOptions) => {
const attachments = options?.attachments || [];
......
......@@ -28,6 +28,7 @@ import { NodePathSelector } from "@/components/NodePathSelector";
import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings";
import { AgentToolsSettings } from "@/components/settings/AgentToolsSettings";
import { ZoomSelector } from "@/components/ZoomSelector";
import { DefaultChatModeSelector } from "@/components/DefaultChatModeSelector";
import { useSetAtom } from "jotai";
import { activeSettingsSectionAtom } from "@/atoms/viewAtoms";
......@@ -312,7 +313,11 @@ export function WorkflowSettings() {
Workflow Settings
</h2>
<div className="space-y-1">
<div className="mt-4">
<DefaultChatModeSelector />
</div>
<div className="space-y-1 mt-4">
<AutoApproveSwitch showToast={false} />
<div className="text-sm text-gray-500 dark:text-gray-400">
This will automatically approve code changes and run them.
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论