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
|
run: ${{ matrix.dist_command }} --publish never
|
||||||
|
|
||||||
- name: Validate packaged bundle (macOS ${{ matrix.arch }})
|
- 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 }})
|
- 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
|
- name: Upload assets to release
|
||||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.agent-teams.app",
|
"appId": "com.agent-teams.app",
|
||||||
"productName": "Agent Teams UI",
|
"productName": "Agent Teams AI",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ const PLATFORM_ARGS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const LINUX_PACKAGE_NAME_OVERRIDES = [
|
const LINUX_PACKAGE_NAME_OVERRIDES = [
|
||||||
'--config.productName=Agent-Teams-UI',
|
'--config.productName=Agent-Teams-AI',
|
||||||
'--config.linux.desktop.entry.Name=Agent Teams UI',
|
'--config.linux.desktop.entry.Name=Agent Teams AI',
|
||||||
];
|
];
|
||||||
|
|
||||||
function buildElectronBuilderInvocations(argv) {
|
function buildElectronBuilderInvocations(argv) {
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,12 @@ function findExecutable(bundlePath, platform) {
|
||||||
const packageJson = fs.existsSync(packageJsonPath)
|
const packageJson = fs.existsSync(packageJsonPath)
|
||||||
? JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
? 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) {
|
for (const name of preferredNames) {
|
||||||
const candidate = path.join(bundlePath, name);
|
const candidate = path.join(bundlePath, name);
|
||||||
if (fs.existsSync(candidate)) return candidate;
|
if (fs.existsSync(candidate)) return candidate;
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ export class CodexAppServerClient {
|
||||||
{
|
{
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
name: 'agent-teams-ai',
|
name: 'agent-teams-ai',
|
||||||
title: 'Agent Teams UI',
|
title: 'Agent Teams AI',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Main process entry point for Agent Teams UI.
|
* Main process entry point for Agent Teams AI.
|
||||||
*
|
*
|
||||||
* Responsibilities:
|
* Responsibilities:
|
||||||
* - Initialize Electron app and main window
|
* - Initialize Electron app and main window
|
||||||
|
|
|
||||||
|
|
@ -1193,10 +1193,10 @@ export class NotificationManager extends EventEmitter {
|
||||||
logger.debug(`[test-notification] creating Notification (platform=${process.platform})`);
|
logger.debug(`[test-notification] creating Notification (platform=${process.platform})`);
|
||||||
const notification = new NotificationClass({
|
const notification = new NotificationClass({
|
||||||
title: 'Test Notification',
|
title: 'Test Notification',
|
||||||
...(isMac ? { subtitle: 'Agent Teams UI' } : {}),
|
...(isMac ? { subtitle: 'Agent Teams AI' } : {}),
|
||||||
body: isMac
|
body: isMac
|
||||||
? 'Notifications are working correctly!'
|
? 'Notifications are working correctly!'
|
||||||
: 'Agent Teams UI\nNotifications are working correctly!',
|
: 'Agent Teams AI\nNotifications are working correctly!',
|
||||||
...(iconPath ? { icon: iconPath } : {}),
|
...(iconPath ? { icon: iconPath } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export class CodexAppServerSessionFactory {
|
||||||
{
|
{
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
name: 'agent-teams-ai',
|
name: 'agent-teams-ai',
|
||||||
title: 'Agent Teams UI',
|
title: 'Agent Teams AI',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
},
|
},
|
||||||
capabilities: {
|
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
|
* Runs the HTTP server + API without Electron, suitable for Docker
|
||||||
* or any headless/remote environment. The renderer is served as
|
* 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:
|
* This module contains:
|
||||||
* - Chunk types (UserChunk, AIChunk, SystemChunk, CompactChunk)
|
* - 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:
|
* These types represent the application's domain model:
|
||||||
* - Projects and sessions
|
* - 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
|
* ParsedMessage is the application's internal representation after parsing
|
||||||
* raw JSONL entries. This module also contains type guards for classifying
|
* 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 = [
|
const LEGACY_USER_DATA_DIR_NAMES = [
|
||||||
'agent-teams-ai',
|
'agent-teams-ai',
|
||||||
|
'Agent Teams AI',
|
||||||
'Agent Teams UI',
|
'Agent Teams UI',
|
||||||
'Claude Agent Teams UI',
|
'Claude Agent Teams UI',
|
||||||
'claude-agent-teams-ui',
|
'claude-agent-teams-ui',
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ export const AdvancedSection = ({
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<p className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
<p className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
||||||
Agent Teams UI
|
Agent Teams AI
|
||||||
</p>
|
</p>
|
||||||
{isElectron && (
|
{isElectron && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Checkbox } from '@renderer/components/ui/checkbox';
|
import { Checkbox } from '@renderer/components/ui/checkbox';
|
||||||
|
import { HoverTooltip } from '@renderer/components/ui/hover-tooltip';
|
||||||
import { Label } from '@renderer/components/ui/label';
|
import { Label } from '@renderer/components/ui/label';
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@renderer/components/ui/tooltip';
|
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
interface LimitContextCheckboxProps {
|
interface LimitContextCheckboxProps {
|
||||||
|
|
@ -44,20 +39,15 @@ export const LimitContextCheckbox: React.FC<LimitContextCheckboxProps> = ({
|
||||||
) : null}
|
) : null}
|
||||||
{disabled && <span className="text-[10px] italic">(always 200K for this model)</span>}
|
{disabled && <span className="text-[10px] italic">(always 200K for this model)</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<TooltipProvider delayDuration={200}>
|
<HoverTooltip
|
||||||
<Tooltip>
|
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."
|
||||||
<TooltipTrigger asChild>
|
title="Enable this to cap Anthropic runtimes at 200K tokens."
|
||||||
<Info
|
contentClassName="max-w-[260px]"
|
||||||
className={`size-3.5 shrink-0 ${disabled ? 'text-text-muted opacity-50' : 'text-text-muted hover:text-text-secondary'} cursor-help`}
|
>
|
||||||
/>
|
<Info
|
||||||
</TooltipTrigger>
|
className={`size-3.5 shrink-0 ${disabled ? 'text-text-muted opacity-50' : 'text-text-muted hover:text-text-secondary'} cursor-help`}
|
||||||
<TooltipContent side="top" className="max-w-[260px]">
|
aria-hidden="true"
|
||||||
<p>
|
/>
|
||||||
Enable this to cap Anthropic runtimes at 200K tokens. Leave it off only when you want
|
</HoverTooltip>
|
||||||
the selected Anthropic model or runtime to use a longer context window when available.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,15 @@ function isModelDetail(lower: string): boolean {
|
||||||
return isSelectedModelDetail(lower) || isFormattedModelDetail(lower);
|
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 {
|
function getStatusLabel(status: ProvisioningProviderCheckStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'checking':
|
case 'checking':
|
||||||
|
|
@ -418,12 +427,13 @@ function hasCompatibilityPendingDetails(checks: ProvisioningProviderCheck[]): bo
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayStatusText(check: ProvisioningProviderCheck): string {
|
function getDisplayStatusText(check: ProvisioningProviderCheck): string {
|
||||||
const modelSummary = getModelDetailSummary(check.details);
|
const publicDetails = getPublicProvisioningDetails(check.details);
|
||||||
|
const modelSummary = getModelDetailSummary(publicDetails);
|
||||||
if (modelSummary) {
|
if (modelSummary) {
|
||||||
return modelSummary;
|
return modelSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summarizedDetails = check.details
|
const summarizedDetails = publicDetails
|
||||||
.map((detail) => summarizeDetail(detail, check.status))
|
.map((detail) => summarizeDetail(detail, check.status))
|
||||||
.filter((detail): detail is ProvisioningDetailSummary => Boolean(detail));
|
.filter((detail): detail is ProvisioningDetailSummary => Boolean(detail));
|
||||||
|
|
||||||
|
|
@ -507,7 +517,8 @@ export function getPrimaryProvisioningFailureDetail(
|
||||||
continue;
|
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('selected model') &&
|
||||||
detail.toLowerCase().includes('is unavailable')
|
detail.toLowerCase().includes('is unavailable')
|
||||||
? true
|
? true
|
||||||
|
|
@ -523,22 +534,23 @@ export function getPrimaryProvisioningFailureDetail(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferredFailure = check.details.find(
|
const publicDetails = getPublicProvisioningDetails(check.details);
|
||||||
|
const preferredFailure = publicDetails.find(
|
||||||
(detail) => getDetailTone(detail, check.status) === 'failure'
|
(detail) => getDetailTone(detail, check.status) === 'failure'
|
||||||
);
|
);
|
||||||
if (preferredFailure) {
|
if (preferredFailure) {
|
||||||
return preferredFailure;
|
return preferredFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonSuccessDetail = check.details.find(
|
const nonSuccessDetail = publicDetails.find(
|
||||||
(detail) => getDetailTone(detail, check.status) !== 'success'
|
(detail) => getDetailTone(detail, check.status) !== 'success'
|
||||||
);
|
);
|
||||||
if (nonSuccessDetail) {
|
if (nonSuccessDetail) {
|
||||||
return nonSuccessDetail;
|
return nonSuccessDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (check.details.length > 0) {
|
if (publicDetails.length > 0) {
|
||||||
return check.details[0];
|
return publicDetails[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -671,8 +683,9 @@ export const ProvisioningProviderStatusList = ({
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-1 pl-5 ${className}`.trim()}>
|
<div className={`space-y-1 pl-5 ${className}`.trim()}>
|
||||||
{checks.map((check) => {
|
{checks.map((check) => {
|
||||||
const visibleDetails = check.details.filter(
|
const suppressDetailsMatchingTrimmed = (suppressDetailsMatching ?? '').trim();
|
||||||
(detail) => detail.trim() !== (suppressDetailsMatching ?? '').trim()
|
const visibleDetails = getPublicProvisioningDetails(check.details).filter(
|
||||||
|
(detail) => detail.trim() !== suppressDetailsMatchingTrimmed
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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.';
|
'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';
|
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 {
|
export function getTeamModelLabel(model: string): string {
|
||||||
return getCatalogTeamModelLabel(model) ?? model;
|
return getCatalogTeamModelLabel(model) ?? model;
|
||||||
}
|
}
|
||||||
|
|
@ -427,7 +471,10 @@ const OpenCodeVirtualizedModelGrid = ({
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateGridWidth = (): void => setGridWidth(element.clientWidth);
|
const updateGridWidth = (): void => {
|
||||||
|
const nextWidth = element.clientWidth;
|
||||||
|
setGridWidth((previousWidth) => (previousWidth === nextWidth ? previousWidth : nextWidth));
|
||||||
|
};
|
||||||
updateGridWidth();
|
updateGridWidth();
|
||||||
|
|
||||||
if (typeof ResizeObserver !== 'undefined') {
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
|
@ -591,9 +638,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
const [selectedOpenCodeSourceIds, setSelectedOpenCodeSourceIds] = useState<Set<string>>(
|
const [selectedOpenCodeSourceIds, setSelectedOpenCodeSourceIds] = useState<Set<string>>(
|
||||||
() => new Set()
|
() => new Set()
|
||||||
);
|
);
|
||||||
|
const selectedProviderId =
|
||||||
const effectiveProviderId =
|
|
||||||
disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId;
|
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 } =
|
const { cliStatus: effectiveCliStatus, providerStatus: runtimeProviderStatus } =
|
||||||
useEffectiveCliProviderStatus(effectiveProviderId);
|
useEffectiveCliProviderStatus(effectiveProviderId);
|
||||||
const multimodelAvailable =
|
const multimodelAvailable =
|
||||||
|
|
@ -624,12 +675,17 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
}
|
}
|
||||||
return 'Uses the runtime default for the selected provider.';
|
return 'Uses the runtime default for the selected provider.';
|
||||||
}, [effectiveProviderId, runtimeProviderStatus]);
|
}, [effectiveProviderId, runtimeProviderStatus]);
|
||||||
|
const getProviderOverrideDisabledReason = (candidateProviderId: string): string | null => {
|
||||||
|
if (!isTeamProviderId(candidateProviderId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerDisabledReasonById?.[candidateProviderId]?.trim() || null;
|
||||||
|
};
|
||||||
const getProviderDisabledReason = (candidateProviderId: string): string | null => {
|
const getProviderDisabledReason = (candidateProviderId: string): string | null => {
|
||||||
if (isTeamProviderId(candidateProviderId)) {
|
const overrideReason = getProviderOverrideDisabledReason(candidateProviderId);
|
||||||
const overrideReason = providerDisabledReasonById?.[candidateProviderId]?.trim();
|
if (overrideReason) {
|
||||||
if (overrideReason) {
|
return overrideReason;
|
||||||
return overrideReason;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidateProviderId === 'opencode') {
|
if (candidateProviderId === 'opencode') {
|
||||||
|
|
@ -670,6 +726,11 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
const isProviderSelectable = (candidateProviderId: string): boolean =>
|
const isProviderSelectable = (candidateProviderId: string): boolean =>
|
||||||
!isProviderTemporarilyDisabled(candidateProviderId) &&
|
!isProviderTemporarilyDisabled(candidateProviderId) &&
|
||||||
(multimodelAvailable || candidateProviderId === 'anthropic');
|
(multimodelAvailable || candidateProviderId === 'anthropic');
|
||||||
|
const isProviderInspectable = (candidateProviderId: string): boolean =>
|
||||||
|
candidateProviderId === 'opencode' &&
|
||||||
|
getProviderOverrideDisabledReason(candidateProviderId) === null &&
|
||||||
|
getProviderDisabledReason(candidateProviderId) !== null &&
|
||||||
|
multimodelAvailable;
|
||||||
const activeProviderSelectable = isProviderSelectable(effectiveProviderId);
|
const activeProviderSelectable = isProviderSelectable(effectiveProviderId);
|
||||||
const getProviderStatusBadge = (candidateProviderId: string): string | null => {
|
const getProviderStatusBadge = (candidateProviderId: string): string | null => {
|
||||||
if (isTeamProviderId(candidateProviderId)) {
|
if (isTeamProviderId(candidateProviderId)) {
|
||||||
|
|
@ -681,7 +742,9 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidateProviderId === 'opencode') {
|
if (candidateProviderId === 'opencode') {
|
||||||
return getProviderDisabledReason(candidateProviderId) ? 'Gated' : null;
|
return getProviderDisabledReason(candidateProviderId)
|
||||||
|
? getOpenCodeReadinessBadgeLabel(runtimeProviderStatusById.get('opencode'))
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerDisabledReason = getProviderDisabledReason(candidateProviderId);
|
const providerDisabledReason = getProviderDisabledReason(candidateProviderId);
|
||||||
|
|
@ -696,10 +759,6 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
const getProviderStatusBadgeLabel = (statusBadge: string | null): string | null => {
|
const getProviderStatusBadgeLabel = (statusBadge: string | null): string | null => {
|
||||||
if (statusBadge === 'Gated') {
|
|
||||||
return 'Gate';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusBadge === 'Multimodel off') {
|
if (statusBadge === 'Multimodel off') {
|
||||||
return 'Off';
|
return 'Off';
|
||||||
}
|
}
|
||||||
|
|
@ -717,10 +776,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isInspectingInactiveProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (normalizedValue !== value) {
|
if (normalizedValue !== value) {
|
||||||
onValueChange(normalizedValue);
|
onValueChange(normalizedValue);
|
||||||
}
|
}
|
||||||
}, [normalizedValue, onValueChange, value]);
|
}, [isInspectingInactiveProvider, normalizedValue, onValueChange, value]);
|
||||||
|
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
if (shouldAwaitRuntimeModelList) {
|
if (shouldAwaitRuntimeModelList) {
|
||||||
|
|
@ -784,29 +846,43 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels) {
|
if (previousSelectedProviderIdRef.current === selectedProviderId) {
|
||||||
queueMicrotask(() => setRecommendedOnly(false));
|
return;
|
||||||
}
|
}
|
||||||
}, [effectiveProviderId, hasRecommendedOpenCodeModels]);
|
previousSelectedProviderIdRef.current = selectedProviderId;
|
||||||
|
setInspectedProviderId(null);
|
||||||
|
}, [selectedProviderId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queueMicrotask(() => setModelQuery(''));
|
if (recommendedOnly && (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels)) {
|
||||||
|
setRecommendedOnly(false);
|
||||||
|
}
|
||||||
|
}, [effectiveProviderId, hasRecommendedOpenCodeModels, recommendedOnly]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (previousEffectiveProviderIdRef.current === effectiveProviderId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previousEffectiveProviderIdRef.current = effectiveProviderId;
|
||||||
|
setModelQuery('');
|
||||||
}, [effectiveProviderId]);
|
}, [effectiveProviderId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (effectiveProviderId !== 'opencode') {
|
if (effectiveProviderId === 'opencode') {
|
||||||
queueMicrotask(() => {
|
return;
|
||||||
setSelectedOpenCodeSourceIds(new Set());
|
|
||||||
setOpenCodeSourceFilterOpen(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [effectiveProviderId]);
|
if (selectedOpenCodeSourceIds.size === 0 && !openCodeSourceFilterOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedOpenCodeSourceIds(new Set());
|
||||||
|
setOpenCodeSourceFilterOpen(false);
|
||||||
|
}, [effectiveProviderId, openCodeSourceFilterOpen, selectedOpenCodeSourceIds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!openCodeSourceFilterOpen) {
|
if (!openCodeSourceFilterOpen && openCodeSourceQuery) {
|
||||||
queueMicrotask(() => setOpenCodeSourceQuery(''));
|
setOpenCodeSourceQuery('');
|
||||||
}
|
}
|
||||||
}, [openCodeSourceFilterOpen]);
|
}, [openCodeSourceFilterOpen, openCodeSourceQuery]);
|
||||||
|
|
||||||
const openCodeSourceOptions = useMemo<OpenCodeSourceOption[]>(() => {
|
const openCodeSourceOptions = useMemo<OpenCodeSourceOption[]>(() => {
|
||||||
if (effectiveProviderId !== 'opencode') {
|
if (effectiveProviderId !== 'opencode') {
|
||||||
|
|
@ -851,7 +927,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
Array.from(selectedOpenCodeSourceIds).filter((sourceId) => availableSourceIds.has(sourceId))
|
Array.from(selectedOpenCodeSourceIds).filter((sourceId) => availableSourceIds.has(sourceId))
|
||||||
);
|
);
|
||||||
if (nextSelectedSourceIds.size !== selectedOpenCodeSourceIds.size) {
|
if (nextSelectedSourceIds.size !== selectedOpenCodeSourceIds.size) {
|
||||||
queueMicrotask(() => setSelectedOpenCodeSourceIds(nextSelectedSourceIds));
|
setSelectedOpenCodeSourceIds(nextSelectedSourceIds);
|
||||||
}
|
}
|
||||||
}, [openCodeSourceOptions, selectedOpenCodeSourceIds]);
|
}, [openCodeSourceOptions, selectedOpenCodeSourceIds]);
|
||||||
|
|
||||||
|
|
@ -1008,6 +1084,32 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
effectiveProviderId === 'opencode' &&
|
effectiveProviderId === 'opencode' &&
|
||||||
!shouldShowOpenCodeCatalogLoading &&
|
!shouldShowOpenCodeCatalogLoading &&
|
||||||
visibleConcreteModelOptionCount > OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD;
|
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 =>
|
const getModelAdvisoryBadgeLabel = (reason: string | null): string =>
|
||||||
reason?.toLowerCase().includes('ping not confirmed') ? 'Ping not confirmed' : 'Note';
|
reason?.toLowerCase().includes('ping not confirmed') ? 'Ping not confirmed' : 'Note';
|
||||||
const renderModelOption = (opt: TeamRuntimeModelOption): React.JSX.Element => {
|
const renderModelOption = (opt: TeamRuntimeModelOption): React.JSX.Element => {
|
||||||
|
|
@ -1040,6 +1142,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
const hasBlockingModelIssue = Boolean(modelIssueReason || modelUnavailableReason);
|
const hasBlockingModelIssue = Boolean(modelIssueReason || modelUnavailableReason);
|
||||||
const hasModelAdvisory = Boolean(modelAdvisoryReason) && !hasBlockingModelIssue;
|
const hasModelAdvisory = Boolean(modelAdvisoryReason) && !hasBlockingModelIssue;
|
||||||
const modelSelectable =
|
const modelSelectable =
|
||||||
|
!isInspectingInactiveProvider &&
|
||||||
activeProviderSelectable &&
|
activeProviderSelectable &&
|
||||||
!modelUnavailableReason &&
|
!modelUnavailableReason &&
|
||||||
!modelDisabledReason &&
|
!modelDisabledReason &&
|
||||||
|
|
@ -1227,8 +1330,16 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
<Tabs
|
<Tabs
|
||||||
value={effectiveProviderId}
|
value={effectiveProviderId}
|
||||||
onValueChange={(nextValue) => {
|
onValueChange={(nextValue) => {
|
||||||
if (isTeamProviderId(nextValue) && isProviderSelectable(nextValue)) {
|
if (!isTeamProviderId(nextValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isProviderSelectable(nextValue)) {
|
||||||
|
setInspectedProviderId(null);
|
||||||
onProviderChange(nextValue);
|
onProviderChange(nextValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isProviderInspectable(nextValue)) {
|
||||||
|
setInspectedProviderId(nextValue);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -1238,6 +1349,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
{PROVIDERS.map((provider) => {
|
{PROVIDERS.map((provider) => {
|
||||||
const providerDisabledReason = getProviderDisabledReason(provider.id);
|
const providerDisabledReason = getProviderDisabledReason(provider.id);
|
||||||
const providerSelectable = isProviderSelectable(provider.id);
|
const providerSelectable = isProviderSelectable(provider.id);
|
||||||
|
const providerInspectable = isProviderInspectable(provider.id);
|
||||||
const statusBadge = getProviderStatusBadge(provider.id);
|
const statusBadge = getProviderStatusBadge(provider.id);
|
||||||
const statusBadgeLabel = getProviderStatusBadgeLabel(statusBadge);
|
const statusBadgeLabel = getProviderStatusBadgeLabel(statusBadge);
|
||||||
|
|
||||||
|
|
@ -1245,7 +1357,8 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
value={provider.id}
|
value={provider.id}
|
||||||
disabled={provider.comingSoon || !providerSelectable}
|
disabled={provider.comingSoon || (!providerSelectable && !providerInspectable)}
|
||||||
|
aria-disabled={!providerSelectable || undefined}
|
||||||
title={
|
title={
|
||||||
providerDisabledReason ??
|
providerDisabledReason ??
|
||||||
(statusBadge === 'Multimodel off'
|
(statusBadge === 'Multimodel off'
|
||||||
|
|
@ -1295,6 +1408,45 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="p-3">
|
<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 ? (
|
{shouldAwaitRuntimeModelList ? (
|
||||||
<p className="mb-2 text-[11px] text-[var(--color-text-muted)]">
|
<p className="mb-2 text-[11px] text-[var(--color-text-muted)]">
|
||||||
Explicit models load from the current runtime. Default remains available while the
|
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.
|
* Re-exports types from shared for backwards compatibility.
|
||||||
* The canonical definitions are in @shared/types/notifications.
|
* 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,
|
* Provides formatters to export session data as plain text, Markdown, or JSON,
|
||||||
* and a download trigger for browser-based file saving.
|
* 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:
|
* These types define:
|
||||||
* - Detected errors from session files
|
* - 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
|
* These types are used for waterfall chart visualization
|
||||||
* and are shared between main and renderer processes.
|
* and are shared between main and renderer processes.
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ describe('electron-builder afterPack', () => {
|
||||||
tempDirs.push(tempDir);
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
writeFile(
|
writeFile(
|
||||||
path.join(tempDir, 'Contents', 'MacOS', 'Agent Teams UI'),
|
path.join(tempDir, 'Contents', 'MacOS', 'Agent Teams AI'),
|
||||||
createMachOBuffer('arm64')
|
createMachOBuffer('arm64')
|
||||||
);
|
);
|
||||||
writeFile(
|
writeFile(
|
||||||
|
|
@ -229,7 +229,7 @@ describe('electron-builder afterPack', () => {
|
||||||
const tempDir = createTempDir();
|
const tempDir = createTempDir();
|
||||||
tempDirs.push(tempDir);
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
writeFile(path.join(tempDir, 'Agent Teams UI.exe'), createPortableExecutableBuffer('x64'));
|
writeFile(path.join(tempDir, 'Agent Teams AI.exe'), createPortableExecutableBuffer('x64'));
|
||||||
writeFile(
|
writeFile(
|
||||||
path.join(
|
path.join(
|
||||||
tempDir,
|
tempDir,
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ describe('electron-builder dist wrapper', () => {
|
||||||
'--linux',
|
'--linux',
|
||||||
'--publish',
|
'--publish',
|
||||||
'never',
|
'never',
|
||||||
'--config.productName=Agent-Teams-UI',
|
'--config.productName=Agent-Teams-AI',
|
||||||
'--config.linux.desktop.entry.Name=Agent Teams UI',
|
'--config.linux.desktop.entry.Name=Agent Teams AI',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -29,8 +29,8 @@ describe('electron-builder dist wrapper', () => {
|
||||||
'--linux',
|
'--linux',
|
||||||
'--publish',
|
'--publish',
|
||||||
'never',
|
'never',
|
||||||
'--config.productName=Agent-Teams-UI',
|
'--config.productName=Agent-Teams-AI',
|
||||||
'--config.linux.desktop.entry.Name=Agent Teams UI',
|
'--config.linux.desktop.entry.Name=Agent Teams AI',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ describe('ClaudeBinaryResolver', () => {
|
||||||
});
|
});
|
||||||
process.cwd = vi.fn(() => workspaceRoot);
|
process.cwd = vi.fn(() => workspaceRoot);
|
||||||
Object.defineProperty(process, 'resourcesPath', {
|
Object.defineProperty(process, 'resourcesPath', {
|
||||||
value: '/Applications/Agent Teams UI.app/Contents/Resources',
|
value: '/Applications/Agent Teams AI.app/Contents/Resources',
|
||||||
configurable: true,
|
configurable: true,
|
||||||
writable: true,
|
writable: true,
|
||||||
});
|
});
|
||||||
|
|
@ -219,7 +219,7 @@ describe('ClaudeBinaryResolver', () => {
|
||||||
|
|
||||||
it('prefers the bundled runtime binary for packaged agent_teams_orchestrator builds', async () => {
|
it('prefers the bundled runtime binary for packaged agent_teams_orchestrator builds', async () => {
|
||||||
const expectedBinary = path.join(
|
const expectedBinary = path.join(
|
||||||
'/Applications/Agent Teams UI.app/Contents/Resources',
|
'/Applications/Agent Teams AI.app/Contents/Resources',
|
||||||
'runtime',
|
'runtime',
|
||||||
'claude-multimodel'
|
'claude-multimodel'
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,14 @@ describe('ClaudeDoctorProbe', () => {
|
||||||
\u001B[2J────────────────────────────────────
|
\u001B[2J────────────────────────────────────
|
||||||
Diagnostics
|
Diagnostics
|
||||||
└ Invoked: /Applications/Agent Teams${' '}
|
└ Invoked: /Applications/Agent Teams${' '}
|
||||||
UI.app/Contents/Resources/runtime/clau
|
AI.app/Contents/Resources/runtime/clau
|
||||||
de-multimodel
|
de-multimodel
|
||||||
└ Config install method: native
|
└ Config install method: native
|
||||||
Press Enter to continue…
|
Press Enter to continue…
|
||||||
`;
|
`;
|
||||||
|
|
||||||
expect(extractDoctorInvokedCandidates(output)).toEqual([
|
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([
|
expect(getLegacyElectronUserDataCandidates(currentPath)).toEqual([
|
||||||
path.join(parentPath, 'agent-teams-ai'),
|
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-agent-teams-ui'),
|
path.join(parentPath, 'claude-agent-teams-ui'),
|
||||||
path.join(parentPath, 'claude-devtools'),
|
path.join(parentPath, 'claude-devtools'),
|
||||||
|
|
@ -291,6 +292,34 @@ describe('electron userData migration', () => {
|
||||||
expect(readFile(currentProductPath, 'data/attachments/team-a/old.txt')).toBe('old');
|
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 () => {
|
it('copies legacy app-owned state and durable renderer storage without Chromium caches', async () => {
|
||||||
const root = createTempRoot();
|
const root = createTempRoot();
|
||||||
const legacyPath = path.join(root, 'Claude Agent Teams UI');
|
const legacyPath = path.join(root, 'Claude Agent Teams UI');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { act } from 'react';
|
import React, { act } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
|
import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
|
||||||
|
|
@ -29,11 +30,13 @@ vi.mock('@renderer/components/ui/tabs', () => {
|
||||||
value,
|
value,
|
||||||
disabled,
|
disabled,
|
||||||
title,
|
title,
|
||||||
|
'aria-disabled': ariaDisabled,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
'aria-disabled'?: boolean;
|
||||||
}) =>
|
}) =>
|
||||||
React.createElement(
|
React.createElement(
|
||||||
'button',
|
'button',
|
||||||
|
|
@ -41,6 +44,7 @@ vi.mock('@renderer/components/ui/tabs', () => {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
disabled,
|
disabled,
|
||||||
title,
|
title,
|
||||||
|
'aria-disabled': ariaDisabled,
|
||||||
'data-state': currentValue === value ? 'active' : 'inactive',
|
'data-state': currentValue === value ? 'active' : 'inactive',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (!disabled) {
|
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);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
@ -1309,7 +1313,8 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
const buttons = Array.from(host.querySelectorAll('button'));
|
const buttons = Array.from(host.querySelectorAll('button'));
|
||||||
const openCodeButton = buttons.find((button) => button.textContent?.includes('OpenCode'));
|
const openCodeButton = buttons.find((button) => button.textContent?.includes('OpenCode'));
|
||||||
expect(openCodeButton).not.toBeNull();
|
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(
|
expect(openCodeButton?.getAttribute('title')).toContain(
|
||||||
'OpenCode runtime status is still loading.'
|
'OpenCode runtime status is still loading.'
|
||||||
);
|
);
|
||||||
|
|
@ -1320,6 +1325,15 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onProviderChange).not.toHaveBeenCalled();
|
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 () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
@ -1361,11 +1375,217 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
const openCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
const openCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
button.textContent?.includes('OpenCode')
|
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(
|
expect(openCodeButton?.getAttribute('title')).toContain(
|
||||||
'OpenCode runtime store needs recovery'
|
'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 () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import React, { act } from 'react';
|
import React, { act } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createInitialProviderChecks,
|
||||||
deriveEffectiveProvisioningPrepareState,
|
deriveEffectiveProvisioningPrepareState,
|
||||||
getPrimaryProvisioningFailureDetail,
|
getPrimaryProvisioningFailureDetail,
|
||||||
getProvisioningFailureHint,
|
getProvisioningFailureHint,
|
||||||
getProvisioningProviderBackendSummary,
|
getProvisioningProviderBackendSummary,
|
||||||
ProvisioningProviderStatusList,
|
ProvisioningProviderStatusList,
|
||||||
createInitialProviderChecks,
|
|
||||||
} from '@renderer/components/team/dialogs/ProvisioningProviderStatusList';
|
} from '@renderer/components/team/dialogs/ProvisioningProviderStatusList';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
describe('ProvisioningProviderStatusList', () => {
|
describe('ProvisioningProviderStatusList', () => {
|
||||||
afterEach(() => {
|
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 () => {
|
it('summarizes OpenCode busy model checks as deferred notes', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue