From 0edc64d99a243eb2de5e32213082807bc419e942 Mon Sep 17 00:00:00 2001 From: KokerZhou <111279477+KokerZhou@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:50:43 +0800 Subject: [PATCH] fix(settings): allow memory as the default session tab (#494) --- packages/config/src/loader.ts | 12 ++++++- packages/config/src/schema.test.ts | 36 +++++++++++++++++++ packages/config/src/schema.ts | 27 +++++++++----- packages/config/src/set-config-field.test.ts | 35 ++++++++++++++++++ packages/shared-types/index.ts | 2 +- .../common/Settings/BasicSettingsTab.vue | 3 +- src/i18n/locales/en-US/settings.json | 3 +- src/i18n/locales/ja-JP/settings.json | 3 +- src/i18n/locales/zh-CN/settings.json | 3 +- src/i18n/locales/zh-TW/settings.json | 3 +- src/pages/group-chat/index.vue | 3 +- src/pages/private-chat/index.vue | 3 +- src/stores/settings.ts | 2 +- 13 files changed, 117 insertions(+), 18 deletions(-) diff --git a/packages/config/src/loader.ts b/packages/config/src/loader.ts index b70c912c..5f75c58f 100644 --- a/packages/config/src/loader.ts +++ b/packages/config/src/loader.ts @@ -138,7 +138,17 @@ export function writeConfigField(section: string, key: string, value: string | n if (!existing[section] || typeof existing[section] !== 'object') { existing[section] = {} } - existing[section][key] = value + if (section === 'ui' && key === 'default_session_tab') { + if (value === 'memory') { + existing.ui.default_session_tab = 'overview' + existing.ui.default_session_tab_override = 'memory' + } else { + existing.ui.default_session_tab = value + delete existing.ui.default_session_tab_override + } + } else { + existing[section][key] = value + } const lines: string[] = [] for (const [sec, entries] of Object.entries(existing)) { diff --git a/packages/config/src/schema.test.ts b/packages/config/src/schema.test.ts index a8afafc7..06e7313c 100644 --- a/packages/config/src/schema.test.ts +++ b/packages/config/src/schema.test.ts @@ -14,8 +14,44 @@ test('normalizes the released overview preference to insights', () => { assert.equal(config.ui.default_session_tab, 'insights') }) +test('accepts memory as a session landing tab', () => { + const config = configSchema.parse({ + ui: { + default_session_tab: 'overview', + default_session_tab_override: 'memory', + }, + }) + + assert.equal(config.ui.default_session_tab, 'memory') +}) + +test('accepts canonical memory input and its parsed output', () => { + const config = configSchema.parse({ ui: { default_session_tab: 'memory' } }) + + assert.equal(config.ui.default_session_tab, 'memory') + assert.equal(configSchema.parse(config).ui.default_session_tab, 'memory') +}) + +test('prefers a later legacy session tab change over the memory override', () => { + const config = configSchema.parse({ + ui: { + default_session_tab: 'ai-chat', + default_session_tab_override: 'memory', + }, + }) + + assert.equal(config.ui.default_session_tab, 'ai-chat') +}) + +test('does not reinterpret a historical overview preference as memory', () => { + const config = configSchema.parse({ ui: { default_session_tab: 'overview' } }) + + assert.equal(config.ui.default_session_tab, 'insights') +}) + test('rejects unknown session landing tabs', () => { assert.throws(() => configSchema.parse({ ui: { default_session_tab: 'unknown' } })) + assert.throws(() => configSchema.parse({ ui: { default_session_tab_override: 'unknown' } })) }) test('defaults Windows close behavior to running in the background', () => { diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts index 5324c42c..96b44850 100644 --- a/packages/config/src/schema.ts +++ b/packages/config/src/schema.ts @@ -30,14 +30,25 @@ export const localeConfigSchema = z.object({ lang: z.string().default(''), }) -export const uiConfigSchema = z.object({ - default_session_tab: z.preprocess( - (value) => (value === 'overview' ? 'insights' : value), - z.enum(['insights', 'ai-chat']).default('insights') - ), - session_gap_threshold: z.number().int().min(60).max(86400).default(1800), - summary_strategy: z.enum(['brief', 'standard']).default('standard'), -}) +const defaultSessionTabSchema = z.preprocess( + (value) => (value === 'overview' ? 'insights' : value), + z.enum(['insights', 'ai-chat', 'memory']).default('insights') +) + +export const uiConfigSchema = z + .object({ + default_session_tab: z.unknown().optional(), + default_session_tab_override: z.literal('memory').optional(), + session_gap_threshold: z.number().int().min(60).max(86400).default(1800), + summary_strategy: z.enum(['brief', 'standard']).default('standard'), + }) + .transform(({ default_session_tab_override, ...config }) => ({ + ...config, + default_session_tab: + default_session_tab_override && config.default_session_tab === 'overview' + ? default_session_tab_override + : defaultSessionTabSchema.parse(config.default_session_tab), + })) export const cliConfigSchema = z.object({ /** Allow --raw output that bypasses privacy preprocessing (user-side opt-in). */ diff --git a/packages/config/src/set-config-field.test.ts b/packages/config/src/set-config-field.test.ts index bc6875dd..3b42bb77 100644 --- a/packages/config/src/set-config-field.test.ts +++ b/packages/config/src/set-config-field.test.ts @@ -3,9 +3,12 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync import { join } from 'node:path' import { tmpdir } from 'node:os' import { after, beforeEach, describe, it } from 'node:test' +import { parse as parseToml } from 'smol-toml' +import { z } from 'zod' const tempHome = mkdtempSync(join(tmpdir(), 'chatlab-config-set-')) process.env.HOME = tempHome +process.env.USERPROFILE = tempHome const [{ setConfigField, ConfigSetError }, { loadConfig }] = await Promise.all([ import('./set-config-field'), @@ -69,6 +72,38 @@ describe('setConfigField', () => { assert.equal(loadConfig().desktop.close_behavior, 'background') }) + it('persists memory without breaking the v0.37.5 UI config schema', () => { + setConfigField('ui.default_session_tab', 'memory') + + assert.equal(loadConfig().ui.default_session_tab, 'memory') + + const saved = parseToml(readFileSync(configToml, 'utf-8')) as { ui: Record } + const releasedUiConfigSchema = z.object({ + default_session_tab: z.preprocess( + (value) => (value === 'overview' ? 'insights' : value), + z.enum(['insights', 'ai-chat']).default('insights') + ), + session_gap_threshold: z.number().int().min(60).max(86400).default(1800), + summary_strategy: z.enum(['brief', 'standard']).default('standard'), + }) + assert.equal(releasedUiConfigSchema.parse(saved.ui).default_session_tab, 'insights') + }) + + it('honors a later session tab write from a v0.37.5 client', () => { + setConfigField('ui.default_session_tab', 'memory') + + let content = readFileSync(configToml, 'utf-8').replace( + 'default_session_tab = "overview"', + 'default_session_tab = "ai-chat"' + ) + writeFileSync(configToml, content, 'utf-8') + assert.equal(loadConfig().ui.default_session_tab, 'ai-chat') + + content = content.replace('default_session_tab = "ai-chat"', 'default_session_tab = "insights"') + writeFileSync(configToml, content, 'utf-8') + assert.equal(loadConfig().ui.default_session_tab, 'insights') + }) + it('writes boolean value and round-trips via loadConfig', () => { const result = setConfigField('cli.allow_raw', 'true') assert.deepEqual(result, { section: 'cli', key: 'allow_raw', value: true }) diff --git a/packages/shared-types/index.ts b/packages/shared-types/index.ts index c6a6118c..ca99a673 100644 --- a/packages/shared-types/index.ts +++ b/packages/shared-types/index.ts @@ -1420,7 +1420,7 @@ export interface Preferences { } export interface UiConfig { - default_session_tab: 'insights' | 'ai-chat' + default_session_tab: 'insights' | 'ai-chat' | 'memory' session_gap_threshold: number summary_strategy?: 'brief' | 'standard' } diff --git a/src/components/common/Settings/BasicSettingsTab.vue b/src/components/common/Settings/BasicSettingsTab.vue index 957bfa27..c9fa512c 100644 --- a/src/components/common/Settings/BasicSettingsTab.vue +++ b/src/components/common/Settings/BasicSettingsTab.vue @@ -11,7 +11,7 @@ import { availableLocales, type LocaleType } from '@/i18n' import NetworkSettingsSection from './NetworkSettingsSection.vue' import UITabs from '@/components/UI/Tabs.vue' import { usePlatformService } from '@/services' -import { IS_ELECTRON } from '@/utils/platform' +import { IS_ELECTRON, IS_WEB_WASM } from '@/utils/platform' import { INSIGHT_CARD_THEMES, type InsightCardThemeId } from '@/utils/insight-card-theme' import type { DesktopCloseBehavior } from '@openchatlab/shared-types' @@ -155,6 +155,7 @@ const currentLocale = computed({ const defaultTabOptions = computed(() => [ { label: t('settings.basic.defaultTab.insights'), value: 'insights' }, { label: t('settings.basic.defaultTab.aiChat'), value: 'ai-chat' }, + ...(!IS_WEB_WASM ? [{ label: t('settings.basic.defaultTab.memory'), value: 'memory' }] : []), ]) // Tools panel position options diff --git a/src/i18n/locales/en-US/settings.json b/src/i18n/locales/en-US/settings.json index 3616563d..ee3f5e49 100644 --- a/src/i18n/locales/en-US/settings.json +++ b/src/i18n/locales/en-US/settings.json @@ -44,7 +44,8 @@ "defaultTab": { "description": "Default page when entering a chat conversation", "insights": "Insights", - "aiChat": "AI Chat" + "aiChat": "AI Chat", + "memory": "Memory" }, "autoLaunch": { "openAtLogin": "Launch at Login", diff --git a/src/i18n/locales/ja-JP/settings.json b/src/i18n/locales/ja-JP/settings.json index 2e81a232..810e472c 100644 --- a/src/i18n/locales/ja-JP/settings.json +++ b/src/i18n/locales/ja-JP/settings.json @@ -44,7 +44,8 @@ "defaultTab": { "description": "チャットに入った時に表示するページ", "insights": "インサイト", - "aiChat": "AI チャット" + "aiChat": "AI チャット", + "memory": "メモリー" }, "autoLaunch": { "openAtLogin": "ログイン時に自動起動", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index ab54bbd4..3dd603ec 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -44,7 +44,8 @@ "defaultTab": { "description": "进入聊天对话后默认显示的页面", "insights": "洞察", - "aiChat": "AI 对话" + "aiChat": "AI 对话", + "memory": "记忆" }, "autoLaunch": { "openAtLogin": "开机自启动", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 58138929..fb953617 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -44,7 +44,8 @@ "defaultTab": { "description": "進入聊天對話後預設顯示的頁面", "insights": "洞察", - "aiChat": "AI 對話" + "aiChat": "AI 對話", + "memory": "記憶" }, "autoLaunch": { "openAtLogin": "開機自動啟動", diff --git a/src/pages/group-chat/index.vue b/src/pages/group-chat/index.vue index 18b00428..dfef88ca 100644 --- a/src/pages/group-chat/index.vue +++ b/src/pages/group-chat/index.vue @@ -85,7 +85,8 @@ const { router, currentSessionId, selectSession: sessionStore.selectSession, - defaultTab: settingsStore.defaultSessionTab, + defaultTab: + IS_WEB_WASM && settingsStore.defaultSessionTab === 'memory' ? 'insights' : settingsStore.defaultSessionTab, validTabIds: tabs.map((tab) => tab.id), }) diff --git a/src/pages/private-chat/index.vue b/src/pages/private-chat/index.vue index 26ce02eb..589e1cf2 100644 --- a/src/pages/private-chat/index.vue +++ b/src/pages/private-chat/index.vue @@ -80,7 +80,8 @@ const { router, currentSessionId, selectSession: sessionStore.selectSession, - defaultTab: settingsStore.defaultSessionTab, + defaultTab: + IS_WEB_WASM && settingsStore.defaultSessionTab === 'memory' ? 'insights' : settingsStore.defaultSessionTab, validTabIds: tabs.map((tab) => tab.id), }) diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 114d77bf..55f2f3ad 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -34,7 +34,7 @@ export const useSettingsStore = defineStore( () => { const locale = ref(getLocale()) - const defaultSessionTab = ref<'insights' | 'ai-chat'>('insights') + const defaultSessionTab = ref<'insights' | 'ai-chat' | 'memory'>('insights') const insightCardTheme = ref(DEFAULT_INSIGHT_CARD_THEME)