Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(desktop): add subagent settings page by likun666661 · Pull Request #1999 · apache/maka · GitHub
Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(desktop): add subagent settings page by likun666661 · Pull Request #1999 · apache/maka · GitHub
Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(desktop): add subagent settings page by likun666661 · Pull Request #1999 · apache/maka · GitHub
Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(desktop): add subagent settings page by likun666661 · Pull Request #1999 · apache/maka · GitHub
Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(desktop): add subagent settings page by likun666661 · Pull Request #1999 · apache/maka · GitHub
Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(desktop): add subagent settings page by likun666661 · Pull Request #1999 · apache/maka · GitHub
Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(desktop): add subagent settings page by likun666661 · Pull Request #1999 · apache/maka · GitHub
Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,49 @@ test('changing the theme in settings applies to the UI', async ({ window: page }
).toBe(true);
});

test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});

await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();

const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
await expect(settings.getByText('可用', { exact: true })).toBeVisible();

await settings.getByRole('button', { name: '编辑', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '编辑子 Agent' });
const description = dialog.getByRole('textbox', { name: '适用场景' });
await description.fill('快速阅读代码,并总结关键调用链。');
await dialog.getByRole('button', { name: '保存', exact: true }).click();

await expect(dialog).toBeHidden();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.description;
})).toBe('快速阅读代码,并总结关键调用链。');
});

test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection, SubagentPreset } from '@maka/core';
import {
subagentPresetAvailability,
suggestSubagentPresetId,
} from '../../renderer/settings/subagent-preset-presentation.js';

function connection(input: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
defaultModel: 'deepseek-chat',
enabled: true,
enabledModelIds: ['deepseek-chat'],
createdAt: 0,
updatedAt: 0,
...input,
};
}

function preset(input: Partial<SubagentPreset> = {}): SubagentPreset {
return {
id: 'fast-reader',
name: 'Fast reader',
description: 'Read large repositories quickly',
profile: 'local_read',
connectionSlug: 'deepseek',
model: 'deepseek-chat',
enabled: true,
...input,
};
}

describe('subagentPresetAvailability', () => {
it('distinguishes disabled and broken model routes from usable presets', () => {
assert.deepEqual(subagentPresetAvailability(preset(), [connection()]), {
kind: 'available',
tone: 'success',
});
assert.equal(subagentPresetAvailability(preset({ enabled: false }), []).kind, 'disabled');
assert.equal(subagentPresetAvailability(preset(), []).kind, 'missing_connection');
assert.equal(
subagentPresetAvailability(preset(), [connection({ enabled: false })]).kind,
'connection_disabled',
);
assert.equal(
subagentPresetAvailability(preset({ model: 'deepseek-reasoner' }), [connection()]).kind,
'model_disabled',
);
});
});

