Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
71 changes: 71 additions & 0 deletions apps/desktop/src/renderer/__tests__/newMakerDraft.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,4 +957,75 @@ describe('newMakerDraft store', () => {
expect(getDraft().collab.workerConfig?.model).toBe('claude-opus-4-7');
});
});

describe('newChatDefaultPermissionMode (新对话默认权限 override)', () => {
it('出厂默认 null,seed 回落到 auto(自动审批)', async () => {
const { getDraft } = await loadModule();
expect(getDraft().newChatDefaultPermissionMode).toBeNull();
expect(getDraft().lastByVendor.cc.permissionMode).toBe('auto');
expect(getDraft().lastByVendor.pi.permissionMode).toBe('auto');
expect(getDraft().lastByVendor.codex.permissionMode).toBe('auto');
});

it('setNewChatDefaultPermissionMode 设 override 后,各 vendor seed 都跟随,且跨重载持久化', async () => {
vi.resetModules();
const { setNewChatDefaultPermissionMode, getDraft } = await loadModule();
setNewChatDefaultPermissionMode('bypassPermissions');
expect(getDraft().newChatDefaultPermissionMode).toBe('bypassPermissions');
expect(getDraft().lastByVendor.cc.permissionMode).toBe('bypassPermissions');
expect(getDraft().lastByVendor.pi.permissionMode).toBe('bypassPermissions');
expect(getDraft().lastByVendor.codex.permissionMode).toBe('bypassPermissions');

// 跨「重启」(重新加载模块)后 override 仍恢复,且 vendor seed 仍跟随。
vi.resetModules();
const m = await loadModule();
expect(m.getDraft().newChatDefaultPermissionMode).toBe('bypassPermissions');
expect(m.getDraft().lastByVendor.cc.permissionMode).toBe('bypassPermissions');
});

it('用户显式选过的 vendor 权限不被 override 覆盖(sanitize 保留显式值)', async () => {
// 先造一份:该 vendor 显式选了 'acceptEdits',同时全局 override 设了 bypass。
memStorage.setItem(
'xdt:newMakerDraft:v1',
JSON.stringify({
vendor: 'cc',
newChatDefaultPermissionMode: 'bypassPermissions',
lastByVendor: { cc: { model: 'm', effort: 'medium', permissionMode: 'acceptEdits', planMode: false, providerId: null } },
}),
);
vi.resetModules();
const { getDraft } = await loadModule();
// 显式选过的保留,不被 global override 顶掉。
expect(getDraft().lastByVendor.cc.permissionMode).toBe('acceptEdits');
// 未显式选过的 vendor seed 仍读全局 override。
expect(getDraft().lastByVendor.pi.permissionMode).toBe('bypassPermissions');
expect(getDraft().newChatDefaultPermissionMode).toBe('bypassPermissions');
});

it('mode 传 null 清除 override,回落系统默认 auto', async () => {
const { setNewChatDefaultPermissionMode, getDraft } = await loadModule();
setNewChatDefaultPermissionMode('bypassPermissions');
expect(getDraft().newChatDefaultPermissionMode).toBe('bypassPermissions');
setNewChatDefaultPermissionMode(null);
expect(getDraft().newChatDefaultPermissionMode).toBeNull();
expect(getDraft().lastByVendor.cc.permissionMode).toBe('auto');
});

it('脏值(非字符串或缺字段)一律归一为 null', async () => {
memStorage.setItem(
'xdt:newMakerDraft:v1',
JSON.stringify({ vendor: 'cc', newChatDefaultPermissionMode: 123 }),
);
vi.resetModules();
const { getDraft } = await loadModule();
expect(getDraft().newChatDefaultPermissionMode).toBeNull();
expect(getDraft().lastByVendor.cc.permissionMode).toBe('auto');
});

it('通用 patchDraft 不能改动 newChatDefaultPermissionMode(专用 setter 契约)', async () => {
const { getDraft, patchDraft } = await loadModule();
patchDraft({ newChatDefaultPermissionMode: 'bypassPermissions' });
expect(getDraft().newChatDefaultPermissionMode).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Settings -> General 的「新建对话默认权限」设置。
*
* 控制所有新 Maker 对话的默认权限模式:auto(自动审批)或 bypassPermissions(完全访问)。
* override 存在时(用户显式选择),未在新建页显式选过权限的 vendor 一律用这个默认;
* 已在某次新建里显式选过权限的 vendor 保留用户自己的那次选择(不被顶掉)。
*
* 纯 renderer 本地偏好,数据正本在 newMakerDraft store(localStorage,按 owner 分区),
* 不走 main 进程 IPC —— 与 IM 默认权限(server prefs)是两套独立机制。
* 有效值 = newChatDefaultPermissionMode(override) ?? seed auto;「恢复默认」= 清 override。
*/

import { useTranslation } from 'react-i18next';

import { PermissionSelector } from '@/components/new-chat/PermissionSelector';
import type { PermissionMode } from '@/lib/userPreferences.types';
import {
setNewChatDefaultPermissionMode,
useNewMakerDraft,
} from '@/state/newMakerDraft';
import { DefaultOverrideControls } from './DefaultOverrideControls';

/** 设置项允许的档位:仅暴露产品批准的 auto(自动审批)/ bypassPermissions(完全访问)。 */
const ALLOWED_MODES = ['auto', 'bypassPermissions'] as const;

export function NewChatDefaultPermissionSection() {
const { t } = useTranslation();
const draft = useNewMakerDraft();
const override = draft.newChatDefaultPermissionMode;
const isCustomized = override != null;
// 有 override 用 override;否则展示系统默认(auto 自动审批)。
const effective = override ?? 'auto';

return (
<div className="flex flex-col gap-[14px]">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="select-none text-14 font-medium leading-[1.2] text-[var(--settings-section-title)]">
{t('settings.newChatDefaults.title')}
</h3>
<p className="mt-1 select-none text-12 leading-[1.45] text-[var(--settings-section-desc)]">
{t('settings.newChatDefaults.description')}
</p>
</div>
<DefaultOverrideControls
isCustomized={isCustomized}
onReset={() => setNewChatDefaultPermissionMode(null)}
/>
</div>

<PermissionSelector
permissionMode={effective}
vendorKey="cc"
triggerVariant="field"
allowedModes={ALLOWED_MODES}
ariaContext={t('settings.newChatDefaults.title')}
onPermissionModeChange={(mode: PermissionMode) => {
setNewChatDefaultPermissionMode(mode);
}}
/>

<p className="select-none text-12 leading-[1.45] text-[var(--settings-section-sublabel)]">
{t('settings.newChatDefaults.hint')}
</p>
</div>
);
}
10 changes: 10 additions & 0 deletions apps/desktop/src/renderer/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { SessionImportSection } from './SessionImportSection';
import { HelpSection } from './HelpSection';
import { HelpAssistantPanel } from './HelpAssistantPanel';
import { AgentResourceSection } from './AgentResourceSection';
import { NewChatDefaultPermissionSection } from './NewChatDefaultPermissionSection';
import { PiPackagesSection } from './PiPackagesSection';
import { CollaborationSection } from './CollaborationSection';
import { BuiltinToolsSection } from './BuiltinToolsSection';
Expand Down Expand Up @@ -373,6 +374,15 @@ export function SettingsView() {
<CollaborationSection />
</section>

{/* Section — 新建对话默认权限 (renderer 本地偏好, 非 IM server prefs)。 */}
<section
id="settings-new-chat-default-permission"
className="py-[18px]"
aria-label={t('settings.newChatDefaults.title')}
>
<NewChatDefaultPermissionSection />
</section>

{/* Section — Agent resource usage (命令并发/进程优先级/工具链限核)。 */}
<section
id="settings-agent-resource"
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/renderer/hooks/useCCSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { AgentKind, Session, WorkspaceKind } from '@/lib/ccAgent.types';
import * as sessionService from '@/lib/sessionService';
import type { ListStatusFilter } from '@/lib/sessionService';
import { sessionsStore } from '@/lib/sessionsStore';
import { getDraft } from '@/state/newMakerDraft';

interface UseCCSessionsOptions {
/** Session status filter — F-PJ-10 V0.5.1。默认 'active'。 */
Expand Down Expand Up @@ -160,8 +161,11 @@ export function useCCSessions(options?: UseCCSessionsOptions): UseCCSessionsRetu
providerId?: string | null;
}): Promise<Session | null> => {
try {
// 新建对话未显式选权限时,用全局「新建对话默认权限」override;无 override 回落
// auto(自动审批)。调用方传入 permissionMode 时以调用方为准(...opts 覆盖)。
const defaultPermissionMode = getDraft().newChatDefaultPermissionMode ?? 'auto';
const newSession = await sessionService.create({
permissionMode: 'auto',
permissionMode: defaultPermissionMode,
fastMode: false,
...opts,
});
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/renderer/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"restored": "Restored default settings",
"restoreFailed": "Failed to restore default settings"
},
"newChatDefaults": {
"title": "Default permission for new chats",
"description": "Applies the chosen permission mode to new chats that haven't picked a permission explicitly. Vendors you've already chosen a permission for keep your choice.",
"hint": "Only auto and Full access are offered here. You can still switch per chat."
},
"tabs": {
"general": "General",
"billing": "Usage and billing",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/renderer/i18n/locales/ja/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"restored": "デフォルト設定に戻しました",
"restoreFailed": "デフォルト設定に戻せませんでした"
},
"newChatDefaults": {
"title": "新規チャットの既定の権限",
"description": "権限を明示的に選んでいない新規チャットに、選んだ権限モードを適用します。既に権限を選んでいるエンジンはその選択を維持します。",
"hint": "ここでは「自動審査」と「完全アクセス」のみ提供します。チャットごとに一時的に切り替えることもできます。"
},
"tabs": {
"general": "一般",
"billing": "使用量と請求",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/renderer/i18n/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"restored": "기본 설정으로 복원했습니다",
"restoreFailed": "기본 설정으로 복원하지 못했습니다"
},
"newChatDefaults": {
"title": "새 채팅의 기본 권한",
"description": "권한을 명시적으로 선택하지 않은 새 채팅에 선택한 권한 모드를 적용합니다. 이미 권한을 선택한 엔진은 해당 선택을 유지합니다.",
"hint": "여기서는 '자동 승인'과 '전체 액세스'만 제공합니다. 채팅별로 임시 전환할 수도 있습니다."
},
"tabs": {
"general": "일반",
"billing": "사용량 및 결제",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/renderer/i18n/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"restored": "已恢复默认设置",
"restoreFailed": "恢复默认设置失败"
},
"newChatDefaults": {
"title": "新建对话的默认权限",
"description": "对未显式选过权限的新对话应用所选权限模式;你已显式选过权限的引擎会保留你的选择。",
"hint": "这里只提供「自动审批」与「完全访问」;你仍可在单次对话里临时切换权限。"
},
"tabs": {
"general": "通用",
"billing": "用量和计费",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/renderer/i18n/locales/zh-TW/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"restored": "已恢復預設設定",
"restoreFailed": "恢復預設設定失敗"
},
"newChatDefaults": {
"title": "新對話的預設權限",
"description": "對未明確選過權限的新對話套用所選權限模式;你已明確選過權限的引擎會保留你的選擇。",
"hint": "這裡只提供「自動審批」與「完全訪問」;你仍可在單次對話裡暫時切換權限。"
},
"tabs": {
"general": "通用",
"billing": "用量和計費",
Expand Down
Loading