Support web search (#1370)

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

## Summary by cubic
Adds web search to Dyad Pro chats with a new UI, tag parsing, and a Pro
Mode toggle that wires through to the engine.

- **New Features**
  - Pro Mode toggle: “Web Search” (settings.enableProWebSearch).
  - New custom tags: dyad-web-search, dyad-web-search-result, dyad-read.
- Collapsible Web Search Result UI with in-progress badge and markdown
rendering.
- Engine integration: passes enable_web_search and activates DyadEngine
when web search is on.

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Will Chen
2025-09-24 19:39:39 -07:00
committed by GitHub
parent 42a406e3ab
commit d96e95c1da
8 changed files with 217 additions and 1 deletions

View File

@@ -19,6 +19,12 @@ import { hasDyadProKey, type UserSettings } from "@/lib/schemas";
export function ProModeSelector() {
const { settings, updateSettings } = useSettings();
const toggleWebSearch = () => {
updateSettings({
enableProWebSearch: !settings?.enableProWebSearch,
});
};
const toggleLazyEdits = () => {
updateSettings({
enableProLazyEditsMode: !settings?.enableProLazyEditsMode,
@@ -105,6 +111,15 @@ export function ProModeSelector() {
settingEnabled={Boolean(settings?.enableDyadPro)}
toggle={toggleProEnabled}
/>
<SelectorRow
id="web-search"
label="Web Search"
description="Search the web for information"
tooltip="Uses the web to search for information"
isTogglable={proModeTogglable}
settingEnabled={Boolean(settings?.enableProWebSearch)}
toggle={toggleWebSearch}
/>
<SelectorRow
id="lazy-edits"
label="Turbo Edits"

View File

@@ -19,6 +19,9 @@ import { DyadProblemSummary } from "./DyadProblemSummary";
import { IpcClient } from "@/ipc/ipc_client";
import { DyadMcpToolCall } from "./DyadMcpToolCall";
import { DyadMcpToolResult } from "./DyadMcpToolResult";
import { DyadWebSearchResult } from "./DyadWebSearchResult";
import { DyadWebSearch } from "./DyadWebSearch";
import { DyadRead } from "./DyadRead";
interface DyadMarkdownParserProps {
content: string;
@@ -124,6 +127,9 @@ function preprocessUnclosedTags(content: string): {
"dyad-chat-summary",
"dyad-edit",
"dyad-codebase-context",
"dyad-web-search-result",
"dyad-web-search",
"dyad-read",
"think",
"dyad-command",
"dyad-mcp-tool-call",
@@ -193,6 +199,9 @@ function parseCustomTags(content: string): ContentPiece[] {
"dyad-chat-summary",
"dyad-edit",
"dyad-codebase-context",
"dyad-web-search-result",
"dyad-web-search",
"dyad-read",
"think",
"dyad-command",
"dyad-mcp-tool-call",
@@ -282,6 +291,40 @@ function renderCustomTag(
const { tag, attributes, content, inProgress } = tagInfo;
switch (tag) {
case "dyad-read":
return (
<DyadRead
node={{
properties: {
path: attributes.path || "",
},
}}
>
{content}
</DyadRead>
);
case "dyad-web-search":
return (
<DyadWebSearch
node={{
properties: {},
}}
>
{content}
</DyadWebSearch>
);
case "dyad-web-search-result":
return (
<DyadWebSearchResult
node={{
properties: {
state: getState({ isStreaming, inProgress }),
},
}}
>
{content}
</DyadWebSearchResult>
);
case "think":
return (
<DyadThink

View File

@@ -0,0 +1,44 @@
import type React from "react";
import type { ReactNode } from "react";
import { FileText } from "lucide-react";
interface DyadReadProps {
children?: ReactNode;
node?: any;
path?: string;
}
export const DyadRead: React.FC<DyadReadProps> = ({
children,
node,
path: pathProp,
}) => {
const path = pathProp || node?.properties?.path || "";
const fileName = path ? path.split("/").pop() : "";
return (
<div className="bg-(--background-lightest) rounded-lg px-4 py-2 border border-border my-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText size={16} className="text-gray-600" />
{fileName && (
<span className="text-gray-700 dark:text-gray-300 font-medium text-sm">
{fileName}
</span>
)}
<div className="text-xs text-gray-600 font-medium">Read</div>
</div>
</div>
{path && (
<div className="text-xs text-gray-500 dark:text-gray-400 font-medium mb-1">
{path}
</div>
)}
{children && (
<div className="text-sm text-gray-600 dark:text-gray-300 mt-2">
{children}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,31 @@
import type React from "react";
import type { ReactNode } from "react";
import { Globe } from "lucide-react";
interface DyadWebSearchProps {
children?: ReactNode;
node?: any;
query?: string;
}
export const DyadWebSearch: React.FC<DyadWebSearchProps> = ({
children,
node: _node,
query: queryProp,
}) => {
const query = queryProp || (typeof children === "string" ? children : "");
return (
<div className="bg-(--background-lightest) rounded-lg px-4 py-2 border my-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Globe size={16} className="text-blue-600" />
<div className="text-xs text-blue-600 font-medium">Web Search</div>
</div>
</div>
<div className="text-sm italic text-gray-600 dark:text-gray-300 mt-2">
{query || children}
</div>
</div>
);
};

View File

@@ -0,0 +1,78 @@
import React, { useEffect, useState } from "react";
import { ChevronDown, ChevronUp, Globe, Loader } from "lucide-react";
import { VanillaMarkdownParser } from "./DyadMarkdownParser";
import { CustomTagState } from "./stateTypes";
interface DyadWebSearchResultProps {
node?: any;
children?: React.ReactNode;
}
export const DyadWebSearchResult: React.FC<DyadWebSearchResultProps> = ({
children,
node,
}) => {
const state = node?.properties?.state as CustomTagState;
const inProgress = state === "pending";
const [isExpanded, setIsExpanded] = useState(inProgress);
// Collapse when transitioning from in-progress to not-in-progress
useEffect(() => {
if (!inProgress && isExpanded) {
setIsExpanded(false);
}
}, [inProgress]);
return (
<div
className={`relative bg-(--background-lightest) dark:bg-zinc-900 hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
inProgress ? "border-blue-500" : "border-border"
}`}
onClick={() => setIsExpanded(!isExpanded)}
role="button"
aria-expanded={isExpanded}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setIsExpanded(!isExpanded);
}
}}
>
{/* Top-left label badge */}
<div
className="absolute top-2 left-2 flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold text-blue-600 bg-white dark:bg-zinc-900"
style={{ zIndex: 1 }}
>
<Globe size={16} className="text-blue-600" />
<span>Web Search Result</span>
{inProgress && (
<Loader size={14} className="ml-1 text-blue-600 animate-spin" />
)}
</div>
{/* Indicator icon */}
<div className="absolute top-2 right-2 p-1 text-gray-500">
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</div>
{/* Main content with smooth transition */}
<div
className="pt-6 overflow-hidden transition-all duration-300 ease-in-out"
style={{
maxHeight: isExpanded ? "none" : "0px",
opacity: isExpanded ? 1 : 0,
marginBottom: isExpanded ? "0" : "-6px",
}}
>
<div className="px-0 text-sm text-gray-600 dark:text-gray-300">
{typeof children === "string" ? (
<VanillaMarkdownParser content={children} />
) : (
children
)}
</div>
</div>
</div>
);
};

View File

@@ -86,7 +86,8 @@ export async function getModelClient(
if (providerConfig.gatewayPrefix != null || dyadEngineUrl) {
const isEngineEnabled =
settings.enableProSmartFilesContextMode ||
settings.enableProLazyEditsMode;
settings.enableProLazyEditsMode ||
settings.enableProWebSearch;
const provider = isEngineEnabled
? createDyadEngine({
apiKey: dyadApiKey,
@@ -100,6 +101,7 @@ export async function getModelClient(
enableSmartFilesContext: settings.enableProSmartFilesContextMode,
// Keep in sync with getCurrentValue in ProModeSelector.tsx
smartContextMode: settings.proSmartContextOption ?? "balanced",
enableWebSearch: settings.enableProWebSearch,
},
settings,
})

View File

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

View File

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