diff --git a/.agents/skills/karpathy-guidelines/SKILL.md b/.agents/skills/karpathy-guidelines/SKILL.md new file mode 100644 index 0000000000..2b7330f5b8 --- /dev/null +++ b/.agents/skills/karpathy-guidelines/SKILL.md @@ -0,0 +1,33 @@ +--- +name: karpathy-guidelines +description: Lightweight coding guardrails for making focused, simple, and verifiable changes in this repo. Use for all coding work. +--- + +# Karpathy Guidelines + +Use this skill whenever you touch code in this repository. + +## Principles + +- Keep the change small and directly tied to the user request. +- Prefer the simplest implementation that fits the existing codebase. +- Read the nearby code first, then match its patterns. +- Avoid unrelated refactors, broad rewrites, or style churn. +- Preserve existing behavior unless the user explicitly asked to change it. +- Treat regressions as a signal to narrow the change, not to add workaround layers. + +## Workflow + +1. Inspect the current implementation and tests around the change. +2. Make the smallest coherent edit. +3. Add or update focused tests when the behavior changes or the risk is non-trivial. +4. Run the narrowest relevant verification first. +5. Report exactly what was verified and anything left unverified. + +## Review Checklist + +- Does this change solve the stated problem without expanding scope? +- Did it preserve existing route/component/data-flow semantics? +- Are new abstractions justified by real complexity? +- Are tests focused on the behavior that could regress? +- Are unrelated files and generated artifacts left alone? diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index 58c4d11603..cca8b221f0 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -74,6 +74,14 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `https://docs.dify.ai${path}`, })) +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_CLOUD_EDITION: true, + } +}) + const mockPush = vi.fn() const mockSetShowPricingModal = vi.fn() const mockSetShowAccountSettingModal = vi.fn() @@ -198,7 +206,7 @@ describe('MainNav', () => { expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/explore/apps') expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps') expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets') - expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/tools') + expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/tools?section=provider') expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/plugins') }) diff --git a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx new file mode 100644 index 0000000000..a380aaea1d --- /dev/null +++ b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx @@ -0,0 +1,110 @@ +import type { AppContextValue } from '@/context/app-context' +import type { ModalContextState } from '@/context/modal-context' +import type { ProviderContextState } from '@/context/provider-context' +import { screen } from '@testing-library/react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' +import { Plan } from '@/app/components/billing/type' +import { useAppContext } from '@/context/app-context' +import { useModalContext } from '@/context/modal-context' +import { useProviderContext } from '@/context/provider-context' +import { useWorkspacesContext } from '@/context/workspace-context' +import WorkspaceCard from '../workspace-card' + +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_CLOUD_EDITION: false, + } +}) + +vi.mock('@/context/app-context', () => ({ + useAppContext: vi.fn(), +})) + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: vi.fn(), +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: vi.fn(), +})) + +vi.mock('@/context/workspace-context', () => ({ + useWorkspacesContext: vi.fn(), +})) + +vi.mock('@/service/common', () => ({ + switchWorkspace: vi.fn(), +})) + +const appContextValue: AppContextValue = { + userProfile: { + id: 'user-1', + name: 'Evan Z', + email: 'evan@example.com', + avatar: '', + avatar_url: '', + is_password_set: true, + }, + mutateUserProfile: vi.fn(), + currentWorkspace: { + id: 'workspace-1', + name: 'Solar Studio', + plan: Plan.sandbox, + status: 'normal', + created_at: 0, + role: 'owner', + providers: [], + trial_credits: 10000, + trial_credits_used: 2500, + next_credit_reset_date: 0, + }, + isCurrentWorkspaceManager: true, + isCurrentWorkspaceOwner: true, + isCurrentWorkspaceEditor: true, + isCurrentWorkspaceDatasetOperator: false, + mutateCurrentWorkspace: vi.fn(), + langGeniusVersionInfo: { + current_env: 'testing', + current_version: '1.0.0', + latest_version: '1.0.0', + release_date: '', + release_notes: '', + version: '1.0.0', + can_auto_update: false, + }, + useSelector: vi.fn(), + isLoadingCurrentWorkspace: false, + isValidatingCurrentWorkspace: false, +} + +describe('WorkspaceCard', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(useAppContext).mockReturnValue(appContextValue) + vi.mocked(useProviderContext).mockReturnValue({ + enableBilling: true, + isEducationAccount: false, + isFetchedPlan: true, + plan: { type: Plan.sandbox }, + } as ProviderContextState) + vi.mocked(useModalContext).mockReturnValue({ + setShowPricingModal: vi.fn(), + setShowAccountSettingModal: vi.fn(), + } as unknown as ModalContextState) + vi.mocked(useWorkspacesContext).mockReturnValue({ + workspaces: [ + { id: 'workspace-1', name: 'Solar Studio', plan: Plan.sandbox, status: 'normal', created_at: 0, current: true }, + ], + }) + }) + + it('hides cloud-only credits and upgrade actions outside cloud edition', () => { + renderWithSystemFeatures() + + expect(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: /common\.mainNav\.workspace\.credits/ })).not.toBeInTheDocument() + expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/main-nav/components/workspace-card.tsx b/web/app/components/main-nav/components/workspace-card.tsx index 98616cb63d..ab1be5d498 100644 --- a/web/app/components/main-nav/components/workspace-card.tsx +++ b/web/app/components/main-nav/components/workspace-card.tsx @@ -7,6 +7,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Plan } from '@/app/components/billing/type' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' +import { IS_CLOUD_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' @@ -113,34 +114,36 @@ const WorkspaceCard = () => { -
- - {enableBilling && ( + {IS_CLOUD_EDITION && ( +
- )} -
+ {enableBilling && ( + + )} +
+ )} {open && (
diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx index 9f9c3214d8..45092cd9fb 100644 --- a/web/app/components/main-nav/index.tsx +++ b/web/app/components/main-nav/index.tsx @@ -53,7 +53,7 @@ const MainNav = ({ activeIcon: 'i-custom-vender-main-nav-knowledge-active', }, { - href: '/tools', + href: '/tools?section=provider', label: t('mainNav.integrations', { ns: 'common' }), active: path => path.startsWith('/tools'), icon: 'i-custom-vender-main-nav-integrations', diff --git a/web/app/components/tools/__tests__/integrations-page.spec.tsx b/web/app/components/tools/__tests__/integrations-page.spec.tsx index cbf2761516..97bed53f85 100644 --- a/web/app/components/tools/__tests__/integrations-page.spec.tsx +++ b/web/app/components/tools/__tests__/integrations-page.spec.tsx @@ -45,6 +45,13 @@ describe('IntegrationsPage', () => { vi.clearAllMocks() }) + it('defaults to the model provider section when no query is provided', () => { + renderIntegrationsPage() + + expect(screen.getByTestId('model-provider-page')).toBeInTheDocument() + expect(screen.getAllByText('common.settings.provider')).toHaveLength(2) + }) + it('renders the model provider section from the section query', () => { renderIntegrationsPage({ section: 'provider' }) diff --git a/web/app/components/tools/integrations-page.tsx b/web/app/components/tools/integrations-page.tsx index c6db598d55..975b58bdd3 100644 --- a/web/app/components/tools/integrations-page.tsx +++ b/web/app/components/tools/integrations-page.tsx @@ -129,7 +129,7 @@ export default function IntegrationsPage() { const { t } = useTranslation() const [sectionParam] = useQueryState('section', parseAsIntegrationSection) const [categoryParam] = useQueryState('category', parseAsToolCategory) - const section = sectionParam ?? sectionByToolCategory[categoryParam ?? 'builtin'] + const section = sectionParam ?? (categoryParam ? sectionByToolCategory[categoryParam] : 'provider') const [providerSearchText, setProviderSearchText] = useState('') const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const providerItem = useMemo(() => ({