feat: allow model selection in all single-agent sessions

Generalize the scratchpad-only model/reasoning-effort selector to work
in any session whose pattern has exactly one agent. This enables model
switching above the chat input for project 1-on-1 chats, not just
scratchpads.

- Rename ScratchpadSessionConfig -> SessionModelConfig across domain,
  contracts, IPC, preload, and renderer
- Replace isScratchpadProject guard with agents.length === 1 check in
  EryxAppService.updateSessionModelConfig and ChatPane pills gate
- Apply session model config in buildEffectivePattern whenever present,
  regardless of project type
- Seed sessionModelConfig on session creation for any single-agent
  pattern

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-25 22:41:46 +01:00
co-authored by Copilot
parent 46b5de472e
commit 968ae37279
10 changed files with 76 additions and 74 deletions
+14 -13
View File
@@ -55,9 +55,9 @@ import {
import type { SessionEventRecord } from '@shared/domain/event';
import {
applySessionApprovalSettings,
applyScratchpadSessionConfig,
applySessionModelConfig,
resolveSessionToolingSelection,
createScratchpadSessionConfig,
createSessionModelConfig,
resolveSessionTitle,
type ChatMessageRecord,
type SessionRecord,
@@ -494,8 +494,8 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
updatedAt: nowIso(),
status: 'idle',
messages: [],
scratchpadConfig: isScratchpadProject(project)
? createScratchpadSessionConfig(normalizedPattern)
sessionModelConfig: normalizedPattern.agents.length === 1
? createSessionModelConfig(normalizedPattern)
: undefined,
tooling: createSessionToolingSelection(),
runs: [],
@@ -553,7 +553,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
}
const project = this.requireProject(workspace, session.projectId);
const pattern = this.requirePattern(workspace, session.patternId);
const effectivePattern = await this.buildEffectivePattern(project, pattern, session);
const effectivePattern = await this.buildEffectivePattern(pattern, session);
const preparedContent = prepareChatMessageContent(content);
if (!preparedContent) {
@@ -740,7 +740,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
return result;
}
async updateScratchpadSessionConfig(
async updateSessionModelConfig(
sessionId: string,
model: string,
reasoningEffort?: ReasoningEffort,
@@ -749,13 +749,15 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
const session = this.requireSession(workspace, sessionId);
const project = this.requireProject(workspace, session.projectId);
const modelCatalog = await this.loadAvailableModelCatalog();
const pattern = this.requirePattern(workspace, session.patternId);
const effectivePattern = normalizePatternModels(pattern, modelCatalog);
if (!isScratchpadProject(project)) {
throw new Error('Only scratchpad sessions can change model settings in chat.');
if (effectivePattern.agents.length !== 1) {
throw new Error('Model override is only supported for single-agent sessions.');
}
if (session.status === 'running') {
throw new Error('Wait for the current scratchpad response to finish before changing model settings.');
throw new Error('Wait for the current response to finish before changing model settings.');
}
const normalizedModel = model.trim();
@@ -767,7 +769,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
throw new Error(`Reasoning effort "${reasoningEffort}" is not supported.`);
}
session.scratchpadConfig = {
session.sessionModelConfig = {
model: normalizedModel,
reasoningEffort: resolveReasoningEffort(selectedModel, reasoningEffort),
};
@@ -1326,12 +1328,11 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
}
private async buildEffectivePattern(
project: ProjectRecord,
pattern: PatternDefinition,
session: SessionRecord,
): Promise<PatternDefinition> {
const patternWithSessionConfig = isScratchpadProject(project)
? applyScratchpadSessionConfig(pattern, session)
const patternWithSessionConfig = session.sessionModelConfig
? applySessionModelConfig(pattern, session)
: pattern;
const patternWithApprovalSettings = applySessionApprovalSettings(patternWithSessionConfig, session);
+4 -4
View File
@@ -19,7 +19,7 @@ import type {
SetSessionPinnedInput,
UpdateSessionApprovalSettingsInput,
UpdateSessionToolingInput,
UpdateScratchpadSessionConfigInput,
UpdateSessionModelConfigInput,
} from '@shared/contracts/ipc';
import type { QuerySessionsInput } from '@shared/domain/sessionLibrary';
import type { AppearanceTheme } from '@shared/domain/tooling';
@@ -108,9 +108,9 @@ export function registerIpcHandlers(window: BrowserWindow, service: EryxAppServi
service.resolveSessionApproval(input.sessionId, input.approvalId, input.decision),
);
ipcMain.handle(
ipcChannels.updateScratchpadSessionConfig,
(_event, input: UpdateScratchpadSessionConfigInput) =>
service.updateScratchpadSessionConfig(input.sessionId, input.model, input.reasoningEffort),
ipcChannels.updateSessionModelConfig,
(_event, input: UpdateSessionModelConfigInput) =>
service.updateSessionModelConfig(input.sessionId, input.model, input.reasoningEffort),
);
ipcMain.handle(ipcChannels.querySessions, (_event, input: QuerySessionsInput) => service.querySessions(input));
ipcMain.handle(ipcChannels.selectProject, (_event, projectId?: string) => service.selectProject(projectId));