Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ version with its date and start a fresh empty `[Unreleased]` above it.

## [Unreleased]

### Added

- Qoder CLI edition switch in settings (Setup section): choose the
international build (`qodercli`, config under `~/.qoder`) or the
China build (`qoderclicn`, config under `~/.qoder-cn`). Auto-
detection, session history, global plugins, and login hints all
follow the selected edition.

### Fixed

- The composer now adapts to narrow sidebars: context chips that do
Expand Down
12 changes: 12 additions & 0 deletions src/core/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,22 @@ export type PermissionMode = 'default' | 'acceptEdits' | 'auto' | 'plan' | 'yolo
/** Opaque device-keyed CLI paths for per-device configuration. */
export type HostnameCliPaths = Record<string, string>;

/** Supported Qoder CLI distribution builds. */
export const QODER_CLI_EDITIONS = ['global', 'cn'] as const;

/**
* Qoder CLI distribution edition: the international build (`qodercli`,
* config under `~/.qoder`) or the China build (`qoderclicn`, config under
* `~/.qoder-cn`).
*/
export type QoderCliEdition = typeof QODER_CLI_EDITIONS[number];

/** Qoder CLI settings stored alongside Qoderian's general preferences. */
export interface QoderSettings {
cliPath: string;
cliPathsByHost: HostnameCliPaths;
/** Selected Qoder CLI distribution build. */
edition: QoderCliEdition;
loadUserSettings: boolean;
enableBangBash: boolean;
discoveredModels: Array<{
Expand Down
4 changes: 2 additions & 2 deletions src/features/chat/ui/toolbar/toolbar-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Notice } from 'obsidian';

import type { QoderRuntimeStatus } from '../../../../core/types/services';
import type { PermissionMode } from '../../../../core/types/settings';
import { getActiveQoderCliEdition, getQoderCliLoginCommand } from '../../../../qoder/config/cli-edition';
import type { QoderModelConfig } from '../../../../qoder/models/qoder-model-config';
import { createIconSvg, QODER_ICON } from '../../../../shared/icons';
import { ClickPopover } from './click-popover';
Expand Down Expand Up @@ -34,7 +35,6 @@ const DEFAULT_RUNTIME_STATUS: QoderRuntimeStatus = {
kind: 'ready',
message: 'Qoder CLI is ready.',
};
const QODER_LOGIN_COMMAND = 'qodercli login';

function getRuntimeStatusLabel(status: QoderRuntimeStatus): string {
switch (status.kind) {
Expand Down Expand Up @@ -137,7 +137,7 @@ export class ModelSelector {
if (status.kind === 'authRequired') {
statusEl.createEl('code', {
cls: 'qoderian-model-runtime-command',
text: QODER_LOGIN_COMMAND,
text: getQoderCliLoginCommand(getActiveQoderCliEdition()),
});
}
if (status.details) {
Expand Down
8 changes: 8 additions & 0 deletions src/features/settings/settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
renderBangBashControl,
renderMcpSection,
renderPluginsSection,
renderQoderCliEditionControl,
renderQoderCliPathControl,
renderQoderCliPathSetting,
renderQoderSettingsTab,
Expand Down Expand Up @@ -66,6 +67,13 @@ export class QoderianSettingTab extends PluginSettingTab {
type: 'group',
heading: t('settings.setup'),
items: [
{
name: t('settings.cliEdition.name'),
desc: t('settings.cliEdition.desc'),
render: (setting) => {
renderQoderCliEditionControl(setting, { plugin: this.plugin });
},
},
{
name: t('settings.cliPath.name'),
desc: getCliPathDescription(),
Expand Down
43 changes: 43 additions & 0 deletions src/features/settings/ui/qoder-settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { expandHomePath } from '../../../core/fs/path';
import type { AppMcpStorage } from '../../../core/types/services';
import { t } from '../../../i18n/i18n';
import type QoderianPlugin from '../../../main';
import { normalizeQoderCliEdition } from '../../../qoder/config/cli-edition';
import {
getQoderSettings,
updateQoderSettings,
Expand All @@ -23,6 +24,10 @@ export interface QoderCliPathSettingContext {
plugin: QoderianPlugin;
}

export interface QoderCliEditionSettingContext {
plugin: QoderianPlugin;
}

export interface SlashCommandsSectionContext {
plugin: QoderianPlugin;
}
Expand Down Expand Up @@ -155,12 +160,50 @@ export function renderQoderCliPathControl(
return validationEl;
}

/**
* Attaches the CLI edition dropdown to `setting`. Switching editions changes
* the executable name and the CLI's user config root, so the resolver cache
* is dropped and every tab restarts, mirroring a CLI path change.
*/
export function renderQoderCliEditionControl(
setting: Setting,
context: QoderCliEditionSettingContext,
): void {
const qoderWorkspace = context.plugin.qoderServices;
const settingsBag = context.plugin.settings as unknown as Record<string, unknown>;

setting.addDropdown((dropdown) => {
dropdown
.addOption('global', t('settings.cliEdition.global'))
.addOption('cn', t('settings.cliEdition.cn'))
.setValue(getQoderSettings(settingsBag).edition)
.onChange(async (value) => {
const edition = normalizeQoderCliEdition(value);
updateQoderSettings(settingsBag, { edition });
await context.plugin.saveSettings();
qoderWorkspace.cliResolver.reset();
await qoderWorkspace.pluginManager.loadPlugins();
const view = context.plugin.getView();
await view?.getTabManager()?.broadcastToAllTabs(
(service) => Promise.resolve(service.cleanup())
);
void qoderWorkspace.agentCatalog.refresh();
});
});
}

/** Renders the "Setup" heading plus the CLI path row (imperative fallback). */
export function renderQoderCliPathSetting(
container: HTMLElement,
context: QoderCliPathSettingContext,
): void {
new Setting(container).setName(t('settings.setup')).setHeading();

const editionSetting = new Setting(container)
.setName(t('settings.cliEdition.name'))
.setDesc(t('settings.cliEdition.desc'));
renderQoderCliEditionControl(editionSetting, context);

const cliPathSetting = new Setting(container)
.setName(t('settings.cliPath.name'))
.setDesc(getCliPathDescription());
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "Linke Seitenleiste",
"mainTab": "Haupteditor-Tab"
},
"cliEdition": {
"name": "Qoder CLI-Ausgabe",
"desc": "Wählen Sie die internationale Version (qodercli, ~/.qoder) oder die China-Version (qoderclicn, ~/.qoder-cn). Geöffnete Tabs werden nach dem Wechsel sofort neu gestartet, laufende Unterhaltungen werden unterbrochen. Der Gesprächsverlauf wird pro Ausgabe getrennt gespeichert (~/.qoder und ~/.qoder-cn) und nicht gemeinsam genutzt.",
"global": "International (qodercli)",
"cn": "China (qoderclicn)"
},
"cliPath": {
"name": "Qoder CLI-Pfad",
"desc": "Benutzerdefinierter Pfad zum Qoder CLI. Leer lassen für automatische Erkennung.",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "Left sidebar",
"mainTab": "Main editor tab"
},
"cliEdition": {
"name": "Qoder CLI edition",
"desc": "Choose the international build (qodercli, ~/.qoder) or the China build (qoderclicn, ~/.qoder-cn). Open tabs restart immediately after switching, interrupting in-progress conversations. Session history is stored separately per edition (~/.qoder and ~/.qoder-cn) and is not shared between editions.",
"global": "International (qodercli)",
"cn": "China (qoderclicn)"
},
"cliPath": {
"name": "Qoder CLI path",
"desc": "Custom path to Qoder CLI. Leave empty for auto-detection.",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "Barra lateral izquierda",
"mainTab": "Pestaña del editor principal"
},
"cliEdition": {
"name": "Edición de Qoder CLI",
"desc": "Elija la versión internacional (qodercli, ~/.qoder) o la versión de China (qoderclicn, ~/.qoder-cn). Las pestañas abiertas se reinician inmediatamente tras el cambio, interrumpiendo las conversaciones en curso. El historial de conversaciones se almacena por separado en cada edición (~/.qoder y ~/.qoder-cn) y no se comparte entre ediciones.",
"global": "Internacional (qodercli)",
"cn": "China (qoderclicn)"
},
"cliPath": {
"name": "Ruta CLI Qoder",
"desc": "Ruta personalizada a Qoder CLI. Dejar vacío para detección automática.",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "Barre latérale gauche",
"mainTab": "Onglet principal de l'éditeur"
},
"cliEdition": {
"name": "Édition de Qoder CLI",
"desc": "Choisissez la version internationale (qodercli, ~/.qoder) ou la version Chine (qoderclicn, ~/.qoder-cn). Les onglets ouverts redémarrent immédiatement après le changement, interrompant les conversations en cours. L'historique des conversations est stocké séparément par édition (~/.qoder et ~/.qoder-cn) et n'est pas partagé entre les éditions.",
"global": "Internationale (qodercli)",
"cn": "Chine (qoderclicn)"
},
"cliPath": {
"name": "Chemin CLI Qoder",
"desc": "Chemin personnalisé vers Qoder CLI. Laisser vide pour la détection automatique.",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "左サイドバー",
"mainTab": "メインエディタタブ"
},
"cliEdition": {
"name": "Qoder CLI エディション",
"desc": "国際版(qodercli、~/.qoder)または中国版(qoderclicn、~/.qoder-cn)を選択します。切り替え後、開いているタブは直ちに再起動し、進行中の会話は中断されます。各エディションの会話履歴は ~/.qoder と ~/.qoder-cn にそれぞれ保存され、相互に表示されません。",
"global": "国際版(qodercli)",
"cn": "中国版(qoderclicn)"
},
"cliPath": {
"name": "Qoder CLI パス",
"desc": "Qoder CLI のカスタムパス。空欄で自動検出を使用。",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "왼쪽 사이드바",
"mainTab": "메인 편집기 탭"
},
"cliEdition": {
"name": "Qoder CLI 에디션",
"desc": "글로벌 버전(qodercli, ~/.qoder) 또는 중국 버전(qoderclicn, ~/.qoder-cn)을 선택하세요. 전환 후 열려 있는 탭은 즉시 다시 시작되며 진행 중인 대화가 중단됩니다. 각 에디션의 대화 기록은 ~/.qoder와 ~/.qoder-cn에 별도로 저장되어 서로 표시되지 않습니다.",
"global": "글로벌 (qodercli)",
"cn": "중국 (qoderclicn)"
},
"cliPath": {
"name": "Qoder CLI 경로",
"desc": "Qoder CLI의 사용자 정의 경로. 비워두면 자동 감지 사용.",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "Barra lateral esquerda",
"mainTab": "Aba do editor principal"
},
"cliEdition": {
"name": "Edição do Qoder CLI",
"desc": "Escolha a versão internacional (qodercli, ~/.qoder) ou a versão China (qoderclicn, ~/.qoder-cn). As abas abertas são reiniciadas imediatamente após a troca, interrompendo conversas em andamento. O histórico de conversas é armazenado separadamente por edição (~/.qoder e ~/.qoder-cn) e não é compartilhado entre edições.",
"global": "Internacional (qodercli)",
"cn": "China (qoderclicn)"
},
"cliPath": {
"name": "Caminho CLI Qoder",
"desc": "Caminho personalizado para Qoder CLI. Deixe vazio para detecção automática.",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "Левая боковая панель",
"mainTab": "Основная вкладка редактора"
},
"cliEdition": {
"name": "Редакция Qoder CLI",
"desc": "Выберите международную сборку (qodercli, ~/.qoder) или китайскую сборку (qoderclicn, ~/.qoder-cn). После переключения открытые вкладки немедленно перезапускаются, прерывая текущие диалоги. История бесед хранится отдельно для каждой редакции (~/.qoder и ~/.qoder-cn) и не является общей между редакциями.",
"global": "Международная (qodercli)",
"cn": "Китайская (qoderclicn)"
},
"cliPath": {
"name": "Путь к CLI Qoder",
"desc": "Пользовательский путь к Qoder CLI. Оставьте пустым для автоматического определения.",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "左侧边栏",
"mainTab": "主编辑器标签页"
},
"cliEdition": {
"name": "Qoder CLI 版本",
"desc": "选择国际版(qodercli,配置目录 ~/.qoder)或国内版(qoderclicn,配置目录 ~/.qoder-cn)。切换后已打开的标签页将立即重启,进行中的对话会中断。两个版本的历史会话分别保存在 ~/.qoder 和 ~/.qoder-cn,互相不可见。",
"global": "国际版(qodercli)",
"cn": "国内版(qoderclicn)"
},
"cliPath": {
"name": "Qoder CLI 路径",
"desc": "Qoder CLI 的自定义路径。留空使用自动检测。",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@
"leftSidebar": "左側邊欄",
"mainTab": "主編輯器分頁"
},
"cliEdition": {
"name": "Qoder CLI 版本",
"desc": "選擇國際版(qodercli,設定目錄 ~/.qoder)或中國大陸版(qoderclicn,設定目錄 ~/.qoder-cn)。切換後已開啟的標籤頁將立即重新啟動,進行中的對話會中斷。各版本的歷史對話分別儲存在 ~/.qoder 和 ~/.qoder-cn,互不可見。",
"global": "國際版(qodercli)",
"cn": "中國大陸版(qoderclicn)"
},
"cliPath": {
"name": "Qoder CLI 路徑",
"desc": "Qoder CLI 的自訂路徑。留空使用自動偵測。",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ export type TranslationKey =
| 'settings.cliPath.descUnix'
| 'settings.cliPath.validation.notExist'
| 'settings.cliPath.validation.isDirectory'
| 'settings.cliEdition.name'
| 'settings.cliEdition.desc'
| 'settings.cliEdition.global'
| 'settings.cliEdition.cn'

// Settings - Language
| 'settings.language.name'
Expand Down
9 changes: 9 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import { type InlineEditContext, InlineEditModal } from './features/inline-edit/
import { QoderianSettingTab } from './features/settings/settings-tab';
import { setLocale, t } from './i18n/i18n';
import type { Locale } from './i18n/types';
import { setActiveQoderCliEdition } from './qoder/config/cli-edition';
import { normalizeQoderSettings } from './qoder/config/qoder-settings-reconciler';
import { getQoderSettings } from './qoder/config/settings';
import { extractUserDisplayContent } from './qoder/prompt/context/prompt-context';
import {
createQoderServices,
Expand Down Expand Up @@ -401,6 +403,7 @@ export default class QoderianPlugin extends Plugin {
this.lastKnownTabManagerState = await this.storage.getTabManagerState();

this.settings = qoderian;
this.syncActiveQoderCliEdition();

// Plan mode is ephemeral — return to the SDK's prompting mode on restart.
if (this.settings.permissionMode === 'plan') {
Expand Down Expand Up @@ -443,9 +446,15 @@ export default class QoderianPlugin extends Plugin {
}

async saveSettings() {
this.syncActiveQoderCliEdition();
await this.storage.saveQoderianSettings(this.settings);
}

/** Keeps the edition-aware path helpers aligned with the persisted settings. */
private syncActiveQoderCliEdition() {
setActiveQoderCliEdition(getQoderSettings(this.settings).edition);
}

getResolvedQoderCliPath(): string | null {
return this.qoderServices.cliResolver.resolveFromSettings(this.settings);
}
Expand Down
13 changes: 9 additions & 4 deletions src/qoder/commands/probe-runtime-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { getEnhancedPath, getMissingNodeError } from '../../core/env/environment
import { getVaultPath } from '../../core/fs/path';
import type { SlashCommand } from '../../core/types';
import type { QoderRuntimeStatus } from '../../core/types/services';
import {
getActiveQoderCliEdition,
getQoderCliBinaryBaseName,
getQoderCliLoginCommand,
} from '../config/cli-edition';
import {
getQoderSettings,
type QoderDiscoveredAgent,
Expand Down Expand Up @@ -179,14 +184,14 @@ export function classifyQoderProbeError(error: unknown, timedOut = false): Qoder
if (/not logged in|login required|please (?:log|sign) in|please run \/?login|sign[- ]?in required|unauthori[sz]ed|authentication required|invalid (?:api key|credential|token)|expired.*(?:credential|token)|credentials? (?:not found|missing)|\b401\b/i.test(details)) {
return {
kind: 'authRequired',
message: 'Qoder CLI is not signed in. Run `qodercli login` in a terminal, then retry.',
message: `Qoder CLI is not signed in. Run \`${getQoderCliLoginCommand(getActiveQoderCliEdition())}\` in a terminal, then retry.`,
details,
};
}
if (/protocol version|incompatible|unsupported (?:cli|sdk|version)|version mismatch/i.test(details)) {
return {
kind: 'incompatible',
message: 'This Qoder CLI version is incompatible. Update qodercli, then retry.',
message: `This Qoder CLI version is incompatible. Update ${getQoderCliBinaryBaseName(getActiveQoderCliEdition())}, then retry.`,
details,
};
}
Expand Down Expand Up @@ -224,7 +229,7 @@ export async function probeRuntimeCatalog(
return {
error: {
kind: 'cliMissing',
message: 'Qoder CLI was not found. Install qodercli or configure its path in Qoderian settings.',
message: `Qoder CLI was not found. Install ${getQoderCliBinaryBaseName(getActiveQoderCliEdition())} or configure its path in Qoderian settings.`,
},
};
}
Expand All @@ -243,7 +248,7 @@ export async function probeRuntimeCatalog(
return {
error: {
kind: 'nodeMissing',
message: `${missingNodeError} Install Node.js or use the native qodercli executable, then retry.`,
message: `${missingNodeError} Install Node.js or use the native ${getQoderCliBinaryBaseName(getActiveQoderCliEdition())} executable, then retry.`,
details: missingNodeError,
},
};
Expand Down
Loading
Loading