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 @@ -34,6 +34,13 @@ version with its date and start a fresh empty `[Unreleased]` above it.
thinking effort levels. Choices persist per model and are applied
to every request.

### Removed

- The global "Effort" dropdown in the input toolbar: reasoning effort
is now configured only through the per-model editor in the model
selector, which already offered the same levels. Models without an
explicit choice fall back to their server default effort.

### Fixed

- The model selector dropdown opens above the model button again on
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Open the chat sidebar from the ribbon icon or the command palette (`Open Qoderia

**Bash Mode (`!`)** — Press `!` in an empty input to run a shell command in the vault directory directly.

**Model & Effort Controls** — Pick a model and reasoning effort below the input, and watch context usage reported by the Qoder Agent SDK.
**Model & Effort Controls** — Pick a model below the input and tune its reasoning effort in the per-model editor, and watch context usage reported by the Qoder Agent SDK.

**MCP Servers** — Connect external tools over the Model Context Protocol (stdio, SSE, HTTP), configured in-app.

Expand All @@ -38,7 +38,7 @@ Open the chat sidebar from the ribbon icon or the command palette (`Open Qoderia
| `efficient` | Fast and cost-effective |
| `lite` | Lightweight and fast |

Adaptive thinking models accept an effort level of `Low`, `Med`, `High`, `XHigh`, or `Max`. The selector consumes the runtime catalog returned by the Qoder Agent SDK, including models configured in qodercli.
Adaptive thinking models accept an effort level of `Low`, `Med`, `High`, `XHigh`, or `Max`, configured per model through the editor in the model selector. The selector consumes the runtime catalog returned by the Qoder Agent SDK, including models configured in qodercli.

## Requirements

Expand Down
1 change: 0 additions & 1 deletion src/app/settings/settings-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const DEFAULT_QODERIAN_SETTINGS: QoderianSettings = {
permissionMode: 'acceptEdits',

model: 'auto',
effortLevel: 'high',
enableAutoTitleGeneration: true,
titleGenerationModel: 'auto',

Expand Down
1 change: 0 additions & 1 deletion src/core/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ export interface QoderianSettings {

// Qoder model and reasoning effort
model: string;
effortLevel: string;
enableAutoTitleGeneration: boolean;
titleGenerationModel: string;

Expand Down
2 changes: 0 additions & 2 deletions src/features/chat/tabs/tab-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ export async function destroyTab(tab: TabData): Promise<void> {
tab.ui.fileContextManager?.destroy();
tab.ui.modelSelector?.destroy();
tab.ui.modelSelector = null;
tab.ui.effortSelector?.destroy();
tab.ui.effortSelector = null;
tab.ui.slashCommandDropdown?.destroy();
tab.ui.slashCommandDropdown = null;
tab.ui.instructionModeManager?.destroy();
Expand Down
2 changes: 0 additions & 2 deletions src/features/chat/tabs/tab-qoder-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { TabData, TabManagerViewHost, TabQoderContext } from './types';

export type TabQoderSettings = Record<string, unknown> & {
model: string;
effortLevel: string;
permissionMode: QoderianSettings['permissionMode'];
};

Expand Down Expand Up @@ -95,7 +94,6 @@ export function refreshTabQoderUI(tab: TabData, plugin: QoderianPlugin): void {
const permissionMode = getTabPermissionMode(tab, plugin);
tab.ui.modelSelector?.updateDisplay();
tab.ui.modelSelector?.renderOptions();
tab.ui.effortSelector?.updateDisplay();
tab.ui.permissionToggle?.updateDisplay();
tab.dom.inputWrapper.toggleClass('qoderian-input-plan-mode', permissionMode === 'plan');
}
Expand Down
9 changes: 0 additions & 9 deletions src/features/chat/tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ export function createTab(options: TabCreateOptions): TabData {
fileContextManager: null,
imageContextManager: null,
modelSelector: null,
effortSelector: null,
externalContextSelector: null,
mcpServerSelector: null,
permissionToggle: null,
Expand Down Expand Up @@ -415,7 +414,6 @@ function initializeInputToolbar(
settings.model = model;
modelConfig.applyModelDefaults(model, settings);
});
tab.ui.effortSelector?.updateDisplay();
tab.ui.modelSelector?.updateDisplay();
tab.ui.modelSelector?.renderOptions();
applyQoderUIGating(tab, plugin);
Expand All @@ -427,7 +425,6 @@ function initializeInputToolbar(
settings.model = model;
modelConfig.applyModelDefaults(model, settings);
});
tab.ui.effortSelector?.updateDisplay();
tab.ui.modelSelector?.updateDisplay();
tab.ui.modelSelector?.renderOptions();

Expand All @@ -438,11 +435,6 @@ function initializeInputToolbar(
tab.state.usage = recalculateUsageForModel(currentUsage, model, newContextWindow);
}
},
onEffortLevelChange: async (effort: string) => {
await updateTabQoderSettings(tab, plugin, (settings) => {
settings.effortLevel = effort;
});
},
onModelOverrideChange: async (model: string, override: Partial<QoderModelOverride>) => {
await updateTabQoderSettings(tab, plugin, (settings) => {
const current = getQoderSettings(settings).modelOverrides;
Expand Down Expand Up @@ -487,7 +479,6 @@ function initializeInputToolbar(
});

tab.ui.modelSelector = toolbarComponents.modelSelector;
tab.ui.effortSelector = toolbarComponents.effortSelector;
tab.ui.contextUsageMeter = toolbarComponents.contextUsageMeter;
tab.ui.externalContextSelector = toolbarComponents.externalContextSelector;
tab.ui.mcpServerSelector = toolbarComponents.mcpServerSelector;
Expand Down
2 changes: 0 additions & 2 deletions src/features/chat/tabs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import type { FileContextManager } from '../ui/file-context/file-context-manager
import type { ImageContextManager } from '../ui/image-context';
import type {
ContextUsageMeter,
EffortSelector,
ExternalContextSelector,
McpServerSelector,
ModelSelector,
Expand Down Expand Up @@ -118,7 +117,6 @@ export interface TabUIComponents {
fileContextManager: FileContextManager | null;
imageContextManager: ImageContextManager | null;
modelSelector: ModelSelector | null;
effortSelector: EffortSelector | null;
externalContextSelector: ExternalContextSelector | null;
mcpServerSelector: McpServerSelector | null;
permissionToggle: PermissionToggle | null;
Expand Down
5 changes: 0 additions & 5 deletions src/features/chat/ui/input-toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ import type {
import type { McpServerManager } from '../../../qoder/mcp/mcp-server-manager';
import { appendCheckIcon, appendMcpIcon } from '../../../shared/icons';
import {
EffortSelector,
ModelSelector,
PermissionToggle,
type ToolbarCallbacks,
} from './toolbar/toolbar-selectors';

export {
EffortSelector,
ModelSelector,
PermissionToggle,
type ToolbarCallbacks,
Expand Down Expand Up @@ -733,22 +731,19 @@ export function createInputToolbar(
callbacks: ToolbarCallbacks
): {
modelSelector: ModelSelector;
effortSelector: EffortSelector;
contextUsageMeter: ContextUsageMeter | null;
externalContextSelector: ExternalContextSelector;
mcpServerSelector: McpServerSelector;
permissionToggle: PermissionToggle;
} {
const modelSelector = new ModelSelector(parentEl, callbacks);
const effortSelector = new EffortSelector(parentEl, callbacks);
const contextUsageMeter = new ContextUsageMeter(parentEl);
const externalContextSelector = new ExternalContextSelector(parentEl, callbacks);
const mcpServerSelector = new McpServerSelector(parentEl);
const permissionToggle = new PermissionToggle(parentEl, callbacks);

return {
modelSelector,
effortSelector,
contextUsageMeter,
externalContextSelector,
mcpServerSelector,
Expand Down
96 changes: 2 additions & 94 deletions src/features/chat/ui/toolbar/toolbar-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,12 @@ function runToolbarAction(action: () => Promise<void>, failureMessage: string):

export interface ToolbarSettings {
model: string;
effortLevel: string;
permissionMode: PermissionMode;
[key: string]: unknown;
}

export interface ToolbarCallbacks {
onModelChange: (model: string) => Promise<void>;
onEffortLevelChange: (effort: string) => Promise<void>;
onPermissionModeChange: (mode: PermissionMode) => Promise<void>;
/** Per-model editor overrides (context window tier, thinking toggle). */
onModelOverrideChange?: (model: string, override: Partial<QoderModelOverride>) => Promise<void>;
Expand Down Expand Up @@ -438,14 +436,8 @@ export class ModelSelector {
});
const defaultEffort = efforts.find(effort => effort.isDefault)?.value
?? efforts[0]?.value;
const globalEffort = typeof settings.effortLevel === 'string'
? settings.effortLevel
: undefined;
// Without an override the global effort wins when the model offers it,
// otherwise the server default applies.
const fallbackEffort = globalEffort && efforts.some(effort => effort.value === globalEffort)
? globalEffort
: defaultEffort;
// Without an override the server default applies.
const fallbackEffort = defaultEffort;
const effectiveEffort = override?.thinkingEffort ?? fallbackEffort;
for (const effort of efforts) {
const selected = effort.value === effectiveEffort;
Expand Down Expand Up @@ -486,90 +478,6 @@ export class ModelSelector {
}
}

export class EffortSelector {
private readonly container: HTMLElement;
private effortEl: HTMLElement | null = null;
private effortGearsEl: HTMLElement | null = null;
private popover: ClickPopover | null = null;

constructor(parentEl: HTMLElement, private readonly callbacks: ToolbarCallbacks) {
this.container = parentEl.createDiv({ cls: 'qoderian-thinking-selector' });
this.render();
}

destroy(): void {
this.popover?.destroy();
this.popover = null;
}

private render(): void {
this.container.empty();
this.effortEl = this.container.createDiv({ cls: 'qoderian-thinking-effort' });
this.effortEl.createSpan({ cls: 'qoderian-thinking-label-text', text: 'Effort:' });
this.effortGearsEl = this.effortEl.createDiv({ cls: 'qoderian-thinking-gears' });
this.updateDisplay();
}

private renderEffortGears(): void {
if (!this.effortGearsEl) return;
this.popover?.destroy();
this.effortGearsEl.empty();

const currentEffort = this.callbacks.getSettings().effortLevel;
const modelConfig = this.callbacks.getModelConfig();
const model = this.callbacks.getSettings().model;
const options = modelConfig.getReasoningOptions(model);
const currentInfo = options.find(effort => effort.value === currentEffort);

const currentEl = this.effortGearsEl.createDiv({
cls: 'qoderian-thinking-current',
text: currentInfo?.label || options[0]?.label || 'High',
});
const optionsEl = this.effortGearsEl.createDiv({ cls: 'qoderian-thinking-options' });

for (const effort of [...options].reverse()) {
const option = optionsEl.createDiv({ cls: 'qoderian-thinking-gear', text: effort.label });
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', String(effort.value === currentEffort));
if (effort.value === currentEffort) option.addClass('selected');

option.addEventListener('click', (event) => {
event.stopPropagation();
this.popover?.close();
runToolbarAction(async () => {
await this.callbacks.onEffortLevelChange(effort.value);
this.updateDisplay();
}, 'Failed to change effort level');
});
}

this.popover = new ClickPopover(
this.effortGearsEl,
currentEl,
optionsEl,
'qoderian-thinking-gears--open',
);
}

updateDisplay(): void {
const settings = this.callbacks.getSettings();
const modelConfig = this.callbacks.getModelConfig();
const options = modelConfig.getReasoningOptions(settings.model);
const defaultValue = modelConfig.getDefaultReasoningValue(settings.model);
const shouldHide = options.length === 0
|| (options.length === 1 && options[0]?.value === defaultValue);

if (shouldHide) {
this.popover?.close();
this.effortEl?.addClass('qoderian-hidden');
return;
}

this.effortEl?.removeClass('qoderian-hidden');
this.renderEffortGears();
}
}

export class PermissionToggle {
private readonly container: HTMLElement;
private buttonEl: HTMLElement | null = null;
Expand Down
12 changes: 4 additions & 8 deletions src/qoder/config/qoder-settings-reconciler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,10 @@ export function normalizeQoderSettings(settings: Record<string, unknown>): boole
}

if (model) {
const allowedEffortLevels = new Set(
qoderModelConfig.getReasoningOptions(model).map((option) => option.value),
);
if (
typeof settings.effortLevel !== 'string'
|| !allowedEffortLevels.has(settings.effortLevel)
) {
settings.effortLevel = qoderModelConfig.getDefaultReasoningValue(model);
// The global effort selector was removed; per-model editor overrides now
// own the reasoning effort. Drop the legacy field so it does not linger.
if ('effortLevel' in settings) {
delete settings.effortLevel;
changed = true;
}
}
Expand Down
7 changes: 0 additions & 7 deletions src/qoder/models/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,6 @@ export function normalizeEffortLevel(
return DEFAULT_EFFORT_LEVEL[normalizeModelId(model)] ?? 'high';
}

export function resolveEffortLevel(
model: string,
effortLevel: unknown,
): EffortLevel {
return normalizeEffortLevel(model, effortLevel);
}

export const CONTEXT_WINDOW_STANDARD = 200_000;
export const CONTEXT_WINDOW_1M = 1_000_000;

Expand Down
15 changes: 14 additions & 1 deletion src/qoder/models/qoder-model-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getQoderSettings, updateQoderSettings } from '../config/settings';
import {
DEFAULT_EFFORT_LEVEL,
EFFORT_LEVELS,
type EffortLevel,
getContextWindowSize,
normalizeEffortLevel,
sortThinkingEfforts,
Expand Down Expand Up @@ -102,7 +103,6 @@ export const qoderModelConfig: QoderModelConfig = {
const target = settings as Record<string, unknown>;

const runtimeModel = toQoderRuntimeModelId(model);
target.effortLevel = normalizeEffortLevel(runtimeModel, target.effortLevel);
updateQoderSettings(target, { lastModel: runtimeModel });
},

Expand All @@ -117,5 +117,18 @@ export const qoderModelConfig: QoderModelConfig = {

};

/**
* Reasoning effort to send to qodercli for a model: the per-model editor
* override when set, otherwise the model's built-in default tier.
*/
export function resolveModelReasoningEffort(
model: string,
settings: Record<string, unknown>,
): EffortLevel {
const runtimeModel = toQoderRuntimeModelId(model);
const override = getQoderSettings(settings).modelOverrides[runtimeModel];
return normalizeEffortLevel(runtimeModel, override?.thinkingEffort);
}

/** Re-export for type-only use elsewhere in the settings UI. */
export type { UIOption };
8 changes: 4 additions & 4 deletions src/qoder/runtime/qoder-cold-start-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
getQoderSettings,
resolveQoderSettingSources,
} from '../config/settings';
import {
resolveEffortLevel,
} from '../models/model-catalog';
import { toQoderRuntimeModelId } from '../models/model-selection';
import {
resolveModelReasoningEffort,
} from '../models/qoder-model-config';
import type { QoderHostContext } from '../qoder-host-context';
import { extractAssistantText } from '../services/extract-assistant-text';
import { createCustomSpawnFunction } from './custom-spawn';
Expand Down Expand Up @@ -105,7 +105,7 @@ export async function runColdStartQuery(
}

if (!config.thinking?.disabled) {
const effortLevel = resolveEffortLevel(selectedModel, settings.effortLevel);
const effortLevel = resolveModelReasoningEffort(selectedModel, settings);
options.extraArgs = { ...options.extraArgs, 'reasoning-effort': effortLevel };
}

Expand Down
Loading
Loading