Skip to content
Open
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
23 changes: 22 additions & 1 deletion src/main/config/config-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,14 @@ export class ConfigStore {
enableThinking: projected.enableThinking,
isConfigured: toBoolean(raw.isConfigured, defaultConfig.isConfigured),
};
// Only set when the profile has a real value: electron-store rejects an explicit
// `undefined` for a present key (see normalizeProfile's own convention above).
if (typeof projected.contextWindow === 'number' && projected.contextWindow > 0) {
result.contextWindow = projected.contextWindow;
}
if (typeof projected.maxTokens === 'number' && projected.maxTokens > 0) {
result.maxTokens = projected.maxTokens;
}
this.normalizeModelIds(result);
return result;
}
Expand All @@ -1045,7 +1053,7 @@ export class ConfigStore {
const activeConfigSet =
nextConfigSets.find((set) => set.id === requestedActiveConfigSetId) || nextConfigSets[0];
const projected = this.projectFromConfigSet(activeConfigSet);
return {
const result: AppConfig = {
...base,
provider: projected.provider,
customProtocol: projected.customProtocol,
Expand All @@ -1058,6 +1066,19 @@ export class ConfigStore {
activeConfigSetId: activeConfigSet.id,
configSets: nextConfigSets,
};
// Clear rather than leave stale: `...base` can carry the previous set's value, and
// electron-store rejects an explicit `undefined` for a present key.
if (typeof projected.contextWindow === 'number' && projected.contextWindow > 0) {
result.contextWindow = projected.contextWindow;
} else {
delete result.contextWindow;
}
if (typeof projected.maxTokens === 'number' && projected.maxTokens > 0) {
result.maxTokens = projected.maxTokens;
} else {
delete result.maxTokens;
}
return result;
}

private buildUniqueConfigSetName(
Expand Down
2 changes: 2 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1911,6 +1911,8 @@ const buildAgentRuntimeSignature = (config: AppConfig): string =>
baseUrl: config.baseUrl,
customProtocol: config.customProtocol,
model: config.model,
contextWindow: config.contextWindow,
maxTokens: config.maxTokens,
enableThinking: config.enableThinking,
memoryEnabled: config.memoryEnabled,
memoryRuntime: config.memoryRuntime,
Expand Down
177 changes: 177 additions & 0 deletions tests/config-store-context-window-projection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
seed: {} as Record<string, unknown>,
}));

vi.mock('electron-store', () => {
class MockStore<T extends Record<string, unknown>> {
public store: Record<string, unknown>;
public path = '/tmp/mock-config-store.json';

constructor(options: { defaults?: Record<string, unknown> }) {
this.store = {
...(options?.defaults || {}),
...mocks.seed,
};
}

get<K extends keyof T>(key: K): T[K] {
return this.store[key as string] as T[K];
}

set(key: string | Record<string, unknown>, value?: unknown): void {
if (typeof key === 'string') {
this.store[key] = value;
return;
}
this.store = {
...this.store,
...key,
};
}

clear(): void {
this.store = {};
}
}

return {
default: MockStore,
};
});

import { ConfigStore } from '../src/main/config/config-store';

describe('ConfigStore contextWindow/maxTokens projection', () => {
beforeEach(() => {
mocks.seed = {};
});

it('projects a custom/Ollama profile contextWindow and maxTokens onto the flat config getAll() returns', () => {
const store = new ConfigStore();

store.update({
provider: 'ollama',
apiKey: '',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.3',
profiles: {
ollama: {
apiKey: '',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.3',
contextWindow: 32000,
maxTokens: 8000,
},
},
});

const config = store.getAll();
expect(config.contextWindow).toBe(32000);
expect(config.maxTokens).toBe(8000);
});

it('keeps contextWindow/maxTokens projected after switching config sets', () => {
const store = new ConfigStore();

store.update({
provider: 'ollama',
apiKey: '',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.3',
profiles: {
ollama: {
apiKey: '',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.3',
contextWindow: 32000,
maxTokens: 8000,
},
},
});
expect(store.getAll().contextWindow).toBe(32000);
expect(store.getAll().maxTokens).toBe(8000);

const created = store.createSet({ name: 'Second set', mode: 'blank' });
const secondSetId = created.configSets.find((set) => set.id !== 'default')!.id;

store.update({
provider: 'custom',
customProtocol: 'openai',
apiKey: 'sk-custom',
baseUrl: 'https://relay.example.com/v1',
model: 'my-model',
profiles: {
'custom:openai': {
apiKey: 'sk-custom',
baseUrl: 'https://relay.example.com/v1',
model: 'my-model',
contextWindow: 64000,
maxTokens: 16000,
},
},
});
expect(store.getAll().contextWindow).toBe(64000);
expect(store.getAll().maxTokens).toBe(16000);

store.switchSet({ id: 'default' });
const defaultSetView = store.getAll();
expect(defaultSetView.contextWindow).toBe(32000);
expect(defaultSetView.maxTokens).toBe(8000);

store.switchSet({ id: secondSetId });
const secondSetView = store.getAll();
expect(secondSetView.contextWindow).toBe(64000);
expect(secondSetView.maxTokens).toBe(16000);
});

it('does not crash on construction when the active profile has no contextWindow/maxTokens override', () => {
expect(() => new ConfigStore()).not.toThrow();
const config = new ConfigStore().getAll();
expect(config.contextWindow).toBeUndefined();
expect(config.maxTokens).toBeUndefined();
});

it('clears contextWindow/maxTokens when switching to a set whose active profile has no override', () => {
const store = new ConfigStore();

store.update({
provider: 'ollama',
apiKey: '',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.3',
profiles: {
ollama: {
apiKey: '',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.3',
contextWindow: 32000,
maxTokens: 8000,
},
},
});
expect(store.getAll().contextWindow).toBe(32000);

const created = store.createSet({ name: 'No override set', mode: 'blank' });
const secondSetId = created.configSets.find((set) => set.id !== 'default')!.id;

expect(() =>
store.update({
provider: 'openrouter',
apiKey: 'sk-or',
model: 'anthropic/claude',
})
).not.toThrow();
const secondSetView = store.getAll();
expect(secondSetView.contextWindow).toBeUndefined();
expect(secondSetView.maxTokens).toBeUndefined();

store.switchSet({ id: 'default' });
expect(store.getAll().contextWindow).toBe(32000);

store.switchSet({ id: secondSetId });
expect(store.getAll().contextWindow).toBeUndefined();
expect(store.getAll().maxTokens).toBeUndefined();
});
});
17 changes: 17 additions & 0 deletions tests/index-agent-runtime-signature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest';
import path from 'node:path';
import fs from 'node:fs';

const indexPath = path.resolve(process.cwd(), 'src/main/index.ts');

describe('buildAgentRuntimeSignature', () => {
it('includes contextWindow and maxTokens so a numeric-only change reloads the running session', () => {
const source = fs.readFileSync(indexPath, 'utf8');
const signatureBlock = source.match(
/const buildAgentRuntimeSignature[\s\S]*?\n \}\);/
)?.[0] || '';

expect(signatureBlock).toContain('contextWindow: config.contextWindow');
expect(signatureBlock).toContain('maxTokens: config.maxTokens');
});
});
Loading