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

View file

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

View file

@ -27,6 +27,7 @@ import {
getProviderScopedTeamModelLabel, getProviderScopedTeamModelLabel,
getTeamModelLabel as getCatalogTeamModelLabel, getTeamModelLabel as getCatalogTeamModelLabel,
getTeamProviderLabel as getCatalogTeamProviderLabel, getTeamProviderLabel as getCatalogTeamProviderLabel,
isAnthropicHaikuTeamModel,
} from '@renderer/utils/teamModelCatalog'; } from '@renderer/utils/teamModelCatalog';
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults'; import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
@ -109,7 +110,7 @@ export function computeEffectiveTeamModel(
const base = extractProviderScopedBaseModel(selectedModel, providerId); const base = extractProviderScopedBaseModel(selectedModel, providerId);
if (limitContext) return base || getAnthropicDefaultTeamModel(true); if (limitContext) return base || getAnthropicDefaultTeamModel(true);
if (base === 'haiku') return base; if (isAnthropicHaikuTeamModel(base)) return base;
return base ? `${base}[1m]` : getAnthropicDefaultTeamModel(limitContext); return base ? `${base}[1m]` : getAnthropicDefaultTeamModel(limitContext);
} }
@ -141,7 +142,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId; disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId;
const defaultModelTooltip = useMemo(() => { const defaultModelTooltip = useMemo(() => {
if (effectiveProviderId === 'anthropic') { 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.'; return 'Uses the runtime default for the selected provider.';
}, [effectiveProviderId]); }, [effectiveProviderId]);

View file

@ -13,6 +13,7 @@ import { Label } from '@renderer/components/ui/label';
import { getTeamColorSet } from '@renderer/constants/teamColors'; import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme'; import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils'; import { cn } from '@renderer/lib/utils';
import { isAnthropicHaikuTeamModel } from '@renderer/utils/teamModelCatalog';
import { getMemberColorByName } from '@shared/constants/memberColors'; import { getMemberColorByName } from '@shared/constants/memberColors';
import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react'; import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react';
@ -151,7 +152,7 @@ export const LeadModelRow = ({
id="lead-limit-context" id="lead-limit-context"
checked={limitContext} checked={limitContext}
onCheckedChange={onLimitContextChange} onCheckedChange={onLimitContextChange}
disabled={model === 'haiku'} disabled={isAnthropicHaikuTeamModel(model)}
/> />
) : null} ) : null}
<div className="flex items-start gap-2 rounded-md border border-sky-500/20 bg-sky-500/5 px-3 py-2"> <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 { import {
getProviderScopedTeamModelLabel, getProviderScopedTeamModelLabel,
isSupportedAnthropicTeamModel,
getRuntimeAwareTeamModelUiDisabledReason, getRuntimeAwareTeamModelUiDisabledReason,
getTeamProviderLabel, getTeamProviderLabel,
getTeamProviderModelOptions, getTeamProviderModelOptions,
@ -231,7 +232,7 @@ export function isTeamModelAvailableForUi(
} }
if (providerId === 'anthropic') { if (providerId === 'anthropic') {
return getFallbackTeamProviderModels(providerId).includes(trimmed); return isSupportedAnthropicTeamModel(trimmed);
} }
return getRuntimeModelAvailability(providerId, trimmed, providerStatus) === 'available'; return getRuntimeModelAvailability(providerId, trimmed, providerStatus) === 'available';

View file

@ -4,6 +4,7 @@ import {
GPT_5_2_CODEX_UI_DISABLED_MODEL, GPT_5_2_CODEX_UI_DISABLED_MODEL,
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL, GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
} from '@shared/utils/providerModelVisibility'; } from '@shared/utils/providerModelVisibility';
import { parseModelString } from '@shared/utils/modelParser';
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types'; import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
@ -39,15 +40,22 @@ const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
gemini: 'Gemini', gemini: 'Gemini',
}; };
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = { const ANTHROPIC_ALIAS_LABELS = {
default: 'Default', opus: 'Opus 4.7',
opus: 'Opus 4.6',
sonnet: 'Sonnet 4.6', sonnet: 'Sonnet 4.6',
haiku: 'Haiku 4.5', 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': 'Sonnet 4.6',
'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)', 'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)',
'claude-opus-4-6': 'Opus 4.6', 'claude-opus-4-6': 'Opus 4.6',
'claude-opus-4-6[1m]': 'Opus 4.6 (1M)', 'claude-opus-4-6[1m]': 'Opus 4.6 (1M)',
'claude-haiku-4-5': 'Haiku 4.5',
'claude-haiku-4-5-20251001': 'Haiku 4.5', 'claude-haiku-4-5-20251001': 'Haiku 4.5',
'gpt-5.4': 'GPT-5.4', 'gpt-5.4': 'GPT-5.4',
'gpt-5.4-mini': 'GPT-5.4 Mini', 'gpt-5.4-mini': 'GPT-5.4 Mini',
@ -66,7 +74,7 @@ const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProv
{ {
anthropic: [ anthropic: [
{ value: '', label: 'Default', badgeLabel: 'Default' }, { 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: 'sonnet', label: 'Sonnet 4.6', badgeLabel: 'Sonnet 4.6' },
{ value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5' }, { value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5' },
], ],
@ -133,6 +141,73 @@ export function getTeamProviderModelOptions(
return TEAM_PROVIDER_MODEL_OPTIONS[providerId]; 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( export function getTeamProviderLabel(
providerId: SupportedProviderId | undefined providerId: SupportedProviderId | undefined
): string | undefined { ): string | undefined {
@ -147,7 +222,13 @@ export function getTeamModelLabel(model: string | undefined): string | undefined
if (!trimmed) { if (!trimmed) {
return undefined; 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( export function getTeamModelBadgeLabel(
@ -165,6 +246,10 @@ export function getTeamModelBadgeLabel(
} }
if (providerId === 'anthropic') { if (providerId === 'anthropic') {
const anthropicLabel = getTeamModelLabel(trimmed);
if (anthropicLabel && anthropicLabel !== trimmed) {
return anthropicLabel;
}
return trimmed.replace(/^claude-/, ''); return trimmed.replace(/^claude-/, '');
} }
if (providerId === 'codex') { 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', () => { it('keeps native Codex-family models branded normally', () => {
expect(formatTeamModelSummary('codex', 'gpt-5.4', 'medium')).toBe('5.4 · Medium'); 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', 'gpt-5.4')).toContain('waiting for Codex runtime verification');
expect(getTeamModelSelectionError('codex', '')).toBeNull(); expect(getTeamModelSelectionError('codex', '')).toBeNull();
expect(getTeamModelSelectionError('anthropic', 'opus')).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]', true, 'anthropic')).toBe('opus');
expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus'); expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus');
expect(computeEffectiveTeamModel('', 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', () => { it('returns haiku as-is', () => {
expect(computeEffectiveTeamModel('haiku', false, 'anthropic')).toBe('haiku'); 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', () => { it('returns non-anthropic models as-is', () => {

View file

@ -38,10 +38,10 @@ function createSpawnEntry(overrides: Partial<MemberSpawnStatusEntry> = {}): Memb
describe('resolveMemberRuntimeSummary', () => { describe('resolveMemberRuntimeSummary', () => {
it('shows the live runtime model for loading members when available', () => { it('shows the live runtime model for loading members when available', () => {
const member = createMember(); 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( 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(normalizeTeamModelForUi('anthropic', 'opus')).toBe('opus');
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull(); 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();
});
}); });