Skip to content

Commit d0d56ca

Browse files
committed
feat(settings): Brave Search key, tool toggles, QOL improvements
Settings: - Brave Search API key field in Settings → Tools section - Toggle switches for fetch_url and web_search tools - Tool toggles persisted in settings.json, respected at runtime QOL: - Note auto-save: debounced 2-second save while editing (no more data loss if app crashes before blur) - Thread delete confirmation dialog before removing conversations - Toggle switch CSS component for reuse Backend: - SettingsData gains toolToggles with fetchUrl/webSearch booleans - ProviderStatus returns braveSearchKeyConfigured + toolToggles - buildToolExecutors respects toggle settings
1 parent e6a13c3 commit d0d56ca

6 files changed

Lines changed: 142 additions & 8 deletions

File tree

src/main/providerService.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,9 @@ export class ProviderService {
295295
model: providers.perplexity.model,
296296
preset: providers.perplexity.preset
297297
},
298-
ollama
298+
ollama,
299+
braveSearchKeyConfigured: Boolean(await this.secureConfig.getToolApiKey("brave")),
300+
toolToggles: settings.toolToggles
299301
};
300302
}
301303

@@ -304,6 +306,13 @@ export class ProviderService {
304306
await this.secureConfig.setProviderApiKey("perplexity", config.perplexityApiKey);
305307
}
306308

309+
if (typeof config.braveSearchApiKey === "string") {
310+
const normalized = config.braveSearchApiKey.trim();
311+
if (normalized) {
312+
await this.secureConfig.setToolApiKey("brave", normalized);
313+
}
314+
}
315+
307316
if (config.providerApiKeys) {
308317
const saves: Array<Promise<void>> = [];
309318
for (const providerId of CLOUD_PROVIDER_IDS) {
@@ -373,6 +382,10 @@ export class ProviderService {
373382
baseUrl: config.ollamaBaseUrl ?? current.providers.ollama.baseUrl,
374383
model: config.ollamaModel ?? current.providers.ollama.model
375384
}
385+
},
386+
toolToggles: {
387+
fetchUrl: config.toolToggles?.fetchUrl ?? current.toolToggles.fetchUrl,
388+
webSearch: config.toolToggles?.webSearch ?? current.toolToggles.webSearch
376389
}
377390
}));
378391

@@ -484,7 +497,8 @@ export class ProviderService {
484497
let finalCitations: Citation[] = [];
485498
const systemPrompt = await buildSystemPrompt(this.contextProviders, request.prompt);
486499
const braveApiKey = await this.secureConfig.getToolApiKey("brave");
487-
const toolExecutors = buildToolExecutors(braveApiKey);
500+
const toolToggles = (await this.storage.getSettings()).toolToggles;
501+
const toolExecutors = buildToolExecutors(braveApiKey, toolToggles);
488502
const toolDefs = getToolDefinitions(toolExecutors);
489503

490504
const onDelta = (delta: string) => {

src/main/storage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export interface SettingsData {
3131
model: string;
3232
};
3333
};
34+
toolToggles: {
35+
fetchUrl: boolean;
36+
webSearch: boolean;
37+
};
3438
}
3539

3640
type LegacySettingsShape = {
@@ -62,6 +66,10 @@ const DEFAULT_SETTINGS: SettingsData = {
6266
baseUrl: "http://localhost:11434",
6367
model: ""
6468
}
69+
},
70+
toolToggles: {
71+
fetchUrl: true,
72+
webSearch: true
6573
}
6674
};
6775

@@ -175,6 +183,10 @@ function normalizeSettings(raw: unknown): SettingsData {
175183
? source.ollamaModel
176184
: DEFAULT_SETTINGS.providers.ollama.model
177185
}
186+
},
187+
toolToggles: {
188+
fetchUrl: (source as Partial<SettingsData>).toolToggles?.fetchUrl !== false,
189+
webSearch: (source as Partial<SettingsData>).toolToggles?.webSearch !== false
178190
}
179191
};
180192
}