describe('suggestSubagentPresetId', () => {
it('creates stable safe ids and resolves collisions', () => {
assert.equal(suggestSubagentPresetId('Fast Code Reader', new Set()), 'fast-code-reader');
assert.equal(suggestSubagentPresetId('快速阅读', new Set()), 'subagent');
assert.equal(
suggestSubagentPresetId('Fast Code Reader', new Set(['fast-code-reader', 'fast-code-reader-2'])),
'fast-code-reader-3',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: '通用', description: '隐身、启动、对话默认与网络代理等系统偏好。' },
appearance: { label: '外观', description: '主题、配色与界面语言。' },
models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' },
subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' },
usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' },
memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' },
'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' },
Expand All@@ -41,6 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
general: { label: 'General', description: 'Privacy, startup, conversation defaults, and network proxy preferences.' },
appearance: { label: 'Appearance', description: 'Theme, color palette, and interface language.' },
models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' },
subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' },
usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' },
memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' },
'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' },
Expand Down
252 changes: 252 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
import type {
SubagentProfile,
ThinkingLevel,
UiCatalog,
UiLocale,
} from '@maka/core';

type ProfileCopy = {
label: string;
description: string;
};

export type SubagentSettingsCopy = {
section: {
title: string;
description: string;
count(enabled: number, total: number): string;
add: string;
limitReached: string;
emptyTitle: string;
emptyDescription: string;
};
row: {
edit: string;
remove: string;
enabled: string;
fallbackDescription: string;
route(profile: string, connection: string, model: string, thinking?: string): string;
};
status: {
available: string;
disabled: string;
missingConnection: string;
connectionDisabled: string;
modelDisabled: string;
};
editor: {
createTitle: string;
createSubtitle: string;
editTitle: string;
editSubtitle: string;
name: string;
namePlaceholder: string;
id: string;
idDescription: string;
idPlaceholder: string;
description: string;
descriptionHelp: string;
descriptionPlaceholder: string;
profile: string;
connection: string;
model: string;
thinking: string;
defaultThinking: string;
enabled: string;
enabledDescription: string;
implementationWarning: string;
noConnection: string;
noModel: string;
requiredName: string;
requiredDescription: string;
invalidId: string;
duplicateId: string;
invalidRoute: string;
cancel: string;
create: string;
save: string;
saving: string;
};
remove: {
title(name: string): string;
description: string;
confirm: string;
cancel: string;
};
toast: {
saveFailed: string;
};
profiles: Record<SubagentProfile, ProfileCopy>;
thinking: Record<ThinkingLevel, string>;
};

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
section: {
title: '已批准的子 Agent',
description: '主 Agent 会根据适用场景,从已启用且可用的配置中选择。每个配置固定自己的能力边界、连接和模型。',
count: (enabled, total) => `已启用 ${enabled} / 共 ${total}`,
add: '添加子 Agent',
limitReached: '已达到 64 个配置的上限',
emptyTitle: '还没有子 Agent 配置',
emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。',
},
row: {
edit: '编辑',
remove: '删除',
enabled: '启用',
fallbackDescription: '尚未填写适用场景',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · 思考 ${thinking}` : ''}`,
},
status: {
available: '可用',
disabled: '已停用',
missingConnection: '连接不存在',
connectionDisabled: '连接已停用',
modelDisabled: '模型未启用',
},
editor: {
createTitle: '添加子 Agent',
createSubtitle: '创建一个可由主 Agent 自动选择的模型配置。',
editTitle: '编辑子 Agent',
editSubtitle: '修改适用场景、能力边界和模型路由。',
name: '显示名称',
namePlaceholder: '快速代码阅读',
id: 'subagent_id',
idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。',
idPlaceholder: 'fast-reader',
description: '适用场景',
descriptionHelp: '写清楚何时应该使用它;主 Agent 主要根据这段描述挑选配置。',
descriptionPlaceholder: '适合快速、低成本地阅读大型仓库',
profile: '能力 Profile',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
defaultThinking: '跟随模型默认',
enabled: '立即启用',
enabledDescription: '启用后,主 Agent 可以选择这个配置。',
implementationWarning: '实现代码 Profile 可以写文件和执行命令,并会在隔离 worktree 中运行。',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
requiredName: '请输入显示名称。',
requiredDescription: '请说明这个子 Agent 的适用场景。',
invalidId: '只能使用字母、数字、点、下划线、冒号和连字符,最多 128 个字符。',
duplicateId: '这个 subagent_id 已经存在。',
invalidRoute: '请选择已启用的连接和模型。',
cancel: '取消',
create: '创建',
save: '保存',
saving: '保存中…',
},
remove: {
title: (name) => `删除“${name}”?`,
description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。',
confirm: '删除',
cancel: '取消',
},
toast: {
saveFailed: '保存子 Agent 配置失败',
},
profiles: {
local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' },
web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' },
implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' },
},
thinking: {
off: '关闭',
minimal: '最少',
low: '低',
medium: '中',
high: '高',
xhigh: '超高',
max: '最大',
},
},
en: {
section: {
title: 'Approved subagents',
description: 'The main agent selects from enabled, available presets based on when each should be used. Every preset fixes its capability boundary, connection, and model.',
count: (enabled, total) => `${enabled} enabled · ${total} total`,
add: 'Add subagent',
limitReached: 'The 64-preset limit has been reached',
emptyTitle: 'No subagent presets yet',
emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.',
},
row: {
edit: 'Edit',
remove: 'Remove',
enabled: 'Enabled',
fallbackDescription: 'No usage guidance yet',
route: (profile, connection, model, thinking) =>
`${profile} · ${connection} / ${model}${thinking ? ` · Thinking ${thinking}` : ''}`,
},
status: {
available: 'Available',
disabled: 'Disabled',
missingConnection: 'Connection missing',
connectionDisabled: 'Connection disabled',
modelDisabled: 'Model not enabled',
},
editor: {
createTitle: 'Add subagent',
createSubtitle: 'Create a model preset that the main agent can select automatically.',
editTitle: 'Edit subagent',
editSubtitle: 'Change its usage guidance, capability boundary, and model route.',
name: 'Display name',
namePlaceholder: 'Fast code reader',
id: 'subagent_id',
idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.',
idPlaceholder: 'fast-reader',
description: 'When to use',
descriptionHelp: 'Describe when this preset is the right choice. The main agent relies primarily on this guidance.',
descriptionPlaceholder: 'Fast, low-cost exploration of large repositories',
profile: 'Capability profile',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
defaultThinking: 'Use model default',
enabled: 'Enable immediately',
enabledDescription: 'When enabled, the main agent may select this preset.',
implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
requiredName: 'Enter a display name.',
requiredDescription: 'Describe when this subagent should be used.',
invalidId: 'Use only letters, numbers, dots, underscores, colons, and hyphens, up to 128 characters.',
duplicateId: 'That subagent_id already exists.',
invalidRoute: 'Select an enabled connection and model.',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
saving: 'Saving…',
},
remove: {
title: (name) => `Remove “${name}”?`,
description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.',
confirm: 'Remove',
cancel: 'Cancel',
},
toast: {
saveFailed: 'Failed to save subagent presets',
},
profiles: {
local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' },
web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' },
implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' },
},
thinking: {
off: 'Off',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
},
} satisfies UiCatalog<SubagentSettingsCopy>;

export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy {
return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale];
}
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/shell-copy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,7 @@ const ZH_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: '通用',
appearance: '外观',
models: '模型',
subagents: '子 Agent',
usage: '使用统计',
memory: '记忆',
'daily-review': '每日回顾',
Expand All@@ -649,6 +650,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
general: 'General',
appearance: 'Appearance',
models: 'Models',
subagents: 'Subagents',
usage: 'Usage',
memory: 'Memory',
'daily-review': 'Daily Review',
Expand Down
Loading
Loading