From 9df57e9fad041749d498716df38bd95f87eaf266 Mon Sep 17 00:00:00 2001 From: Leszek Date: Fri, 26 Jun 2026 00:23:52 +0200 Subject: [PATCH 1/2] feat: configurable Deep Research concurrency and max sources Add deepResearchConcurrency and deepResearchMaxSources settings with UI controls in the web search section, wired through resolveSearchConfig so they persist across restarts. processQueue and collectResearchSources honor the limits, falling back to 3 / 20 when unset. --- .../settings/sections/web-search-section.tsx | 64 +++++++++++++++++++ src/i18n/en.json | 6 ++ src/i18n/zh.json | 6 ++ src/lib/deep-research.ts | 9 ++- src/stores/wiki-store.ts | 6 ++ 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/components/settings/sections/web-search-section.tsx b/src/components/settings/sections/web-search-section.tsx index 6ef8e50f4..9ba72f093 100644 --- a/src/components/settings/sections/web-search-section.tsx +++ b/src/components/settings/sections/web-search-section.tsx @@ -148,6 +148,14 @@ export function WebSearchSection() { setTimeout(() => setSavedId((cur) => (cur === "anytxt" ? null : cur)), 1500) } + function updateDeepResearch( + patch: Pick, + ) { + persist(resolveSearchConfig({ ...resolvedConfig, ...patch })).catch(() => {}) + setSavedId("deepResearch") + setTimeout(() => setSavedId((cur) => (cur === "deepResearch" ? null : cur)), 1500) + } + return (
@@ -273,6 +281,62 @@ export function WebSearchSection() {

+
+
+
+ +

+ {t("settings.sections.webSearch.deepResearchDescription")} +

+
+ {savedId === "deepResearch" && ( + + {t("settings.sections.webSearch.savedBadge")} + + )} +
+
+
+ + { + const n = Math.floor(Number(e.target.value)) + updateDeepResearch({ + deepResearchConcurrency: Number.isFinite(n) ? Math.min(8, Math.max(1, n)) : 1, + }) + }} + /> +

+ {t("settings.sections.webSearch.deepResearchConcurrencyHint")} +

+
+
+ + { + const n = Math.floor(Number(e.target.value)) + updateDeepResearch({ + deepResearchMaxSources: Number.isFinite(n) ? Math.min(100, Math.max(1, n)) : 1, + }) + }} + /> +

+ {t("settings.sections.webSearch.deepResearchMaxSourcesHint")} +

