From 3e52008c7ab094e73cb551104958f90ff6cc654f Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 19 May 2026 12:55:33 +0300 Subject: [PATCH] feat(app): rename display name to Agent Teams AI --- .github/workflows/release.yml | 4 +- package.json | 2 +- scripts/electron-builder/dist-invocations.cjs | 4 +- scripts/electron-builder/smokePackagedApp.cjs | 7 +- .../codex/CodexAppServerClient.ts | 2 +- src/main/index.ts | 2 +- .../infrastructure/NotificationManager.ts | 4 +- .../CodexAppServerSessionFactory.ts | 2 +- src/main/standalone.ts | 2 +- src/main/types/chunks.ts | 2 +- src/main/types/domain.ts | 2 +- src/main/types/messages.ts | 2 +- src/main/utils/electronUserDataMigration.ts | 1 + .../settings/sections/AdvancedSection.tsx | 2 +- .../team/dialogs/LimitContextCheckbox.tsx | 32 +-- .../ProvisioningProviderStatusList.tsx | 31 ++- .../team/dialogs/TeamModelSelector.tsx | 212 +++++++++++++--- src/renderer/types/notifications.ts | 2 +- src/renderer/utils/sessionExporter.ts | 2 +- src/shared/types/notifications.ts | 2 +- src/shared/types/visualization.ts | 2 +- .../build/electronBuilderAfterPack.test.ts | 4 +- .../build/electronBuilderDistScript.test.ts | 8 +- .../team/ClaudeBinaryResolver.test.ts | 4 +- .../services/team/ClaudeDoctorProbe.test.ts | 4 +- .../utils/electronUserDataMigration.test.ts | 29 +++ .../TeamModelSelectorDisabledState.test.ts | 228 +++++++++++++++++- .../ProvisioningProviderStatusList.test.ts | 42 +++- 28 files changed, 544 insertions(+), 96 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac64fa02..601173e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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' }} diff --git a/package.json b/package.json index 4f123949..b0d73263 100644 --- a/package.json +++ b/package.json @@ -246,7 +246,7 @@ }, "build": { "appId": "com.agent-teams.app", - "productName": "Agent Teams UI", + "productName": "Agent Teams AI", "directories": { "output": "release" }, diff --git a/scripts/electron-builder/dist-invocations.cjs b/scripts/electron-builder/dist-invocations.cjs index a8f7db1c..4ff479ee 100644 --- a/scripts/electron-builder/dist-invocations.cjs +++ b/scripts/electron-builder/dist-invocations.cjs @@ -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) { diff --git a/scripts/electron-builder/smokePackagedApp.cjs b/scripts/electron-builder/smokePackagedApp.cjs index ec4ed780..c9b9d969 100644 --- a/scripts/electron-builder/smokePackagedApp.cjs +++ b/scripts/electron-builder/smokePackagedApp.cjs @@ -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; diff --git a/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts b/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts index 6aeeeac7..5b8951b6 100644 --- a/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +++ b/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts @@ -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: { diff --git a/src/main/index.ts b/src/main/index.ts index 8cc26c0c..e04a503a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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 diff --git a/src/main/services/infrastructure/NotificationManager.ts b/src/main/services/infrastructure/NotificationManager.ts index 7b3bad30..9043cec7 100644 --- a/src/main/services/infrastructure/NotificationManager.ts +++ b/src/main/services/infrastructure/NotificationManager.ts @@ -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 } : {}), }); diff --git a/src/main/services/infrastructure/codexAppServer/CodexAppServerSessionFactory.ts b/src/main/services/infrastructure/codexAppServer/CodexAppServerSessionFactory.ts index 8218e9a3..9d200b98 100644 --- a/src/main/services/infrastructure/codexAppServer/CodexAppServerSessionFactory.ts +++ b/src/main/services/infrastructure/codexAppServer/CodexAppServerSessionFactory.ts @@ -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: { diff --git a/src/main/standalone.ts b/src/main/standalone.ts index eda481d9..e263608a 100644 --- a/src/main/standalone.ts +++ b/src/main/standalone.ts @@ -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 diff --git a/src/main/types/chunks.ts b/src/main/types/chunks.ts index fcd9e54c..6c6c0d1e 100644 --- a/src/main/types/chunks.ts +++ b/src/main/types/chunks.ts @@ -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) diff --git a/src/main/types/domain.ts b/src/main/types/domain.ts index b0221f56..e771c9c4 100644 --- a/src/main/types/domain.ts +++ b/src/main/types/domain.ts @@ -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 diff --git a/src/main/types/messages.ts b/src/main/types/messages.ts index a7d9f6e2..dbcaa44a 100644 --- a/src/main/types/messages.ts +++ b/src/main/types/messages.ts @@ -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 diff --git a/src/main/utils/electronUserDataMigration.ts b/src/main/utils/electronUserDataMigration.ts index d09fc9dc..7376fe61 100644 --- a/src/main/utils/electronUserDataMigration.ts +++ b/src/main/utils/electronUserDataMigration.ts @@ -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', diff --git a/src/renderer/components/settings/sections/AdvancedSection.tsx b/src/renderer/components/settings/sections/AdvancedSection.tsx index 949a3c00..ca8a5251 100644 --- a/src/renderer/components/settings/sections/AdvancedSection.tsx +++ b/src/renderer/components/settings/sections/AdvancedSection.tsx @@ -172,7 +172,7 @@ export const AdvancedSection = ({

- Agent Teams UI + Agent Teams AI

{isElectron && (
); diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx index 10f6da61..2e1a9a00 100644 --- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx +++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx @@ -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 (
{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 ( diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx index 3890ad47..3d8d281a 100644 --- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx +++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx @@ -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 = ({ const [selectedOpenCodeSourceIds, setSelectedOpenCodeSourceIds] = useState>( () => new Set() ); - - const effectiveProviderId = + const selectedProviderId = disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId; + const [inspectedProviderId, setInspectedProviderId] = useState(null); + const previousEffectiveProviderIdRef = useRef(selectedProviderId); + const previousSelectedProviderIdRef = useRef(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 = ({ } 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 = ({ 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 = ({ } 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 = ({ 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 = ({ ); 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 = ({ ); 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(() => { if (effectiveProviderId !== 'opencode') { @@ -851,7 +927,7 @@ export const TeamModelSelector: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ { - 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 = ({ {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 = ({ = ({ ) : null}
+ {activeProviderStatusPanel ? ( +
+
+ {activeProviderStatusPanel.tone === 'ready' ? ( + + ) : ( + + )} +
+

{activeProviderStatusPanel.title}

+

{activeProviderStatusPanel.summary}

+

{activeProviderStatusPanel.message}

+ {activeProviderStatusPanel.reason ? ( +

Reason: {activeProviderStatusPanel.reason}

+ ) : null} + {activeProviderStatusPanel.actionLabel ? ( + + ) : null} +
+
+
+ ) : null} {shouldAwaitRuntimeModelList ? (

Explicit models load from the current runtime. Default remains available while the diff --git a/src/renderer/types/notifications.ts b/src/renderer/types/notifications.ts index 784dbf76..1f9cf2b5 100644 --- a/src/renderer/types/notifications.ts +++ b/src/renderer/types/notifications.ts @@ -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. diff --git a/src/renderer/utils/sessionExporter.ts b/src/renderer/utils/sessionExporter.ts index 879568b1..94b8826d 100644 --- a/src/renderer/utils/sessionExporter.ts +++ b/src/renderer/utils/sessionExporter.ts @@ -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. diff --git a/src/shared/types/notifications.ts b/src/shared/types/notifications.ts index 8de0313f..2ff31565 100644 --- a/src/shared/types/notifications.ts +++ b/src/shared/types/notifications.ts @@ -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 diff --git a/src/shared/types/visualization.ts b/src/shared/types/visualization.ts index 4be1ad80..40cdb61c 100644 --- a/src/shared/types/visualization.ts +++ b/src/shared/types/visualization.ts @@ -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. diff --git a/test/main/build/electronBuilderAfterPack.test.ts b/test/main/build/electronBuilderAfterPack.test.ts index 8835dbfc..150cbaf8 100644 --- a/test/main/build/electronBuilderAfterPack.test.ts +++ b/test/main/build/electronBuilderAfterPack.test.ts @@ -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, diff --git a/test/main/build/electronBuilderDistScript.test.ts b/test/main/build/electronBuilderDistScript.test.ts index 79ae4377..520ec55a 100644 --- a/test/main/build/electronBuilderDistScript.test.ts +++ b/test/main/build/electronBuilderDistScript.test.ts @@ -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', ], }, ]); diff --git a/test/main/services/team/ClaudeBinaryResolver.test.ts b/test/main/services/team/ClaudeBinaryResolver.test.ts index fb0848c4..d8cd7eff 100644 --- a/test/main/services/team/ClaudeBinaryResolver.test.ts +++ b/test/main/services/team/ClaudeBinaryResolver.test.ts @@ -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' ); diff --git a/test/main/services/team/ClaudeDoctorProbe.test.ts b/test/main/services/team/ClaudeDoctorProbe.test.ts index 7b6f0639..83c9b820 100644 --- a/test/main/services/team/ClaudeDoctorProbe.test.ts +++ b/test/main/services/team/ClaudeDoctorProbe.test.ts @@ -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', ]); }); diff --git a/test/main/utils/electronUserDataMigration.test.ts b/test/main/utils/electronUserDataMigration.test.ts index d3096ff2..3b00d4ff 100644 --- a/test/main/utils/electronUserDataMigration.test.ts +++ b/test/main/utils/electronUserDataMigration.test.ts @@ -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'); diff --git a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts index 6476019b..220b2a0b 100644 --- a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts +++ b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts @@ -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(); diff --git a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts index 01af0818..4d7c8d95 100644 --- a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts +++ b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts @@ -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');