src/main/tools/registry.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import { createWebSearchTool } from "./webSearch";
66
* Build the list of active tool executors based on available API keys.
77
* Tools whose prerequisites are not met are silently excluded.
88
*/
9-
export function buildToolExecutors(braveApiKey?: string | null): ToolExecutor[] {
10-
const tools: ToolExecutor[] = [fetchUrlTool];
9+
export function buildToolExecutors(
10+
braveApiKey?: string | null,
11+
toggles?: { fetchUrl?: boolean; webSearch?: boolean }
12+
): ToolExecutor[] {
13+
const tools: ToolExecutor[] = [];
1114

12-
if (braveApiKey) {
15+
if (toggles?.fetchUrl !== false) {
16+
tools.push(fetchUrlTool);
17+
}
18+
19+
if (toggles?.webSearch !== false && braveApiKey) {
1320
tools.push(createWebSearchTool(braveApiKey));
1421
}
1522

src/renderer/App.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,9 @@ export function App() {
652652
const [shortcutDraft, setShortcutDraft] = useState("CommandOrControl+Shift+Space");
653653
const [activeModelDraft, setActiveModelDraft] = useState(modelKey("search", ""));
654654
const [providerKeyDrafts, setProviderKeyDrafts] = useState<Record<CloudProviderId, string>>(() => buildProviderDrafts());
655+
const [braveSearchKeyDraft, setBraveSearchKeyDraft] = useState("");
656+
const [toolFetchUrlEnabled, setToolFetchUrlEnabled] = useState(true);
657+
const [toolWebSearchEnabled, setToolWebSearchEnabled] = useState(true);
655658
const [selectedCloudModelsDraft, setSelectedCloudModelsDraft] = useState<Record<CloudProviderId, string[]>>(() => normalizeSelectedCloudModels());
656659
const [openRouterModelDraft, setOpenRouterModelDraft] = useState("");
657660
const [customLocalModelDraft, setCustomLocalModelDraft] = useState("");
@@ -825,6 +828,8 @@ export function App() {
825828
setSelectedCloudModelsDraft(nextStatus.selectedCloudModels);
826829
setOllamaStatus(nextOllamaStatus);
827830
setShortcutDraft(nextStatus.shortcut);
831+
setToolFetchUrlEnabled(nextStatus.toolToggles.fetchUrl);
832+
setToolWebSearchEnabled(nextStatus.toolToggles.webSearch);
828833
setSettingsMode(nextStatus.preferredMode === "local" ? "local" : "cloud");
829834
const hasActiveCloudKey = Boolean(nextStatus.cloudProviderKeys?.[nextStatus.activeCloudProvider]);
830835
setActiveModelDraft(
@@ -1191,6 +1196,14 @@ export function App() {
11911196
pendingDeltasRef.current.clear();
11921197
}, []);
11931198

1199+
useEffect(() => {
1200+
if (!activeNoteId) return;
1201+
const timer = setTimeout(() => {
1202+
void saveNote(activeNoteId, noteTitleDraft, noteContentDraft);
1203+
}, 2000);
1204+
return () => clearTimeout(timer);
1205+
}, [activeNoteId, noteTitleDraft, noteContentDraft]);
1206+
11941207
async function stopPendingResponse(nextError?: string, refreshAfterStop = true) {
11951208
streamSequenceRef.current += 1;
11961209
const activeStreamId = activeStreamIdRef.current;
@@ -1355,6 +1368,15 @@ export function App() {
13551368
});
13561369
}
13571370

1371+
async function saveBraveSearchKey() {
1372+
await persistConfig({ braveSearchApiKey: braveSearchKeyDraft });
1373+
setBraveSearchKeyDraft(""); // Clear after save since we don't show the actual key
1374+
}
1375+
1376+
async function saveToolToggles(patch: { fetchUrl?: boolean; webSearch?: boolean }) {
1377+
await persistConfig({ toolToggles: patch });
1378+
}
1379+
13581380
async function toggleSelectedCloudModel(providerId: CloudProviderId, modelId: string) {
13591381
const current = selectedCloudModelsDraft[providerId] ?? [];
13601382
const next = current.includes(modelId)
@@ -1783,6 +1805,7 @@ export function App() {
17831805
}
17841806

17851807
async function deleteThread(id: string) {
1808+
if (!confirm("Delete this conversation?")) return;
17861809
try {
17871810
setError(null);
17881811
await getRobinBridge().chat.deleteThread(id);
@@ -1990,9 +2013,6 @@ export function App() {
19902013
onClick={() => { void selectThread(thread.id); setSidebarTab("chats"); setScreen("chat"); }}
19912014
onContextMenu={(event) => {
19922015
event.preventDefault();
1993-
if (!window.confirm("Delete this chat?")) {
1994-
return;
1995-
}
19962016
void deleteThread(thread.id);
19972017
}}
19982018
>
@@ -2112,6 +2132,45 @@ export function App() {
21122132
})}
21132133
</div>
21142134

2135+
<p className="setting-title" style={{ marginTop: 20 }}>Tools</p>
2136+
<div className="provider-list">
2137+
<article className="provider-row">
2138+
<div className="provider-row-label">
2139+
<p className="provider-name">Brave Search</p>
2140+
</div>
2141+
<input
2142+
className="field-input provider-key-input"
2143+
type="text"
2144+
placeholder={status?.braveSearchKeyConfigured ? "Key configured ✓" : "API key for web search"}
2145+
value={braveSearchKeyDraft}
2146+
autoCapitalize="off"
2147+
autoCorrect="off"
2148+
spellCheck={false}
2149+
onChange={(e) => setBraveSearchKeyDraft(e.target.value)}
2150+
onBlur={() => { if (braveSearchKeyDraft.trim()) void saveBraveSearchKey(); }}
2151+
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); if (braveSearchKeyDraft.trim()) void saveBraveSearchKey(); } }}
2152+
/>
2153+
</article>
2154+
<article className="provider-row">
2155+
<div className="provider-row-label">
2156+
<p className="provider-name">Fetch URL</p>
2157+
</div>
2158+
<label className="toggle-switch">
2159+
<input type="checkbox" checked={toolFetchUrlEnabled} onChange={(e) => { setToolFetchUrlEnabled(e.target.checked); void saveToolToggles({ fetchUrl: e.target.checked }); }} />
2160+
<span className="toggle-slider" />
2161+
</label>
2162+
</article>
2163+
<article className="provider-row">
2164+
<div className="provider-row-label">
2165+
<p className="provider-name">Web Search</p>
2166+
</div>
2167+
<label className="toggle-switch">
2168+
<input type="checkbox" checked={toolWebSearchEnabled} onChange={(e) => { setToolWebSearchEnabled(e.target.checked); void saveToolToggles({ webSearch: e.target.checked }); }} />
2169+
<span className="toggle-slider" />
2170+
</label>
2171+
</article>
2172+
</div>
2173+
21152174
<div className="settings-cloud-models-panel">
21162175
<p className="setting-title setting-title-sub">Models Visible In Chat</p>
21172176
<div className="settings-cloud-provider-groups">

src/renderer/styles.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,3 +2235,41 @@ button:focus-visible {
22352235
.cap-badge:hover {
22362236
opacity: 1;
22372237
}
2238+
2239+
/* ── Toggle Switch ───────────────────────────────────── */
2240+
.toggle-switch {
2241+
position: relative;
2242+
display: inline-block;
2243+
width: 36px;
2244+
height: 20px;
2245+
cursor: pointer;
2246+
}
2247+
.toggle-switch input {
2248+
opacity: 0;
2249+
width: 0;
2250+
height: 0;
2251+
}
2252+
.toggle-slider {
2253+
position: absolute;
2254+
inset: 0;
2255+
background: var(--border);
2256+
border-radius: 10px;
2257+
transition: background 0.2s;
2258+
}
2259+
.toggle-slider::before {
2260+
content: "";
2261+
position: absolute;
2262+
width: 14px;
2263+
height: 14px;
2264+
left: 3px;
2265+
bottom: 3px;
2266+
background: white;
2267+
border-radius: 50%;
2268+
transition: transform 0.2s;
2269+
}
2270+
.toggle-switch input:checked + .toggle-slider {
2271+
background: var(--accent);
2272+
}
2273+
.toggle-switch input:checked + .toggle-slider::before {
2274+
transform: translateX(16px);
2275+
}

src/shared/contracts.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export interface ProviderStatus {
114114
preset: string;
115115
};
116116
ollama: OllamaStatus;
117+
braveSearchKeyConfigured: boolean;
118+
toolToggles: { fetchUrl: boolean; webSearch: boolean };
117119
}
118120

119121
export interface CloudModelCatalogItem {
@@ -152,6 +154,8 @@ export interface SaveConfigInput {
152154
activeCloudProvider?: CloudProviderId;
153155
providerApiKeys?: Partial<Record<CloudProviderId, string>>;
154156
selectedCloudModels?: Partial<Record<CloudProviderId, string[]>>;
157+
braveSearchApiKey?: string;
158+
toolToggles?: { fetchUrl?: boolean; webSearch?: boolean };
155159
}
156160

157161
export type ChatStreamEvent =

0 commit comments

Comments
 (0)