From bda2e160f703409b816e9222b89b65521b260e9b Mon Sep 17 00:00:00 2001 From: Diego Serrano <129707357+diegoserranobst@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:35:59 -0400 Subject: [PATCH] fix(team): prevent double [1m] suffix on model string during re-launch --- .../team/dialogs/LaunchTeamDialog.tsx | 6 ++-- .../team/dialogs/TeamModelSelector.tsx | 3 +- .../team/dialogs/launchDialogPrefill.ts | 10 +++++- src/renderer/store/slices/teamSlice.ts | 4 +-- src/renderer/utils/teamModelContext.ts | 8 +++++ .../components/team/TeamModelSelector.test.ts | 36 ++++++++++++++++++- .../team/dialogs/launchDialogPrefill.test.ts | 35 ++++++++++++++++++ 7 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 src/renderer/utils/teamModelContext.ts diff --git a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx index dd0075b5..dcca38bb 100644 --- a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx @@ -575,6 +575,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen multimodelEnabled, storedProviderId, storedEffort: storedEffort === null ? 'medium' : storedEffort, + storedLimitContext: localStorage.getItem('team:lastLimitContext') === 'true', getStoredModel: getStoredTeamModel, }); setSavedLaunchProviderId(savedProviderId); @@ -590,10 +591,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen setSelectedProviderIdRaw(launchPrefill.providerId); setSelectedModelRaw(launchPrefill.model); setSelectedEffortRaw(launchPrefill.effort); - setLimitContextRaw( - savedRequest?.limitContext === true || - localStorage.getItem('team:lastLimitContext') === 'true' - ); + setLimitContextRaw(launchPrefill.limitContext); setSkipPermissionsRaw( savedRequest?.skipPermissions ?? localStorage.getItem('team:lastSkipPermissions') !== 'false' diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx index c1aa208a..2afa54b6 100644 --- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx +++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx @@ -16,6 +16,7 @@ import { GEMINI_UI_DISABLED_REASON, isGeminiUiFrozen, } from '@renderer/utils/geminiUiFreeze'; +import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext'; import { doesTeamModelCarryProviderBrand, getProviderScopedTeamModelLabel, @@ -99,7 +100,7 @@ export function computeEffectiveTeamModel( limitContext: boolean, providerId: 'anthropic' | 'codex' | 'gemini' = 'anthropic' ): string | undefined { - const base = selectedModel || undefined; + const base = stripTrailingOneMillionSuffixes(selectedModel); if (providerId !== 'anthropic') return base; if (limitContext) return base; if (base === 'haiku') return base; diff --git a/src/renderer/components/team/dialogs/launchDialogPrefill.ts b/src/renderer/components/team/dialogs/launchDialogPrefill.ts index 874a3164..c359b14b 100644 --- a/src/renderer/components/team/dialogs/launchDialogPrefill.ts +++ b/src/renderer/components/team/dialogs/launchDialogPrefill.ts @@ -1,4 +1,5 @@ import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze'; +import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext'; import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability'; import { isLeadMember } from '@shared/utils/leadDetection'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; @@ -9,6 +10,7 @@ interface PreviousLaunchParamsLike { providerId?: TeamProviderId; model?: string; effort?: string; + limitContext?: boolean; } interface LaunchDialogPrefillInput { @@ -18,6 +20,7 @@ interface LaunchDialogPrefillInput { multimodelEnabled: boolean; storedProviderId: TeamProviderId; storedEffort: string; + storedLimitContext: boolean; getStoredModel: (providerId: TeamProviderId) => string; } @@ -25,6 +28,7 @@ interface LaunchDialogPrefillResult { providerId: TeamProviderId; model: string; effort: string; + limitContext: boolean; } function normalizeModelCandidate(model: string | undefined): string { @@ -32,7 +36,7 @@ function normalizeModelCandidate(model: string | undefined): string { if (!trimmed || trimmed === 'default' || trimmed === '__default__') { return ''; } - return trimmed; + return stripTrailingOneMillionSuffixes(trimmed) ?? ''; } function canReuseModelForSelectedProvider( @@ -52,6 +56,7 @@ export function resolveLaunchDialogPrefill({ multimodelEnabled, storedProviderId, storedEffort, + storedLimitContext, getStoredModel, }: LaunchDialogPrefillInput): LaunchDialogPrefillResult { const currentLead = members.find((member) => isLeadMember(member)); @@ -88,6 +93,8 @@ export function resolveLaunchDialogPrefill({ const effort = currentLead?.effort ?? savedRequest?.effort ?? previousLaunchParams?.effort ?? storedEffort; + const limitContext = + previousLaunchParams?.limitContext ?? savedRequest?.limitContext ?? storedLimitContext; return { providerId, @@ -95,5 +102,6 @@ export function resolveLaunchDialogPrefill({ ? normalizeTeamModelForUi(providerId, matchingModel) : getStoredModel(providerId), effort, + limitContext, }; } diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index d52244ea..a0b8f8b2 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -6,6 +6,7 @@ import { canDisplayTaskChangesForOptions, type TaskChangeRequestOptions, } from '@renderer/utils/taskChangeRequest'; +import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext'; import { IpcError, unwrapIpc } from '@renderer/utils/unwrapIpc'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { createLogger } from '@shared/utils/logger'; @@ -1220,8 +1221,7 @@ function saveLaunchParams(teamName: string, params: TeamLaunchParams): void { * E.g. 'opus[1m]' → 'opus', 'sonnet' → 'sonnet', undefined → undefined. */ function extractBaseModel(raw?: string): string | undefined { - if (!raw) return undefined; - return raw.replace(/\[1m\]$/, '') || undefined; + return stripTrailingOneMillionSuffixes(raw); } const TOOL_APPROVAL_PREFIX = 'team:toolApprovalSettings:'; diff --git a/src/renderer/utils/teamModelContext.ts b/src/renderer/utils/teamModelContext.ts new file mode 100644 index 00000000..7229cff0 --- /dev/null +++ b/src/renderer/utils/teamModelContext.ts @@ -0,0 +1,8 @@ +export function stripTrailingOneMillionSuffixes(model: string | undefined): string | undefined { + const trimmed = model?.trim(); + if (!trimmed) { + return undefined; + } + + return trimmed.replace(/(?:\[1m\])+$/, '') || undefined; +} diff --git a/test/renderer/components/team/TeamModelSelector.test.ts b/test/renderer/components/team/TeamModelSelector.test.ts index 87ae7485..5e7f2a47 100644 --- a/test/renderer/components/team/TeamModelSelector.test.ts +++ b/test/renderer/components/team/TeamModelSelector.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector'; +import { + computeEffectiveTeamModel, + formatTeamModelSummary, +} from '@renderer/components/team/dialogs/TeamModelSelector'; import { GPT_5_1_CODEX_MINI_UI_DISABLED_REASON, GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON, @@ -36,3 +39,34 @@ describe('formatTeamModelSummary', () => { expect(normalizeTeamModelForUi('codex', 'gpt-5.4-mini')).toBe('gpt-5.4-mini'); }); }); + +describe('computeEffectiveTeamModel', () => { + it('appends [1m] for anthropic models', () => { + expect(computeEffectiveTeamModel('opus', false, 'anthropic')).toBe('opus[1m]'); + expect(computeEffectiveTeamModel('sonnet', false, 'anthropic')).toBe('sonnet[1m]'); + }); + + it('does not double-append [1m] when input already has it', () => { + expect(computeEffectiveTeamModel('opus[1m]', false, 'anthropic')).toBe('opus[1m]'); + expect(computeEffectiveTeamModel('sonnet[1m]', false, 'anthropic')).toBe('sonnet[1m]'); + expect(computeEffectiveTeamModel('opus[1m][1m]', false, 'anthropic')).toBe('opus[1m]'); + }); + + it('defaults to opus[1m] when no model selected', () => { + expect(computeEffectiveTeamModel('', false, 'anthropic')).toBe('opus[1m]'); + }); + + it('returns base model without [1m] when limitContext is true', () => { + expect(computeEffectiveTeamModel('opus', true, 'anthropic')).toBe('opus'); + expect(computeEffectiveTeamModel('opus[1m]', true, 'anthropic')).toBe('opus'); + expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus'); + }); + + it('returns haiku as-is', () => { + expect(computeEffectiveTeamModel('haiku', false, 'anthropic')).toBe('haiku'); + }); + + it('returns non-anthropic models as-is', () => { + expect(computeEffectiveTeamModel('gpt-5.4', false, 'codex')).toBe('gpt-5.4'); + }); +}); diff --git a/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts b/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts index 2b1d4d58..b91c03f1 100644 --- a/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts +++ b/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts @@ -38,6 +38,7 @@ describe('resolveLaunchDialogPrefill', () => { multimodelEnabled: true, storedProviderId: 'anthropic', storedEffort: 'medium', + storedLimitContext: false, getStoredModel: createStoredModelGetter({ anthropic: 'haiku', codex: 'gpt-5.4', @@ -48,6 +49,7 @@ describe('resolveLaunchDialogPrefill', () => { providerId: 'codex', model: 'gpt-5.4', effort: 'medium', + limitContext: false, }); }); @@ -78,6 +80,7 @@ describe('resolveLaunchDialogPrefill', () => { multimodelEnabled: true, storedProviderId: 'anthropic', storedEffort: 'medium', + storedLimitContext: false, getStoredModel: createStoredModelGetter({ anthropic: 'haiku', codex: 'gpt-5.4', @@ -88,6 +91,7 @@ describe('resolveLaunchDialogPrefill', () => { providerId: 'codex', model: 'gpt-5.4', effort: 'medium', + limitContext: false, }); }); @@ -103,6 +107,7 @@ describe('resolveLaunchDialogPrefill', () => { multimodelEnabled: true, storedProviderId: 'anthropic', storedEffort: 'medium', + storedLimitContext: false, getStoredModel: createStoredModelGetter({ anthropic: 'haiku', codex: 'gpt-5.4', @@ -113,6 +118,7 @@ describe('resolveLaunchDialogPrefill', () => { providerId: 'codex', model: 'gpt-5.3-codex', effort: 'high', + limitContext: false, }); }); @@ -134,6 +140,7 @@ describe('resolveLaunchDialogPrefill', () => { multimodelEnabled: true, storedProviderId: 'anthropic', storedEffort: 'medium', + storedLimitContext: false, getStoredModel: createStoredModelGetter({ anthropic: 'haiku', codex: 'gpt-5.4', @@ -144,6 +151,34 @@ describe('resolveLaunchDialogPrefill', () => { providerId: 'anthropic', model: 'haiku', effort: 'medium', + limitContext: false, + }); + }); + + it('prefers per-team launch params for limitContext over stale global storage', () => { + const result = resolveLaunchDialogPrefill({ + members: [], + savedRequest: null, + previousLaunchParams: { + providerId: 'anthropic', + model: 'opus[1m][1m]', + effort: 'high', + limitContext: true, + }, + multimodelEnabled: true, + storedProviderId: 'anthropic', + storedEffort: 'medium', + storedLimitContext: false, + getStoredModel: createStoredModelGetter({ + anthropic: 'haiku', + }), + }); + + expect(result).toEqual({ + providerId: 'anthropic', + model: 'opus', + effort: 'high', + limitContext: true, }); }); });