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 && ({activeProviderStatusPanel.title}
+{activeProviderStatusPanel.summary}
+{activeProviderStatusPanel.message}
+ {activeProviderStatusPanel.reason ? ( +Reason: {activeProviderStatusPanel.reason}
+ ) : null} + {activeProviderStatusPanel.actionLabel ? ( + + ) : null} +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');