Skip to content
Open
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
22 changes: 22 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,28 @@ test('wide settings gutters scroll the whole main pane', async ({ window: page }
await expect(content).toBeVisible();
});

test('temporary subagent policy can be enabled and persisted', async ({ window: page }) => {
await ensureSidebarExpanded(page);
await page.getByRole('button', { name: '设置', exact: true }).click();
await page.getByRole('button', { name: '子 Agent', exact: true }).click();

const main = page.getByRole('main', { name: '设置内容' });
await expect(main.getByRole('heading', { name: '临时子 Agent' })).toBeVisible();
const enabled = main.getByRole('switch', { name: '允许临时子 Agent' });
await enabled.click();
await main.getByRole('button', { name: '保存临时策略' }).click();

await expect
.poll(async () => (await page.evaluate(() => window.maka.settings.get())).subagents.adHoc)
.toMatchObject({
enabled: true,
maxProfile: 'local_read',
connectionSlug: 'e2e',
model: 'claude-sonnet-4-5-20250929',
});
await expect(enabled).toBeChecked();
});

test('appearance choice content stays vertically centered in stretched grid rows', async ({ window: page }) => {
await page.evaluate(async () => {
await window.maka.settings.update({ personalization: { uiLocale: 'en' } });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,17 @@ function createModuleFixture(options: {
async updateRuntimePolicy(
createMutation: (value: RuntimePolicy) => {
kind: string;
value: RuntimePolicy["networkProxy"];
value: unknown;
},
) {
const mutation = createMutation(policy);
if (mutation.kind === "set_network_proxy") {
policy = { ...policy, networkProxy: mutation.value };
policy = { ...policy, networkProxy: mutation.value as RuntimePolicy["networkProxy"] };
} else if (mutation.kind === "set_subagents") {
policy = {
...policy,
subagents: mutation.value as RuntimePolicy["subagents"],
};
}
policyRevision += 1;
return { revision: policyRevision, policy };
Expand Down Expand Up @@ -265,6 +270,22 @@ test("runtime settings project credential status without a password value", asyn
assert.equal("password" in settings.network.proxy, false);
});

test("subagent preset updates preserve the existing ad-hoc policy", async () => {
const fixture = createModuleFixture();
const current = fixture.policy();
const adHoc = {
enabled: true,
maxProfile: "local_read" as const,
connectionSlug: "worker-provider",
model: "gpt-5-mini",
};
// Seed the host policy through the same mutation seam used by the module.
await fixture.module.update({ subagents: { presets: [], adHoc } });
await fixture.module.update({ subagents: { presets: [] } });
assert.deepEqual(fixture.policy().subagents, { presets: [], adHoc });
assert.notDeepEqual(fixture.policy(), current);
});

test("spread-back derived and legacy password fields never enter Runtime policy", async () => {
const fixture = createModuleFixture({ configured: true });

Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/main/runtime-host-settings-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,12 @@ async function applyHostPatchWithoutLane(
}
}
if (patch.subagents) {
await client.updateRuntimePolicy(() => ({
await client.updateRuntimePolicy((policy) => ({
kind: "set_subagents",
value: patch.subagents!,
value: {
...policy.subagents,
...patch.subagents,
},
}));
}
return skippedCredentials;
Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/src/renderer/locales/settings-subagents-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ type ProfileCopy = {
};

export type SubagentSettingsCopy = {
adHoc: {
title: string;
description: string;
enabled: string;
enabledDescription: string;
profile: string;
profileDescription: string;
connection: string;
model: string;
thinking: string;
noConnection: string;
noModel: string;
save: string;
saveFailed: string;
};
section: {
title: string;
count(total: number): string;
Expand Down Expand Up @@ -101,6 +116,21 @@ export type SubagentSettingsCopy = {

const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
zh: {
adHoc: {
title: '临时子 Agent',
description: '明确启用后,主 Agent 才能创建一次性的任务角色。这里固定它可用的最高能力、连接和模型。',
enabled: '允许临时子 Agent',
enabledDescription: '关闭后,临时角色不会出现在 agent_list 中,也无法通过 agent_spawn 创建。',
profile: '最高能力 Profile',
profileDescription: '临时角色只能使用不高于此 Profile 的固定能力边界。',
connection: '模型连接',
model: '模型',
thinking: '思考级别',
noConnection: '请先在“模型”页启用一个模型连接。',
noModel: '所选连接没有已启用的模型。',
save: '保存临时策略',
saveFailed: '保存临时子 Agent 策略失败',
},
section: {
title: '已批准的子 Agent',
count: (total) => `共 ${total} 个配置`,
Expand Down Expand Up @@ -183,6 +213,21 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
},
},
en: {
adHoc: {
title: 'Temporary subagent',
description: 'When explicitly enabled, the main agent may create one-off task roles. These settings fix their maximum capability, connection, and model.',
enabled: 'Allow temporary subagents',
enabledDescription: 'When off, the route is omitted from agent_list and agent_spawn cannot create it.',
profile: 'Maximum capability profile',
profileDescription: 'Temporary roles cannot exceed this fixed capability boundary.',
connection: 'Model connection',
model: 'Model',
thinking: 'Thinking level',
noConnection: 'Enable a model connection on the Models page first.',
noModel: 'The selected connection has no enabled models.',
save: 'Save temporary policy',
saveFailed: 'Failed to save temporary subagent policy',
},
section: {
title: 'Approved subagents',
count: (total) => `${total} presets`,
Expand Down
Loading