feat(app): rename display name to Agent Teams AI

This commit is contained in:
777genius 2026-05-19 12:55:33 +03:00
parent dc04cbfad7
commit 3e52008c7a
28 changed files with 544 additions and 96 deletions

View file

@ -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' }}

View file

@ -246,7 +246,7 @@
},
"build": {
"appId": "com.agent-teams.app",
"productName": "Agent Teams UI",
"productName": "Agent Teams AI",
"directories": {
"output": "release"
},

View file

@ -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) {

View file

@ -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;

View file

@ -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: {

View file

@ -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

View file

@ -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 } : {}),
});

View file

@ -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: {

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -172,7 +172,7 @@ export const AdvancedSection = ({
<div>
<div className="flex items-center gap-3">
<p className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
Agent Teams UI
Agent Teams AI
</p>
{isElectron && (
<button

View file

@ -1,13 +1,8 @@
import React from 'react';
import { Checkbox } from '@renderer/components/ui/checkbox';
import { HoverTooltip } from '@renderer/components/ui/hover-tooltip';
import { Label } from '@renderer/components/ui/label';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@renderer/components/ui/tooltip';
import { Info } from 'lucide-react';
interface LimitContextCheckboxProps {
@ -44,20 +39,15 @@ export const LimitContextCheckbox: React.FC<LimitContextCheckboxProps> = ({
) : null}
{disabled && <span className="text-[10px] italic">(always 200K for this model)</span>}
</Label>
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<Info
className={`size-3.5 shrink-0 ${disabled ? 'text-text-muted opacity-50' : 'text-text-muted hover:text-text-secondary'} cursor-help`}
/>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[260px]">
<p>
Enable this to cap Anthropic runtimes at 200K tokens. Leave it off only when you want
the selected Anthropic model or runtime to use a longer context window when available.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<HoverTooltip
content="Enable this to cap Anthropic runtimes at 200K tokens. Leave it off only when you want the selected Anthropic model or runtime to use a longer context window when available."
title="Enable this to cap Anthropic runtimes at 200K tokens."
contentClassName="max-w-[260px]"
>
<Info
className={`size-3.5 shrink-0 ${disabled ? 'text-text-muted opacity-50' : 'text-text-muted hover:text-text-secondary'} cursor-help`}
aria-hidden="true"
/>
</HoverTooltip>
</div>
);

View file

@ -177,6 +177,15 @@ function isModelDetail(lower: string): boolean {
return isSelectedModelDetail(lower) || isFormattedModelDetail(lower);
}
function isInternalProvisioningDetail(detail: string): boolean {
const normalized = detail.trim().toLowerCase();
return normalized === 'opencode_app_mcp_tool_proof_persisted_cache_hit';
}
function getPublicProvisioningDetails(details: string[]): string[] {
return details.filter((detail) => !isInternalProvisioningDetail(detail));
}
function getStatusLabel(status: ProvisioningProviderCheckStatus): string {
switch (status) {
case 'checking':
@ -418,12 +427,13 @@ function hasCompatibilityPendingDetails(checks: ProvisioningProviderCheck[]): bo
}
function getDisplayStatusText(check: ProvisioningProviderCheck): string {
const modelSummary = getModelDetailSummary(check.details);
const publicDetails = getPublicProvisioningDetails(check.details);
const modelSummary = getModelDetailSummary(publicDetails);
if (modelSummary) {
return modelSummary;
}
const summarizedDetails = check.details
const summarizedDetails = publicDetails
.map((detail) => summarizeDetail(detail, check.status))
.filter((detail): detail is ProvisioningDetailSummary => Boolean(detail));
@ -507,7 +517,8 @@ export function getPrimaryProvisioningFailureDetail(
continue;
}
const unavailableDetail = check.details.find((detail) =>
const publicDetails = getPublicProvisioningDetails(check.details);
const unavailableDetail = publicDetails.find((detail) =>
detail.toLowerCase().includes('selected model') &&
detail.toLowerCase().includes('is unavailable')
? true
@ -523,22 +534,23 @@ export function getPrimaryProvisioningFailureDetail(
continue;
}
const preferredFailure = check.details.find(
const publicDetails = getPublicProvisioningDetails(check.details);
const preferredFailure = publicDetails.find(
(detail) => getDetailTone(detail, check.status) === 'failure'
);
if (preferredFailure) {
return preferredFailure;
}
const nonSuccessDetail = check.details.find(
const nonSuccessDetail = publicDetails.find(
(detail) => getDetailTone(detail, check.status) !== 'success'
);
if (nonSuccessDetail) {
return nonSuccessDetail;
}
if (check.details.length > 0) {
return check.details[0];
if (publicDetails.length > 0) {
return publicDetails[0];
}
}
@ -671,8 +683,9 @@ export const ProvisioningProviderStatusList = ({
return (
<div className={`space-y-1 pl-5 ${className}`.trim()}>
{checks.map((check) => {
const visibleDetails = check.details.filter(
(detail) => detail.trim() !== (suppressDetailsMatching ?? '').trim()
const suppressDetailsMatchingTrimmed = (suppressDetailsMatching ?? '').trim();
const visibleDetails = getPublicProvisioningDetails(check.details).filter(
(detail) => detail.trim() !== suppressDetailsMatchingTrimmed
);
return (

View file

@ -333,6 +333,50 @@ export const OPENCODE_ONE_SHOT_DISABLED_REASON =
'OpenCode team launch is available for normal teams, but scheduled one-shot prompts still run through claude -p. Choose Anthropic, Codex, or Gemini for one-shot schedules.';
export const OPENCODE_ONE_SHOT_DISABLED_BADGE_LABEL = 'team only';
function getOpenCodeReadinessBadgeLabel(
providerStatus: CliProviderStatus | null | undefined
): string {
if (!providerStatus) {
return 'Check';
}
if (!providerStatus.supported) {
return 'Install';
}
if (!providerStatus.authenticated) {
return 'Auth';
}
return 'Setup';
}
function getOpenCodeReadinessSummary(providerStatus: CliProviderStatus | null | undefined): string {
if (!providerStatus) {
return 'OpenCode status: checking runtime';
}
const parts = [
providerStatus.supported ? 'runtime detected' : 'runtime missing',
providerStatus.authenticated ? 'provider connected' : 'provider not connected',
providerStatus.capabilities.teamLaunch ? 'team launch ready' : 'team launch blocked',
];
return `OpenCode status: ${parts.join(' · ')}`;
}
function getOpenCodeReadinessMessage(providerStatus: CliProviderStatus | null | undefined): string {
if (!providerStatus) {
return 'The app is still checking the OpenCode runtime. Wait for provider status to finish, then try again.';
}
if (!providerStatus.supported) {
return 'OpenCode is not installed, not found, or the detected runtime is not supported. Install or update OpenCode, then refresh provider status.';
}
if (!providerStatus.authenticated) {
return 'OpenCode is detected, but it does not have a connected provider. Connect a provider in OpenCode, then refresh provider status.';
}
if (!providerStatus.capabilities.teamLaunch) {
return 'OpenCode is installed and authenticated, but Agent Teams launch readiness is blocked.';
}
return 'OpenCode is ready for team launch.';
}
export function getTeamModelLabel(model: string): string {
return getCatalogTeamModelLabel(model) ?? model;
}
@ -427,7 +471,10 @@ const OpenCodeVirtualizedModelGrid = ({
return undefined;
}
const updateGridWidth = (): void => setGridWidth(element.clientWidth);
const updateGridWidth = (): void => {
const nextWidth = element.clientWidth;
setGridWidth((previousWidth) => (previousWidth === nextWidth ? previousWidth : nextWidth));
};
updateGridWidth();
if (typeof ResizeObserver !== 'undefined') {
@ -591,9 +638,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
const [selectedOpenCodeSourceIds, setSelectedOpenCodeSourceIds] = useState<Set<string>>(
() => new Set()
);
const effectiveProviderId =
const selectedProviderId =
disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId;
const [inspectedProviderId, setInspectedProviderId] = useState<TeamProviderId | null>(null);
const previousEffectiveProviderIdRef = useRef<TeamProviderId>(selectedProviderId);
const previousSelectedProviderIdRef = useRef<TeamProviderId>(selectedProviderId);
const effectiveProviderId = inspectedProviderId ?? selectedProviderId;
const isInspectingInactiveProvider = inspectedProviderId !== null;
const { cliStatus: effectiveCliStatus, providerStatus: runtimeProviderStatus } =
useEffectiveCliProviderStatus(effectiveProviderId);
const multimodelAvailable =
@ -624,12 +675,17 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
}
return 'Uses the runtime default for the selected provider.';
}, [effectiveProviderId, runtimeProviderStatus]);
const getProviderOverrideDisabledReason = (candidateProviderId: string): string | null => {
if (!isTeamProviderId(candidateProviderId)) {
return null;
}
return providerDisabledReasonById?.[candidateProviderId]?.trim() || null;
};
const getProviderDisabledReason = (candidateProviderId: string): string | null => {
if (isTeamProviderId(candidateProviderId)) {
const overrideReason = providerDisabledReasonById?.[candidateProviderId]?.trim();
if (overrideReason) {
return overrideReason;
}
const overrideReason = getProviderOverrideDisabledReason(candidateProviderId);
if (overrideReason) {
return overrideReason;
}
if (candidateProviderId === 'opencode') {
@ -670,6 +726,11 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
const isProviderSelectable = (candidateProviderId: string): boolean =>
!isProviderTemporarilyDisabled(candidateProviderId) &&
(multimodelAvailable || candidateProviderId === 'anthropic');
const isProviderInspectable = (candidateProviderId: string): boolean =>
candidateProviderId === 'opencode' &&
getProviderOverrideDisabledReason(candidateProviderId) === null &&
getProviderDisabledReason(candidateProviderId) !== null &&
multimodelAvailable;
const activeProviderSelectable = isProviderSelectable(effectiveProviderId);
const getProviderStatusBadge = (candidateProviderId: string): string | null => {
if (isTeamProviderId(candidateProviderId)) {
@ -681,7 +742,9 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
}
if (candidateProviderId === 'opencode') {
return getProviderDisabledReason(candidateProviderId) ? 'Gated' : null;
return getProviderDisabledReason(candidateProviderId)
? getOpenCodeReadinessBadgeLabel(runtimeProviderStatusById.get('opencode'))
: null;
}
const providerDisabledReason = getProviderDisabledReason(candidateProviderId);
@ -696,10 +759,6 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
return null;
};
const getProviderStatusBadgeLabel = (statusBadge: string | null): string | null => {
if (statusBadge === 'Gated') {
return 'Gate';
}
if (statusBadge === 'Multimodel off') {
return 'Off';
}
@ -717,10 +776,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
);
useEffect(() => {
if (isInspectingInactiveProvider) {
return;
}
if (normalizedValue !== value) {
onValueChange(normalizedValue);
}
}, [normalizedValue, onValueChange, value]);
}, [isInspectingInactiveProvider, normalizedValue, onValueChange, value]);
const modelOptions = useMemo(() => {
if (shouldAwaitRuntimeModelList) {
@ -784,29 +846,43 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
);
useEffect(() => {
if (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels) {
queueMicrotask(() => setRecommendedOnly(false));
if (previousSelectedProviderIdRef.current === selectedProviderId) {
return;
}
}, [effectiveProviderId, hasRecommendedOpenCodeModels]);
previousSelectedProviderIdRef.current = selectedProviderId;
setInspectedProviderId(null);
}, [selectedProviderId]);
useEffect(() => {
queueMicrotask(() => setModelQuery(''));
if (recommendedOnly && (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels)) {
setRecommendedOnly(false);
}
}, [effectiveProviderId, hasRecommendedOpenCodeModels, recommendedOnly]);
useEffect(() => {
if (previousEffectiveProviderIdRef.current === effectiveProviderId) {
return;
}
previousEffectiveProviderIdRef.current = effectiveProviderId;
setModelQuery('');
}, [effectiveProviderId]);
useEffect(() => {
if (effectiveProviderId !== 'opencode') {
queueMicrotask(() => {
setSelectedOpenCodeSourceIds(new Set());
setOpenCodeSourceFilterOpen(false);
});
if (effectiveProviderId === 'opencode') {
return;
}
}, [effectiveProviderId]);
if (selectedOpenCodeSourceIds.size === 0 && !openCodeSourceFilterOpen) {
return;
}
setSelectedOpenCodeSourceIds(new Set());
setOpenCodeSourceFilterOpen(false);
}, [effectiveProviderId, openCodeSourceFilterOpen, selectedOpenCodeSourceIds]);
useEffect(() => {
if (!openCodeSourceFilterOpen) {
queueMicrotask(() => setOpenCodeSourceQuery(''));
if (!openCodeSourceFilterOpen && openCodeSourceQuery) {
setOpenCodeSourceQuery('');
}
}, [openCodeSourceFilterOpen]);
}, [openCodeSourceFilterOpen, openCodeSourceQuery]);
const openCodeSourceOptions = useMemo<OpenCodeSourceOption[]>(() => {
if (effectiveProviderId !== 'opencode') {
@ -851,7 +927,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
Array.from(selectedOpenCodeSourceIds).filter((sourceId) => availableSourceIds.has(sourceId))
);
if (nextSelectedSourceIds.size !== selectedOpenCodeSourceIds.size) {
queueMicrotask(() => setSelectedOpenCodeSourceIds(nextSelectedSourceIds));
setSelectedOpenCodeSourceIds(nextSelectedSourceIds);
}
}, [openCodeSourceOptions, selectedOpenCodeSourceIds]);
@ -1008,6 +1084,32 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
effectiveProviderId === 'opencode' &&
!shouldShowOpenCodeCatalogLoading &&
visibleConcreteModelOptionCount > OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD;
const activeProviderDisabledReason = activeProviderSelectable
? null
: getProviderDisabledReason(effectiveProviderId);
const canActivateInspectedOpenCode =
effectiveProviderId === 'opencode' && isInspectingInactiveProvider && activeProviderSelectable;
const activeProviderStatusPanel =
activeProviderDisabledReason && effectiveProviderId === 'opencode'
? {
tone: 'warning' as const,
title: 'OpenCode is not ready for team launch',
summary: getOpenCodeReadinessSummary(runtimeProviderStatus),
message: getOpenCodeReadinessMessage(runtimeProviderStatus),
reason: activeProviderDisabledReason,
actionLabel: null,
}
: canActivateInspectedOpenCode
? {
tone: 'ready' as const,
title: 'OpenCode is ready',
summary: getOpenCodeReadinessSummary(runtimeProviderStatus),
message:
'OpenCode passed provider readiness. Select it to use OpenCode models for this team.',
reason: null,
actionLabel: 'Use OpenCode',
}
: null;
const getModelAdvisoryBadgeLabel = (reason: string | null): string =>
reason?.toLowerCase().includes('ping not confirmed') ? 'Ping not confirmed' : 'Note';
const renderModelOption = (opt: TeamRuntimeModelOption): React.JSX.Element => {
@ -1040,6 +1142,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
const hasBlockingModelIssue = Boolean(modelIssueReason || modelUnavailableReason);
const hasModelAdvisory = Boolean(modelAdvisoryReason) && !hasBlockingModelIssue;
const modelSelectable =
!isInspectingInactiveProvider &&
activeProviderSelectable &&
!modelUnavailableReason &&
!modelDisabledReason &&
@ -1227,8 +1330,16 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
<Tabs
value={effectiveProviderId}
onValueChange={(nextValue) => {
if (isTeamProviderId(nextValue) && isProviderSelectable(nextValue)) {
if (!isTeamProviderId(nextValue)) {
return;
}
if (isProviderSelectable(nextValue)) {
setInspectedProviderId(null);
onProviderChange(nextValue);
return;
}
if (isProviderInspectable(nextValue)) {
setInspectedProviderId(nextValue);
}
}}
>
@ -1238,6 +1349,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
{PROVIDERS.map((provider) => {
const providerDisabledReason = getProviderDisabledReason(provider.id);
const providerSelectable = isProviderSelectable(provider.id);
const providerInspectable = isProviderInspectable(provider.id);
const statusBadge = getProviderStatusBadge(provider.id);
const statusBadgeLabel = getProviderStatusBadgeLabel(statusBadge);
@ -1245,7 +1357,8 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
<TabsTrigger
key={provider.id}
value={provider.id}
disabled={provider.comingSoon || !providerSelectable}
disabled={provider.comingSoon || (!providerSelectable && !providerInspectable)}
aria-disabled={!providerSelectable || undefined}
title={
providerDisabledReason ??
(statusBadge === 'Multimodel off'
@ -1295,6 +1408,45 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
) : null}
<div className="p-3">
{activeProviderStatusPanel ? (
<div
data-testid="team-model-selector-provider-status"
className={cn(
'mb-3 rounded-md border px-3 py-2 text-[11px] leading-relaxed',
activeProviderStatusPanel.tone === 'ready'
? 'border-emerald-300/30 bg-emerald-300/10 text-emerald-100'
: 'border-amber-300/30 bg-amber-300/10 text-amber-100'
)}
>
<div className="flex items-start gap-2">
{activeProviderStatusPanel.tone === 'ready' ? (
<CheckCircle2 className="mt-0.5 size-3.5 shrink-0 text-emerald-200" />
) : (
<AlertTriangle className="mt-0.5 size-3.5 shrink-0 text-amber-200" />
)}
<div className="min-w-0 space-y-1">
<p className="font-medium">{activeProviderStatusPanel.title}</p>
<p className="opacity-90">{activeProviderStatusPanel.summary}</p>
<p>{activeProviderStatusPanel.message}</p>
{activeProviderStatusPanel.reason ? (
<p className="opacity-90">Reason: {activeProviderStatusPanel.reason}</p>
) : null}
{activeProviderStatusPanel.actionLabel ? (
<button
type="button"
className="mt-1 inline-flex h-7 items-center rounded-md border border-emerald-300/35 bg-emerald-300/10 px-2.5 text-[11px] font-medium text-emerald-100 transition-colors hover:border-emerald-200/50 hover:bg-emerald-300/15"
onClick={() => {
setInspectedProviderId(null);
onProviderChange('opencode');
}}
>
{activeProviderStatusPanel.actionLabel}
</button>
) : null}
</div>
</div>
</div>
) : null}
{shouldAwaitRuntimeModelList ? (
<p className="mb-2 text-[11px] text-[var(--color-text-muted)]">
Explicit models load from the current runtime. Default remains available while the

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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,

View file

@ -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',
],
},
]);

View file

@ -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'
);

View file

@ -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',
]);
});

View file

@ -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');

View file

@ -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();

View file

@ -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');