Files
moreminimore-vibe/src/components/ProModeSelector.tsx
Will Chen 40aeed1456 default to deep context (#1891)
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Default Smart Files context to deep and align UI and stream handler
logic so any non-balanced option uses deep when Pro mode is enabled.
> 
> - **Smart Context behavior**
> - UI (`src/components/ProModeSelector.tsx`): default
`getCurrentValue()` to `"deep"` when Pro Smart Files mode is enabled
without an explicit option.
> - Engine (`src/ipc/handlers/chat_stream_handlers.ts`):
`isDeepContextEnabled` now requires `enableProSmartFilesContextMode` and
treats any option other than `"balanced"` as deep; `smartContextMode`
set accordingly and `versioned_files` used when deep.
> - **Snapshots**
> - Update e2e snapshots to reflect `smart_context_mode: "deep"` and new
`dyad_options.versioned_files` structure.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
7426890467d60b671a7a9712f7544a35ed922981. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->





<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Default smart files context to deep and align the UI and stream handler
so anything not “balanced” uses deep when Pro Smart Files mode is on.
This prevents mismatches and makes deep context the default.

- **Bug Fixes**
  - ProModeSelector: default to deep when no option is set.
- chat_stream_handlers: isDeepContextEnabled requires Pro Smart Files
mode and treats non-“balanced” as deep; smartContextMode set to deep
accordingly.
  - Engine payload: use versioned_files; update e2e snapshots.

<sup>Written for commit 7426890467d60b671a7a9712f7544a35ed922981.
Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2025-12-04 22:13:49 -08:00

397 lines
12 KiB
TypeScript

import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Sparkles, Info } from "lucide-react";
import { useSettings } from "@/hooks/useSettings";
import { IpcClient } from "@/ipc/ipc_client";
import { hasDyadProKey, type UserSettings } from "@/lib/schemas";
export function ProModeSelector() {
const { settings, updateSettings } = useSettings();
const toggleWebSearch = () => {
updateSettings({
enableProWebSearch: !settings?.enableProWebSearch,
});
};
const handleTurboEditsChange = (newValue: "off" | "v1" | "v2") => {
updateSettings({
enableProLazyEditsMode: newValue !== "off",
proLazyEditsMode: newValue,
});
};
const handleSmartContextChange = (newValue: "off" | "deep" | "balanced") => {
if (newValue === "off") {
updateSettings({
enableProSmartFilesContextMode: false,
proSmartContextOption: undefined,
});
} else if (newValue === "deep") {
updateSettings({
enableProSmartFilesContextMode: true,
proSmartContextOption: "deep",
});
} else if (newValue === "balanced") {
updateSettings({
enableProSmartFilesContextMode: true,
proSmartContextOption: "balanced",
});
}
};
const toggleProEnabled = () => {
updateSettings({
enableDyadPro: !settings?.enableDyadPro,
});
};
const hasProKey = settings ? hasDyadProKey(settings) : false;
const proModeTogglable = hasProKey && Boolean(settings?.enableDyadPro);
return (
<Popover>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="has-[>svg]:px-1.5 flex items-center gap-1.5 h-8 border-primary/50 hover:bg-primary/10 font-medium shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
>
<Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium text-xs-sm">Pro</span>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Configure Dyad Pro settings</TooltipContent>
</Tooltip>
<PopoverContent className="w-80 border-primary/20">
<div className="space-y-4">
<div className="space-y-1">
<h4 className="font-medium flex items-center gap-1.5">
<Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium">Dyad Pro</span>
</h4>
<div className="h-px bg-gradient-to-r from-primary/50 via-primary/20 to-transparent" />
</div>
{!hasProKey && (
<div className="text-sm text-center text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<a
className="inline-flex items-center justify-center gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
onClick={() => {
IpcClient.getInstance().openExternalUrl(
"https://dyad.sh/pro#ai",
);
}}
>
Unlock Pro modes
</a>
</TooltipTrigger>
<TooltipContent>
Visit dyad.sh/pro to unlock Pro features
</TooltipContent>
</Tooltip>
</div>
)}
<div className="flex flex-col gap-5">
<SelectorRow
id="pro-enabled"
label="Enable Dyad Pro"
tooltip="Uses Dyad Pro AI credits for the main AI model and Pro modes."
isTogglable={hasProKey}
settingEnabled={Boolean(settings?.enableDyadPro)}
toggle={toggleProEnabled}
/>
<SelectorRow
id="web-search"
label="Web Access"
tooltip="Allows Dyad to access the web (e.g. search for information)"
isTogglable={proModeTogglable}
settingEnabled={Boolean(settings?.enableProWebSearch)}
toggle={toggleWebSearch}
/>
<TurboEditsSelector
isTogglable={proModeTogglable}
settings={settings}
onValueChange={handleTurboEditsChange}
/>
<SmartContextSelector
isTogglable={proModeTogglable}
settings={settings}
onValueChange={handleSmartContextChange}
/>
</div>
</div>
</PopoverContent>
</Popover>
);
}
function SelectorRow({
id,
label,
tooltip,
isTogglable,
settingEnabled,
toggle,
}: {
id: string;
label: string;
tooltip: string;
isTogglable: boolean;
settingEnabled: boolean;
toggle: () => void;
}) {
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Label
htmlFor={id}
className={!isTogglable ? "text-muted-foreground/50" : ""}
>
{label}
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-72">
{tooltip}
</TooltipContent>
</Tooltip>
</div>
<Switch
id={id}
checked={isTogglable ? settingEnabled : false}
onCheckedChange={toggle}
disabled={!isTogglable}
/>
</div>
);
}
function TurboEditsSelector({
isTogglable,
settings,
onValueChange,
}: {
isTogglable: boolean;
settings: UserSettings | null;
onValueChange: (value: "off" | "v1" | "v2") => void;
}) {
// Determine current value based on settings
const getCurrentValue = (): "off" | "v1" | "v2" => {
if (!settings?.enableProLazyEditsMode) {
return "off";
}
if (settings?.proLazyEditsMode === "v1") {
return "v1";
}
if (settings?.proLazyEditsMode === "v2") {
return "v2";
}
// Keep in sync with getModelClient in get_model_client.ts
// If enabled but no option set (undefined/falsey), it's v1
return "v1";
};
const currentValue = getCurrentValue();
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className={!isTogglable ? "text-muted-foreground/50" : ""}>
Turbo Edits
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-72">
Edits files efficiently without full rewrites.
<br />
<ul className="list-disc ml-4">
<li>
<b>Classic:</b> Uses a smaller model to complete edits.
</li>
<li>
<b>Search & replace:</b> Find and replaces specific text blocks.
</li>
</ul>
</TooltipContent>
</Tooltip>
</div>
<div
className="inline-flex rounded-md border border-input"
data-testid="turbo-edits-selector"
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "off" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("off")}
disabled={!isTogglable}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
>
Off
</Button>
</TooltipTrigger>
<TooltipContent>Disable Turbo Edits</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "v1" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("v1")}
disabled={!isTogglable}
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
>
Classic
</Button>
</TooltipTrigger>
<TooltipContent>
Uses a smaller model to complete edits
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "v2" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("v2")}
disabled={!isTogglable}
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
>
Search & replace
</Button>
</TooltipTrigger>
<TooltipContent>
Find and replaces specific text blocks
</TooltipContent>
</Tooltip>
</div>
</div>
);
}
function SmartContextSelector({
isTogglable,
settings,
onValueChange,
}: {
isTogglable: boolean;
settings: UserSettings | null;
onValueChange: (value: "off" | "balanced" | "deep") => void;
}) {
// Determine current value based on settings
const getCurrentValue = (): "off" | "conservative" | "balanced" | "deep" => {
if (!settings?.enableProSmartFilesContextMode) {
return "off";
}
if (settings?.proSmartContextOption === "deep") {
return "deep";
}
if (settings?.proSmartContextOption === "balanced") {
return "balanced";
}
// Keep logic in sync with isDeepContextEnabled in chat_stream_handlers.ts
return "deep";
};
const currentValue = getCurrentValue();
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className={!isTogglable ? "text-muted-foreground/50" : ""}>
Smart Context
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-72">
Selects the most relevant files as context to save credits working
on large codebases.
</TooltipContent>
</Tooltip>
</div>
<div
className="inline-flex rounded-md border border-input"
data-testid="smart-context-selector"
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "off" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("off")}
disabled={!isTogglable}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
>
Off
</Button>
</TooltipTrigger>
<TooltipContent>Disable Smart Context</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "balanced" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("balanced")}
disabled={!isTogglable}
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
>
Balanced
</Button>
</TooltipTrigger>
<TooltipContent>
Selects most relevant files with balanced context size
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "deep" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("deep")}
disabled={!isTogglable}
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
>
Deep
</Button>
</TooltipTrigger>
<TooltipContent>
<b>Experimental:</b> Keeps full conversation history for maximum
context and cache-optimized to control costs
</TooltipContent>
</Tooltip>
</div>
</div>
);
}