+
+
+
+
{SEARCH_PROVIDERS.map((provider) => { diff --git a/src/i18n/en.json b/src/i18n/en.json index ef3945f3e..557c6fe5c 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -413,6 +413,12 @@ "sourceWeb": "Web Search", "sourceAnyTxt": "AnyTXT", "sourceBoth": "Both", + "deepResearchTitle": "Deep Research", + "deepResearchDescription": "Tune how many research tasks run at once and how many sources are merged into each page.", + "deepResearchConcurrency": "Concurrent research tasks", + "deepResearchConcurrencyHint": "How many Deep Research tasks run in parallel. Lower this on rate-limited or slow endpoints.", + "deepResearchMaxSources": "Max merged sources", + "deepResearchMaxSourcesHint": "Upper bound on the total web + local results merged into each research page.", "webProviders": "Web Search providers", "anyTxtTitle": "AnyTXT local file search", "anyTxtDescription": "Use AnyTXT's local JSON-RPC API as a Deep Research source. Keep ATGUI.exe running before starting research.", diff --git a/src/i18n/zh.json b/src/i18n/zh.json index d95a1440f..e9b7b4a8a 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -413,6 +413,12 @@ "sourceWeb": "网页搜索", "sourceAnyTxt": "AnyTXT", "sourceBoth": "两者都用", + "deepResearchTitle": "深度研究", + "deepResearchDescription": "调整同时运行的研究任务数量,以及合并到每个页面的来源数量。", + "deepResearchConcurrency": "并发研究任务", + "deepResearchConcurrencyHint": "同时并行运行的 Deep Research 任务数量。在速率受限或较慢的端点上请调低。", + "deepResearchMaxSources": "最大合并来源数", + "deepResearchMaxSourcesHint": "合并到每个研究页面的网页与本地结果总数上限。", "webProviders": "网页搜索 Provider", "anyTxtTitle": "AnyTXT 本地文件搜索", "anyTxtDescription": "把 AnyTXT 的本地 JSON-RPC API 作为 Deep Research 的信息来源。开始研究前请保持 ATGUI.exe 运行。", diff --git a/src/lib/deep-research.ts b/src/lib/deep-research.ts index febc12b4d..f72849ee3 100644 --- a/src/lib/deep-research.ts +++ b/src/lib/deep-research.ts @@ -88,6 +88,7 @@ export async function collectResearchSources( options: CollectResearchSourceOptions = {}, ): Promise { const resolvedSearchConfig = resolveSearchConfig(searchConfig) + const maxSources = resolvedSearchConfig.deepResearchMaxSources ?? MAX_RESEARCH_SOURCES const sourceMode = resolvedSearchConfig.deepResearchSource ?? "web" const useWeb = sourceMode === "web" || sourceMode === "both" const useAnyTxt = hasAnyTxtSource(resolvedSearchConfig) && hasConfiguredAnyTxt(resolvedSearchConfig.anyTxt) @@ -99,9 +100,9 @@ export async function collectResearchSources( function addResults(results: import("./web-search").WebSearchResult[]) { for (const r of results) { - if (allResults.length >= MAX_RESEARCH_SOURCES) { + if (allResults.length >= maxSources) { if (!cappedWarned) { - console.info(`[DeepResearch] capped at ${MAX_RESEARCH_SOURCES} research sources; later results were truncated.`) + console.info(`[DeepResearch] capped at ${maxSources} research sources; later results were truncated.`) cappedWarned = true } return @@ -170,7 +171,9 @@ function processQueue( ) { const store = useResearchStore.getState() const running = store.getRunningCount() - const available = store.maxConcurrent - running + const resolved = resolveSearchConfig(searchConfig) + const concurrencyLimit = resolved.deepResearchConcurrency ?? store.maxConcurrent + const available = concurrencyLimit - running for (let i = 0; i < available; i++) { const next = useResearchStore.getState().getNextQueued() diff --git a/src/stores/wiki-store.ts b/src/stores/wiki-store.ts index a381495d1..2572a7a1e 100644 --- a/src/stores/wiki-store.ts +++ b/src/stores/wiki-store.ts @@ -92,6 +92,10 @@ interface SearchApiConfig { providerConfigs?: SearchProviderConfigs deepResearchSource?: DeepResearchSource anyTxt?: AnyTxtConfig + /** Max Deep Research tasks running in parallel. Falls back to 3 when unset. */ + deepResearchConcurrency?: number + /** Upper bound on merged web + local sources per research page. Falls back to 20 when unset. */ + deepResearchMaxSources?: number } interface EmbeddingConfig { @@ -414,6 +418,8 @@ export const useWikiStore = create((set) => ({ searXngCategories: ["general"], providerConfigs: {}, deepResearchSource: "web", + deepResearchConcurrency: 3, + deepResearchMaxSources: 20, anyTxt: { enabled: false, endpoint: "http://127.0.0.1:9920", From fa43e442be4006f1718edb89fdf814d1bde33e49 Mon Sep 17 00:00:00 2001 From: Leszek Date: Fri, 26 Jun 2026 00:35:53 +0200 Subject: [PATCH 2/2] fix: allow clearing Deep Research number inputs Empty input now sets the value to undefined and shows the default via placeholder, instead of clamping to 1 on every keystroke. Lets users backspace and retype values; falls back to 3 / 20 when unset. --- .../settings/sections/web-search-section.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/settings/sections/web-search-section.tsx b/src/components/settings/sections/web-search-section.tsx index 9ba72f093..bbe9a14aa 100644 --- a/src/components/settings/sections/web-search-section.tsx +++ b/src/components/settings/sections/web-search-section.tsx @@ -303,11 +303,17 @@ export function WebSearchSection() { min={1} max={8} step={1} - value={resolvedConfig.deepResearchConcurrency ?? 3} + value={resolvedConfig.deepResearchConcurrency ?? ""} + placeholder="3" onChange={(e) => { - const n = Math.floor(Number(e.target.value)) + const val = e.target.value.trim() + if (!val) { + updateDeepResearch({ deepResearchConcurrency: undefined }) + return + } + const n = Math.floor(Number(val)) updateDeepResearch({ - deepResearchConcurrency: Number.isFinite(n) ? Math.min(8, Math.max(1, n)) : 1, + deepResearchConcurrency: Number.isFinite(n) ? Math.min(8, Math.max(1, n)) : undefined, }) }} /> @@ -322,11 +328,17 @@ export function WebSearchSection() { min={1} max={100} step={1} - value={resolvedConfig.deepResearchMaxSources ?? 20} + value={resolvedConfig.deepResearchMaxSources ?? ""} + placeholder="20" onChange={(e) => { - const n = Math.floor(Number(e.target.value)) + const val = e.target.value.trim() + if (!val) { + updateDeepResearch({ deepResearchMaxSources: undefined }) + return + } + const n = Math.floor(Number(val)) updateDeepResearch({ - deepResearchMaxSources: Number.isFinite(n) ? Math.min(100, Math.max(1, n)) : 1, + deepResearchMaxSources: Number.isFinite(n) ? Math.min(100, Math.max(1, n)) : undefined, }) }} />