feat(app): rename display name to Agent Teams AI
This commit is contained in:
parent
dc04cbfad7
commit
3e52008c7a
28 changed files with 544 additions and 96 deletions
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -411,10 +411,10 @@ jobs:
|
|||
run: ${{ matrix.dist_command }} --publish never
|
||||
|
||||
- name: Validate packaged bundle (macOS ${{ matrix.arch }})
|
||||
run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin ${{ matrix.arch }}
|
||||
run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Agent Teams AI.app" darwin ${{ matrix.arch }}
|
||||
|
||||
- name: Smoke packaged app (macOS ${{ matrix.arch }})
|
||||
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin
|
||||
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams AI.app" darwin
|
||||
|
||||
- name: Upload assets to release
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@
|
|||
},
|
||||
"build": {
|
||||
"appId": "com.agent-teams.app",
|
||||
"productName": "Agent Teams UI",
|
||||
"productName": "Agent Teams AI",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ const PLATFORM_ARGS = {
|
|||
};
|
||||
|
||||
const LINUX_PACKAGE_NAME_OVERRIDES = [
|
||||
'--config.productName=Agent-Teams-UI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams UI',
|
||||
'--config.productName=Agent-Teams-AI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams AI',
|
||||
];
|
||||
|
||||
function buildElectronBuilderInvocations(argv) {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,12 @@ function findExecutable(bundlePath, platform) {
|
|||
const packageJson = fs.existsSync(packageJsonPath)
|
||||
? JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||||
: {};
|
||||
const preferredNames = [packageJson.name, 'agent-teams-ai', 'Agent Teams UI'].filter(Boolean);
|
||||
const preferredNames = [
|
||||
packageJson.name,
|
||||
'agent-teams-ai',
|
||||
'Agent Teams AI',
|
||||
'Agent Teams UI',
|
||||
].filter(Boolean);
|
||||
for (const name of preferredNames) {
|
||||
const candidate = path.join(bundlePath, name);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ export class CodexAppServerClient {
|
|||
{
|
||||
clientInfo: {
|
||||
name: 'agent-teams-ai',
|
||||
title: 'Agent Teams UI',
|
||||
title: 'Agent Teams AI',
|
||||
version: '0.1.0',
|
||||
},
|
||||
capabilities: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Main process entry point for Agent Teams UI.
|
||||
* Main process entry point for Agent Teams AI.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Initialize Electron app and main window
|
||||
|
|
|
|||
|
|
@ -1193,10 +1193,10 @@ export class NotificationManager extends EventEmitter {
|
|||
logger.debug(`[test-notification] creating Notification (platform=${process.platform})`);
|
||||
const notification = new NotificationClass({
|
||||
title: 'Test Notification',
|
||||
...(isMac ? { subtitle: 'Agent Teams UI' } : {}),
|
||||
...(isMac ? { subtitle: 'Agent Teams AI' } : {}),
|
||||
body: isMac
|
||||
? 'Notifications are working correctly!'
|
||||
: 'Agent Teams UI\nNotifications are working correctly!',
|
||||
: 'Agent Teams AI\nNotifications are working correctly!',
|
||||
...(iconPath ? { icon: iconPath } : {}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export class CodexAppServerSessionFactory {
|
|||
{
|
||||
clientInfo: {
|
||||
name: 'agent-teams-ai',
|
||||
title: 'Agent Teams UI',
|
||||
title: 'Agent Teams AI',
|
||||
version: '0.1.0',
|
||||
},
|
||||
capabilities: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Standalone (non-Electron) entry point for Agent Teams UI.
|
||||
* Standalone (non-Electron) entry point for Agent Teams AI.
|
||||
*
|
||||
* Runs the HTTP server + API without Electron, suitable for Docker
|
||||
* or any headless/remote environment. The renderer is served as
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Chunk and visualization types for Agent Teams UI.
|
||||
* Chunk and visualization types for Agent Teams AI.
|
||||
*
|
||||
* This module contains:
|
||||
* - Chunk types (UserChunk, AIChunk, SystemChunk, CompactChunk)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Domain/business entity types for Agent Teams UI.
|
||||
* Domain/business entity types for Agent Teams AI.
|
||||
*
|
||||
* These types represent the application's domain model:
|
||||
* - Projects and sessions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Parsed message types and type guards for Agent Teams UI.
|
||||
* Parsed message types and type guards for Agent Teams AI.
|
||||
*
|
||||
* ParsedMessage is the application's internal representation after parsing
|
||||
* raw JSONL entries. This module also contains type guards for classifying
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as path from 'path';
|
|||
|
||||
const LEGACY_USER_DATA_DIR_NAMES = [
|
||||
'agent-teams-ai',
|
||||
'Agent Teams AI',
|
||||
'Agent Teams UI',
|
||||
'Claude Agent Teams UI',
|
||||
'claude-agent-teams-ui',
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ export const AdvancedSection = ({
|
|||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
||||
Agent Teams UI
|
||||
Agent Teams AI
|
||||
</p>
|
||||
{isElectron && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Checkbox } from '@renderer/components/ui/checkbox';
|
||||
import { HoverTooltip } from '@renderer/components/ui/hover-tooltip';
|
||||
import { Label } from '@renderer/components/ui/label';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@renderer/components/ui/tooltip';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
interface LimitContextCheckboxProps {
|
||||
|
|
@ -44,20 +39,15 @@ export const LimitContextCheckbox: React.FC<LimitContextCheckboxProps> = ({
|
|||
) : null}
|
||||
{disabled && <span className="text-[10px] italic">(always 200K for this model)</span>}
|
||||
</Label>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info
|
||||
className={`size-3.5 shrink-0 ${disabled ? 'text-text-muted opacity-50' : 'text-text-muted hover:text-text-secondary'} cursor-help`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[260px]">
|
||||
<p>
|
||||
Enable this to cap Anthropic runtimes at 200K tokens. Leave it off only when you want
|
||||
the selected Anthropic model or runtime to use a longer context window when available.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<HoverTooltip
|
||||
content="Enable this to cap Anthropic runtimes at 200K tokens. Leave it off only when you want the selected Anthropic model or runtime to use a longer context window when available."
|
||||
title="Enable this to cap Anthropic runtimes at 200K tokens."
|
||||
contentClassName="max-w-[260px]"
|
||||
>
|
||||
<Info
|
||||
className={`size-3.5 shrink-0 ${disabled ? 'text-text-muted opacity-50' : 'text-text-muted hover:text-text-secondary'} cursor-help`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</HoverTooltip>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -177,6 +177,15 @@ function isModelDetail(lower: string): boolean {
|
|||
return isSelectedModelDetail(lower) || isFormattedModelDetail(lower);
|
||||
}
|
||||
|
||||
function isInternalProvisioningDetail(detail: string): boolean {
|
||||
const normalized = detail.trim().toLowerCase();
|
||||
return normalized === 'opencode_app_mcp_tool_proof_persisted_cache_hit';
|
||||
}
|
||||
|
||||
function getPublicProvisioningDetails(details: string[]): string[] {
|
||||
return details.filter((detail) => !isInternalProvisioningDetail(detail));
|
||||
}
|
||||
|
||||
function getStatusLabel(status: ProvisioningProviderCheckStatus): string {
|
||||
switch (status) {
|
||||
case 'checking':
|
||||
|
|
@ -418,12 +427,13 @@ function hasCompatibilityPendingDetails(checks: ProvisioningProviderCheck[]): bo
|
|||
}
|
||||
|
||||
function getDisplayStatusText(check: ProvisioningProviderCheck): string {
|
||||
const modelSummary = getModelDetailSummary(check.details);
|
||||
const publicDetails = getPublicProvisioningDetails(check.details);
|
||||
const modelSummary = getModelDetailSummary(publicDetails);
|
||||
if (modelSummary) {
|
||||
return modelSummary;
|
||||
}
|
||||
|
||||
const summarizedDetails = check.details
|
||||
const summarizedDetails = publicDetails
|
||||
.map((detail) => summarizeDetail(detail, check.status))
|
||||
.filter((detail): detail is ProvisioningDetailSummary => Boolean(detail));
|
||||
|
||||
|
|
@ -507,7 +517,8 @@ export function getPrimaryProvisioningFailureDetail(
|
|||
continue;
|
||||
}
|
||||
|
||||
const unavailableDetail = check.details.find((detail) =>
|
||||
const publicDetails = getPublicProvisioningDetails(check.details);
|
||||
const unavailableDetail = publicDetails.find((detail) =>
|
||||
detail.toLowerCase().includes('selected model') &&
|
||||
detail.toLowerCase().includes('is unavailable')
|
||||
? true
|
||||
|
|
@ -523,22 +534,23 @@ export function getPrimaryProvisioningFailureDetail(
|
|||
continue;
|
||||
}
|
||||
|
||||
const preferredFailure = check.details.find(
|
||||
const publicDetails = getPublicProvisioningDetails(check.details);
|
||||
const preferredFailure = publicDetails.find(
|
||||
(detail) => getDetailTone(detail, check.status) === 'failure'
|
||||
);
|
||||
if (preferredFailure) {
|
||||
return preferredFailure;
|
||||
}
|
||||
|
||||
const nonSuccessDetail = check.details.find(
|
||||
const nonSuccessDetail = publicDetails.find(
|
||||
(detail) => getDetailTone(detail, check.status) !== 'success'
|
||||
);
|
||||
if (nonSuccessDetail) {
|
||||
return nonSuccessDetail;
|
||||
}
|
||||
|
||||
if (check.details.length > 0) {
|
||||
return check.details[0];
|
||||
if (publicDetails.length > 0) {
|
||||
return publicDetails[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -671,8 +683,9 @@ export const ProvisioningProviderStatusList = ({
|
|||
return (
|
||||
<div className={`space-y-1 pl-5 ${className}`.trim()}>
|
||||
{checks.map((check) => {
|
||||
const visibleDetails = check.details.filter(
|
||||
(detail) => detail.trim() !== (suppressDetailsMatching ?? '').trim()
|
||||
const suppressDetailsMatchingTrimmed = (suppressDetailsMatching ?? '').trim();
|
||||
const visibleDetails = getPublicProvisioningDetails(check.details).filter(
|
||||
(detail) => detail.trim() !== suppressDetailsMatchingTrimmed
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -333,6 +333,50 @@ export const OPENCODE_ONE_SHOT_DISABLED_REASON =
|
|||
'OpenCode team launch is available for normal teams, but scheduled one-shot prompts still run through claude -p. Choose Anthropic, Codex, or Gemini for one-shot schedules.';
|
||||
export const OPENCODE_ONE_SHOT_DISABLED_BADGE_LABEL = 'team only';
|
||||
|
||||
function getOpenCodeReadinessBadgeLabel(
|
||||
providerStatus: CliProviderStatus | null | undefined
|
||||
): string {
|
||||
if (!providerStatus) {
|
||||
return 'Check';
|
||||
}
|
||||
if (!providerStatus.supported) {
|
||||
return 'Install';
|
||||
}
|
||||
if (!providerStatus.authenticated) {
|
||||
return 'Auth';
|
||||
}
|
||||
return 'Setup';
|
||||
}
|
||||
|
||||
function getOpenCodeReadinessSummary(providerStatus: CliProviderStatus | null | undefined): string {
|
||||
if (!providerStatus) {
|
||||
return 'OpenCode status: checking runtime';
|
||||
}
|
||||
|
||||
const parts = [
|
||||
providerStatus.supported ? 'runtime detected' : 'runtime missing',
|
||||
providerStatus.authenticated ? 'provider connected' : 'provider not connected',
|
||||
providerStatus.capabilities.teamLaunch ? 'team launch ready' : 'team launch blocked',
|
||||
];
|
||||
return `OpenCode status: ${parts.join(' · ')}`;
|
||||
}
|
||||
|
||||
function getOpenCodeReadinessMessage(providerStatus: CliProviderStatus | null | undefined): string {
|
||||
if (!providerStatus) {
|
||||
return 'The app is still checking the OpenCode runtime. Wait for provider status to finish, then try again.';
|
||||
}
|
||||
if (!providerStatus.supported) {
|
||||
return 'OpenCode is not installed, not found, or the detected runtime is not supported. Install or update OpenCode, then refresh provider status.';
|
||||
}
|
||||
if (!providerStatus.authenticated) {
|
||||
return 'OpenCode is detected, but it does not have a connected provider. Connect a provider in OpenCode, then refresh provider status.';
|
||||
}
|
||||
if (!providerStatus.capabilities.teamLaunch) {
|
||||
return 'OpenCode is installed and authenticated, but Agent Teams launch readiness is blocked.';
|
||||
}
|
||||
return 'OpenCode is ready for team launch.';
|
||||
}
|
||||
|
||||
export function getTeamModelLabel(model: string): string {
|
||||
return getCatalogTeamModelLabel(model) ?? model;
|
||||
}
|
||||
|
|
@ -427,7 +471,10 @@ const OpenCodeVirtualizedModelGrid = ({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const updateGridWidth = (): void => setGridWidth(element.clientWidth);
|
||||
const updateGridWidth = (): void => {
|
||||
const nextWidth = element.clientWidth;
|
||||
setGridWidth((previousWidth) => (previousWidth === nextWidth ? previousWidth : nextWidth));
|
||||
};
|
||||
updateGridWidth();
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
|
|
@ -591,9 +638,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
const [selectedOpenCodeSourceIds, setSelectedOpenCodeSourceIds] = useState<Set<string>>(
|
||||
() => new Set()
|
||||
);
|
||||
|
||||
const effectiveProviderId =
|
||||
const selectedProviderId =
|
||||
disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId;
|
||||
const [inspectedProviderId, setInspectedProviderId] = useState<TeamProviderId | null>(null);
|
||||
const previousEffectiveProviderIdRef = useRef<TeamProviderId>(selectedProviderId);
|
||||
const previousSelectedProviderIdRef = useRef<TeamProviderId>(selectedProviderId);
|
||||
const effectiveProviderId = inspectedProviderId ?? selectedProviderId;
|
||||
const isInspectingInactiveProvider = inspectedProviderId !== null;
|
||||
const { cliStatus: effectiveCliStatus, providerStatus: runtimeProviderStatus } =
|
||||
useEffectiveCliProviderStatus(effectiveProviderId);
|
||||
const multimodelAvailable =
|
||||
|
|
@ -624,12 +675,17 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
}
|
||||
return 'Uses the runtime default for the selected provider.';
|
||||
}, [effectiveProviderId, runtimeProviderStatus]);
|
||||
const getProviderOverrideDisabledReason = (candidateProviderId: string): string | null => {
|
||||
if (!isTeamProviderId(candidateProviderId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return providerDisabledReasonById?.[candidateProviderId]?.trim() || null;
|
||||
};
|
||||
const getProviderDisabledReason = (candidateProviderId: string): string | null => {
|
||||
if (isTeamProviderId(candidateProviderId)) {
|
||||
const overrideReason = providerDisabledReasonById?.[candidateProviderId]?.trim();
|
||||
if (overrideReason) {
|
||||
return overrideReason;
|
||||
}
|
||||
const overrideReason = getProviderOverrideDisabledReason(candidateProviderId);
|
||||
if (overrideReason) {
|
||||
return overrideReason;
|
||||
}
|
||||
|
||||
if (candidateProviderId === 'opencode') {
|
||||
|
|
@ -670,6 +726,11 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
const isProviderSelectable = (candidateProviderId: string): boolean =>
|
||||
!isProviderTemporarilyDisabled(candidateProviderId) &&
|
||||
(multimodelAvailable || candidateProviderId === 'anthropic');
|
||||
const isProviderInspectable = (candidateProviderId: string): boolean =>
|
||||
candidateProviderId === 'opencode' &&
|
||||
getProviderOverrideDisabledReason(candidateProviderId) === null &&
|
||||
getProviderDisabledReason(candidateProviderId) !== null &&
|
||||
multimodelAvailable;
|
||||
const activeProviderSelectable = isProviderSelectable(effectiveProviderId);
|
||||
const getProviderStatusBadge = (candidateProviderId: string): string | null => {
|
||||
if (isTeamProviderId(candidateProviderId)) {
|
||||
|
|
@ -681,7 +742,9 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
}
|
||||
|
||||
if (candidateProviderId === 'opencode') {
|
||||
return getProviderDisabledReason(candidateProviderId) ? 'Gated' : null;
|
||||
return getProviderDisabledReason(candidateProviderId)
|
||||
? getOpenCodeReadinessBadgeLabel(runtimeProviderStatusById.get('opencode'))
|
||||
: null;
|
||||
}
|
||||
|
||||
const providerDisabledReason = getProviderDisabledReason(candidateProviderId);
|
||||
|
|
@ -696,10 +759,6 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
return null;
|
||||
};
|
||||
const getProviderStatusBadgeLabel = (statusBadge: string | null): string | null => {
|
||||
if (statusBadge === 'Gated') {
|
||||
return 'Gate';
|
||||
}
|
||||
|
||||
if (statusBadge === 'Multimodel off') {
|
||||
return 'Off';
|
||||
}
|
||||
|
|
@ -717,10 +776,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInspectingInactiveProvider) {
|
||||
return;
|
||||
}
|
||||
if (normalizedValue !== value) {
|
||||
onValueChange(normalizedValue);
|
||||
}
|
||||
}, [normalizedValue, onValueChange, value]);
|
||||
}, [isInspectingInactiveProvider, normalizedValue, onValueChange, value]);
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
if (shouldAwaitRuntimeModelList) {
|
||||
|
|
@ -784,29 +846,43 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels) {
|
||||
queueMicrotask(() => setRecommendedOnly(false));
|
||||
if (previousSelectedProviderIdRef.current === selectedProviderId) {
|
||||
return;
|
||||
}
|
||||
}, [effectiveProviderId, hasRecommendedOpenCodeModels]);
|
||||
previousSelectedProviderIdRef.current = selectedProviderId;
|
||||
setInspectedProviderId(null);
|
||||
}, [selectedProviderId]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => setModelQuery(''));
|
||||
if (recommendedOnly && (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels)) {
|
||||
setRecommendedOnly(false);
|
||||
}
|
||||
}, [effectiveProviderId, hasRecommendedOpenCodeModels, recommendedOnly]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousEffectiveProviderIdRef.current === effectiveProviderId) {
|
||||
return;
|
||||
}
|
||||
previousEffectiveProviderIdRef.current = effectiveProviderId;
|
||||
setModelQuery('');
|
||||
}, [effectiveProviderId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (effectiveProviderId !== 'opencode') {
|
||||
queueMicrotask(() => {
|
||||
setSelectedOpenCodeSourceIds(new Set());
|
||||
setOpenCodeSourceFilterOpen(false);
|
||||
});
|
||||
if (effectiveProviderId === 'opencode') {
|
||||
return;
|
||||
}
|
||||
}, [effectiveProviderId]);
|
||||
if (selectedOpenCodeSourceIds.size === 0 && !openCodeSourceFilterOpen) {
|
||||
return;
|
||||
}
|
||||
setSelectedOpenCodeSourceIds(new Set());
|
||||
setOpenCodeSourceFilterOpen(false);
|
||||
}, [effectiveProviderId, openCodeSourceFilterOpen, selectedOpenCodeSourceIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!openCodeSourceFilterOpen) {
|
||||
queueMicrotask(() => setOpenCodeSourceQuery(''));
|
||||
if (!openCodeSourceFilterOpen && openCodeSourceQuery) {
|
||||
setOpenCodeSourceQuery('');
|
||||
}
|
||||
}, [openCodeSourceFilterOpen]);
|
||||
}, [openCodeSourceFilterOpen, openCodeSourceQuery]);
|
||||
|
||||
const openCodeSourceOptions = useMemo<OpenCodeSourceOption[]>(() => {
|
||||
if (effectiveProviderId !== 'opencode') {
|
||||
|
|
@ -851,7 +927,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
Array.from(selectedOpenCodeSourceIds).filter((sourceId) => availableSourceIds.has(sourceId))
|
||||
);
|
||||
if (nextSelectedSourceIds.size !== selectedOpenCodeSourceIds.size) {
|
||||
queueMicrotask(() => setSelectedOpenCodeSourceIds(nextSelectedSourceIds));
|
||||
setSelectedOpenCodeSourceIds(nextSelectedSourceIds);
|
||||
}
|
||||
}, [openCodeSourceOptions, selectedOpenCodeSourceIds]);
|
||||
|
||||
|
|
@ -1008,6 +1084,32 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
effectiveProviderId === 'opencode' &&
|
||||
!shouldShowOpenCodeCatalogLoading &&
|
||||
visibleConcreteModelOptionCount > OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD;
|
||||
const activeProviderDisabledReason = activeProviderSelectable
|
||||
? null
|
||||
: getProviderDisabledReason(effectiveProviderId);
|
||||
const canActivateInspectedOpenCode =
|
||||
effectiveProviderId === 'opencode' && isInspectingInactiveProvider && activeProviderSelectable;
|
||||
const activeProviderStatusPanel =
|
||||
activeProviderDisabledReason && effectiveProviderId === 'opencode'
|
||||
? {
|
||||
tone: 'warning' as const,
|
||||
title: 'OpenCode is not ready for team launch',
|
||||
summary: getOpenCodeReadinessSummary(runtimeProviderStatus),
|
||||
message: getOpenCodeReadinessMessage(runtimeProviderStatus),
|
||||
reason: activeProviderDisabledReason,
|
||||
actionLabel: null,
|
||||
}
|
||||
: canActivateInspectedOpenCode
|
||||
? {
|
||||
tone: 'ready' as const,
|
||||
title: 'OpenCode is ready',
|
||||
summary: getOpenCodeReadinessSummary(runtimeProviderStatus),
|
||||
message:
|
||||
'OpenCode passed provider readiness. Select it to use OpenCode models for this team.',
|
||||
reason: null,
|
||||
actionLabel: 'Use OpenCode',
|
||||
}
|
||||
: null;
|
||||
const getModelAdvisoryBadgeLabel = (reason: string | null): string =>
|
||||
reason?.toLowerCase().includes('ping not confirmed') ? 'Ping not confirmed' : 'Note';
|
||||
const renderModelOption = (opt: TeamRuntimeModelOption): React.JSX.Element => {
|
||||
|
|
@ -1040,6 +1142,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
const hasBlockingModelIssue = Boolean(modelIssueReason || modelUnavailableReason);
|
||||
const hasModelAdvisory = Boolean(modelAdvisoryReason) && !hasBlockingModelIssue;
|
||||
const modelSelectable =
|
||||
!isInspectingInactiveProvider &&
|
||||
activeProviderSelectable &&
|
||||
!modelUnavailableReason &&
|
||||
!modelDisabledReason &&
|
||||
|
|
@ -1227,8 +1330,16 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
<Tabs
|
||||
value={effectiveProviderId}
|
||||
onValueChange={(nextValue) => {
|
||||
if (isTeamProviderId(nextValue) && isProviderSelectable(nextValue)) {
|
||||
if (!isTeamProviderId(nextValue)) {
|
||||
return;
|
||||
}
|
||||
if (isProviderSelectable(nextValue)) {
|
||||
setInspectedProviderId(null);
|
||||
onProviderChange(nextValue);
|
||||
return;
|
||||
}
|
||||
if (isProviderInspectable(nextValue)) {
|
||||
setInspectedProviderId(nextValue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -1238,6 +1349,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
{PROVIDERS.map((provider) => {
|
||||
const providerDisabledReason = getProviderDisabledReason(provider.id);
|
||||
const providerSelectable = isProviderSelectable(provider.id);
|
||||
const providerInspectable = isProviderInspectable(provider.id);
|
||||
const statusBadge = getProviderStatusBadge(provider.id);
|
||||
const statusBadgeLabel = getProviderStatusBadgeLabel(statusBadge);
|
||||
|
||||
|
|
@ -1245,7 +1357,8 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
<TabsTrigger
|
||||
key={provider.id}
|
||||
value={provider.id}
|
||||
disabled={provider.comingSoon || !providerSelectable}
|
||||
disabled={provider.comingSoon || (!providerSelectable && !providerInspectable)}
|
||||
aria-disabled={!providerSelectable || undefined}
|
||||
title={
|
||||
providerDisabledReason ??
|
||||
(statusBadge === 'Multimodel off'
|
||||
|
|
@ -1295,6 +1408,45 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
) : null}
|
||||
|
||||
<div className="p-3">
|
||||
{activeProviderStatusPanel ? (
|
||||
<div
|
||||
data-testid="team-model-selector-provider-status"
|
||||
className={cn(
|
||||
'mb-3 rounded-md border px-3 py-2 text-[11px] leading-relaxed',
|
||||
activeProviderStatusPanel.tone === 'ready'
|
||||
? 'border-emerald-300/30 bg-emerald-300/10 text-emerald-100'
|
||||
: 'border-amber-300/30 bg-amber-300/10 text-amber-100'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{activeProviderStatusPanel.tone === 'ready' ? (
|
||||
<CheckCircle2 className="mt-0.5 size-3.5 shrink-0 text-emerald-200" />
|
||||
) : (
|
||||
<AlertTriangle className="mt-0.5 size-3.5 shrink-0 text-amber-200" />
|
||||
)}
|
||||
<div className="min-w-0 space-y-1">
|
||||
<p className="font-medium">{activeProviderStatusPanel.title}</p>
|
||||
<p className="opacity-90">{activeProviderStatusPanel.summary}</p>
|
||||
<p>{activeProviderStatusPanel.message}</p>
|
||||
{activeProviderStatusPanel.reason ? (
|
||||
<p className="opacity-90">Reason: {activeProviderStatusPanel.reason}</p>
|
||||
) : null}
|
||||
{activeProviderStatusPanel.actionLabel ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1 inline-flex h-7 items-center rounded-md border border-emerald-300/35 bg-emerald-300/10 px-2.5 text-[11px] font-medium text-emerald-100 transition-colors hover:border-emerald-200/50 hover:bg-emerald-300/15"
|
||||
onClick={() => {
|
||||
setInspectedProviderId(null);
|
||||
onProviderChange('opencode');
|
||||
}}
|
||||
>
|
||||
{activeProviderStatusPanel.actionLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{shouldAwaitRuntimeModelList ? (
|
||||
<p className="mb-2 text-[11px] text-[var(--color-text-muted)]">
|
||||
Explicit models load from the current runtime. Default remains available while the
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Notification and configuration types for Agent Teams UI.
|
||||
* Notification and configuration types for Agent Teams AI.
|
||||
*
|
||||
* Re-exports types from shared for backwards compatibility.
|
||||
* The canonical definitions are in @shared/types/notifications.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Session export utilities for Agent Teams UI.
|
||||
* Session export utilities for Agent Teams AI.
|
||||
*
|
||||
* Provides formatters to export session data as plain text, Markdown, or JSON,
|
||||
* and a download trigger for browser-based file saving.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Notification and configuration types for Agent Teams UI.
|
||||
* Notification and configuration types for Agent Teams AI.
|
||||
*
|
||||
* These types define:
|
||||
* - Detected errors from session files
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Visualization-specific types for Agent Teams UI.
|
||||
* Visualization-specific types for Agent Teams AI.
|
||||
*
|
||||
* These types are used for waterfall chart visualization
|
||||
* and are shared between main and renderer processes.
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ describe('electron-builder afterPack', () => {
|
|||
tempDirs.push(tempDir);
|
||||
|
||||
writeFile(
|
||||
path.join(tempDir, 'Contents', 'MacOS', 'Agent Teams UI'),
|
||||
path.join(tempDir, 'Contents', 'MacOS', 'Agent Teams AI'),
|
||||
createMachOBuffer('arm64')
|
||||
);
|
||||
writeFile(
|
||||
|
|
@ -229,7 +229,7 @@ describe('electron-builder afterPack', () => {
|
|||
const tempDir = createTempDir();
|
||||
tempDirs.push(tempDir);
|
||||
|
||||
writeFile(path.join(tempDir, 'Agent Teams UI.exe'), createPortableExecutableBuffer('x64'));
|
||||
writeFile(path.join(tempDir, 'Agent Teams AI.exe'), createPortableExecutableBuffer('x64'));
|
||||
writeFile(
|
||||
path.join(
|
||||
tempDir,
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ describe('electron-builder dist wrapper', () => {
|
|||
'--linux',
|
||||
'--publish',
|
||||
'never',
|
||||
'--config.productName=Agent-Teams-UI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams UI',
|
||||
'--config.productName=Agent-Teams-AI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams AI',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -29,8 +29,8 @@ describe('electron-builder dist wrapper', () => {
|
|||
'--linux',
|
||||
'--publish',
|
||||
'never',
|
||||
'--config.productName=Agent-Teams-UI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams UI',
|
||||
'--config.productName=Agent-Teams-AI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams AI',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ describe('ClaudeBinaryResolver', () => {
|
|||
});
|
||||
process.cwd = vi.fn(() => workspaceRoot);
|
||||
Object.defineProperty(process, 'resourcesPath', {
|
||||
value: '/Applications/Agent Teams UI.app/Contents/Resources',
|
||||
value: '/Applications/Agent Teams AI.app/Contents/Resources',
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
|
@ -219,7 +219,7 @@ describe('ClaudeBinaryResolver', () => {
|
|||
|
||||
it('prefers the bundled runtime binary for packaged agent_teams_orchestrator builds', async () => {
|
||||
const expectedBinary = path.join(
|
||||
'/Applications/Agent Teams UI.app/Contents/Resources',
|
||||
'/Applications/Agent Teams AI.app/Contents/Resources',
|
||||
'runtime',
|
||||
'claude-multimodel'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ describe('ClaudeDoctorProbe', () => {
|
|||
\u001B[2J────────────────────────────────────
|
||||
Diagnostics
|
||||
└ Invoked: /Applications/Agent Teams${' '}
|
||||
UI.app/Contents/Resources/runtime/clau
|
||||
AI.app/Contents/Resources/runtime/clau
|
||||
de-multimodel
|
||||
└ Config install method: native
|
||||
Press Enter to continue…
|
||||
`;
|
||||
|
||||
expect(extractDoctorInvokedCandidates(output)).toEqual([
|
||||
'/Applications/Agent Teams UI.app/Contents/Resources/runtime/claude-multimodel',
|
||||
'/Applications/Agent Teams AI.app/Contents/Resources/runtime/claude-multimodel',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ describe('electron userData migration', () => {
|
|||
|
||||
expect(getLegacyElectronUserDataCandidates(currentPath)).toEqual([
|
||||
path.join(parentPath, 'agent-teams-ai'),
|
||||
path.join(parentPath, 'Agent Teams AI'),
|
||||
path.join(parentPath, 'Claude Agent Teams UI'),
|
||||
path.join(parentPath, 'claude-agent-teams-ui'),
|
||||
path.join(parentPath, 'claude-devtools'),
|
||||
|
|
@ -291,6 +292,34 @@ describe('electron userData migration', () => {
|
|||
expect(readFile(currentProductPath, 'data/attachments/team-a/old.txt')).toBe('old');
|
||||
});
|
||||
|
||||
it('reuses existing agent-teams-ai data when the current product name is Agent Teams AI', () => {
|
||||
const root = createTempRoot();
|
||||
const completedNewPath = path.join(root, 'agent-teams-ai');
|
||||
const currentProductPath = path.join(root, 'Agent Teams AI');
|
||||
const olderProductPath = path.join(root, 'Agent Teams UI');
|
||||
const app = new FakeElectronApp(currentProductPath);
|
||||
|
||||
writeFile(currentProductPath, 'Preferences', '{}');
|
||||
writeFile(completedNewPath, 'data/attachments/team-a/current.txt', 'current');
|
||||
writeFile(olderProductPath, 'data/attachments/team-a/old.txt', 'old');
|
||||
|
||||
const result = migrateElectronUserDataDirectory(app);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
currentPath: currentProductPath,
|
||||
legacyPath: completedNewPath,
|
||||
migrated: false,
|
||||
fallbackToLegacy: false,
|
||||
reason: 'legacy-reused',
|
||||
});
|
||||
expect(app.setPathCalls).toEqual([
|
||||
{ name: 'userData', value: completedNewPath },
|
||||
{ name: 'sessionData', value: completedNewPath },
|
||||
]);
|
||||
expect(readFile(completedNewPath, 'data/attachments/team-a/current.txt')).toBe('current');
|
||||
expect(readFile(olderProductPath, 'data/attachments/team-a/old.txt')).toBe('old');
|
||||
});
|
||||
|
||||
it('copies legacy app-owned state and durable renderer storage without Chromium caches', async () => {
|
||||
const root = createTempRoot();
|
||||
const legacyPath = path.join(root, 'Claude Agent Teams UI');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
|
||||
|
|
@ -29,11 +30,13 @@ vi.mock('@renderer/components/ui/tabs', () => {
|
|||
value,
|
||||
disabled,
|
||||
title,
|
||||
'aria-disabled': ariaDisabled,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
'aria-disabled'?: boolean;
|
||||
}) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
|
|
@ -41,6 +44,7 @@ vi.mock('@renderer/components/ui/tabs', () => {
|
|||
type: 'button',
|
||||
disabled,
|
||||
title,
|
||||
'aria-disabled': ariaDisabled,
|
||||
'data-state': currentValue === value ? 'active' : 'inactive',
|
||||
onClick: () => {
|
||||
if (!disabled) {
|
||||
|
|
@ -1283,7 +1287,7 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('shows OpenCode as readiness-gated and keeps it non-selectable', async () => {
|
||||
it('opens readiness-gated OpenCode as diagnostics without selecting it', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
|
|
@ -1309,7 +1313,8 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
const buttons = Array.from(host.querySelectorAll('button'));
|
||||
const openCodeButton = buttons.find((button) => button.textContent?.includes('OpenCode'));
|
||||
expect(openCodeButton).not.toBeNull();
|
||||
expect(openCodeButton?.hasAttribute('disabled')).toBe(true);
|
||||
expect(openCodeButton?.hasAttribute('disabled')).toBe(false);
|
||||
expect(openCodeButton?.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(openCodeButton?.getAttribute('title')).toContain(
|
||||
'OpenCode runtime status is still loading.'
|
||||
);
|
||||
|
|
@ -1320,6 +1325,15 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
});
|
||||
|
||||
expect(onProviderChange).not.toHaveBeenCalled();
|
||||
const activeOpenCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes('OpenCode')
|
||||
);
|
||||
expect(activeOpenCodeButton?.getAttribute('data-state')).toBe('active');
|
||||
expect(host.textContent).toContain('OpenCode is not ready for team launch');
|
||||
expect(host.textContent).toContain('OpenCode status: checking runtime');
|
||||
expect(host.textContent).toContain(
|
||||
'The app is still checking the OpenCode runtime. Wait for provider status to finish, then try again.'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
|
@ -1361,11 +1375,217 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
const openCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes('OpenCode')
|
||||
);
|
||||
expect(openCodeButton?.hasAttribute('disabled')).toBe(true);
|
||||
expect(openCodeButton?.hasAttribute('disabled')).toBe(false);
|
||||
expect(openCodeButton?.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(openCodeButton?.getAttribute('title')).toContain(
|
||||
'OpenCode runtime store needs recovery'
|
||||
);
|
||||
expect(openCodeButton?.textContent).toContain('Gate');
|
||||
expect(openCodeButton?.textContent).toContain('Setup');
|
||||
|
||||
await act(async () => {
|
||||
openCodeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('OpenCode is not ready for team launch');
|
||||
expect(host.textContent).toContain(
|
||||
'OpenCode status: runtime detected · provider connected · team launch blocked'
|
||||
);
|
||||
expect(host.textContent).toContain(
|
||||
'OpenCode is installed and authenticated, but Agent Teams launch readiness is blocked.'
|
||||
);
|
||||
expect(host.textContent).toContain('Reason: OpenCode runtime store needs recovery');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps inspected OpenCode explicit until the user selects it after readiness recovers', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliStatus = {
|
||||
providers: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
statusMessage: 'OpenCode team launch is gated',
|
||||
detailMessage: 'OpenCode runtime store needs recovery',
|
||||
capabilities: { teamLaunch: false },
|
||||
models: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const onProviderChange = vi.fn();
|
||||
const render = (): void => {
|
||||
root.render(
|
||||
React.createElement(TeamModelSelector, {
|
||||
providerId: 'anthropic',
|
||||
onProviderChange,
|
||||
value: '',
|
||||
onValueChange: () => undefined,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const openCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes('OpenCode')
|
||||
);
|
||||
await act(async () => {
|
||||
openCodeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onProviderChange).not.toHaveBeenCalled();
|
||||
expect(host.textContent).toContain('OpenCode is not ready for team launch');
|
||||
|
||||
storeState.cliStatus = {
|
||||
providers: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
detailMessage: null,
|
||||
statusMessage: null,
|
||||
capabilities: {
|
||||
teamLaunch: true,
|
||||
},
|
||||
models: ['openrouter/minimax/minimax-m2.5-free'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onProviderChange).not.toHaveBeenCalled();
|
||||
expect(host.textContent).toContain('OpenCode is ready');
|
||||
expect(host.textContent).toContain('Use OpenCode');
|
||||
|
||||
const useOpenCodeButton = Array.from(host.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.trim() === 'Use OpenCode'
|
||||
);
|
||||
await act(async () => {
|
||||
useOpenCodeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onProviderChange).toHaveBeenCalledWith('opencode');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not normalize the selected model while viewing OpenCode readiness diagnostics', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const onValueChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(TeamModelSelector, {
|
||||
providerId: 'anthropic',
|
||||
onProviderChange: () => undefined,
|
||||
value: 'claude-opus-4-7[1m]',
|
||||
onValueChange,
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const openCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes('OpenCode')
|
||||
);
|
||||
await act(async () => {
|
||||
openCodeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('OpenCode is not ready for team launch');
|
||||
expect(onValueChange).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('can leave OpenCode diagnostics for another provider tab', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliStatus = {
|
||||
providers: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
statusMessage: 'OpenCode team launch is gated',
|
||||
detailMessage: 'OpenCode runtime store needs recovery',
|
||||
capabilities: { teamLaunch: false },
|
||||
models: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const onProviderChange = vi.fn();
|
||||
|
||||
const ControlledSelector = (): React.JSX.Element => {
|
||||
const [provider, setProvider] = React.useState<'anthropic' | 'codex'>('anthropic');
|
||||
return React.createElement(TeamModelSelector, {
|
||||
providerId: provider,
|
||||
onProviderChange: (nextProvider) => {
|
||||
onProviderChange(nextProvider);
|
||||
if (nextProvider === 'anthropic' || nextProvider === 'codex') {
|
||||
setProvider(nextProvider);
|
||||
}
|
||||
},
|
||||
value: '',
|
||||
onValueChange: () => undefined,
|
||||
});
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(React.createElement(ControlledSelector));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const getTab = (label: string): HTMLButtonElement | undefined =>
|
||||
Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes(label)
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
getTab('OpenCode')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(getTab('OpenCode')?.getAttribute('data-state')).toBe('active');
|
||||
expect(host.textContent).toContain('OpenCode is not ready for team launch');
|
||||
|
||||
await act(async () => {
|
||||
getTab('Codex')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onProviderChange).toHaveBeenCalledWith('codex');
|
||||
expect(getTab('Codex')?.getAttribute('data-state')).toBe('active');
|
||||
expect(host.textContent).not.toContain('OpenCode is not ready for team launch');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
createInitialProviderChecks,
|
||||
deriveEffectiveProvisioningPrepareState,
|
||||
getPrimaryProvisioningFailureDetail,
|
||||
getProvisioningFailureHint,
|
||||
getProvisioningProviderBackendSummary,
|
||||
ProvisioningProviderStatusList,
|
||||
createInitialProviderChecks,
|
||||
} from '@renderer/components/team/dialogs/ProvisioningProviderStatusList';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
describe('ProvisioningProviderStatusList', () => {
|
||||
afterEach(() => {
|
||||
|
|
@ -237,6 +237,44 @@ describe('ProvisioningProviderStatusList', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('hides internal OpenCode MCP proof cache markers from preflight details', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(ProvisioningProviderStatusList, {
|
||||
checks: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
status: 'ready',
|
||||
backendSummary: 'OpenCode CLI',
|
||||
details: ['opencode_app_mcp_tool_proof_persisted_cache_hit', 'big-pickle - verified'],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain(
|
||||
'OpenCode (OpenCode CLI): Selected model checks - 1 verified'
|
||||
);
|
||||
expect(host.textContent).toContain('big-pickle - verified');
|
||||
expect(host.textContent).not.toContain('opencode_app_mcp_tool_proof_persisted_cache_hit');
|
||||
|
||||
const detailLines = Array.from(host.querySelectorAll('p'));
|
||||
expect(detailLines).toHaveLength(1);
|
||||
expect(detailLines[0]?.textContent).toBe('big-pickle - verified');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('summarizes OpenCode busy model checks as deferred notes', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
|
|
|
|||
Loading…
Reference in a new issue