smart context v3 (#1022)

<!-- This is an auto-generated description by cubic. -->

## Summary by cubic
Adds Smart Context v3 with selectable modes (Off, Conservative,
Balanced) and surfaces token savings in chat. Also improves token
estimation by counting per-file tokens when Smart Context is enabled.

- **New Features**
- Smart Context selector in Pro settings with three options.
Conservative is the default when enabled without an explicit choice.
- New setting: proSmartContextOption ("balanced"); undefined implies
Conservative.
- Engine now receives enable_smart_files_context and smart_context_mode.
- Chat shows a DyadTokenSavings card when the message contains
token-savings?original-tokens=...&smart-context-tokens=..., with percent
saved and a tooltip for exact tokens.
- Token estimation uses extracted file contents for accuracy when Pro +
Smart Context is on; otherwise falls back to formatted codebase output.

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Will Chen
2025-08-20 14:16:07 -07:00
committed by GitHub
parent 34215db141
commit 4e9a927a7b
18 changed files with 764 additions and 26 deletions

View File

@@ -14,7 +14,7 @@ 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 } from "@/lib/schemas";
import { hasDyadProKey, type UserSettings } from "@/lib/schemas";
export function ProModeSelector() {
const { settings, updateSettings } = useSettings();
@@ -25,10 +25,25 @@ export function ProModeSelector() {
});
};
const toggleSmartContext = () => {
updateSettings({
enableProSmartFilesContextMode: !settings?.enableProSmartFilesContextMode,
});
const handleSmartContextChange = (
newValue: "off" | "conservative" | "balanced",
) => {
if (newValue === "off") {
updateSettings({
enableProSmartFilesContextMode: false,
proSmartContextOption: undefined,
});
} else if (newValue === "conservative") {
updateSettings({
enableProSmartFilesContextMode: true,
proSmartContextOption: undefined, // Conservative is the default when enabled but no option set
});
} else if (newValue === "balanced") {
updateSettings({
enableProSmartFilesContextMode: true,
proSmartContextOption: "balanced",
});
}
};
const toggleProEnabled = () => {
@@ -99,14 +114,10 @@ export function ProModeSelector() {
settingEnabled={Boolean(settings?.enableProLazyEditsMode)}
toggle={toggleLazyEdits}
/>
<SelectorRow
id="smart-context"
label="Smart Context"
description="Optimizes your AI's code context"
tooltip="Improve efficiency and save credits working on large codebases."
<SmartContextSelector
isTogglable={proModeTogglable}
settingEnabled={Boolean(settings?.enableProSmartFilesContextMode)}
toggle={toggleSmartContext}
settings={settings}
onValueChange={handleSmartContextChange}
/>
</div>
</div>
@@ -168,3 +179,83 @@ function SelectorRow({
</div>
);
}
function SmartContextSelector({
isTogglable,
settings,
onValueChange,
}: {
isTogglable: boolean;
settings: UserSettings | null;
onValueChange: (value: "off" | "conservative" | "balanced") => void;
}) {
// Determine current value based on settings
const getCurrentValue = (): "off" | "conservative" | "balanced" => {
if (!settings?.enableProSmartFilesContextMode) {
return "off";
}
if (settings?.proSmartContextOption === "balanced") {
return "balanced";
}
// If enabled but no option set (undefined/falsey), it's conservative
return "conservative";
};
const currentValue = getCurrentValue();
return (
<div className="space-y-3">
<div className="space-y-1.5">
<Label className={!isTogglable ? "text-muted-foreground/50" : ""}>
Smart Context
</Label>
<div className="flex items-center gap-1">
<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">
Improve efficiency and save credits working on large codebases.
</TooltipContent>
</Tooltip>
<p
className={`text-xs ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
>
Optimizes your AI's code context
</p>
</div>
</div>
<div className="inline-flex rounded-md border border-input">
<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>
<Button
variant={currentValue === "conservative" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("conservative")}
disabled={!isTogglable}
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
>
Conservative
</Button>
<Button
variant={currentValue === "balanced" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("balanced")}
disabled={!isTogglable}
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
>
Balanced
</Button>
</div>
</div>
);
}

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { Brain, ChevronDown, ChevronUp, Loader } from "lucide-react";
import { VanillaMarkdownParser } from "./DyadMarkdownParser";
import { CustomTagState } from "./stateTypes";
import { DyadTokenSavings } from "./DyadTokenSavings";
interface DyadThinkProps {
node?: any;
@@ -13,6 +14,26 @@ export const DyadThink: React.FC<DyadThinkProps> = ({ children, node }) => {
const inProgress = state === "pending";
const [isExpanded, setIsExpanded] = useState(inProgress);
// Check if content matches token savings format
const tokenSavingsMatch =
typeof children === "string"
? children.match(
/^dyad-token-savings\?original-tokens=([0-9.]+)&smart-context-tokens=([0-9.]+)$/,
)
: null;
// If it's token savings format, render DyadTokenSavings component
if (tokenSavingsMatch) {
const originalTokens = parseFloat(tokenSavingsMatch[1]);
const smartContextTokens = parseFloat(tokenSavingsMatch[2]);
return (
<DyadTokenSavings
originalTokens={originalTokens}
smartContextTokens={smartContextTokens}
/>
);
}
// Collapse when transitioning from in-progress to not-in-progress
useEffect(() => {
if (!inProgress && isExpanded) {

View File

@@ -0,0 +1,36 @@
import React from "react";
import { Zap } from "lucide-react";
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
interface DyadTokenSavingsProps {
originalTokens: number;
smartContextTokens: number;
}
export const DyadTokenSavings: React.FC<DyadTokenSavingsProps> = ({
originalTokens,
smartContextTokens,
}) => {
const tokensSaved = originalTokens - smartContextTokens;
const percentageSaved = Math.round((tokensSaved / originalTokens) * 100);
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="bg-green-50 dark:bg-green-950 hover:bg-green-100 dark:hover:bg-green-900 rounded-lg px-4 py-2 border border-green-200 dark:border-green-800 my-2 cursor-pointer">
<div className="flex items-center gap-2 text-green-700 dark:text-green-300">
<Zap size={16} className="text-green-600 dark:text-green-400" />
<span className="text-xs font-medium">
Saved {percentageSaved}% of codebase tokens with Smart Context
</span>
</div>
</div>
</TooltipTrigger>
<TooltipContent side="top" align="center">
<div className="text-left">
Saved {Math.round(tokensSaved).toLocaleString()} tokens
</div>
</TooltipContent>
</Tooltip>
);
};

View File

@@ -86,13 +86,23 @@ export function registerTokenCountHandlers() {
if (chat.app) {
const appPath = getDyadAppPath(chat.app.path);
codebaseInfo = (
await extractCodebase({
appPath,
chatContext: validateChatContext(chat.app.chatContext),
})
).formattedOutput;
codebaseTokens = estimateTokens(codebaseInfo);
const { formattedOutput, files } = await extractCodebase({
appPath,
chatContext: validateChatContext(chat.app.chatContext),
});
codebaseInfo = formattedOutput;
if (settings.enableDyadPro && settings.enableProSmartFilesContextMode) {
codebaseTokens = estimateTokens(
files
// It doesn't need to be the exact format but it's just to get a token estimate
.map(
(file) => `<dyad-file=${file.path}>${file.content}</dyad-file>`,
)
.join("\n\n"),
);
} else {
codebaseTokens = estimateTokens(codebaseInfo);
}
logger.log(
`Extracted codebase information from ${appPath}, tokens: ${codebaseTokens}`,
);

View File

@@ -84,6 +84,7 @@ export async function getModelClient(
? false
: settings.enableProLazyEditsMode,
enableSmartFilesContext: settings.enableProSmartFilesContextMode,
smartContextMode: settings.proSmartContextOption,
},
settings,
})

View File

@@ -44,6 +44,7 @@ or to provide a custom fetch implementation for e.g. testing.
dyadOptions: {
enableLazyEdits?: boolean;
enableSmartFilesContext?: boolean;
smartContextMode?: "balanced";
};
settings: UserSettings;
}
@@ -149,6 +150,7 @@ export function createDyadEngine(
enable_lazy_edits: options.dyadOptions.enableLazyEdits,
enable_smart_files_context:
options.dyadOptions.enableSmartFilesContext,
smart_context_mode: options.dyadOptions.smartContextMode,
};
}

View File

@@ -160,6 +160,7 @@ export const UserSettingsSchema = z.object({
thinkingBudget: z.enum(["low", "medium", "high"]).optional(),
enableProLazyEditsMode: z.boolean().optional(),
enableProSmartFilesContextMode: z.boolean().optional(),
proSmartContextOption: z.enum(["balanced"]).optional(),
selectedTemplateId: z.string(),
enableSupabaseWriteSqlMigration: z.boolean().optional(),
selectedChatMode: ChatModeSchema.optional(),