From 52aa3c5c02b225c4379e928dff0e91b1ea195d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 12 Jun 2026 10:51:05 +0800 Subject: [PATCH] feat(desktop): add Auto-dream setting (#800) Expose the existing CLI Auto-dream switch in General settings with an explicit opt-in confirmation before enabling background memory consolidation. Tested: bun run check:desktop Tested: ./bin/claude-haha --help Tested: CLAUDE_CONFIG_DIR temp settings true/false autoDreamEnabled check Tested: real provider Auto-dream trigger against temporary memory copy Scope-risk: narrow Confidence: high --- .../src/__tests__/generalSettings.test.tsx | 39 +++++++ desktop/src/i18n/locales/en.ts | 9 ++ desktop/src/i18n/locales/jp.ts | 9 ++ desktop/src/i18n/locales/kr.ts | 9 ++ desktop/src/i18n/locales/zh-TW.ts | 9 ++ desktop/src/i18n/locales/zh.ts | 9 ++ desktop/src/pages/Settings.tsx | 65 ++++++++++++ desktop/src/stores/settingsStore.test.ts | 100 ++++++++++++++++++ desktop/src/stores/settingsStore.ts | 15 +++ desktop/src/types/settings.ts | 1 + 10 files changed, 265 insertions(+) diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index 29476fe0..324e70e3 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -204,6 +204,7 @@ describe('Settings > General tab', () => { locale: 'en', theme: 'light', thinkingEnabled: true, + autoDreamEnabled: false, skipWebFetchPreflight: true, desktopNotificationsEnabled: true, chatSendBehavior: 'enter', @@ -242,6 +243,9 @@ describe('Settings > General tab', () => { setThinkingEnabled: vi.fn().mockImplementation(async (enabled: boolean) => { useSettingsStore.setState({ thinkingEnabled: enabled }) }), + setAutoDreamEnabled: vi.fn().mockImplementation(async (enabled: boolean) => { + useSettingsStore.setState({ autoDreamEnabled: enabled }) + }), setTheme: vi.fn().mockImplementation(async (theme: ThemeMode) => { useSettingsStore.setState({ theme }) }), @@ -718,6 +722,41 @@ describe('Settings > General tab', () => { expect(useSettingsStore.getState().setThinkingEnabled).toHaveBeenCalledWith(false) }) + it('keeps Auto-dream disabled by default and confirms before enabling it', async () => { + render() + + fireEvent.click(screen.getByText('General')) + + const toggle = screen.getByLabelText('Enable Auto-dream') + expect(toggle).not.toBeChecked() + fireEvent.click(toggle) + + expect(useSettingsStore.getState().setAutoDreamEnabled).not.toHaveBeenCalled() + const dialog = screen.getByRole('dialog', { name: 'Enable Auto-dream?' }) + expect(within(dialog).getByText(/Keep the desktop app running/i)).toBeInTheDocument() + expect(within(dialog).getByText(/uses additional model tokens/i)).toBeInTheDocument() + + await act(async () => { + fireEvent.click(within(dialog).getByRole('button', { name: 'Enable Auto-dream' })) + }) + + expect(useSettingsStore.getState().setAutoDreamEnabled).toHaveBeenCalledWith(true) + expect(screen.getByLabelText('Enable Auto-dream')).toBeChecked() + }) + + it('lets the user disable Auto-dream without a confirmation dialog', async () => { + useSettingsStore.setState({ autoDreamEnabled: true }) + render() + + fireEvent.click(screen.getByText('General')) + await act(async () => { + fireEvent.click(screen.getByLabelText('Enable Auto-dream')) + }) + + expect(screen.queryByRole('dialog', { name: 'Enable Auto-dream?' })).not.toBeInTheDocument() + expect(useSettingsStore.getState().setAutoDreamEnabled).toHaveBeenCalledWith(false) + }) + it('uses the shared dropdown for response language', () => { render() diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 2fa83257..4e1ea3d2 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -970,6 +970,15 @@ export const en = { 'settings.general.thinkingDescription': 'Controls whether new sessions start with model thinking enabled. When off, compatible providers such as DeepSeek receive an explicit non-thinking parameter.', 'settings.general.thinkingEnabled': 'Enable thinking mode', 'settings.general.thinkingHint': 'Turn this off to start new sessions with --thinking disabled; useful for DeepSeek V4 Flash/Pro and other non-thinking workflows.', + 'settings.general.autoDreamTitle': 'Auto-dream', + 'settings.general.autoDreamDescription': 'Periodically consolidates auto-memory in the background after enough sessions have changed.', + 'settings.general.autoDreamEnabled': 'Enable Auto-dream', + 'settings.general.autoDreamHintOn': 'The desktop app can run memory consolidation after eligible sessions finish.', + 'settings.general.autoDreamHintOff': 'Off by default because it can run background model calls.', + 'settings.general.autoDreamConfirmTitle': 'Enable Auto-dream?', + 'settings.general.autoDreamConfirmKeepRunning': 'Keep the desktop app running so background memory consolidation can start after eligible sessions finish.', + 'settings.general.autoDreamConfirmTokenCost': 'Auto-dream uses additional model tokens when it reviews and rewrites memory files.', + 'settings.general.autoDreamConfirmEnable': 'Enable Auto-dream', 'settings.general.notificationsTitle': 'System Notifications', 'settings.general.notificationsDescription': 'Use native OS notifications for permission prompts, agent replies, and scheduled task results.', 'settings.general.notificationsEnabled': 'Enable system notifications', diff --git a/desktop/src/i18n/locales/jp.ts b/desktop/src/i18n/locales/jp.ts index 06e486b2..9a383993 100644 --- a/desktop/src/i18n/locales/jp.ts +++ b/desktop/src/i18n/locales/jp.ts @@ -972,6 +972,15 @@ export const jp: Record = { 'settings.general.thinkingDescription': '新しいセッションをモデルの思考を有効にして開始するかどうかを制御します。オフの場合、DeepSeek などの対応プロバイダーには明示的に非思考パラメーターが渡されます。', 'settings.general.thinkingEnabled': '思考モードを有効にする', 'settings.general.thinkingHint': '新しいセッションを --thinking 無効で開始するにはこれをオフにします。DeepSeek V4 Flash/Pro やその他の非思考ワークフローに便利です。', + 'settings.general.autoDreamTitle': 'Auto-dream', + 'settings.general.autoDreamDescription': '十分なセッションが変更された後、バックグラウンドで auto-memory を定期的に統合します。', + 'settings.general.autoDreamEnabled': 'Auto-dream を有効にする', + 'settings.general.autoDreamHintOn': '対象セッションの終了後、デスクトップアプリがバックグラウンドで記憶を整理できます。', + 'settings.general.autoDreamHintOff': 'バックグラウンドのモデル呼び出しが発生する可能性があるため、既定ではオフです。', + 'settings.general.autoDreamConfirmTitle': 'Auto-dream を有効にしますか?', + 'settings.general.autoDreamConfirmKeepRunning': '対象セッションの終了後にバックグラウンドの記憶整理を開始できるよう、デスクトップアプリを起動したままにしてください。', + 'settings.general.autoDreamConfirmTokenCost': 'Auto-dream は記憶ファイルの確認と書き換え時に追加のモデル Token を使用します。', + 'settings.general.autoDreamConfirmEnable': 'Auto-dream を有効にする', 'settings.general.notificationsTitle': 'システム通知', 'settings.general.notificationsDescription': '権限の確認、エージェントの応答、スケジュールタスクの結果に OS ネイティブの通知を使用します。', 'settings.general.notificationsEnabled': 'システム通知を有効にする', diff --git a/desktop/src/i18n/locales/kr.ts b/desktop/src/i18n/locales/kr.ts index fec144f4..e7e97abc 100644 --- a/desktop/src/i18n/locales/kr.ts +++ b/desktop/src/i18n/locales/kr.ts @@ -972,6 +972,15 @@ export const kr: Record = { 'settings.general.thinkingDescription': '새 세션을 모델 사고를 사용으로 시작할지 제어합니다. 꺼져 있으면 DeepSeek 같은 호환 공급자에 명시적인 비사고 매개변수가 전달됩니다.', 'settings.general.thinkingEnabled': '사고 모드 사용', 'settings.general.thinkingHint': '새 세션을 --thinking 비활성화로 시작하려면 이를 끄세요. DeepSeek V4 Flash/Pro 및 기타 비사고 워크플로에 유용합니다.', + 'settings.general.autoDreamTitle': 'Auto-dream', + 'settings.general.autoDreamDescription': '충분한 세션이 변경된 후 백그라운드에서 auto-memory를 주기적으로 통합합니다.', + 'settings.general.autoDreamEnabled': 'Auto-dream 사용', + 'settings.general.autoDreamHintOn': '대상 세션이 끝나면 데스크톱 앱이 백그라운드에서 메모리를 정리할 수 있습니다.', + 'settings.general.autoDreamHintOff': '백그라운드 모델 호출을 실행할 수 있으므로 기본값은 꺼짐입니다.', + 'settings.general.autoDreamConfirmTitle': 'Auto-dream을 사용하시겠습니까?', + 'settings.general.autoDreamConfirmKeepRunning': '대상 세션이 끝난 후 백그라운드 메모리 정리를 시작할 수 있도록 데스크톱 앱을 계속 실행해 두세요.', + 'settings.general.autoDreamConfirmTokenCost': 'Auto-dream은 메모리 파일을 검토하고 다시 쓸 때 추가 모델 Token을 사용합니다.', + 'settings.general.autoDreamConfirmEnable': 'Auto-dream 사용', 'settings.general.notificationsTitle': '시스템 알림', 'settings.general.notificationsDescription': '권한 확인, 에이전트 응답, 예약 작업 결과에 OS 기본 알림을 사용합니다.', 'settings.general.notificationsEnabled': '시스템 알림 사용', diff --git a/desktop/src/i18n/locales/zh-TW.ts b/desktop/src/i18n/locales/zh-TW.ts index 524a6da6..cad674a9 100644 --- a/desktop/src/i18n/locales/zh-TW.ts +++ b/desktop/src/i18n/locales/zh-TW.ts @@ -972,6 +972,15 @@ export const zh: Record = { 'settings.general.thinkingDescription': '控制新會話是否啟用模型思考。關閉後,DeepSeek 等相容供應商會收到顯式非思考模式引數。', 'settings.general.thinkingEnabled': '啟用思考模式', 'settings.general.thinkingHint': '關閉後會以 --thinking disabled 啟動新會話;適合 DeepSeek V4 Flash/Pro 等需要非思考模式的模型。', + 'settings.general.autoDreamTitle': '自動做夢', + 'settings.general.autoDreamDescription': '在累積足夠會話後,背景定期整理和壓縮 auto-memory。', + 'settings.general.autoDreamEnabled': '啟用自動做夢', + 'settings.general.autoDreamHintOn': '符合條件的會話結束後,桌面端可以在背景執行記憶整理。', + 'settings.general.autoDreamHintOff': '預設關閉,因為它可能發起背景模型呼叫。', + 'settings.general.autoDreamConfirmTitle': '啟用自動做夢?', + 'settings.general.autoDreamConfirmKeepRunning': '請保持桌面端處於啟用狀態,這樣符合條件的會話結束後才能啟動背景記憶整理。', + 'settings.general.autoDreamConfirmTokenCost': '自動做夢會在審查和改寫記憶檔案時額外消耗模型 Token。', + 'settings.general.autoDreamConfirmEnable': '啟用自動做夢', 'settings.general.notificationsTitle': '系統通知', 'settings.general.notificationsDescription': '使用作業系統原生通知提醒授權確認、Agent 回覆完成和定時任務結果。', 'settings.general.notificationsEnabled': '啟用系統通知', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 31eaeb18..9533ab82 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -972,6 +972,15 @@ export const zh: Record = { 'settings.general.thinkingDescription': '控制新会话是否启用模型思考。关闭后,DeepSeek 等兼容供应商会收到显式非思考模式参数。', 'settings.general.thinkingEnabled': '启用思考模式', 'settings.general.thinkingHint': '关闭后会以 --thinking disabled 启动新会话;适合 DeepSeek V4 Flash/Pro 等需要非思考模式的模型。', + 'settings.general.autoDreamTitle': '自动做梦', + 'settings.general.autoDreamDescription': '在积累足够会话后,后台定期整理和压缩 auto-memory。', + 'settings.general.autoDreamEnabled': '启用自动做梦', + 'settings.general.autoDreamHintOn': '符合条件的会话结束后,桌面端可以在后台执行记忆整理。', + 'settings.general.autoDreamHintOff': '默认关闭,因为它可能发起后台模型调用。', + 'settings.general.autoDreamConfirmTitle': '启用自动做梦?', + 'settings.general.autoDreamConfirmKeepRunning': '请保持桌面端处于启用状态,这样符合条件的会话结束后才能启动后台记忆整理。', + 'settings.general.autoDreamConfirmTokenCost': '自动做梦会在审查和改写记忆文件时额外消耗模型 Token。', + 'settings.general.autoDreamConfirmEnable': '启用自动做梦', 'settings.general.notificationsTitle': '系统通知', 'settings.general.notificationsDescription': '使用操作系统原生通知提醒授权确认、Agent 回复完成和定时任务结果。', 'settings.general.notificationsEnabled': '启用系统通知', diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 68eb41e3..a848095c 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -1438,6 +1438,8 @@ export function GeneralSettings() { const { thinkingEnabled, setThinkingEnabled, + autoDreamEnabled, + setAutoDreamEnabled, locale, setLocale, theme, @@ -1480,6 +1482,8 @@ export function GeneralSettings() { const [isSavingNetwork, setIsSavingNetwork] = useState(false) const [notificationPermission, setNotificationPermission] = useState('default') const [notificationActionRunning, setNotificationActionRunning] = useState(false) + const [autoDreamConfirmOpen, setAutoDreamConfirmOpen] = useState(false) + const [autoDreamActionRunning, setAutoDreamActionRunning] = useState(false) const [modeSwitchConfirmOpen, setModeSwitchConfirmOpen] = useState(false) const [pendingMode, setPendingMode] = useState(null) const [pendingPortableDir, setPendingPortableDir] = useState(null) @@ -1662,6 +1666,24 @@ export function GeneralSettings() { } } + const handleAutoDreamToggle = (enabled: boolean) => { + if (enabled) { + setAutoDreamConfirmOpen(true) + return + } + void setAutoDreamEnabled(false) + } + + const confirmAutoDreamEnable = async () => { + setAutoDreamActionRunning(true) + try { + await setAutoDreamEnabled(true) + setAutoDreamConfirmOpen(false) + } finally { + setAutoDreamActionRunning(false) + } + } + const handleNotificationPermissionAction = async () => { setNotificationActionRunning(true) try { @@ -2094,6 +2116,31 @@ export function GeneralSettings() { +
+

{t('settings.general.autoDreamTitle')}

+

{t('settings.general.autoDreamDescription')}

+ +
+

{t('settings.general.traceTitle')}

{t('settings.general.traceDescription')}

@@ -2615,6 +2662,24 @@ export function GeneralSettings() { confirmVariant="primary" loading={modeActionRunning} /> + { + if (!autoDreamActionRunning) setAutoDreamConfirmOpen(false) + }} + onConfirm={() => void confirmAutoDreamEnable()} + title={t('settings.general.autoDreamConfirmTitle')} + body={( +
+

{t('settings.general.autoDreamConfirmKeepRunning')}

+

{t('settings.general.autoDreamConfirmTokenCost')}

+
+ )} + confirmLabel={t('settings.general.autoDreamConfirmEnable')} + cancelLabel={t('common.cancel')} + confirmVariant="primary" + loading={autoDreamActionRunning} + />
) } diff --git a/desktop/src/stores/settingsStore.test.ts b/desktop/src/stores/settingsStore.test.ts index b7fde807..e4e3beed 100644 --- a/desktop/src/stores/settingsStore.test.ts +++ b/desktop/src/stores/settingsStore.test.ts @@ -753,6 +753,106 @@ describe('settingsStore thinking persistence', () => { }) }) +describe('settingsStore Auto-dream persistence', () => { + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + window.localStorage.clear() + }) + + it('keeps Auto-dream off unless user settings opt in', async () => { + vi.doMock('../api/settings', () => ({ + settingsApi: { + getUser: vi.fn().mockResolvedValue({}), + updateUser: vi.fn(), + getPermissionMode: vi.fn().mockResolvedValue({ mode: 'default' }), + setPermissionMode: vi.fn(), + getCliLauncherStatus: vi.fn(), + }, + })) + vi.doMock('../api/models', () => ({ + modelsApi: { + list: vi.fn().mockResolvedValue({ models: [] }), + getCurrent: vi.fn().mockResolvedValue({ model: null }), + setCurrent: vi.fn(), + getEffort: vi.fn().mockResolvedValue({ level: 'medium' }), + setEffort: vi.fn(), + }, + })) + vi.doMock('../api/h5Access', () => ({ + h5AccessApi: { + get: vi.fn().mockResolvedValue({ + settings: { + enabled: false, + tokenPreview: null, + allowedOrigins: [], + publicBaseUrl: null, + }, + }), + enable: vi.fn(), + disable: vi.fn(), + regenerate: vi.fn(), + update: vi.fn(), + }, + })) + + const { useSettingsStore } = await import('./settingsStore') + + expect(useSettingsStore.getState().autoDreamEnabled).toBe(false) + await useSettingsStore.getState().fetchAll() + expect(useSettingsStore.getState().autoDreamEnabled).toBe(false) + }) + + it('hydrates and persists Auto-dream explicitly', async () => { + const updateUser = vi.fn().mockResolvedValue({}) + + vi.doMock('../api/settings', () => ({ + settingsApi: { + getUser: vi.fn().mockResolvedValue({ autoDreamEnabled: true }), + updateUser, + getPermissionMode: vi.fn().mockResolvedValue({ mode: 'default' }), + setPermissionMode: vi.fn(), + getCliLauncherStatus: vi.fn(), + }, + })) + vi.doMock('../api/models', () => ({ + modelsApi: { + list: vi.fn().mockResolvedValue({ models: [] }), + getCurrent: vi.fn().mockResolvedValue({ model: null }), + setCurrent: vi.fn(), + getEffort: vi.fn().mockResolvedValue({ level: 'medium' }), + setEffort: vi.fn(), + }, + })) + vi.doMock('../api/h5Access', () => ({ + h5AccessApi: { + get: vi.fn().mockResolvedValue({ + settings: { + enabled: false, + tokenPreview: null, + allowedOrigins: [], + publicBaseUrl: null, + }, + }), + enable: vi.fn(), + disable: vi.fn(), + regenerate: vi.fn(), + update: vi.fn(), + }, + })) + + const { useSettingsStore } = await import('./settingsStore') + + await useSettingsStore.getState().fetchAll() + expect(useSettingsStore.getState().autoDreamEnabled).toBe(true) + + await useSettingsStore.getState().setAutoDreamEnabled(false) + + expect(updateUser).toHaveBeenCalledWith({ autoDreamEnabled: false }) + expect(useSettingsStore.getState().autoDreamEnabled).toBe(false) + }) +}) + describe('settingsStore desktop terminal shell persistence', () => { beforeEach(() => { vi.resetModules() diff --git a/desktop/src/stores/settingsStore.ts b/desktop/src/stores/settingsStore.ts index c1033f43..cb30d4c8 100644 --- a/desktop/src/stores/settingsStore.ts +++ b/desktop/src/stores/settingsStore.ts @@ -60,6 +60,7 @@ type SettingsStore = { currentModel: ModelInfo | null effortLevel: EffortLevel thinkingEnabled: boolean + autoDreamEnabled: boolean availableModels: ModelInfo[] activeProviderName: string | null locale: Locale @@ -95,6 +96,7 @@ type SettingsStore = { setModel: (modelId: string) => Promise setEffort: (level: EffortLevel) => Promise setThinkingEnabled: (enabled: boolean) => Promise + setAutoDreamEnabled: (enabled: boolean) => Promise setLocale: (locale: Locale) => void setTheme: (theme: ThemeMode) => Promise setChatSendBehavior: (behavior: ChatSendBehavior) => Promise @@ -169,6 +171,7 @@ export const useSettingsStore = create((set, get) => ({ currentModel: null, effortLevel: 'max', thinkingEnabled: true, + autoDreamEnabled: false, availableModels: [], activeProviderName: null, locale: getStoredLocale(), @@ -231,6 +234,7 @@ export const useSettingsStore = create((set, get) => ({ currentModel: model, effortLevel: level, thinkingEnabled: userSettings.alwaysThinkingEnabled !== false, + autoDreamEnabled: userSettings.autoDreamEnabled === true, theme, chatSendBehavior: normalizeChatSendBehavior(userSettings.chatSendBehavior), outputStyle: normalizeOutputStyle(userSettings.outputStyle), @@ -301,6 +305,17 @@ export const useSettingsStore = create((set, get) => ({ } }, + setAutoDreamEnabled: async (enabled) => { + const prev = get().autoDreamEnabled + set({ autoDreamEnabled: enabled }) + try { + await settingsApi.updateUser({ autoDreamEnabled: enabled }) + } catch (error) { + set({ autoDreamEnabled: prev }) + throw error + } + }, + setLocale: (locale) => { set({ locale }) try { localStorage.setItem(LOCALE_STORAGE_KEY, locale) } catch { /* noop */ } diff --git a/desktop/src/types/settings.ts b/desktop/src/types/settings.ts index c3066f7d..45a645aa 100644 --- a/desktop/src/types/settings.ts +++ b/desktop/src/types/settings.ts @@ -102,6 +102,7 @@ export type UserSettings = { modelContext?: string effort?: EffortLevel alwaysThinkingEnabled?: boolean + autoDreamEnabled?: boolean permissionMode?: PermissionMode theme?: ThemeMode chatSendBehavior?: ChatSendBehavior