Merge remote-tracking branch 'origin/dev' into spike/team-snapshot-split-plan

This commit is contained in:
777genius 2026-04-18 12:29:28 +03:00
commit 42850e5e97
9 changed files with 131 additions and 14 deletions

View file

@ -55,7 +55,7 @@
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const response = await client.messages.create({
model: 'claude-opus-4-6',
model: 'claude-opus-4-7',
messages: [{ role: 'user', content: 'Send message to teammate...' }],
tools: [/* SendMessage, TaskUpdate, etc. */]
});

View file

@ -1,7 +1,7 @@
{
"name": "claude-agent-teams-ui",
"type": "module",
"version": "1.1.0",
"version": "1.3.0",
"description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls",
"license": "AGPL-3.0",
"author": {
@ -303,7 +303,8 @@
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"node-pty"
"node-pty",
"cpu-features"
]
},
"knip": {

View file

@ -27,6 +27,7 @@ import {
getProviderScopedTeamModelLabel,
getTeamModelLabel as getCatalogTeamModelLabel,
getTeamProviderLabel as getCatalogTeamProviderLabel,
isAnthropicHaikuTeamModel,
} from '@renderer/utils/teamModelCatalog';
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
@ -109,7 +110,7 @@ export function computeEffectiveTeamModel(
const base = extractProviderScopedBaseModel(selectedModel, providerId);
if (limitContext) return base || getAnthropicDefaultTeamModel(true);
if (base === 'haiku') return base;
if (isAnthropicHaikuTeamModel(base)) return base;
return base ? `${base}[1m]` : getAnthropicDefaultTeamModel(limitContext);
}
@ -141,7 +142,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId;
const defaultModelTooltip = useMemo(() => {
if (effectiveProviderId === 'anthropic') {
return 'Uses the Claude team default model.\nResolves to Opus 1M, or Opus 200K when Limit context is enabled.';
return 'Uses the Claude team default model.\nResolves to Opus 4.7 with 1M context, or Opus 4.7 with 200K context when Limit context is enabled.';
}
return 'Uses the runtime default for the selected provider.';
}, [effectiveProviderId]);

View file

@ -13,6 +13,7 @@ import { Label } from '@renderer/components/ui/label';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { isAnthropicHaikuTeamModel } from '@renderer/utils/teamModelCatalog';
import { getMemberColorByName } from '@shared/constants/memberColors';
import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react';
@ -151,7 +152,7 @@ export const LeadModelRow = ({
id="lead-limit-context"
checked={limitContext}
onCheckedChange={onLimitContextChange}
disabled={model === 'haiku'}
disabled={isAnthropicHaikuTeamModel(model)}
/>
) : null}
<div className="flex items-start gap-2 rounded-md border border-sky-500/20 bg-sky-500/5 px-3 py-2">

View file

@ -1,5 +1,6 @@
import {
getProviderScopedTeamModelLabel,
isSupportedAnthropicTeamModel,
getRuntimeAwareTeamModelUiDisabledReason,
getTeamProviderLabel,
getTeamProviderModelOptions,
@ -231,7 +232,7 @@ export function isTeamModelAvailableForUi(
}
if (providerId === 'anthropic') {
return getFallbackTeamProviderModels(providerId).includes(trimmed);
return isSupportedAnthropicTeamModel(trimmed);
}
return getRuntimeModelAvailability(providerId, trimmed, providerStatus) === 'available';

View file

@ -4,6 +4,7 @@ import {
GPT_5_2_CODEX_UI_DISABLED_MODEL,
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
} from '@shared/utils/providerModelVisibility';
import { parseModelString } from '@shared/utils/modelParser';
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
@ -39,15 +40,22 @@ const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
gemini: 'Gemini',
};
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
default: 'Default',
opus: 'Opus 4.6',
const ANTHROPIC_ALIAS_LABELS = {
opus: 'Opus 4.7',
sonnet: 'Sonnet 4.6',
haiku: 'Haiku 4.5',
} as const;
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
default: 'Default',
...ANTHROPIC_ALIAS_LABELS,
'claude-opus-4-7': 'Opus 4.7',
'claude-opus-4-7[1m]': 'Opus 4.7 (1M)',
'claude-sonnet-4-6': 'Sonnet 4.6',
'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)',
'claude-opus-4-6': 'Opus 4.6',
'claude-opus-4-6[1m]': 'Opus 4.6 (1M)',
'claude-haiku-4-5': 'Haiku 4.5',
'claude-haiku-4-5-20251001': 'Haiku 4.5',
'gpt-5.4': 'GPT-5.4',
'gpt-5.4-mini': 'GPT-5.4 Mini',
@ -66,7 +74,7 @@ const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProv
{
anthropic: [
{ value: '', label: 'Default', badgeLabel: 'Default' },
{ value: 'opus', label: 'Opus 4.6', badgeLabel: 'Opus 4.6' },
{ value: 'opus', label: 'Opus 4.7', badgeLabel: 'Opus 4.7' },
{ value: 'sonnet', label: 'Sonnet 4.6', badgeLabel: 'Sonnet 4.6' },
{ value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5' },
],
@ -133,6 +141,73 @@ export function getTeamProviderModelOptions(
return TEAM_PROVIDER_MODEL_OPTIONS[providerId];
}
function splitOneMillionContextSuffix(model: string): {
baseModel: string;
hasOneMillion: boolean;
} {
const hasOneMillion = /\[1m\]$/i.test(model);
return {
baseModel: model.replace(/\[1m\]$/i, ''),
hasOneMillion,
};
}
function formatParsedClaudeModelLabel(model: string): string | null {
const trimmed = model.trim();
if (!trimmed) {
return null;
}
const { baseModel, hasOneMillion } = splitOneMillionContextSuffix(trimmed);
const parsedModel = parseModelString(baseModel);
if (!parsedModel) {
return null;
}
const familyLabel = parsedModel.family.charAt(0).toUpperCase() + parsedModel.family.slice(1);
const versionLabel =
parsedModel.minorVersion == null
? `${parsedModel.majorVersion}`
: `${parsedModel.majorVersion}.${parsedModel.minorVersion}`;
return `${familyLabel} ${versionLabel}${hasOneMillion ? ' (1M)' : ''}`;
}
const SUPPORTED_ANTHROPIC_TEAM_MODELS = new Set<string>([
'opus',
'opus[1m]',
'sonnet',
'sonnet[1m]',
'haiku',
'claude-opus-4-7',
'claude-opus-4-7[1m]',
'claude-opus-4-6',
'claude-opus-4-6[1m]',
'claude-sonnet-4-6',
'claude-sonnet-4-6[1m]',
'claude-haiku-4-5',
'claude-haiku-4-5-20251001',
]);
export function isSupportedAnthropicTeamModel(model: string | undefined): boolean {
const trimmed = model?.trim();
if (!trimmed) {
return false;
}
return SUPPORTED_ANTHROPIC_TEAM_MODELS.has(trimmed);
}
export function isAnthropicHaikuTeamModel(model: string | undefined): boolean {
const trimmed = model?.trim();
if (!trimmed) {
return false;
}
const { baseModel } = splitOneMillionContextSuffix(trimmed);
return baseModel === 'haiku' || baseModel.startsWith('claude-haiku-');
}
export function getTeamProviderLabel(
providerId: SupportedProviderId | undefined
): string | undefined {
@ -147,7 +222,13 @@ export function getTeamModelLabel(model: string | undefined): string | undefined
if (!trimmed) {
return undefined;
}
return TEAM_MODEL_LABEL_OVERRIDES[trimmed] ?? trimmed;
const overrideLabel = TEAM_MODEL_LABEL_OVERRIDES[trimmed];
if (overrideLabel) {
return overrideLabel;
}
return formatParsedClaudeModelLabel(trimmed) ?? trimmed;
}
export function getTeamModelBadgeLabel(
@ -165,6 +246,10 @@ export function getTeamModelBadgeLabel(
}
if (providerId === 'anthropic') {
const anthropicLabel = getTeamModelLabel(trimmed);
if (anthropicLabel && anthropicLabel !== trimmed) {
return anthropicLabel;
}
return trimmed.replace(/^claude-/, '');
}
if (providerId === 'codex') {

View file

@ -22,6 +22,15 @@ describe('formatTeamModelSummary', () => {
);
});
it('formats current Anthropic Opus model ids with the latest 4.7 label', () => {
expect(formatTeamModelSummary('anthropic', 'claude-opus-4-7', 'high')).toBe(
'Anthropic · Opus 4.7 · High'
);
expect(formatTeamModelSummary('codex', 'claude-opus-4-7', 'medium')).toBe(
'Opus 4.7 · via Codex · Medium'
);
});
it('keeps native Codex-family models branded normally', () => {
expect(formatTeamModelSummary('codex', 'gpt-5.4', 'medium')).toBe('5.4 · Medium');
});
@ -106,6 +115,7 @@ describe('formatTeamModelSummary', () => {
expect(getTeamModelSelectionError('codex', 'gpt-5.4')).toContain('waiting for Codex runtime verification');
expect(getTeamModelSelectionError('codex', '')).toBeNull();
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
});
});
@ -130,10 +140,16 @@ describe('computeEffectiveTeamModel', () => {
expect(computeEffectiveTeamModel('opus[1m]', true, 'anthropic')).toBe('opus');
expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus');
expect(computeEffectiveTeamModel('', true, 'anthropic')).toBe('opus');
expect(computeEffectiveTeamModel('claude-opus-4-7[1m]', true, 'anthropic')).toBe(
'claude-opus-4-7'
);
});
it('returns haiku as-is', () => {
expect(computeEffectiveTeamModel('haiku', false, 'anthropic')).toBe('haiku');
expect(computeEffectiveTeamModel('claude-haiku-4-5-20251001', false, 'anthropic')).toBe(
'claude-haiku-4-5-20251001'
);
});
it('returns non-anthropic models as-is', () => {

View file

@ -38,10 +38,10 @@ function createSpawnEntry(overrides: Partial<MemberSpawnStatusEntry> = {}): Memb
describe('resolveMemberRuntimeSummary', () => {
it('shows the live runtime model for loading members when available', () => {
const member = createMember();
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-6', runtimeAlive: true });
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-7', runtimeAlive: true });
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
'Anthropic · Opus 4.6 · Medium'
'Anthropic · Opus 4.7 · Medium'
);
});

View file

@ -116,4 +116,16 @@ describe('teamModelAvailability', () => {
expect(normalizeTeamModelForUi('anthropic', 'opus')).toBe('opus');
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
});
it('keeps known Anthropic full model ids selectable without runtime verification', () => {
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7')).toBe('claude-opus-4-7');
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7[1m]')).toBe(
'claude-opus-4-7[1m]'
);
expect(normalizeTeamModelForUi('anthropic', 'claude-haiku-4-5-20251001')).toBe(
'claude-haiku-4-5-20251001'
);
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
expect(getTeamModelSelectionError('anthropic', 'claude-haiku-4-5-20251001')).toBeNull();
});
});