fix(team): refine provider diagnostics and model UI
This commit is contained in:
parent
4e3183d186
commit
96b9eab346
13 changed files with 327 additions and 31 deletions
|
|
@ -1041,6 +1041,7 @@ interface AuthStatusCommandResponse {
|
||||||
interface RuntimeProviderLaunchFacts {
|
interface RuntimeProviderLaunchFacts {
|
||||||
defaultModel: string | null;
|
defaultModel: string | null;
|
||||||
modelIds: Set<string>;
|
modelIds: Set<string>;
|
||||||
|
modelListParsed?: boolean;
|
||||||
modelCatalog: CliProviderModelCatalog | null;
|
modelCatalog: CliProviderModelCatalog | null;
|
||||||
runtimeCapabilities: CliProviderRuntimeCapabilities | null;
|
runtimeCapabilities: CliProviderRuntimeCapabilities | null;
|
||||||
providerStatus?:
|
providerStatus?:
|
||||||
|
|
@ -1141,6 +1142,20 @@ function isCodexEffortRuntimeSupported(
|
||||||
return reasoning?.configPassthrough === true && reasoning.values.includes(effort);
|
return reasoning?.configPassthrough === true && reasoning.values.includes(effort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasAuthoritativeCodexLaunchCatalog(
|
||||||
|
facts: Pick<
|
||||||
|
RuntimeProviderLaunchFacts,
|
||||||
|
'modelIds' | 'modelListParsed' | 'modelCatalog' | 'runtimeCapabilities'
|
||||||
|
>
|
||||||
|
): boolean {
|
||||||
|
if (facts.modelIds.size > 0 || facts.modelCatalog != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
facts.modelListParsed === true && facts.runtimeCapabilities?.modelCatalog?.dynamic === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getAnthropicFastModeDefault(): boolean {
|
function getAnthropicFastModeDefault(): boolean {
|
||||||
return (
|
return (
|
||||||
ConfigManager.getInstance().getConfig().providerConnections.anthropic.fastModeDefault === true
|
ConfigManager.getInstance().getConfig().providerConnections.anthropic.fastModeDefault === true
|
||||||
|
|
@ -6117,11 +6132,13 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
let defaultModel: string | null = null;
|
let defaultModel: string | null = null;
|
||||||
let modelIds = new Set<string>();
|
let modelIds = new Set<string>();
|
||||||
|
let modelListParsed = false;
|
||||||
if (modelListResult.status === 'fulfilled') {
|
if (modelListResult.status === 'fulfilled') {
|
||||||
try {
|
try {
|
||||||
const parsed = extractJsonObjectFromCli<ProviderModelListCommandResponse>(
|
const parsed = extractJsonObjectFromCli<ProviderModelListCommandResponse>(
|
||||||
modelListResult.value.stdout
|
modelListResult.value.stdout
|
||||||
);
|
);
|
||||||
|
modelListParsed = true;
|
||||||
const provider = parsed.providers?.[params.providerId];
|
const provider = parsed.providers?.[params.providerId];
|
||||||
defaultModel =
|
defaultModel =
|
||||||
typeof provider?.defaultModel === 'string' && provider.defaultModel.trim().length > 0
|
typeof provider?.defaultModel === 'string' && provider.defaultModel.trim().length > 0
|
||||||
|
|
@ -6202,6 +6219,7 @@ export class TeamProvisioningService {
|
||||||
})
|
})
|
||||||
: defaultModel,
|
: defaultModel,
|
||||||
modelIds,
|
modelIds,
|
||||||
|
modelListParsed,
|
||||||
modelCatalog,
|
modelCatalog,
|
||||||
runtimeCapabilities,
|
runtimeCapabilities,
|
||||||
providerStatus,
|
providerStatus,
|
||||||
|
|
@ -6400,6 +6418,10 @@ export class TeamProvisioningService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasAuthoritativeCodexLaunchCatalog(params.facts)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${params.actorLabel} uses Codex model "${explicitModel}", but this Agent Teams runtime does not declare dynamic Codex model launch support yet. Upgrade the runtime or pick a listed Codex model.`
|
`${params.actorLabel} uses Codex model "${explicitModel}", but this Agent Teams runtime does not declare dynamic Codex model launch support yet. Upgrade the runtime or pick a listed Codex model.`
|
||||||
);
|
);
|
||||||
|
|
@ -17152,9 +17174,18 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
const dynamicCatalog = params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === true;
|
const dynamicCatalog = params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === true;
|
||||||
const hasAuthoritativeCatalog =
|
const hasAuthoritativeCatalog =
|
||||||
availableModels.size > 0 ||
|
params.providerId === 'codex'
|
||||||
params.runtimeFacts.modelCatalog != null ||
|
? hasAuthoritativeCodexLaunchCatalog(params.runtimeFacts)
|
||||||
params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === false;
|
: availableModels.size > 0 ||
|
||||||
|
params.runtimeFacts.modelCatalog != null ||
|
||||||
|
params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === false;
|
||||||
|
|
||||||
|
if (params.providerId === 'codex' && (dynamicCatalog || !hasAuthoritativeCatalog)) {
|
||||||
|
return {
|
||||||
|
kind: 'available',
|
||||||
|
resolvedModelId: trimmedModelId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (dynamicCatalog || !hasAuthoritativeCatalog) {
|
if (dynamicCatalog || !hasAuthoritativeCatalog) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,9 @@ export function getProvisioningProviderBackendSummary(
|
||||||
suffixes.push('runtime missing');
|
suffixes.push('runtime missing');
|
||||||
break;
|
break;
|
||||||
case 'degraded':
|
case 'degraded':
|
||||||
suffixes.push('degraded');
|
if (inferredProviderId !== 'codex') {
|
||||||
|
suffixes.push('degraded');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,7 @@ function isSuppressibleGenericPreflightWarning(detail: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
lower.includes('one-shot diagnostic') ||
|
||||||
lower.includes('preflight check failed') ||
|
lower.includes('preflight check failed') ||
|
||||||
(lower.includes('preflight check for `') && lower.includes('-p` did not complete')) ||
|
(lower.includes('preflight check for `') && lower.includes('-p` did not complete')) ||
|
||||||
lower.includes('preflight ping completed but did not return the expected pong')
|
lower.includes('preflight ping completed but did not return the expected pong')
|
||||||
|
|
@ -413,6 +414,13 @@ function resolveModelResultFromBatch(
|
||||||
/selected model .* is compatible\. deep verification pending\./i.test(entry)
|
/selected model .* is compatible\. deep verification pending\./i.test(entry)
|
||||||
);
|
);
|
||||||
if (hasCompatibilityLine) {
|
if (hasCompatibilityLine) {
|
||||||
|
if (providerId !== 'opencode') {
|
||||||
|
return {
|
||||||
|
status: 'ready',
|
||||||
|
line: buildModelAvailableLine(providerId, modelId),
|
||||||
|
warningLine: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: 'notes',
|
status: 'notes',
|
||||||
line: buildModelCompatibilityPendingLine(providerId, modelId),
|
line: buildModelCompatibilityPendingLine(providerId, modelId),
|
||||||
|
|
@ -452,6 +460,13 @@ function resolveModelResultFromBatch(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.ready && (result.warnings?.length ?? 0) > 0 && !hasModelScopedEntries) {
|
if (result.ready && (result.warnings?.length ?? 0) > 0 && !hasModelScopedEntries) {
|
||||||
|
if (providerId !== 'opencode') {
|
||||||
|
return {
|
||||||
|
status: 'ready',
|
||||||
|
line: buildModelAvailableLine(providerId, modelId),
|
||||||
|
warningLine: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: 'notes',
|
status: 'notes',
|
||||||
line: buildModelCompatibilityPendingLine(providerId, modelId),
|
line: buildModelCompatibilityPendingLine(providerId, modelId),
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ vi.mock('@renderer/hooks/useTheme', () => ({
|
||||||
|
|
||||||
vi.mock('@renderer/utils/teamModelCatalog', () => ({
|
vi.mock('@renderer/utils/teamModelCatalog', () => ({
|
||||||
isAnthropicHaikuTeamModel: () => false,
|
isAnthropicHaikuTeamModel: () => false,
|
||||||
|
isAnthropicSonnetTeamModel: (model: string | undefined) =>
|
||||||
|
model === 'sonnet' || model === 'claude-sonnet-4-6' || model === 'sonnet[1m]',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../ui/button', () => ({
|
vi.mock('../../ui/button', () => ({
|
||||||
|
|
@ -78,9 +80,12 @@ vi.mock('../../ui/button', () => ({
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { LeadModelRow } from './LeadModelRow';
|
import { ANTHROPIC_LONG_CONTEXT_PRICING_URL, LeadModelRow } from './LeadModelRow';
|
||||||
|
|
||||||
function renderLeadModelRow(): { host: HTMLDivElement; root: ReturnType<typeof createRoot> } {
|
function renderLeadModelRow(overrides: Partial<React.ComponentProps<typeof LeadModelRow>> = {}): {
|
||||||
|
host: HTMLDivElement;
|
||||||
|
root: ReturnType<typeof createRoot>;
|
||||||
|
} {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
const root = createRoot(host);
|
const root = createRoot(host);
|
||||||
|
|
@ -98,6 +103,7 @@ function renderLeadModelRow(): { host: HTMLDivElement; root: ReturnType<typeof c
|
||||||
onLimitContextChange: () => undefined,
|
onLimitContextChange: () => undefined,
|
||||||
syncModelsWithTeammates: true,
|
syncModelsWithTeammates: true,
|
||||||
onSyncModelsWithTeammatesChange: () => undefined,
|
onSyncModelsWithTeammatesChange: () => undefined,
|
||||||
|
...overrides,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -128,4 +134,38 @@ describe('LeadModelRow', () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('warns that unchecked 200K limit can put Sonnet on Anthropic Extra Usage', () => {
|
||||||
|
const { host, root } = renderLeadModelRow({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'sonnet',
|
||||||
|
limitContext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Sonnet with 1M context can use Anthropic Extra Usage');
|
||||||
|
expect(host.textContent).toContain('Requests over 200K input tokens');
|
||||||
|
const docsLink = host.querySelector(`a[href="${ANTHROPIC_LONG_CONTEXT_PRICING_URL}"]`);
|
||||||
|
|
||||||
|
expect(docsLink?.textContent).toContain('Anthropic pricing docs');
|
||||||
|
expect(docsLink?.getAttribute('target')).toBe('_blank');
|
||||||
|
expect(docsLink?.getAttribute('rel')).toBe('noreferrer');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show the Sonnet Extra Usage warning when 200K limit is enabled', () => {
|
||||||
|
const { host, root } = renderLeadModelRow({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'sonnet',
|
||||||
|
limitContext: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).not.toContain('Anthropic Extra Usage');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ 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 { agentAvatarUrl } from '@renderer/utils/memberHelpers';
|
import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
|
||||||
import { isAnthropicHaikuTeamModel } from '@renderer/utils/teamModelCatalog';
|
import {
|
||||||
|
isAnthropicHaikuTeamModel,
|
||||||
|
isAnthropicSonnetTeamModel,
|
||||||
|
} from '@renderer/utils/teamModelCatalog';
|
||||||
import { resolveTeamLeadColorName } from '@shared/utils/teamMemberColors';
|
import { resolveTeamLeadColorName } from '@shared/utils/teamMemberColors';
|
||||||
import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react';
|
import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -22,6 +25,11 @@ import { Button } from '../../ui/button';
|
||||||
|
|
||||||
import type { EffortLevel, TeamProviderId } from '@shared/types';
|
import type { EffortLevel, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
|
export const ANTHROPIC_SONNET_EXTRA_USAGE_WARNING =
|
||||||
|
'Sonnet with 1M context can use Anthropic Extra Usage. Requests over 200K input tokens are billed at premium long-context rates; enable Limit context to 200K tokens to avoid that billing path.';
|
||||||
|
export const ANTHROPIC_LONG_CONTEXT_PRICING_URL =
|
||||||
|
'https://platform.claude.com/docs/en/about-claude/pricing';
|
||||||
|
|
||||||
interface LeadModelRowProps {
|
interface LeadModelRowProps {
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
model: string;
|
model: string;
|
||||||
|
|
@ -61,6 +69,12 @@ export const LeadModelRow = ({
|
||||||
: 'Default';
|
: 'Default';
|
||||||
const modelButtonAriaLabel = `${getTeamProviderLabel(providerId)} provider, ${modelButtonLabel}`;
|
const modelButtonAriaLabel = `${getTeamProviderLabel(providerId)} provider, ${modelButtonLabel}`;
|
||||||
const hasModelIssue = Boolean(modelIssueText);
|
const hasModelIssue = Boolean(modelIssueText);
|
||||||
|
const showSonnetExtraUsageWarning =
|
||||||
|
providerId === 'anthropic' && !limitContext && isAnthropicSonnetTeamModel(model);
|
||||||
|
const warningMessages = [warningText?.trim() || null].filter((message): message is string =>
|
||||||
|
Boolean(message)
|
||||||
|
);
|
||||||
|
const hasWarnings = warningMessages.length > 0 || showSonnetExtraUsageWarning;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -132,11 +146,29 @@ export const LeadModelRow = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{warningText ? (
|
{hasWarnings ? (
|
||||||
<div className="md:col-span-3">
|
<div className="md:col-span-3">
|
||||||
<div className="bg-amber-500/8 ml-3 flex items-start gap-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
|
<div className="bg-amber-500/8 ml-3 flex items-start gap-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
|
||||||
<Info className="mt-0.5 size-3.5 shrink-0 text-amber-300" />
|
<Info className="mt-0.5 size-3.5 shrink-0 text-amber-300" />
|
||||||
<p>{warningText}</p>
|
<div className="space-y-1">
|
||||||
|
{warningMessages.map((message) => (
|
||||||
|
<p key={message}>{message}</p>
|
||||||
|
))}
|
||||||
|
{showSonnetExtraUsageWarning ? (
|
||||||
|
<p>
|
||||||
|
{ANTHROPIC_SONNET_EXTRA_USAGE_WARNING}{' '}
|
||||||
|
<a
|
||||||
|
href={ANTHROPIC_LONG_CONTEXT_PRICING_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="font-medium text-amber-100 underline underline-offset-2 hover:text-white"
|
||||||
|
>
|
||||||
|
Read Anthropic pricing docs
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -250,10 +250,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
#splash-status {
|
#splash-status {
|
||||||
min-height: 16px;
|
min-height: 8px;
|
||||||
font-family:
|
font-family:
|
||||||
ui-sans-serif,
|
ui-sans-serif,
|
||||||
system-ui,
|
system-ui,
|
||||||
|
|
@ -261,21 +261,21 @@
|
||||||
BlinkMacSystemFont,
|
BlinkMacSystemFont,
|
||||||
'Segoe UI',
|
'Segoe UI',
|
||||||
sans-serif;
|
sans-serif;
|
||||||
font-size: 12px;
|
font-size: 8px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(212, 212, 216, 0.72);
|
color: rgba(212, 212, 216, 0.72);
|
||||||
line-height: 1.35;
|
line-height: 1.25;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
#splash-elapsed::before {
|
#splash-elapsed::before {
|
||||||
content: '·';
|
content: '·';
|
||||||
margin-right: 6px;
|
margin-right: 4px;
|
||||||
color: rgba(212, 212, 216, 0.34);
|
color: rgba(212, 212, 216, 0.34);
|
||||||
}
|
}
|
||||||
#splash-elapsed {
|
#splash-elapsed {
|
||||||
font-family:
|
font-family:
|
||||||
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||||
font-size: 11px;
|
font-size: 8px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(212, 212, 216, 0.5);
|
color: rgba(212, 212, 216, 0.5);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -295,14 +295,18 @@
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
#splash-timeline {
|
#splash-timeline {
|
||||||
display: flex;
|
display: none;
|
||||||
width: min(320px, 78vw);
|
width: min(320px, 78vw);
|
||||||
max-height: 128px;
|
max-height: 58px;
|
||||||
margin-top: 14px;
|
margin-top: 8px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
#splash.splash-status-error #splash-timeline,
|
||||||
|
#splash.splash-status-slow #splash-timeline {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.splash-step {
|
.splash-step {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 8px minmax(0, 1fr) auto;
|
grid-template-columns: 8px minmax(0, 1fr) auto;
|
||||||
|
|
@ -494,6 +498,18 @@
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
window.__claudeTeamsSplashStartedAt = performance.now();
|
window.__claudeTeamsSplashStartedAt = performance.now();
|
||||||
|
window.__claudeTeamsSplashStaticTimer = window.setInterval(function () {
|
||||||
|
var elapsed = document.getElementById('splash-elapsed');
|
||||||
|
var splash = document.getElementById('splash');
|
||||||
|
if (!elapsed || !splash) {
|
||||||
|
window.clearInterval(window.__claudeTeamsSplashStaticTimer);
|
||||||
|
window.__claudeTeamsSplashStaticTimer = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var startedAt = window.__claudeTeamsSplashStartedAt || performance.now();
|
||||||
|
var seconds = Math.max(0, Math.floor((performance.now() - startedAt) / 1000));
|
||||||
|
elapsed.textContent = seconds + 's';
|
||||||
|
}, 1000);
|
||||||
var theme =
|
var theme =
|
||||||
localStorage.getItem('agent-teams-theme-cache') ||
|
localStorage.getItem('agent-teams-theme-cache') ||
|
||||||
localStorage.getItem('claude-devtools-theme-cache');
|
localStorage.getItem('claude-devtools-theme-cache');
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import type { AppStartupStatus, AppStartupStep } from '@shared/types/api';
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__claudeTeamsUiDidInit?: boolean;
|
__claudeTeamsUiDidInit?: boolean;
|
||||||
|
__claudeTeamsSplashStaticTimer?: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ let startupTicker: number | undefined;
|
||||||
|
|
||||||
const SLOW_STEP_MS = 7_000;
|
const SLOW_STEP_MS = 7_000;
|
||||||
const VERY_SLOW_STEP_MS = 14_000;
|
const VERY_SLOW_STEP_MS = 14_000;
|
||||||
const TIMELINE_STEP_LIMIT = 6;
|
const TIMELINE_STEP_LIMIT = 3;
|
||||||
|
|
||||||
function getStartupErrorText(status: AppStartupStatus): string {
|
function getStartupErrorText(status: AppStartupStatus): string {
|
||||||
return status.error ? `Startup failed: ${status.error}` : 'Startup failed. Please restart.';
|
return status.error ? `Startup failed: ${status.error}` : 'Startup failed. Please restart.';
|
||||||
|
|
@ -142,6 +143,12 @@ function updateStartupSplash(status: AppStartupStatus): void {
|
||||||
renderStartupTimeline(status);
|
renderStartupTimeline(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopStaticSplashTimer(): void {
|
||||||
|
if (window.__claudeTeamsSplashStaticTimer === undefined) return;
|
||||||
|
window.clearInterval(window.__claudeTeamsSplashStaticTimer);
|
||||||
|
window.__claudeTeamsSplashStaticTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function startStartupTicker(): void {
|
function startStartupTicker(): void {
|
||||||
if (startupTicker !== undefined) return;
|
if (startupTicker !== undefined) return;
|
||||||
startupTicker = window.setInterval(() => {
|
startupTicker = window.setInterval(() => {
|
||||||
|
|
@ -194,13 +201,16 @@ async function bootstrapRenderer(): Promise<void> {
|
||||||
if (nextStatus.ready) {
|
if (nextStatus.ready) {
|
||||||
finished = true;
|
finished = true;
|
||||||
cleanup();
|
cleanup();
|
||||||
|
stopStaticSplashTimer();
|
||||||
stopStartupTicker();
|
stopStartupTicker();
|
||||||
mountApp();
|
mountApp();
|
||||||
} else if (nextStatus.error) {
|
} else if (nextStatus.error) {
|
||||||
finished = true;
|
finished = true;
|
||||||
cleanup();
|
cleanup();
|
||||||
|
stopStaticSplashTimer();
|
||||||
stopStartupTicker();
|
stopStartupTicker();
|
||||||
} else {
|
} else {
|
||||||
|
stopStaticSplashTimer();
|
||||||
startStartupTicker();
|
startStartupTicker();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -210,6 +220,7 @@ async function bootstrapRenderer(): Promise<void> {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[startup] status bridge unavailable: ${String(error)}`);
|
console.warn(`[startup] status bridge unavailable: ${String(error)}`);
|
||||||
cleanup();
|
cleanup();
|
||||||
|
stopStaticSplashTimer();
|
||||||
stopStartupTicker();
|
stopStartupTicker();
|
||||||
mountApp();
|
mountApp();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,16 @@ export function isAnthropicHaikuTeamModel(model: string | undefined): boolean {
|
||||||
return baseModel === 'haiku' || baseModel.startsWith('claude-haiku-');
|
return baseModel === 'haiku' || baseModel.startsWith('claude-haiku-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAnthropicSonnetTeamModel(model: string | undefined): boolean {
|
||||||
|
const trimmed = model?.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { baseModel } = splitOneMillionContextSuffix(trimmed);
|
||||||
|
return baseModel === 'sonnet' || baseModel.startsWith('claude-sonnet-');
|
||||||
|
}
|
||||||
|
|
||||||
export function getTeamProviderLabel(
|
export function getTeamProviderLabel(
|
||||||
providerId: SupportedProviderId | undefined
|
providerId: SupportedProviderId | undefined
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
|
|
||||||
|
|
@ -2005,7 +2005,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps missing models compatible when the runtime catalog is dynamic', async () => {
|
it('treats missing Codex models as launchable when the runtime catalog is dynamic', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -2033,13 +2033,77 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
details: ['Selected model future-model is compatible. Deep verification pending.'],
|
details: ['Selected model future-model is available for launch.'],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
blockingMessages: [],
|
blockingMessages: [],
|
||||||
});
|
});
|
||||||
expect(spawnProbe).not.toHaveBeenCalled();
|
expect(spawnProbe).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats explicit Codex models as launchable when the runtime model list is unparsable', async () => {
|
||||||
|
execCliMock.mockImplementation(async (_binaryPath: string | null, args: string[]) => {
|
||||||
|
if (args[0] === 'model' && args[1] === 'list' && args.includes('codex')) {
|
||||||
|
return {
|
||||||
|
stdout: 'Codex model list is temporarily unavailable',
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (args[0] === 'runtime' && args[1] === 'status' && args.includes('codex')) {
|
||||||
|
return {
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
providers: {
|
||||||
|
codex: {
|
||||||
|
runtimeCapabilities: {
|
||||||
|
modelCatalog: { dynamic: false, source: 'runtime' },
|
||||||
|
reasoningEffort: {
|
||||||
|
supported: true,
|
||||||
|
values: ['low', 'medium', 'high'],
|
||||||
|
configPassthrough: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return defaultExecCliMockImplementation(_binaryPath, args);
|
||||||
|
});
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
vi.spyOn(svc as any, 'getCachedOrProbeResult').mockResolvedValue({
|
||||||
|
claudePath: '/fake/claude',
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
});
|
||||||
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||||
|
env: {
|
||||||
|
PATH: '/usr/bin',
|
||||||
|
SHELL: '/bin/zsh',
|
||||||
|
},
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
});
|
||||||
|
const spawnProbe = vi.spyOn(svc as any, 'spawnProbe');
|
||||||
|
|
||||||
|
const result = await svc.prepareForProvisioning(tempRoot, {
|
||||||
|
forceFresh: true,
|
||||||
|
providerId: 'codex',
|
||||||
|
modelIds: ['gpt-5.5'],
|
||||||
|
modelVerificationMode: 'compatibility',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ready).toBe(true);
|
||||||
|
expect(result.details).toEqual(['Selected model gpt-5.5 is available for launch.']);
|
||||||
|
expect(result.message).toBe('CLI is warmed up and ready to launch');
|
||||||
|
expect(spawnProbe).not.toHaveBeenCalled();
|
||||||
|
expect(vi.mocked(console.warn).mock.calls.map((call) => call.join(' '))).toEqual([
|
||||||
|
'[Service:TeamProvisioning] [codex] Failed to parse runtime model list for launch validation: No JSON object found in CLI output',
|
||||||
|
]);
|
||||||
|
vi.mocked(console.warn).mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => {
|
it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({
|
vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({
|
||||||
|
|
|
||||||
|
|
@ -469,6 +469,45 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
||||||
expect(spawnCli).not.toHaveBeenCalled();
|
expect(spawnCli).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows explicit Codex models when launch model list parsing is degraded', async () => {
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
|
||||||
|
const { child } = createFakeChild();
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||||
|
env: {},
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
providerArgs: [],
|
||||||
|
}));
|
||||||
|
(svc as any).readRuntimeProviderLaunchFacts = vi.fn(async () => ({
|
||||||
|
defaultModel: null,
|
||||||
|
modelIds: new Set(),
|
||||||
|
modelListParsed: false,
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: { modelCatalog: { dynamic: false, source: 'runtime' } },
|
||||||
|
providerStatus: null,
|
||||||
|
}));
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
(svc as any).pathExists = vi.fn(async () => false);
|
||||||
|
|
||||||
|
const { runId } = await svc.createTeam(
|
||||||
|
{
|
||||||
|
teamName: 'codex-future-model-launchable',
|
||||||
|
cwd: process.cwd(),
|
||||||
|
members: [],
|
||||||
|
providerId: 'codex',
|
||||||
|
model: 'gpt-5.5',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(spawnCli).toHaveBeenCalled();
|
||||||
|
await svc.cancelProvisioning(runId);
|
||||||
|
});
|
||||||
|
|
||||||
it('restart teammate message keeps the exact teammate identity and avoids duplicate semantics', () => {
|
it('restart teammate message keeps the exact teammate identity and avoids duplicate semantics', () => {
|
||||||
const message = buildRestartMemberSpawnMessage('forge-labs', 'Forge Labs', 'lead', {
|
const message = buildRestartMemberSpawnMessage('forge-labs', 'Forge Labs', 'lead', {
|
||||||
name: 'alice',
|
name: 'alice',
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,33 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
).toBe('Codex native');
|
).toBe('Codex native');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show non-blocking Codex degraded backend state in provisioning summaries', () => {
|
||||||
|
expect(
|
||||||
|
getProvisioningProviderBackendSummary({
|
||||||
|
providerId: 'codex',
|
||||||
|
selectedBackendId: 'codex-native',
|
||||||
|
resolvedBackendId: 'codex-native',
|
||||||
|
backend: {
|
||||||
|
kind: 'codex-native',
|
||||||
|
label: 'Codex native',
|
||||||
|
},
|
||||||
|
availableBackends: [
|
||||||
|
{
|
||||||
|
id: 'codex-native',
|
||||||
|
label: 'Codex native',
|
||||||
|
description: 'Use codex exec JSON mode.',
|
||||||
|
selectable: false,
|
||||||
|
recommended: false,
|
||||||
|
available: true,
|
||||||
|
state: 'degraded',
|
||||||
|
audience: 'general',
|
||||||
|
statusMessage: 'Ready with degraded account verification.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).toBe('Codex native');
|
||||||
|
});
|
||||||
|
|
||||||
it('normalizes persisted legacy codex fallback summaries to Codex native', () => {
|
it('normalizes persisted legacy codex fallback summaries to Codex native', () => {
|
||||||
expect(
|
expect(
|
||||||
getProvisioningProviderBackendSummary({
|
getProvisioningProviderBackendSummary({
|
||||||
|
|
|
||||||
|
|
@ -570,7 +570,7 @@ describe('runProviderPrepareDiagnostics', () => {
|
||||||
expect(result.details).toEqual(['5.4 Mini - verified', '5.4 - verified']);
|
expect(result.details).toEqual(['5.4 Mini - verified', '5.4 - verified']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not synthesize verified from a generic runtime preflight note alone', async () => {
|
it('treats launchable Codex compatibility as ready and suppresses generic preflight notes', async () => {
|
||||||
const prepareProvisioning = vi.fn<
|
const prepareProvisioning = vi.fn<
|
||||||
(
|
(
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
|
|
@ -593,16 +593,13 @@ describe('runProviderPrepareDiagnostics', () => {
|
||||||
prepareProvisioning,
|
prepareProvisioning,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.status).toBe('notes');
|
expect(result.status).toBe('ready');
|
||||||
expect(result.warnings).toEqual(['orchestrator-cli preflight check failed (exit code 1).']);
|
expect(result.warnings).toEqual([]);
|
||||||
expect(result.details).toEqual([
|
expect(result.details).toEqual(['5.4 - available for launch']);
|
||||||
'orchestrator-cli preflight check failed (exit code 1).',
|
|
||||||
'5.4 - compatible, deep verification pending...',
|
|
||||||
]);
|
|
||||||
expect(result.modelResultsById).toEqual({
|
expect(result.modelResultsById).toEqual({
|
||||||
'gpt-5.4': {
|
'gpt-5.4': {
|
||||||
status: 'notes',
|
status: 'ready',
|
||||||
line: '5.4 - compatible, deep verification pending...',
|
line: '5.4 - available for launch',
|
||||||
warningLine: null,
|
warningLine: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { getVisibleTeamProviderModels } from '@renderer/utils/teamModelCatalog';
|
import {
|
||||||
|
getVisibleTeamProviderModels,
|
||||||
|
isAnthropicSonnetTeamModel,
|
||||||
|
} from '@renderer/utils/teamModelCatalog';
|
||||||
|
|
||||||
describe('teamModelCatalog', () => {
|
describe('teamModelCatalog', () => {
|
||||||
it('filters UI-disabled Codex models from provider badge lists', () => {
|
it('filters UI-disabled Codex models from provider badge lists', () => {
|
||||||
|
|
@ -43,4 +46,13 @@ describe('teamModelCatalog', () => {
|
||||||
'claude-sonnet-4-6[1m]',
|
'claude-sonnet-4-6[1m]',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('detects Sonnet aliases with or without 1M suffix', () => {
|
||||||
|
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
|
||||||
|
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);
|
||||||
|
expect(isAnthropicSonnetTeamModel('claude-sonnet-4-6')).toBe(true);
|
||||||
|
expect(isAnthropicSonnetTeamModel('claude-sonnet-4-6[1m]')).toBe(true);
|
||||||
|
expect(isAnthropicSonnetTeamModel('opus')).toBe(false);
|
||||||
|
expect(isAnthropicSonnetTeamModel('haiku')).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue