From b7c4029d72bf5554d24392f3bc099f137313109d Mon Sep 17 00:00:00 2001 From: Yishen Tu Date: Tue, 5 May 2026 12:36:29 +0800 Subject: [PATCH] feat: support chat view placement setting --- src/app/settings/ClaudianSettingsStorage.ts | 52 +++++++++++++++-- src/app/settings/defaultSettings.ts | 2 +- src/core/types/settings.ts | 11 +++- src/features/settings/ClaudianSettings.ts | 20 ++++--- src/i18n/locales/de.json | 9 ++- src/i18n/locales/en.json | 9 ++- src/i18n/locales/es.json | 9 ++- src/i18n/locales/fr.json | 9 ++- src/i18n/locales/ja.json | 9 ++- src/i18n/locales/ko.json | 9 ++- src/i18n/locales/pt.json | 9 ++- src/i18n/locales/ru.json | 9 ++- src/i18n/locales/zh-CN.json | 9 ++- src/i18n/locales/zh-TW.json | 9 ++- src/i18n/types.ts | 7 ++- src/main.ts | 20 +++++-- tests/__mocks__/obsidian.ts | 6 ++ tests/integration/main.test.ts | 57 +++++++++++++++++-- .../storage/ClaudianSettingsStorage.test.ts | 41 +++++++++++++ .../unit/providers/claude/types/types.test.ts | 6 +- 20 files changed, 251 insertions(+), 61 deletions(-) diff --git a/src/app/settings/ClaudianSettingsStorage.ts b/src/app/settings/ClaudianSettingsStorage.ts index a12da7e84..b56846d62 100644 --- a/src/app/settings/ClaudianSettingsStorage.ts +++ b/src/app/settings/ClaudianSettingsStorage.ts @@ -12,12 +12,14 @@ import { resolveEnvironmentSnippetScope, } from '../../core/providers/providerEnvironment'; import type { VaultFileAdapter } from '../../core/storage/VaultFileAdapter'; -import type { - ClaudianSettings, - EnvironmentScope, - EnvSnippet, - HiddenProviderCommands, - ProviderConfigMap, +import { + CHAT_VIEW_PLACEMENTS, + type ChatViewPlacement, + type ClaudianSettings, + type EnvironmentScope, + type EnvSnippet, + type HiddenProviderCommands, + type ProviderConfigMap, } from '../../core/types/settings'; import { getClaudeProviderSettings, @@ -83,11 +85,43 @@ function stripLegacyFields(settings: Record): Record, + normalized: ChatViewPlacement, +): boolean { + return 'openInMainTab' in stored + || ( + 'chatViewPlacement' in stored + && stored.chatViewPlacement !== normalized + ); +} + function normalizeProviderConfigs(value: unknown): ProviderConfigMap { if (!value || typeof value !== 'object' || Array.isArray(value)) { return {}; @@ -196,6 +230,10 @@ export class ClaudianSettingsStorage { ); const envSnippets = normalizeEnvSnippets(stored.envSnippets); const providerConfigs = normalizeProviderConfigs(stored.providerConfigs); + const chatViewPlacement = normalizeChatViewPlacement( + stored.chatViewPlacement, + stored.openInMainTab, + ); const legacyProviderSettings = { ...stored, hiddenProviderCommands, @@ -211,6 +249,7 @@ export class ClaudianSettingsStorage { envSnippets, hiddenProviderCommands, providerConfigs, + chatViewPlacement, }; const merged = { @@ -239,6 +278,7 @@ export class ClaudianSettingsStorage { || 'allowedExportPaths' in stored || 'enableBlocklist' in stored || 'blockedCommands' in stored + || shouldPersistChatViewPlacementMigration(stored, chatViewPlacement) || JSON.stringify(envSnippets) !== JSON.stringify(stored.envSnippets ?? []) ) ) { diff --git a/src/app/settings/defaultSettings.ts b/src/app/settings/defaultSettings.ts index da92d20ed..ed10dceb7 100644 --- a/src/app/settings/defaultSettings.ts +++ b/src/app/settings/defaultSettings.ts @@ -45,7 +45,7 @@ export const DEFAULT_CLAUDIAN_SETTINGS: ClaudianSettings = { maxTabs: 3, tabBarPosition: 'input', enableAutoScroll: true, - openInMainTab: false, + chatViewPlacement: 'right-sidebar', hiddenProviderCommands: getDefaultHiddenProviderCommands(), }; diff --git a/src/core/types/settings.ts b/src/core/types/settings.ts index cf53db21b..c6f939d2f 100644 --- a/src/core/types/settings.ts +++ b/src/core/types/settings.ts @@ -55,6 +55,15 @@ export interface KeyboardNavigationSettings { /** Tab bar position setting. */ export type TabBarPosition = 'input' | 'header'; +export const CHAT_VIEW_PLACEMENTS = [ + 'right-sidebar', + 'left-sidebar', + 'main-tab', +] as const; + +/** Workspace location used when opening the Claudian chat view. */ +export type ChatViewPlacement = typeof CHAT_VIEW_PLACEMENTS[number]; + /** Result from instruction refinement agent query. */ export interface InstructionRefineResult { success: boolean; @@ -132,7 +141,7 @@ export interface ClaudianSettings { maxTabs: number; tabBarPosition: TabBarPosition; enableAutoScroll: boolean; - openInMainTab: boolean; + chatViewPlacement: ChatViewPlacement; // Provider command visibility hiddenProviderCommands: HiddenProviderCommands; diff --git a/src/features/settings/ClaudianSettings.ts b/src/features/settings/ClaudianSettings.ts index 2557a3a27..991637e7e 100644 --- a/src/features/settings/ClaudianSettings.ts +++ b/src/features/settings/ClaudianSettings.ts @@ -8,6 +8,7 @@ import { import { ProviderRegistry } from '../../core/providers/ProviderRegistry'; import { ProviderWorkspaceRegistry } from '../../core/providers/ProviderWorkspaceRegistry'; import type { ProviderId } from '../../core/providers/types'; +import type { ChatViewPlacement } from '../../core/types/settings'; import { getAvailableLocales, getLocaleDisplayName, setLocale, t } from '../../i18n/i18n'; import type { Locale, TranslationKey } from '../../i18n/types'; import type ClaudianPlugin from '../../main'; @@ -231,16 +232,19 @@ export class ClaudianSettingTab extends PluginSettingTab { }); new Setting(container) - .setName(t('settings.openInMainTab.name')) - .setDesc(t('settings.openInMainTab.desc')) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.openInMainTab) + .setName(t('settings.chatViewPlacement.name')) + .setDesc(t('settings.chatViewPlacement.desc')) + .addDropdown((dropdown) => { + dropdown + .addOption('right-sidebar', t('settings.chatViewPlacement.rightSidebar')) + .addOption('left-sidebar', t('settings.chatViewPlacement.leftSidebar')) + .addOption('main-tab', t('settings.chatViewPlacement.mainTab')) + .setValue(this.plugin.settings.chatViewPlacement) .onChange(async (value) => { - this.plugin.settings.openInMainTab = value; + this.plugin.settings.chatViewPlacement = value as ChatViewPlacement; await this.plugin.saveSettings(); - }) - ); + }); + }); new Setting(container) .setName(t('settings.enableAutoScroll.name')) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 81845345b..c4f0555ee 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -279,9 +279,12 @@ "name": "Automatisches Scrollen während Streaming", "desc": "Automatisch nach unten scrollen, während Claude Antworten streamt. Deaktivieren, um oben zu bleiben und von Anfang an zu lesen." }, - "openInMainTab": { - "name": "Im Haupteditorbereich öffnen", - "desc": "Chat-Panel als Haupttab im zentralen Editorbereich statt in der rechten Seitenleiste öffnen" + "chatViewPlacement": { + "name": "Claudian öffnen in", + "desc": "Wählen Sie, wo das Chat-Panel beim Erstellen geöffnet wird", + "rightSidebar": "Rechte Seitenleiste", + "leftSidebar": "Linke Seitenleiste", + "mainTab": "Haupteditor-Tab" }, "cliPath": { "name": "Claude CLI-Pfad", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b7c17ae4b..a5d6b7eff 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -279,9 +279,12 @@ "name": "Auto-scroll during streaming", "desc": "Automatically scroll to the bottom as Claude streams responses. Disable to stay at the top and read from the beginning." }, - "openInMainTab": { - "name": "Open in main editor area", - "desc": "Open chat panel as a main tab in the center editor area instead of the right sidebar" + "chatViewPlacement": { + "name": "Open Claudian in", + "desc": "Choose where the chat panel opens when it is created", + "rightSidebar": "Right sidebar", + "leftSidebar": "Left sidebar", + "mainTab": "Main editor tab" }, "cliPath": { "name": "Claude CLI path", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 52c463237..f0185ad99 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -279,9 +279,12 @@ "name": "Desplazamiento automático durante streaming", "desc": "Desplazarse automáticamente hacia abajo mientras Claude transmite respuestas. Desactivar para quedarse arriba y leer desde el principio." }, - "openInMainTab": { - "name": "Abrir en área de editor principal", - "desc": "Abrir el panel de chat como una pestaña principal en el área de editor central en lugar de la barra lateral derecha" + "chatViewPlacement": { + "name": "Abrir Claudian en", + "desc": "Elige dónde se abre el panel de chat cuando se crea", + "rightSidebar": "Barra lateral derecha", + "leftSidebar": "Barra lateral izquierda", + "mainTab": "Pestaña del editor principal" }, "cliPath": { "name": "Ruta CLI Claude", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 9e95a3cfa..37a911ccb 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -279,9 +279,12 @@ "name": "Défilement automatique pendant le streaming", "desc": "Défiler automatiquement vers le bas pendant que Claude diffuse les réponses. Désactiver pour rester en haut et lire depuis le début." }, - "openInMainTab": { - "name": "Ouvrir dans la zone d'éditeur principale", - "desc": "Ouvrir le panneau de chat comme un onglet principal dans la zone d'éditeur centrale au lieu de la barre latérale droite" + "chatViewPlacement": { + "name": "Ouvrir Claudian dans", + "desc": "Choisissez où le panneau de chat s'ouvre lors de sa création", + "rightSidebar": "Barre latérale droite", + "leftSidebar": "Barre latérale gauche", + "mainTab": "Onglet principal de l'éditeur" }, "cliPath": { "name": "Chemin CLI Claude", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 8c59c9cec..07113eb22 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -279,9 +279,12 @@ "name": "ストリーミング中の自動スクロール", "desc": "Claudeが応答をストリーミングしている間、自動的に下にスクロールします。無効にすると上部に留まり、最初から読むことができます。" }, - "openInMainTab": { - "name": "メインエディタ領域で開く", - "desc": "チャットパネルを右サイドバーではなく、中央エディタ領域のメインタブとして開きます" + "chatViewPlacement": { + "name": "Claudian を開く場所", + "desc": "チャットパネルを作成するときに開く場所を選択します", + "rightSidebar": "右サイドバー", + "leftSidebar": "左サイドバー", + "mainTab": "メインエディタタブ" }, "cliPath": { "name": "Claude CLI パス", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index 4570986f0..a4c80da01 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -279,9 +279,12 @@ "name": "스트리밍 중 자동 스크롤", "desc": "Claude가 응답을 스트리밍하는 동안 자동으로 아래로 스크롤합니다. 비활성화하면 상단에 머물러 처음부터 읽을 수 있습니다." }, - "openInMainTab": { - "name": "메인 편집기 영역에서 열기", - "desc": "채팅 패널을 오른쪽 사이드바가 아닌 중앙 편집기 영역의 메인 탭으로 엽니다" + "chatViewPlacement": { + "name": "Claudian 열 위치", + "desc": "채팅 패널이 생성될 때 열릴 위치를 선택합니다", + "rightSidebar": "오른쪽 사이드바", + "leftSidebar": "왼쪽 사이드바", + "mainTab": "메인 편집기 탭" }, "cliPath": { "name": "Claude CLI 경로", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index bb4157685..34d1bbfae 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -279,9 +279,12 @@ "name": "Rolagem automática durante streaming", "desc": "Rolar automaticamente para baixo enquanto o Claude transmite respostas. Desativar para ficar no topo e ler desde o início." }, - "openInMainTab": { - "name": "Abrir na área do editor principal", - "desc": "Abrir o painel de chat como uma aba principal na área do editor central em vez da barra lateral direita" + "chatViewPlacement": { + "name": "Abrir Claudian em", + "desc": "Escolha onde o painel de chat abre quando é criado", + "rightSidebar": "Barra lateral direita", + "leftSidebar": "Barra lateral esquerda", + "mainTab": "Aba do editor principal" }, "cliPath": { "name": "Caminho CLI Claude", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 83dd432bb..a36f10dfd 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -279,9 +279,12 @@ "name": "Автопрокрутка во время потоковой передачи", "desc": "Автоматически прокручивать вниз, пока Claude передает ответы. Отключите, чтобы оставаться наверху и читать с начала." }, - "openInMainTab": { - "name": "Открывать в основной области редактора", - "desc": "Открывать панель чата в виде основной вкладки в центральной области редактора вместо правой боковой панели" + "chatViewPlacement": { + "name": "Открывать Claudian в", + "desc": "Выберите, где открывается панель чата при создании", + "rightSidebar": "Правая боковая панель", + "leftSidebar": "Левая боковая панель", + "mainTab": "Основная вкладка редактора" }, "cliPath": { "name": "Путь к CLI Claude", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 3c406f5ed..6839877dc 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -279,9 +279,12 @@ "name": "流式传输时自动滚动", "desc": "在 Claude 流式传输响应时自动滚动到底部。禁用后将停留在顶部,从头开始阅读。" }, - "openInMainTab": { - "name": "在主编辑器区域打开", - "desc": "在中央编辑器区域以主标签页形式打开聊天面板,而不是在右侧边栏" + "chatViewPlacement": { + "name": "Claudian 打开位置", + "desc": "选择新建聊天面板时的打开位置", + "rightSidebar": "右侧边栏", + "leftSidebar": "左侧边栏", + "mainTab": "主编辑器标签页" }, "cliPath": { "name": "Claude CLI 路径", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index ec2c9ae17..44dcbf552 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -279,9 +279,12 @@ "name": "串流傳輸時自動捲動", "desc": "在 Claude 串流傳輸回應時自動捲動到底部。停用後將停留在頂部,從頭開始閱讀。" }, - "openInMainTab": { - "name": "在主編輯器區域開啟", - "desc": "在中央編輯器區域以主分頁形式開啟聊天面板,而不是在右側邊欄" + "chatViewPlacement": { + "name": "Claudian 開啟位置", + "desc": "選擇新建聊天面板時的開啟位置", + "rightSidebar": "右側邊欄", + "leftSidebar": "左側邊欄", + "mainTab": "主編輯器分頁" }, "cliPath": { "name": "Claude CLI 路徑", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index e1712df0c..1150bd384 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -221,8 +221,11 @@ export type TranslationKey = | 'settings.tabBarPosition.header' | 'settings.enableAutoScroll.name' | 'settings.enableAutoScroll.desc' - | 'settings.openInMainTab.name' - | 'settings.openInMainTab.desc' + | 'settings.chatViewPlacement.name' + | 'settings.chatViewPlacement.desc' + | 'settings.chatViewPlacement.rightSidebar' + | 'settings.chatViewPlacement.leftSidebar' + | 'settings.chatViewPlacement.mainTab' | 'settings.cliPath.name' | 'settings.cliPath.desc' | 'settings.cliPath.descWindows' diff --git a/src/main.ts b/src/main.ts index 0a9e59392..a538f575d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ patchSetMaxListenersForElectron(); import './providers'; -import type { Editor } from 'obsidian'; +import type { Editor, WorkspaceLeaf } from 'obsidian'; import { MarkdownView, Notice, Plugin } from 'obsidian'; import { DEFAULT_CLAUDIAN_SETTINGS } from './app/settings/defaultSettings'; @@ -29,7 +29,7 @@ import type { import { VIEW_TYPE_CLAUDIAN, } from './core/types'; -import type { EnvironmentScope } from './core/types/settings'; +import type { ChatViewPlacement, EnvironmentScope } from './core/types/settings'; import { ClaudianView } from './features/chat/ClaudianView'; import { type InlineEditContext, InlineEditModal } from './features/inline-edit/ui/InlineEditModal'; import { ClaudianSettingTab } from './features/settings/ClaudianSettings'; @@ -188,9 +188,7 @@ export default class ClaudianPlugin extends Plugin { let leaf = workspace.getLeavesOfType(VIEW_TYPE_CLAUDIAN)[0]; if (!leaf) { - const newLeaf = this.settings.openInMainTab - ? workspace.getLeaf('tab') - : workspace.getRightLeaf(false); + const newLeaf = this.getLeafForPlacement(this.settings.chatViewPlacement); if (newLeaf) { await newLeaf.setViewState({ type: VIEW_TYPE_CLAUDIAN, @@ -205,6 +203,18 @@ export default class ClaudianPlugin extends Plugin { } } + private getLeafForPlacement(placement: ChatViewPlacement): WorkspaceLeaf | null { + const { workspace } = this.app; + switch (placement) { + case 'main-tab': + return workspace.getLeaf('tab'); + case 'left-sidebar': + return workspace.getLeftLeaf(false); + case 'right-sidebar': + return workspace.getRightLeaf(false); + } + } + private canCreateNewTab(): boolean { const view = this.getView(); const tabManager = view?.getTabManager(); diff --git a/tests/__mocks__/obsidian.ts b/tests/__mocks__/obsidian.ts index 1d4666e56..b0bcee805 100644 --- a/tests/__mocks__/obsidian.ts +++ b/tests/__mocks__/obsidian.ts @@ -74,6 +74,12 @@ export class App { getRightLeaf: jest.fn().mockReturnValue({ setViewState: jest.fn().mockResolvedValue(undefined), }), + getLeftLeaf: jest.fn().mockReturnValue({ + setViewState: jest.fn().mockResolvedValue(undefined), + }), + getLeaf: jest.fn().mockReturnValue({ + setViewState: jest.fn().mockResolvedValue(undefined), + }), revealLeaf: jest.fn(), }; } diff --git a/tests/integration/main.test.ts b/tests/integration/main.test.ts index 167e9587c..a7bba4a2e 100644 --- a/tests/integration/main.test.ts +++ b/tests/integration/main.test.ts @@ -50,6 +50,9 @@ describe('ClaudianPlugin', () => { getRightLeaf: jest.fn().mockReturnValue({ setViewState: jest.fn().mockResolvedValue(undefined), }), + getLeftLeaf: jest.fn().mockReturnValue({ + setViewState: jest.fn().mockResolvedValue(undefined), + }), getLeaf: jest.fn().mockReturnValue({ setViewState: jest.fn().mockResolvedValue(undefined), }), @@ -130,7 +133,7 @@ describe('ClaudianPlugin', () => { expect(mockApp.workspace.revealLeaf).toHaveBeenCalledWith(mockLeaf); }); - it('should create new leaf in right sidebar if view does not exist', async () => { + it('should create new leaf in right sidebar by default if view does not exist', async () => { const mockRightLeaf = { setViewState: jest.fn().mockResolvedValue(undefined), }; @@ -147,6 +150,26 @@ describe('ClaudianPlugin', () => { }); }); + it('should create new leaf in left sidebar when chatViewPlacement is left-sidebar', async () => { + const mockLeftLeaf = { + setViewState: jest.fn().mockResolvedValue(undefined), + }; + mockApp.workspace.getLeavesOfType.mockReturnValue([]); + mockApp.workspace.getLeftLeaf.mockReturnValue(mockLeftLeaf); + + await plugin.onload(); + plugin.settings.chatViewPlacement = 'left-sidebar'; + await plugin.activateView(); + + expect(mockApp.workspace.getLeftLeaf).toHaveBeenCalledWith(false); + expect(mockApp.workspace.getRightLeaf).not.toHaveBeenCalled(); + expect(mockApp.workspace.getLeaf).not.toHaveBeenCalled(); + expect(mockLeftLeaf.setViewState).toHaveBeenCalledWith({ + type: VIEW_TYPE_CLAUDIAN, + active: true, + }); + }); + it('should handle null right leaf gracefully', async () => { mockApp.workspace.getLeavesOfType.mockReturnValue([]); mockApp.workspace.getRightLeaf.mockReturnValue(null); @@ -157,7 +180,7 @@ describe('ClaudianPlugin', () => { await expect(plugin.activateView()).resolves.not.toThrow(); }); - it('should create new leaf in main editor area when openInMainTab is enabled', async () => { + it('should create new leaf in main editor area when chatViewPlacement is main-tab', async () => { const mockMainLeaf = { setViewState: jest.fn().mockResolvedValue(undefined), }; @@ -165,23 +188,24 @@ describe('ClaudianPlugin', () => { mockApp.workspace.getLeaf.mockReturnValue(mockMainLeaf); await plugin.onload(); - plugin.settings.openInMainTab = true; + plugin.settings.chatViewPlacement = 'main-tab'; await plugin.activateView(); expect(mockApp.workspace.getLeaf).toHaveBeenCalledWith('tab'); expect(mockApp.workspace.getRightLeaf).not.toHaveBeenCalled(); + expect(mockApp.workspace.getLeftLeaf).not.toHaveBeenCalled(); expect(mockMainLeaf.setViewState).toHaveBeenCalledWith({ type: VIEW_TYPE_CLAUDIAN, active: true, }); }); - it('should handle null main leaf gracefully when openInMainTab is enabled', async () => { + it('should handle null main leaf gracefully when chatViewPlacement is main-tab', async () => { mockApp.workspace.getLeavesOfType.mockReturnValue([]); mockApp.workspace.getLeaf.mockReturnValue(null); await plugin.onload(); - plugin.settings.openInMainTab = true; + plugin.settings.chatViewPlacement = 'main-tab'; await expect(plugin.activateView()).resolves.not.toThrow(); }); @@ -259,6 +283,29 @@ describe('ClaudianPlugin', () => { expect(plugin.settings).toEqual(DEFAULT_SETTINGS); }); + it('should migrate legacy openInMainTab true to main-tab placement', async () => { + mockApp.vault.adapter.exists.mockImplementation(async (path: string) => { + return path === '.claudian/claudian-settings.json'; + }); + mockApp.vault.adapter.read.mockImplementation(async (path: string) => { + if (path === '.claudian/claudian-settings.json') { + return JSON.stringify({ openInMainTab: true }); + } + return ''; + }); + + await plugin.loadSettings(); + + expect(plugin.settings.chatViewPlacement).toBe('main-tab'); + const writeCall = (mockApp.vault.adapter.write as jest.Mock).mock.calls.find( + ([path]) => path === '.claudian/claudian-settings.json', + ); + expect(writeCall).toBeDefined(); + const content = JSON.parse(writeCall[1]); + expect(content.chatViewPlacement).toBe('main-tab'); + expect(content).not.toHaveProperty('openInMainTab'); + }); + it('should reconcile model from environment and persist when changed', async () => { // Mock claudian-settings.json with environment variables mockApp.vault.adapter.exists.mockImplementation(async (path: string) => { diff --git a/tests/unit/providers/claude/storage/ClaudianSettingsStorage.test.ts b/tests/unit/providers/claude/storage/ClaudianSettingsStorage.test.ts index b7688f532..bae81b530 100644 --- a/tests/unit/providers/claude/storage/ClaudianSettingsStorage.test.ts +++ b/tests/unit/providers/claude/storage/ClaudianSettingsStorage.test.ts @@ -87,6 +87,47 @@ describe('ClaudianSettingsStorage', () => { expect(result.thinkingBudget).toBe(DEFAULT_SETTINGS.thinkingBudget); }); + it('migrates legacy openInMainTab true to main-tab placement', async () => { + mockAdapter.exists.mockResolvedValue(true); + mockAdapter.read.mockResolvedValue(JSON.stringify({ + openInMainTab: true, + })); + + const result = await storage.load(); + const writtenContent = JSON.parse(mockAdapter.write.mock.calls[0][1]); + + expect(result.chatViewPlacement).toBe('main-tab'); + expect(writtenContent.chatViewPlacement).toBe('main-tab'); + expect(writtenContent).not.toHaveProperty('openInMainTab'); + }); + + it('migrates legacy openInMainTab false to right-sidebar placement', async () => { + mockAdapter.exists.mockResolvedValue(true); + mockAdapter.read.mockResolvedValue(JSON.stringify({ + openInMainTab: false, + })); + + const result = await storage.load(); + const writtenContent = JSON.parse(mockAdapter.write.mock.calls[0][1]); + + expect(result.chatViewPlacement).toBe('right-sidebar'); + expect(writtenContent.chatViewPlacement).toBe('right-sidebar'); + expect(writtenContent).not.toHaveProperty('openInMainTab'); + }); + + it('normalizes invalid chatViewPlacement values', async () => { + mockAdapter.exists.mockResolvedValue(true); + mockAdapter.read.mockResolvedValue(JSON.stringify({ + chatViewPlacement: 'floating-window', + })); + + const result = await storage.load(); + const writtenContent = JSON.parse(mockAdapter.write.mock.calls[0][1]); + + expect(result.chatViewPlacement).toBe('right-sidebar'); + expect(writtenContent.chatViewPlacement).toBe('right-sidebar'); + }); + it('should strip legacy blocklist fields from loaded data', async () => { mockAdapter.exists.mockResolvedValue(true); mockAdapter.read.mockResolvedValue(JSON.stringify({ diff --git a/tests/unit/providers/claude/types/types.test.ts b/tests/unit/providers/claude/types/types.test.ts index 43fa46b4c..fbbd62e4d 100644 --- a/tests/unit/providers/claude/types/types.test.ts +++ b/tests/unit/providers/claude/types/types.test.ts @@ -94,7 +94,7 @@ describe('types.ts', () => { enableSonnet1M: false, tabBarPosition: 'input', enableAutoScroll: true, - openInMainTab: false, + chatViewPlacement: 'right-sidebar', hiddenProviderCommands: { claude: [], codex: [], @@ -145,7 +145,7 @@ describe('types.ts', () => { enableSonnet1M: false, tabBarPosition: 'input', enableAutoScroll: true, - openInMainTab: false, + chatViewPlacement: 'right-sidebar', hiddenProviderCommands: { claude: [], codex: [], @@ -197,7 +197,7 @@ describe('types.ts', () => { enableSonnet1M: false, tabBarPosition: 'header', enableAutoScroll: false, - openInMainTab: false, + chatViewPlacement: 'right-sidebar', hiddenProviderCommands: { claude: [], codex: [],