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 type { SessionEventRecord } from '@shared/domain/event';
import { import {
applySessionApprovalSettings, applySessionApprovalSettings,
applyScratchpadSessionConfig, applySessionModelConfig,
resolveSessionToolingSelection, resolveSessionToolingSelection,
createScratchpadSessionConfig, createSessionModelConfig,
resolveSessionTitle, resolveSessionTitle,
type ChatMessageRecord, type ChatMessageRecord,
type SessionRecord, type SessionRecord,
@@ -494,8 +494,8 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
updatedAt: nowIso(), updatedAt: nowIso(),
status: 'idle', status: 'idle',
messages: [], messages: [],
scratchpadConfig: isScratchpadProject(project) sessionModelConfig: normalizedPattern.agents.length === 1
? createScratchpadSessionConfig(normalizedPattern) ? createSessionModelConfig(normalizedPattern)
: undefined, : undefined,
tooling: createSessionToolingSelection(), tooling: createSessionToolingSelection(),
runs: [], runs: [],
@@ -553,7 +553,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
} }
const project = this.requireProject(workspace, session.projectId); const project = this.requireProject(workspace, session.projectId);
const pattern = this.requirePattern(workspace, session.patternId); 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); const preparedContent = prepareChatMessageContent(content);
if (!preparedContent) { if (!preparedContent) {
@@ -740,7 +740,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
return result; return result;
} }
async updateScratchpadSessionConfig( async updateSessionModelConfig(
sessionId: string, sessionId: string,
model: string, model: string,
reasoningEffort?: ReasoningEffort, reasoningEffort?: ReasoningEffort,
@@ -749,13 +749,15 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
const session = this.requireSession(workspace, sessionId); const session = this.requireSession(workspace, sessionId);
const project = this.requireProject(workspace, session.projectId); const project = this.requireProject(workspace, session.projectId);
const modelCatalog = await this.loadAvailableModelCatalog(); const modelCatalog = await this.loadAvailableModelCatalog();
const pattern = this.requirePattern(workspace, session.patternId);
const effectivePattern = normalizePatternModels(pattern, modelCatalog);
if (!isScratchpadProject(project)) { if (effectivePattern.agents.length !== 1) {
throw new Error('Only scratchpad sessions can change model settings in chat.'); throw new Error('Model override is only supported for single-agent sessions.');
} }
if (session.status === 'running') { 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(); const normalizedModel = model.trim();
@@ -767,7 +769,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
throw new Error(`Reasoning effort "${reasoningEffort}" is not supported.`); throw new Error(`Reasoning effort "${reasoningEffort}" is not supported.`);
} }
session.scratchpadConfig = { session.sessionModelConfig = {
model: normalizedModel, model: normalizedModel,
reasoningEffort: resolveReasoningEffort(selectedModel, reasoningEffort), reasoningEffort: resolveReasoningEffort(selectedModel, reasoningEffort),
}; };
@@ -1326,12 +1328,11 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
} }
private async buildEffectivePattern( private async buildEffectivePattern(
project: ProjectRecord,
pattern: PatternDefinition, pattern: PatternDefinition,
session: SessionRecord, session: SessionRecord,
): Promise<PatternDefinition> { ): Promise<PatternDefinition> {
const patternWithSessionConfig = isScratchpadProject(project) const patternWithSessionConfig = session.sessionModelConfig
? applyScratchpadSessionConfig(pattern, session) ? applySessionModelConfig(pattern, session)
: pattern; : pattern;
const patternWithApprovalSettings = applySessionApprovalSettings(patternWithSessionConfig, session); const patternWithApprovalSettings = applySessionApprovalSettings(patternWithSessionConfig, session);
+4 -4
View File
@@ -19,7 +19,7 @@ import type {
SetSessionPinnedInput, SetSessionPinnedInput,
UpdateSessionApprovalSettingsInput, UpdateSessionApprovalSettingsInput,
UpdateSessionToolingInput, UpdateSessionToolingInput,
UpdateScratchpadSessionConfigInput, UpdateSessionModelConfigInput,
} from '@shared/contracts/ipc'; } from '@shared/contracts/ipc';
import type { QuerySessionsInput } from '@shared/domain/sessionLibrary'; import type { QuerySessionsInput } from '@shared/domain/sessionLibrary';
import type { AppearanceTheme } from '@shared/domain/tooling'; 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), service.resolveSessionApproval(input.sessionId, input.approvalId, input.decision),
); );
ipcMain.handle( ipcMain.handle(
ipcChannels.updateScratchpadSessionConfig, ipcChannels.updateSessionModelConfig,
(_event, input: UpdateScratchpadSessionConfigInput) => (_event, input: UpdateSessionModelConfigInput) =>
service.updateScratchpadSessionConfig(input.sessionId, input.model, input.reasoningEffort), service.updateSessionModelConfig(input.sessionId, input.model, input.reasoningEffort),
); );
ipcMain.handle(ipcChannels.querySessions, (_event, input: QuerySessionsInput) => service.querySessions(input)); ipcMain.handle(ipcChannels.querySessions, (_event, input: QuerySessionsInput) => service.querySessions(input));
ipcMain.handle(ipcChannels.selectProject, (_event, projectId?: string) => service.selectProject(projectId)); ipcMain.handle(ipcChannels.selectProject, (_event, projectId?: string) => service.selectProject(projectId));
+2 -2
View File
@@ -34,8 +34,8 @@ const api: ElectronApi = {
sendSessionMessage: (input) => ipcRenderer.invoke(ipcChannels.sendSessionMessage, input), sendSessionMessage: (input) => ipcRenderer.invoke(ipcChannels.sendSessionMessage, input),
cancelSessionTurn: (input) => ipcRenderer.invoke(ipcChannels.cancelSessionTurn, input), cancelSessionTurn: (input) => ipcRenderer.invoke(ipcChannels.cancelSessionTurn, input),
resolveSessionApproval: (input) => ipcRenderer.invoke(ipcChannels.resolveSessionApproval, input), resolveSessionApproval: (input) => ipcRenderer.invoke(ipcChannels.resolveSessionApproval, input),
updateScratchpadSessionConfig: (input) => updateSessionModelConfig: (input) =>
ipcRenderer.invoke(ipcChannels.updateScratchpadSessionConfig, input), ipcRenderer.invoke(ipcChannels.updateSessionModelConfig, input),
querySessions: (input) => ipcRenderer.invoke(ipcChannels.querySessions, input), querySessions: (input) => ipcRenderer.invoke(ipcChannels.querySessions, input),
selectProject: (projectId) => ipcRenderer.invoke(ipcChannels.selectProject, projectId), selectProject: (projectId) => ipcRenderer.invoke(ipcChannels.selectProject, projectId),
selectPattern: (patternId) => ipcRenderer.invoke(ipcChannels.selectPattern, patternId), selectPattern: (patternId) => ipcRenderer.invoke(ipcChannels.selectPattern, patternId),
+5 -5
View File
@@ -27,7 +27,7 @@ import { createDefaultToolApprovalPolicy } from '@shared/domain/approval';
import { listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling'; import { listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
import { syncPatternGraph, type PatternDefinition } from '@shared/domain/pattern'; import { syncPatternGraph, type PatternDefinition } from '@shared/domain/pattern';
import { isScratchpadProject, SCRATCHPAD_PROJECT_ID } from '@shared/domain/project'; import { isScratchpadProject, SCRATCHPAD_PROJECT_ID } from '@shared/domain/project';
import { applyScratchpadSessionConfig } from '@shared/domain/session'; import { applySessionModelConfig } from '@shared/domain/session';
import type { AppearanceTheme, LspProfileDefinition, McpServerDefinition } from '@shared/domain/tooling'; import type { AppearanceTheme, LspProfileDefinition, McpServerDefinition } from '@shared/domain/tooling';
import type { WorkspaceState } from '@shared/domain/workspace'; import type { WorkspaceState } from '@shared/domain/workspace';
import { createId, nowIso } from '@shared/utils/ids'; import { createId, nowIso } from '@shared/utils/ids';
@@ -159,8 +159,8 @@ export default function App() {
} }
const patternWithSessionConfig = const patternWithSessionConfig =
projectForSession && isScratchpadProject(projectForSession) projectForSession && selectedSession.sessionModelConfig
? applyScratchpadSessionConfig(basePattern, selectedSession) ? applySessionModelConfig(basePattern, selectedSession)
: basePattern; : basePattern;
return normalizePatternModels(patternWithSessionConfig, availableModels); return normalizePatternModels(patternWithSessionConfig, availableModels);
@@ -250,8 +250,8 @@ export default function App() {
onResolveApproval={(approvalId, decision) => onResolveApproval={(approvalId, decision) =>
api.resolveSessionApproval({ sessionId: selectedSession.id, approvalId, decision }) api.resolveSessionApproval({ sessionId: selectedSession.id, approvalId, decision })
} }
onUpdateScratchpadConfig={(config) => onUpdateSessionModelConfig={(config) =>
api.updateScratchpadSessionConfig({ api.updateSessionModelConfig({
sessionId: selectedSession.id, sessionId: selectedSession.id,
model: config.model, model: config.model,
reasoningEffort: config.reasoningEffort, reasoningEffort: config.reasoningEffort,
+24 -23
View File
@@ -36,7 +36,7 @@ interface ChatPaneProps {
onSend: (content: string) => Promise<void>; onSend: (content: string) => Promise<void>;
onCancelTurn?: () => void; onCancelTurn?: () => void;
onResolveApproval?: (approvalId: string, decision: ApprovalDecision) => Promise<unknown>; onResolveApproval?: (approvalId: string, decision: ApprovalDecision) => Promise<unknown>;
onUpdateScratchpadConfig?: (config: { onUpdateSessionModelConfig?: (config: {
model: string; model: string;
reasoningEffort?: ReasoningEffort; reasoningEffort?: ReasoningEffort;
}) => Promise<unknown>; }) => Promise<unknown>;
@@ -54,7 +54,7 @@ export function ChatPane({
onSend, onSend,
onCancelTurn, onCancelTurn,
onResolveApproval, onResolveApproval,
onUpdateScratchpadConfig, onUpdateSessionModelConfig,
onUpdateSessionTooling, onUpdateSessionTooling,
onUpdateSessionApprovalSettings, onUpdateSessionApprovalSettings,
}: ChatPaneProps) { }: ChatPaneProps) {
@@ -62,7 +62,7 @@ export function ChatPane({
const [configError, setConfigError] = useState<string>(); const [configError, setConfigError] = useState<string>();
const [approvalError, setApprovalError] = useState<string>(); const [approvalError, setApprovalError] = useState<string>();
const [isResolvingApproval, setIsResolvingApproval] = useState(false); const [isResolvingApproval, setIsResolvingApproval] = useState(false);
const [isUpdatingScratchpadConfig, setIsUpdatingScratchpadConfig] = useState(false); const [isUpdatingSessionModelConfig, setIsUpdatingSessionModelConfig] = useState(false);
const transcriptRef = useRef<HTMLDivElement>(null); const transcriptRef = useRef<HTMLDivElement>(null);
const composerRef = useRef<MarkdownComposerHandle>(null); const composerRef = useRef<MarkdownComposerHandle>(null);
@@ -71,11 +71,12 @@ export function ChatPane({
const queuedApprovals = (session.pendingApprovalQueue ?? []).filter((a) => a.status === 'pending'); const queuedApprovals = (session.pendingApprovalQueue ?? []).filter((a) => a.status === 'pending');
const totalPendingCount = (pendingApproval ? 1 : 0) + queuedApprovals.length; const totalPendingCount = (pendingApproval ? 1 : 0) + queuedApprovals.length;
const isScratchpad = isScratchpadProject(project); const isScratchpad = isScratchpadProject(project);
const isSingleAgent = pattern.agents.length === 1;
const primaryAgent = pattern.agents[0]; const primaryAgent = pattern.agents[0];
const selectedModel = primaryAgent ? findModel(primaryAgent.model, availableModels) : undefined; const selectedModel = primaryAgent ? findModel(primaryAgent.model, availableModels) : undefined;
const supportedEfforts = getSupportedReasoningEfforts(selectedModel); const supportedEfforts = getSupportedReasoningEfforts(selectedModel);
const scratchpadReasoningEffort = resolveReasoningEffort(selectedModel, primaryAgent?.reasoningEffort); const sessionReasoningEffort = resolveReasoningEffort(selectedModel, primaryAgent?.reasoningEffort);
const isComposerDisabled = isSessionBusy || isUpdatingScratchpadConfig; const isComposerDisabled = isSessionBusy || isUpdatingSessionModelConfig;
const canSubmitInput = hasComposerContent && !isComposerDisabled; const canSubmitInput = hasComposerContent && !isComposerDisabled;
const toolSelection = useMemo(() => resolveSessionToolingSelection(session), [session]); const toolSelection = useMemo(() => resolveSessionToolingSelection(session), [session]);
@@ -108,37 +109,37 @@ export function ChatPane({
setConfigError(undefined); setConfigError(undefined);
setApprovalError(undefined); setApprovalError(undefined);
setIsResolvingApproval(false); setIsResolvingApproval(false);
setIsUpdatingScratchpadConfig(false); setIsUpdatingSessionModelConfig(false);
}, [session.id]); }, [session.id]);
function handleComposerSubmit(content: string) { function handleComposerSubmit(content: string) {
void onSend(content); void onSend(content);
} }
async function handleScratchpadConfigChange(config: { async function handleSessionModelConfigChange(config: {
model: string; model: string;
reasoningEffort?: ReasoningEffort; reasoningEffort?: ReasoningEffort;
}) { }) {
if (!isScratchpad || !primaryAgent || isComposerDisabled || !onUpdateScratchpadConfig) { if (!isSingleAgent || !primaryAgent || isComposerDisabled || !onUpdateSessionModelConfig) {
return; return;
} }
if ( if (
config.model === primaryAgent.model && config.model === primaryAgent.model &&
config.reasoningEffort === scratchpadReasoningEffort config.reasoningEffort === sessionReasoningEffort
) { ) {
return; return;
} }
setConfigError(undefined); setConfigError(undefined);
setIsUpdatingScratchpadConfig(true); setIsUpdatingSessionModelConfig(true);
try { try {
await onUpdateScratchpadConfig(config); await onUpdateSessionModelConfig(config);
} catch (error) { } catch (error) {
setConfigError(error instanceof Error ? error.message : String(error)); setConfigError(error instanceof Error ? error.message : String(error));
} finally { } finally {
setIsUpdatingScratchpadConfig(false); setIsUpdatingSessionModelConfig(false);
} }
} }
@@ -338,8 +339,8 @@ export function ChatPane({
</div> </div>
)} )}
{/* Scratchpad pills — tools/approval left, model/reasoning right */} {/* Session config pills — tools/approval left, model/reasoning right */}
{isScratchpad && ( {isSingleAgent && (
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
{hasConfigurableTools && onUpdateSessionTooling && ( {hasConfigurableTools && onUpdateSessionTooling && (
<InlineToolsPill <InlineToolsPill
@@ -366,9 +367,9 @@ export function ChatPane({
models={availableModels} models={availableModels}
onChange={(modelId) => { onChange={(modelId) => {
const nextModel = findModel(modelId, availableModels); const nextModel = findModel(modelId, availableModels);
void handleScratchpadConfigChange({ void handleSessionModelConfigChange({
model: modelId, model: modelId,
reasoningEffort: resolveReasoningEffort(nextModel, scratchpadReasoningEffort), reasoningEffort: resolveReasoningEffort(nextModel, sessionReasoningEffort),
}); });
}} }}
value={primaryAgent.model} value={primaryAgent.model}
@@ -376,15 +377,15 @@ export function ChatPane({
<InlineThinkingPill <InlineThinkingPill
disabled={isComposerDisabled} disabled={isComposerDisabled}
onChange={(reasoningEffort) => onChange={(reasoningEffort) =>
void handleScratchpadConfigChange({ void handleSessionModelConfigChange({
model: primaryAgent.model, model: primaryAgent.model,
reasoningEffort, reasoningEffort,
}) })
} }
supportedEfforts={supportedEfforts} supportedEfforts={supportedEfforts}
value={scratchpadReasoningEffort} value={sessionReasoningEffort}
/> />
{isUpdatingScratchpadConfig && ( {isUpdatingSessionModelConfig && (
<Loader2 className="size-3 animate-spin text-zinc-500" /> <Loader2 className="size-3 animate-spin text-zinc-500" />
)} )}
</div> </div>
@@ -392,8 +393,8 @@ export function ChatPane({
</div> </div>
)} )}
{/* Session config pills — tool & approval controls (non-scratchpad) */} {/* Session config pills — tool & approval controls (multi-agent) */}
{!isScratchpad && (hasConfigurableTools || hasToolCallApproval) && ( {!isSingleAgent && (hasConfigurableTools || hasToolCallApproval) && (
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
{hasConfigurableTools && onUpdateSessionTooling && ( {hasConfigurableTools && onUpdateSessionTooling && (
<InlineToolsPill <InlineToolsPill
@@ -427,8 +428,8 @@ export function ChatPane({
? 'Awaiting approval...' ? 'Awaiting approval...'
: isSessionBusy : isSessionBusy
? 'Waiting for response...' ? 'Waiting for response...'
: isUpdatingScratchpadConfig : isUpdatingSessionModelConfig
? 'Saving scratchpad settings...' ? 'Saving model settings...'
: 'Message...' : 'Message...'
} }
> >
+1 -1
View File
@@ -27,7 +27,7 @@ export const ipcChannels = {
cancelSessionTurn: 'sessions:cancel-turn', cancelSessionTurn: 'sessions:cancel-turn',
resolveSessionApproval: 'sessions:resolve-approval', resolveSessionApproval: 'sessions:resolve-approval',
querySessions: 'sessions:query', querySessions: 'sessions:query',
updateScratchpadSessionConfig: 'sessions:update-scratchpad-config', updateSessionModelConfig: 'sessions:update-model-config',
selectProject: 'selection:project', selectProject: 'selection:project',
selectPattern: 'selection:pattern', selectPattern: 'selection:pattern',
selectSession: 'selection:session', selectSession: 'selection:session',
+2 -2
View File
@@ -36,7 +36,7 @@ export interface ResolveSessionApprovalInput {
decision: ApprovalDecision; decision: ApprovalDecision;
} }
export interface UpdateScratchpadSessionConfigInput { export interface UpdateSessionModelConfigInput {
sessionId: string; sessionId: string;
model: string; model: string;
reasoningEffort?: ReasoningEffort; reasoningEffort?: ReasoningEffort;
@@ -126,7 +126,7 @@ export interface ElectronApi {
sendSessionMessage(input: SendSessionMessageInput): Promise<void>; sendSessionMessage(input: SendSessionMessageInput): Promise<void>;
cancelSessionTurn(input: CancelSessionTurnInput): Promise<void>; cancelSessionTurn(input: CancelSessionTurnInput): Promise<void>;
resolveSessionApproval(input: ResolveSessionApprovalInput): Promise<WorkspaceState>; resolveSessionApproval(input: ResolveSessionApprovalInput): Promise<WorkspaceState>;
updateScratchpadSessionConfig(input: UpdateScratchpadSessionConfigInput): Promise<WorkspaceState>; updateSessionModelConfig(input: UpdateSessionModelConfigInput): Promise<WorkspaceState>;
querySessions(input: QuerySessionsInput): Promise<SessionQueryResult[]>; querySessions(input: QuerySessionsInput): Promise<SessionQueryResult[]>;
selectProject(projectId?: string): Promise<WorkspaceState>; selectProject(projectId?: string): Promise<WorkspaceState>;
selectPattern(patternId?: string): Promise<WorkspaceState>; selectPattern(patternId?: string): Promise<WorkspaceState>;
+11 -11
View File
@@ -16,7 +16,7 @@ export type ChatRole = 'system' | 'user' | 'assistant';
export type SessionStatus = 'idle' | 'running' | 'error'; export type SessionStatus = 'idle' | 'running' | 'error';
export type SessionTitleSource = 'auto' | 'manual'; export type SessionTitleSource = 'auto' | 'manual';
export interface ScratchpadSessionConfig { export interface SessionModelConfig {
model: string; model: string;
reasoningEffort?: ReasoningEffort; reasoningEffort?: ReasoningEffort;
} }
@@ -43,7 +43,7 @@ export interface SessionRecord {
isArchived?: boolean; isArchived?: boolean;
messages: ChatMessageRecord[]; messages: ChatMessageRecord[];
lastError?: string; lastError?: string;
scratchpadConfig?: ScratchpadSessionConfig; sessionModelConfig?: SessionModelConfig;
tooling?: SessionToolingSelection; tooling?: SessionToolingSelection;
approvalSettings?: SessionApprovalSettings; approvalSettings?: SessionApprovalSettings;
pendingApproval?: PendingApprovalRecord; pendingApproval?: PendingApprovalRecord;
@@ -63,9 +63,9 @@ export function resolveSessionTitle(
return buildSessionTitle(pattern, messages); return buildSessionTitle(pattern, messages);
} }
export function createScratchpadSessionConfig( export function createSessionModelConfig(
pattern: PatternDefinition, pattern: PatternDefinition,
): ScratchpadSessionConfig | undefined { ): SessionModelConfig | undefined {
const primaryAgent = pattern.agents[0]; const primaryAgent = pattern.agents[0];
if (!primaryAgent) { if (!primaryAgent) {
return undefined; return undefined;
@@ -89,27 +89,27 @@ export function resolveSessionApprovalSettings(
return normalizeSessionApprovalSettings(session.approvalSettings); return normalizeSessionApprovalSettings(session.approvalSettings);
} }
export function resolveScratchpadSessionConfig( export function resolveSessionModelConfig(
session: SessionRecord, session: SessionRecord,
pattern: PatternDefinition, pattern: PatternDefinition,
): ScratchpadSessionConfig | undefined { ): SessionModelConfig | undefined {
const defaults = createScratchpadSessionConfig(pattern); const defaults = createSessionModelConfig(pattern);
if (!defaults) { if (!defaults) {
return undefined; return undefined;
} }
const overrideModel = session.scratchpadConfig?.model.trim(); const overrideModel = session.sessionModelConfig?.model.trim();
return { return {
model: overrideModel || defaults.model, model: overrideModel || defaults.model,
reasoningEffort: session.scratchpadConfig?.reasoningEffort ?? defaults.reasoningEffort, reasoningEffort: session.sessionModelConfig?.reasoningEffort ?? defaults.reasoningEffort,
}; };
} }
export function applyScratchpadSessionConfig( export function applySessionModelConfig(
pattern: PatternDefinition, pattern: PatternDefinition,
session: SessionRecord, session: SessionRecord,
): PatternDefinition { ): PatternDefinition {
const config = resolveScratchpadSessionConfig(session, pattern); const config = resolveSessionModelConfig(session, pattern);
const primaryAgent = pattern.agents[0]; const primaryAgent = pattern.agents[0];
if (!config || !primaryAgent) { if (!config || !primaryAgent) {
return pattern; return pattern;
+1 -1
View File
@@ -174,7 +174,7 @@ export function duplicateSessionRecord(
isPinned: false, isPinned: false,
isArchived: false, isArchived: false,
lastError: undefined, lastError: undefined,
scratchpadConfig: session.scratchpadConfig ? { ...session.scratchpadConfig } : undefined, sessionModelConfig: session.sessionModelConfig ? { ...session.sessionModelConfig } : undefined,
tooling: session.tooling tooling: session.tooling
? { ? {
enabledMcpServerIds: [...session.tooling.enabledMcpServerIds], enabledMcpServerIds: [...session.tooling.enabledMcpServerIds],
+12 -12
View File
@@ -3,12 +3,12 @@ import { describe, expect, test } from 'bun:test';
import type { PatternDefinition } from '@shared/domain/pattern'; import type { PatternDefinition } from '@shared/domain/pattern';
import { import {
applySessionApprovalSettings, applySessionApprovalSettings,
applyScratchpadSessionConfig, applySessionModelConfig,
createScratchpadSessionConfig, createSessionModelConfig,
resolveSessionApprovalSettings, resolveSessionApprovalSettings,
resolveSessionToolingSelection, resolveSessionToolingSelection,
resolveSessionTitle, resolveSessionTitle,
resolveScratchpadSessionConfig, resolveSessionModelConfig,
type SessionRecord, type SessionRecord,
} from '@shared/domain/session'; } from '@shared/domain/session';
@@ -58,18 +58,18 @@ function createSession(overrides?: Partial<SessionRecord>): SessionRecord {
}; };
} }
describe('scratchpad session config helpers', () => { describe('session model config helpers', () => {
test('captures the initial scratchpad model settings from the primary agent', () => { test('captures the initial model settings from the primary agent', () => {
expect(createScratchpadSessionConfig(createPattern())).toEqual({ expect(createSessionModelConfig(createPattern())).toEqual({
model: 'gpt-5.4', model: 'gpt-5.4',
reasoningEffort: 'high', reasoningEffort: 'high',
}); });
}); });
test('resolves persisted scratchpad overrides over the pattern defaults', () => { test('resolves persisted session overrides over the pattern defaults', () => {
const config = resolveScratchpadSessionConfig( const config = resolveSessionModelConfig(
createSession({ createSession({
scratchpadConfig: { sessionModelConfig: {
model: 'claude-opus-4.5', model: 'claude-opus-4.5',
reasoningEffort: 'medium', reasoningEffort: 'medium',
}, },
@@ -83,12 +83,12 @@ describe('scratchpad session config helpers', () => {
}); });
}); });
test('applies scratchpad settings only to the primary agent', () => { test('applies session model settings only to the primary agent', () => {
const pattern = createPattern(); const pattern = createPattern();
const updated = applyScratchpadSessionConfig( const updated = applySessionModelConfig(
pattern, pattern,
createSession({ createSession({
scratchpadConfig: { sessionModelConfig: {
model: 'gpt-5.4-mini', model: 'gpt-5.4-mini',
reasoningEffort: 'low', reasoningEffort: 'low',
}, },