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

## [Unreleased]

### Added

- An opt-in Memory toggle in Settings → Experimental lets the Qoder CLI save
and reload reusable information across conversations. Memories live in the
Qoder CLI memory directory and are shared with the terminal CLI; toggling
it restarts the running session once.

## [1.0.12] - 2026-09-20

### Added
Expand Down
6 changes: 6 additions & 0 deletions src/core/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export interface QoderSettings {
edition: QoderCliEdition;
loadUserSettings: boolean;
enableBangBash: boolean;
/**
* Let the CLI generate and consume memory across conversations. Memories
* live in the Qoder CLI memory directory and are shared with the terminal
* CLI.
*/
enableMemory: boolean;
discoveredModels: Array<{
value: string;
displayName: string;
Expand Down
19 changes: 19 additions & 0 deletions src/features/settings/settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,15 @@ export class QoderianSettingTab extends PluginSettingTab {
return () => { injected?.remove(); };
},
},
{
name: t('settings.enableMemory.name'),
desc: t('settings.enableMemory.desc'),
control: {
type: 'toggle',
key: 'enableMemory',
defaultValue: qoderSettings.enableMemory,
},
},
],
},
];
Expand All @@ -392,6 +401,10 @@ export class QoderianSettingTab extends PluginSettingTab {
const settingsBag = this.plugin.settings as unknown as Record<string, unknown>;
return getQoderSettings(settingsBag).loadUserSettings;
}
if (key === 'enableMemory') {
const settingsBag = this.plugin.settings as unknown as Record<string, unknown>;
return getQoderSettings(settingsBag).enableMemory;
}
if (key === 'titleGenerationModel') {
return this.plugin.settings.titleGenerationModel || 'auto';
}
Expand All @@ -417,6 +430,12 @@ export class QoderianSettingTab extends PluginSettingTab {
return this.plugin.saveSettings();
}

if (key === 'enableMemory') {
const settingsBag = this.plugin.settings as unknown as Record<string, unknown>;
updateQoderSettings(settingsBag, { enableMemory: Boolean(value) });
return this.plugin.saveSettings();
}

