Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
const readTool = tool('Read', 'read');
const writeTool = tool('Write', 'file_write');
const computerTool = tool('maka_computer', 'computer_use');
const webSearchTool = tool('WebSearch', 'web_read');
const availability: ToolAvailabilityConfig = {
economy: true,
groups: [
Expand Down Expand Up @@ -285,6 +286,30 @@ describe('Desktop backend tool surface', () => {
);
});

it('omits unavailable WebSearch after the Deep Research allowlist and keeps it when ready', async () => {
const input = inputFor('claude-sonnet-4-5-20250929');
input.header.labels = ['mode:deep_research'];
const builtinTools = [readTool, webSearchTool];

const unavailable = await resolveDesktopBackendToolSurface(
makeDeps({
builtinTools,
resolveWebSearchAvailability: async () => false,
}),
input,
);
assert.equal(unavailable.selectedTools.some((tool) => tool.name === 'WebSearch'), false);

const ready = await resolveDesktopBackendToolSurface(
makeDeps({
builtinTools,
resolveWebSearchAvailability: async () => true,
}),
input,
);
assert.equal(ready.selectedTools.some((tool) => tool.name === 'WebSearch'), true);
});

it('uses explicit preview inputs without reading a nonexistent session plan', async () => {
let connectionReads = 0;
let planReads = 0;
Expand Down Expand Up @@ -336,6 +361,7 @@ function makeDeps(
deepResearchTools: [],
computerUseTools: [computerTool],
builtinTools: [readTool, writeTool, computerTool],
resolveWebSearchAvailability: async () => true,
toolEconomy: availability.economy,
planStore,
...overrides,
Expand Down
35 changes: 35 additions & 0 deletions apps/desktop/src/main/__tests__/settings-runtime-effects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { createDefaultSettings } from '@maka/core';
import type { BotRegistry } from '@maka/runtime';
import type { createSettingsStore } from '@maka/storage';
import type { KeepSystemAwakeController } from '../keep-system-awake.js';
import { createSettingsRuntimeEffects } from '../settings-runtime-effects.js';

test('webSearch and privacy settings refresh idle backends', async () => {
const settings = createDefaultSettings();
let refreshes = 0;
const effects = createSettingsRuntimeEffects({
settingsStore: {
get: async () => settings,
} as ReturnType<typeof createSettingsStore>,
botRegistry: {} as BotRegistry,
keepSystemAwake: {} as KeepSystemAwakeController,
safeSendToRenderer: () => {},
refreshIdleBackends: async () => {
refreshes += 1;
},
});

await effects.applySettingsRuntimeEffects(settings, {
webSearch: { enabled: true },
});
await effects.applySettingsRuntimeEffects(settings, {
privacy: { incognitoActive: true },
});
await effects.applySettingsRuntimeEffects(settings, {
appearance: { theme: 'dark' },
});

assert.equal(refreshes, 2);
});
68 changes: 68 additions & 0 deletions apps/desktop/src/main/__tests__/web-search-surface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { createDefaultSettings, mergeSettings } from '@maka/core';
import type { SettingsStore } from '@maka/storage';
import { resolveDesktopWebSearchAvailability } from '../web-search/surface.js';

describe('Desktop WebSearch availability', () => {
const cases = [
{
name: 'disabled',
settings: mergeSettings(createDefaultSettings(), {
webSearch: {
enabled: false,
providers: { tavily: { apiKey: 'configured-key' } },
},
}),
privacy: { incognitoActive: false },
expected: false,
},
{
name: 'credential not configured',
settings: mergeSettings(createDefaultSettings(), {
webSearch: {
enabled: true,
providers: { tavily: { apiKey: '' } },
},
}),
privacy: { incognitoActive: false },
expected: false,
},
{
name: 'privacy mode',
settings: mergeSettings(createDefaultSettings(), {
webSearch: {
enabled: true,
providers: { tavily: { apiKey: 'configured-key' } },
},
}),
privacy: { incognitoActive: true },
expected: false,
},
{
name: 'ready',
settings: mergeSettings(createDefaultSettings(), {
webSearch: {
enabled: true,
providers: { tavily: { apiKey: 'configured-key' } },
},
}),
privacy: { incognitoActive: false },
expected: true,
},
] as const;

for (const fixture of cases) {
it(`reports ${fixture.name}`, async () => {
const available = await resolveDesktopWebSearchAvailability({
settingsStore: {
get: async () => fixture.settings,
} as Pick<SettingsStore, 'get'>,
getPrivacyContext: async () => fixture.privacy,
env: {},
});

assert.equal(available, fixture.expected);
});
}
});
7 changes: 7 additions & 0 deletions apps/desktop/src/main/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import { registerDailyReviewIpc } from './daily-review-ipc-main.js';
import { registerInspectorIpc } from './inspector-ipc-main.js';
import { registerUsageIpc } from './usage-ipc-main.js';
import { registerWebSearchIpc } from './web-search-ipc-main.js';
import { resolveDesktopWebSearchAvailability } from './web-search/surface.js';
import { registerNotificationsIpc } from './notifications-ipc-main.js';
import { registerAppIpc } from './app-ipc-main.js';
import { createAppUpdateService } from './app-update-service.js';
Expand Down Expand Up @@ -746,6 +747,11 @@ const desktopBackendToolSurfaceDeps = {
deepResearchTools,
computerUseTools,
builtinTools,
resolveWebSearchAvailability: () =>
resolveDesktopWebSearchAvailability({
settingsStore,
getPrivacyContext: getWorkspacePrivacyContext,
}),
toolEconomy: desktopProductToolSurface.identity.policy.economy,
planStore,
getAgentGraphSupervisorTools: (sessionId: string) =>
Expand Down Expand Up @@ -1291,6 +1297,7 @@ const { normalizeSettingsPatch, applySettingsRuntimeEffects, handleExternalSetti
botRegistry,
keepSystemAwake,
safeSendToRenderer,
refreshIdleBackends: () => runtime.refreshIdleBackends(),
});

async function updateAgentSettings(patch: UpdateAppSettingsInput): Promise<AppSettings> {
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/src/main/desktop-backend-tool-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
import type { McpClientManager } from '@maka/mcp';
import { computerUseToolsForModel } from './computer-use-model-tools.js';
import type { ReadyConnection } from './chat-readiness.js';
import { webSearchToolsForAvailability } from './web-search/surface.js';

export interface DesktopBackendToolSurfaceDeps {
isComputerUseRealModelE2e: boolean;
Expand All @@ -42,6 +43,7 @@ export interface DesktopBackendToolSurfaceDeps {
deepResearchTools: readonly MakaTool[];
computerUseTools: readonly MakaTool[];
builtinTools: readonly MakaTool[];
resolveWebSearchAvailability: () => Promise<boolean>;
toolEconomy: boolean;
planStore: PlanStore;
getAgentGraphSupervisorTools?: (
Expand Down Expand Up @@ -202,10 +204,19 @@ export async function resolveDesktopBackendToolSurface(
...buildMcpTools(deps.mcpManager),
...(isDeepResearchSession(input.header.labels) ? deps.deepResearchTools : []),
];
const candidateTools =
const deepResearchCandidateTools =
!input.tools && isDeepResearchSession(input.header.labels)
? unscopedCandidateTools.filter(isDeepResearchToolAllowed)
: unscopedCandidateTools;
const webSearchAvailable = deepResearchCandidateTools.some(
(tool) => tool.name === 'WebSearch',
)
? await deps.resolveWebSearchAvailability()
: true;
const candidateTools = webSearchToolsForAvailability(
deepResearchCandidateTools,
webSearchAvailable,
);
const toolEconomy = deps.isComputerUseRealModelE2e ? false : deps.toolEconomy;

const planControlTools = input.tools
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/main/settings-runtime-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface SettingsRuntimeEffectsDeps {
botRegistry: BotRegistry;
keepSystemAwake: KeepSystemAwakeController;
safeSendToRenderer: (channel: string, ...args: unknown[]) => void;
refreshIdleBackends: () => Promise<void>;
}

export interface SettingsRuntimeEffects {
Expand Down Expand Up @@ -46,6 +47,7 @@ export function createSettingsRuntimeEffects(
botRegistry,
keepSystemAwake,
safeSendToRenderer,
refreshIdleBackends,
} = deps;

async function normalizeSettingsPatch(patch: UpdateAppSettingsInput): Promise<UpdateAppSettingsInput> {
Expand All @@ -67,6 +69,11 @@ export function createSettingsRuntimeEffects(
// capability reflects the user's choice without waiting for a relaunch.
keepSystemAwake.apply(settings.system.keepSystemAwake);
}
if (patch.webSearch || patch.privacy) {
void refreshIdleBackends().catch((error) => {
console.warn('[settings] failed to refresh backend tool snapshots:', error);
});
}
}

async function handleExternalSettingsChange(): Promise<void> {
Expand All @@ -76,6 +83,8 @@ export function createSettingsRuntimeEffects(
network: settings.network,
botChat: settings.botChat,
system: settings.system,
webSearch: settings.webSearch,
privacy: settings.privacy,
};
await applySettingsRuntimeEffects(settings, fullPatch);
} catch (error) {
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/main/web-search/surface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { validateWorkspacePrivacyContext } from '@maka/core';
import type { MakaTool } from '@maka/runtime';
import type { SettingsStore } from '@maka/storage';
import { resolveTavilyApiKey } from './credentials.js';

const WEB_SEARCH_TOOL_NAME = 'WebSearch';

export function webSearchToolsForAvailability(
tools: readonly MakaTool[],
available: boolean,
): MakaTool[] {
if (available) return [...tools];
return tools.filter((tool) => tool.name !== WEB_SEARCH_TOOL_NAME);
}

export async function resolveDesktopWebSearchAvailability(deps: {
settingsStore: Pick<SettingsStore, 'get'>;
getPrivacyContext: () => Promise<unknown>;
env?: NodeJS.ProcessEnv;
}): Promise<boolean> {
const privacy = validateWorkspacePrivacyContext(await deps.getPrivacyContext());
if (!privacy.ok || privacy.value.incognitoActive) return false;

const settings = await deps.settingsStore.get();
return (
settings.webSearch.enabled &&
resolveTavilyApiKey({ settings, ...(deps.env ? { env: deps.env } : {}) }).length > 0
);
}
Loading
Loading