Skip to content

Commit b306137

Browse files
luoxuanzaoQoder-AI
andauthored
feat: support China edition CLI (qoderclicn) with settings toggle (#20)
* feat: support China edition CLI (qoderclicn) with settings toggle Add a Qoder CLI edition setting (international qodercli / China qoderclicn) so users can switch between the two CLI builds, whose binary names and config roots differ (~/.qoder vs ~/.qoder-cn). - New cli-edition module with per-edition binary names, config roots, login commands, and an active-edition registry synced on settings load/save - Edition-aware CLI discovery, including the CN installer layout (~/.qoder-cn/bin/qoderclicn/qoderclicn-<version>); resolver cache keyed on edition - Edition-aware SDK projects path (session history), global plugin discovery, login hint, and CLI/Node missing error messages - Settings dropdown (declarative + imperative) that resets the resolver, reloads plugins, restarts open tabs, and refreshes the runtime catalog on switch - i18n keys in all 10 locales; unit tests for edition module and CN discovery; CHANGELOG entry Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com> * test: make cli-edition config-root assertions path-separator agnostic The config-root test hardcoded POSIX separators while getQoderCliHomeDir builds paths with path.join, which fails on the Windows CI runner. Build expectations with path.join as well. Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com> --------- Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
1 parent ce094ba commit b306137

28 files changed

Lines changed: 511 additions & 48 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ version with its date and start a fresh empty `[Unreleased]` above it.
1111

1212
## [Unreleased]
1313

14+
### Added
15+
16+
- Qoder CLI edition switch in settings (Setup section): choose the
17+
international build (`qodercli`, config under `~/.qoder`) or the
18+
China build (`qoderclicn`, config under `~/.qoder-cn`). Auto-
19+
detection, session history, global plugins, and login hints all
20+
follow the selected edition.
21+
1422
### Fixed
1523

1624
- The composer now adapts to narrow sidebars: context chips that do

src/core/types/settings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,22 @@ export type PermissionMode = 'default' | 'acceptEdits' | 'auto' | 'plan' | 'yolo
6363
/** Opaque device-keyed CLI paths for per-device configuration. */
6464
export type HostnameCliPaths = Record<string, string>;
6565

66+
/** Supported Qoder CLI distribution builds. */
67+
export const QODER_CLI_EDITIONS = ['global', 'cn'] as const;
68+
69+
/**
70+
* Qoder CLI distribution edition: the international build (`qodercli`,
71+
* config under `~/.qoder`) or the China build (`qoderclicn`, config under
72+
* `~/.qoder-cn`).
73+
*/
74+
export type QoderCliEdition = typeof QODER_CLI_EDITIONS[number];
75+
6676
/** Qoder CLI settings stored alongside Qoderian's general preferences. */
6777
export interface QoderSettings {
6878
cliPath: string;
6979
cliPathsByHost: HostnameCliPaths;
80+
/** Selected Qoder CLI distribution build. */
81+
edition: QoderCliEdition;
7082
loadUserSettings: boolean;
7183
enableBangBash: boolean;
7284
discoveredModels: Array<{

src/features/chat/ui/toolbar/toolbar-selectors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Notice } from 'obsidian';
22

33
import type { QoderRuntimeStatus } from '../../../../core/types/services';
44
import type { PermissionMode } from '../../../../core/types/settings';
5+
import { getActiveQoderCliEdition, getQoderCliLoginCommand } from '../../../../qoder/config/cli-edition';
56
import type { QoderModelConfig } from '../../../../qoder/models/qoder-model-config';
67
import { createIconSvg, QODER_ICON } from '../../../../shared/icons';
78
import { ClickPopover } from './click-popover';
@@ -34,7 +35,6 @@ const DEFAULT_RUNTIME_STATUS: QoderRuntimeStatus = {
3435
kind: 'ready',
3536
message: 'Qoder CLI is ready.',
3637
};
37-
const QODER_LOGIN_COMMAND = 'qodercli login';
3838

3939
function getRuntimeStatusLabel(status: QoderRuntimeStatus): string {
4040
switch (status.kind) {
@@ -137,7 +137,7 @@ export class ModelSelector {
137137
if (status.kind === 'authRequired') {
138138
statusEl.createEl('code', {
139139
cls: 'qoderian-model-runtime-command',
140-
text: QODER_LOGIN_COMMAND,
140+
text: getQoderCliLoginCommand(getActiveQoderCliEdition()),
141141
});
142142
}
143143
if (status.details) {

src/features/settings/settings-tab.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
renderBangBashControl,
1414
renderMcpSection,
1515
renderPluginsSection,
16+
renderQoderCliEditionControl,
1617
renderQoderCliPathControl,
1718
renderQoderCliPathSetting,
1819
renderQoderSettingsTab,
@@ -66,6 +67,13 @@ export class QoderianSettingTab extends PluginSettingTab {
6667
type: 'group',
6768
heading: t('settings.setup'),
6869
items: [
70+
{
71+
name: t('settings.cliEdition.name'),
72+
desc: t('settings.cliEdition.desc'),
73+
render: (setting) => {
74+
renderQoderCliEditionControl(setting, { plugin: this.plugin });
75+
},
76+
},
6977
{
7078
name: t('settings.cliPath.name'),
7179
desc: getCliPathDescription(),

src/features/settings/ui/qoder-settings-tab.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { expandHomePath } from '../../../core/fs/path';
66
import type { AppMcpStorage } from '../../../core/types/services';
77
import { t } from '../../../i18n/i18n';
88
import type QoderianPlugin from '../../../main';
9+
import { normalizeQoderCliEdition } from '../../../qoder/config/cli-edition';
910
import {
1011
getQoderSettings,
1112
updateQoderSettings,
@@ -23,6 +24,10 @@ export interface QoderCliPathSettingContext {
2324
plugin: QoderianPlugin;
2425
}
2526

27+
export interface QoderCliEditionSettingContext {
28+
plugin: QoderianPlugin;
29+
}
30+
2631
export interface SlashCommandsSectionContext {
2732
plugin: QoderianPlugin;
2833
}
@@ -155,12 +160,50 @@ export function renderQoderCliPathControl(
155160
return validationEl;
156161
}
157162

163+
/**
164+
* Attaches the CLI edition dropdown to `setting`. Switching editions changes
165+
* the executable name and the CLI's user config root, so the resolver cache
166+
* is dropped and every tab restarts, mirroring a CLI path change.
167+
*/
168+
export function renderQoderCliEditionControl(
169+
setting: Setting,
170+
context: QoderCliEditionSettingContext,
171+
): void {
172+
const qoderWorkspace = context.plugin.qoderServices;
173+
const settingsBag = context.plugin.settings as unknown as Record<string, unknown>;
174+
175+
setting.addDropdown((dropdown) => {
176+
dropdown
177+
.addOption('global', t('settings.cliEdition.global'))
178+
.addOption('cn', t('settings.cliEdition.cn'))
179+
.setValue(getQoderSettings(settingsBag).edition)
180+
.onChange(async (value) => {
181+
const edition = normalizeQoderCliEdition(value);
182+
updateQoderSettings(settingsBag, { edition });
183+
await context.plugin.saveSettings();
184+
qoderWorkspace.cliResolver.reset();
185+
await qoderWorkspace.pluginManager.loadPlugins();
186+
const view = context.plugin.getView();
187+
await view?.getTabManager()?.broadcastToAllTabs(
188+
(service) => Promise.resolve(service.cleanup())
189+
);
190+
void qoderWorkspace.agentCatalog.refresh();
191+
});
192+
});
193+
}
194+
158195
/** Renders the "Setup" heading plus the CLI path row (imperative fallback). */
159196
export function renderQoderCliPathSetting(
160197
container: HTMLElement,
161198
context: QoderCliPathSettingContext,
162199
): void {
163200
new Setting(container).setName(t('settings.setup')).setHeading();
201+
202+
const editionSetting = new Setting(container)
203+
.setName(t('settings.cliEdition.name'))
204+
.setDesc(t('settings.cliEdition.desc'));
205+
renderQoderCliEditionControl(editionSetting, context);
206+
164207
const cliPathSetting = new Setting(container)
165208
.setName(t('settings.cliPath.name'))
166209
.setDesc(getCliPathDescription());

src/i18n/locales/de.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@
254254
"leftSidebar": "Linke Seitenleiste",
255255
"mainTab": "Haupteditor-Tab"
256256
},
257+
"cliEdition": {
258+
"name": "Qoder CLI-Ausgabe",
259+
"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.",
260+
"global": "International (qodercli)",
261+
"cn": "China (qoderclicn)"
262+
},
257263
"cliPath": {
258264
"name": "Qoder CLI-Pfad",
259265
"desc": "Benutzerdefinierter Pfad zum Qoder CLI. Leer lassen für automatische Erkennung.",

src/i18n/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@
254254
"leftSidebar": "Left sidebar",
255255
"mainTab": "Main editor tab"
256256
},
257+
"cliEdition": {
258+
"name": "Qoder CLI edition",
259+
"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.",
260+
"global": "International (qodercli)",
261+
"cn": "China (qoderclicn)"
262+
},
257263
"cliPath": {
258264
"name": "Qoder CLI path",
259265
"desc": "Custom path to Qoder CLI. Leave empty for auto-detection.",

src/i18n/locales/es.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@
254254
"leftSidebar": "Barra lateral izquierda",
255255
"mainTab": "Pestaña del editor principal"
256256
},
257+
"cliEdition": {
258+
"name": "Edición de Qoder CLI",
259+
"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.",
260+
"global": "Internacional (qodercli)",
261+
"cn": "China (qoderclicn)"
262+
},
257263
"cliPath": {
258264
"name": "Ruta CLI Qoder",
259265
"desc": "Ruta personalizada a Qoder CLI. Dejar vacío para detección automática.",

src/i18n/locales/fr.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@
254254
"leftSidebar": "Barre latérale gauche",
255255
"mainTab": "Onglet principal de l'éditeur"
256256
},
257+
"cliEdition": {
258+
"name": "Édition de Qoder CLI",
259+
"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.",
260+
"global": "Internationale (qodercli)",
261+
"cn": "Chine (qoderclicn)"
262+
},
257263
"cliPath": {
258264
"name": "Chemin CLI Qoder",
259265
"desc": "Chemin personnalisé vers Qoder CLI. Laisser vide pour la détection automatique.",

src/i18n/locales/ja.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@
254254
"leftSidebar": "左サイドバー",
255255
"mainTab": "メインエディタタブ"
256256
},
257+
"cliEdition": {
258+
"name": "Qoder CLI エディション",
259+
"desc": "国際版(qodercli、~/.qoder)または中国版(qoderclicn、~/.qoder-cn)を選択します。切り替え後、開いているタブは直ちに再起動し、進行中の会話は中断されます。各エディションの会話履歴は ~/.qoder と ~/.qoder-cn にそれぞれ保存され、相互に表示されません。",
260+
"global": "国際版(qodercli)",
261+
"cn": "中国版(qoderclicn)"
262+
},
257263
"cliPath": {
258264
"name": "Qoder CLI パス",
259265
"desc": "Qoder CLI のカスタムパス。空欄で自動検出を使用。",

0 commit comments

Comments
 (0)