return this.persistControlValue(key, value);
}
// Unreachable: Obsidian < 1.13 never calls setControlValue.
Expand Down
12 changes: 12 additions & 0 deletions src/features/settings/ui/qoder-settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,4 +392,16 @@ export function renderQoderSettingsTab(container: HTMLElement, context: QoderSet
.setName(t('settings.enableBangBash.name'))
.setDesc(t('settings.enableBangBash.desc'));
renderBangBashControl(bangBashSetting, container, context);

new Setting(container)
.setName(t('settings.enableMemory.name'))
.setDesc(t('settings.enableMemory.desc'))
.addToggle((toggle) =>
toggle
.setValue(qoderSettings.enableMemory)
.onChange(async (value) => {
updateQoderSettings(settingsBag, { enableMemory: value });
await context.plugin.saveSettings();
})
);
}
4 changes: 4 additions & 0 deletions src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "Benutzer-Qoder-Einstellungen laden",
"desc": "Lädt ~/.qoder/settings.json. Wenn aktiviert, können Benutzer-Qoder-CLI-Berechtigungsregeln den Sicherheitsmodus umgehen."
},
"enableMemory": {
"name": "Gedächtnis",
"desc": "Speichert wiederverwendbare Informationen automatisch und lädt sie über Unterhaltungen hinweg. Die Daten liegen im Gedächtnisverzeichnis der Qoder CLI und werden mit der Terminal-CLI geteilt."
},
"qoderSafeMode": {
"name": "Berechtigungen im sicheren Modus",
"desc": "Berechtigungsverhalten bei aktiviertem sicheren Modus.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "Load user Qoder settings",
"desc": "Load ~/.qoder/settings.json. When enabled, user's Qoder CLI permission rules may bypass Safe mode."
},
"enableMemory": {
"name": "Memory",
"desc": "Automatically save and load reusable information across conversations. Memories live in your Qoder CLI memory directory and are shared with the terminal CLI."
},
"qoderSafeMode": {
"name": "Safe mode permissions",
"desc": "Permission behavior used while the Safe toggle is on.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "Cargar configuración de usuario Qoder",
"desc": "Carga ~/.qoder/settings.json. Cuando está habilitado, las reglas de permisos del usuario pueden eludir el modo seguro."
},
"enableMemory": {
"name": "Memoria",
"desc": "Guarda y carga automáticamente información reutilizable entre conversaciones. Los recuerdos se almacenan en el directorio de memoria de Qoder CLI y se comparten con la CLI de terminal."
},
"qoderSafeMode": {
"name": "Permisos del modo seguro",
"desc": "Comportamiento de permisos cuando el modo seguro está activado.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "Charger les paramètres utilisateur Qoder",
"desc": "Charge ~/.qoder/settings.json. Lorsqu'activé, les règles de permission de l'utilisateur peuvent contourner le mode sécurisé."
},
"enableMemory": {
"name": "Mémoire",
"desc": "Enregistre et charge automatiquement les informations réutilisables entre les conversations. Les mémoires sont stockées dans le répertoire de mémoire de Qoder CLI et partagées avec la CLI du terminal."
},
"qoderSafeMode": {
"name": "Autorisations du mode sécurisé",
"desc": "Comportement des autorisations lorsque le mode sécurisé est activé.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "ユーザーQoder設定を読み込む",
"desc": "~/.qoder/settings.json を読み込みます。有効にすると、ユーザーの Qoder CLI 許可ルールがセキュリティモードをバイパスする可能性があります。"
},
"enableMemory": {
"name": "メモリ",
"desc": "会話をまたいで再利用できる情報を自動的に保存・読み込みします。メモリは Qoder CLI のメモリディレクトリに保存され、ターミナル CLI と共有されます。"
},
"qoderSafeMode": {
"name": "セーフモードの権限",
"desc": "セーフモードが有効なときに使用する権限動作です。",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "사용자 Qoder 설정 로드",
"desc": "~/.qoder/settings.json을 로드합니다. 활성화하면 사용자의 Qoder CLI 허용 규칙이 보안 모드를 우회할 수 있습니다."
},
"enableMemory": {
"name": "메모리",
"desc": "대화를 넘어 재사용할 수 있는 정보를 자동으로 저장하고 불러옵니다. 메모리는 Qoder CLI 메모리 디렉터리에 저장되며 터미널 CLI와 공유됩니다."
},
"qoderSafeMode": {
"name": "안전 모드 권한",
"desc": "안전 모드가 켜져 있을 때 사용할 권한 동작입니다.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "Carregar configurações do usuário Qoder",
"desc": "Carrega ~/.qoder/settings.json. Quando habilitado, as regras de permissão do usuário podem ignorar o modo seguro."
},
"enableMemory": {
"name": "Memória",
"desc": "Salva e carrega automaticamente informações reutilizáveis entre conversas. As memórias ficam no diretório de memória do Qoder CLI e são compartilhadas com a CLI do terminal."
},
"qoderSafeMode": {
"name": "Permissões do modo seguro",
"desc": "Comportamento das permissões quando o modo seguro está ativo.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "Загружать пользовательские настройки Qoder",
"desc": "Загружает ~/.qoder/settings.json. При включении пользовательские правила разрешений Qoder CLI могут обходить безопасный режим."
},
"enableMemory": {
"name": "Память",
"desc": "Автоматически сохраняет полезную информацию между беседами и загружает её при следующих разговорах. Воспоминания хранятся в каталоге памяти Qoder CLI и используются совместно с терминальным CLI."
},
"qoderSafeMode": {
"name": "Разрешения безопасного режима",
"desc": "Правила разрешений при включённом безопасном режиме.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "加载用户 Qoder 设置",
"desc": "加载 ~/.qoder/settings.json。启用后,用户的 Qoder CLI 权限规则可能绕过安全模式。"
},
"enableMemory": {
"name": "记忆",
"desc": "自动保存并加载跨会话可复用的信息。记忆存放在 Qoder CLI 的记忆目录中,与终端 CLI 共用。"
},
"qoderSafeMode": {
"name": "安全模式权限",
"desc": "开启安全模式时使用的权限策略。",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@
"name": "載入使用者 Qoder 設定",
"desc": "載入 ~/.qoder/settings.json。啟用後,使用者的 Qoder CLI 權限規則可能繞過安全模式。"
},
"enableMemory": {
"name": "記憶",
"desc": "自動儲存並載入跨工作階段可重複使用的資訊。記憶存放在 Qoder CLI 的記憶目錄中,與終端 CLI 共用。"
},
"qoderSafeMode": {
"name": "安全模式權限",
"desc": "開啟安全模式時使用的權限策略。",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ export type TranslationKey =
| 'settings.safety'
| 'settings.loadUserSettings.name'
| 'settings.loadUserSettings.desc'
| 'settings.enableMemory.name'
| 'settings.enableMemory.desc'
| 'settings.qoderSafeMode.name'
| 'settings.qoderSafeMode.desc'
| 'settings.qoderSafeMode.modes.acceptEdits'
Expand Down
3 changes: 3 additions & 0 deletions src/qoder/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const DEFAULT_QODER_SETTINGS: Readonly<QoderSettings> = Object.freeze({
edition: 'global',
loadUserSettings: true,
enableBangBash: false,
enableMemory: false,
discoveredModels: [],
discoveredAgents: [],
lastModel: 'auto',
Expand Down Expand Up @@ -238,6 +239,8 @@ export function getQoderSettings(
?? DEFAULT_QODER_SETTINGS.loadUserSettings,
enableBangBash: (config.enableBangBash as boolean | undefined)
?? DEFAULT_QODER_SETTINGS.enableBangBash,
enableMemory: (config.enableMemory as boolean | undefined)
?? DEFAULT_QODER_SETTINGS.enableMemory,
discoveredModels: normalizeQoderDiscoveredModels(config.discoveredModels),
discoveredAgents: normalizeQoderDiscoveredAgents(config.discoveredAgents),
lastModel: (config.lastModel as string | undefined) ?? DEFAULT_QODER_SETTINGS.lastModel,
Expand Down
8 changes: 8 additions & 0 deletions src/qoder/runtime/qoder-query-options-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export class QueryOptionsBuilder {

// These require restart (cannot be updated dynamically)
if (currentConfig.systemPromptKey !== newConfig.systemPromptKey) return true;
if (currentConfig.memoryEnabled !== newConfig.memoryEnabled) return true;
if (currentConfig.disallowedToolsKey !== newConfig.disallowedToolsKey) return true;
if (currentConfig.mcpServersKey !== newConfig.mcpServersKey) return true;
if (currentConfig.pluginsKey !== newConfig.pluginsKey) return true;
Expand Down Expand Up @@ -118,6 +119,7 @@ export class QueryOptionsBuilder {
effortLevel: resolveModelReasoningEffort(runtimeModel, ctx.settings),
permissionMode: ctx.settings.permissionMode,
sdkPermissionMode,
memoryEnabled: qoderSettings.enableMemory,
systemPromptKey: computeSystemPromptKey(systemPromptSettings),
disallowedToolsKey,
mcpServersKey: '', // Dynamic via setMcpServers, not tracked for restart
Expand Down Expand Up @@ -278,6 +280,12 @@ export class QueryOptionsBuilder {
includePartialMessages: true,
};

if (qoderSettings.enableMemory) {
// CLI-native memory: the CLI writes memories after turns and injects
// them into later turns, using the same store as the terminal CLI.
options.memory = { mode: 'native' };
}

options.spawnQoderCLIProcess = createCustomSpawnFunction(ctx.enhancedPath);

return { options, qoderSettings };
Expand Down
2 changes: 2 additions & 0 deletions src/qoder/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export interface PersistentQueryConfig {
effortLevel: EffortLevel;
permissionMode: PermissionMode | null;
sdkPermissionMode: SDKPermissionMode | null;
/** Memory is a query-launch option, so toggling it restarts the session. */
memoryEnabled: boolean;
systemPromptKey: string;
disallowedToolsKey: string;
mcpServersKey: string;
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/i18n/locales.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const localizedKeys = [
'settings.enableBangBash.name',
'settings.enableBangBash.desc',
'settings.enableBangBash.validation.noNode',
'settings.enableMemory.name',
'settings.enableMemory.desc',
'settings.requireCommandOrControlEnterToSend.name',
'settings.requireCommandOrControlEnterToSend.desc',
'settings.qoderSafeMode.name',
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/qoder/runtime/qoder-chat-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down Expand Up @@ -2716,6 +2717,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down Expand Up @@ -2779,6 +2781,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down Expand Up @@ -2821,6 +2824,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down Expand Up @@ -2871,6 +2875,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down Expand Up @@ -2909,6 +2914,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down Expand Up @@ -2955,6 +2961,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down Expand Up @@ -3012,6 +3019,7 @@ describe('QoderChatRuntime', () => {
model: 'qoder-3-5-sonnet',
effortLevel: 'high',
permissionMode: 'ask',
memoryEnabled: false,
systemPromptKey: '',
disallowedToolsKey: '',
mcpServersKey: '{}',
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/qoder/runtime/qoder-query-options-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,46 @@ describe('QueryOptionsBuilder model policy', () => {
});
});
});

describe('QueryOptionsBuilder memory', () => {
it('leaves memory off by default on persistent and cold-start queries', () => {
const persistent = QueryOptionsBuilder.buildPersistentQueryOptions(
createContext('default'),
);
const coldStart = QueryOptionsBuilder.buildColdStartQueryOptions(
createColdStartContext(),
);

expect(persistent.memory).toBeUndefined();
expect(coldStart.memory).toBeUndefined();
});

it('requests CLI-native memory on persistent queries when enabled', () => {
const ctx = createContext('default');
ctx.settings.qoder.enableMemory = true;

const options = QueryOptionsBuilder.buildPersistentQueryOptions(ctx);

expect(options.memory).toEqual({ mode: 'native' });
});

it('carries native memory into chat cold-start queries when enabled', () => {
const ctx = createColdStartContext();
ctx.settings.qoder.enableMemory = true;

const options = QueryOptionsBuilder.buildColdStartQueryOptions(ctx);

expect(options.memory).toEqual({ mode: 'native' });
});

it('restarts the session when the memory setting is toggled', () => {
const off = QueryOptionsBuilder.buildPersistentQueryConfig(createContext('default'));

const onCtx = createContext('default');
onCtx.settings.qoder.enableMemory = true;
const on = QueryOptionsBuilder.buildPersistentQueryConfig(onCtx);

expect(QueryOptionsBuilder.needsRestart(off, on)).toBe(true);
expect(QueryOptionsBuilder.needsRestart(off, off)).toBe(false);
});
});
Loading