diff --git a/apps/desktop/e2e/first-run.spec.ts b/apps/desktop/e2e/first-run.spec.ts index ab87860eaf..35f7df8126 100644 --- a/apps/desktop/e2e/first-run.spec.ts +++ b/apps/desktop/e2e/first-run.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from './fixtures'; +import { PROVIDER_REGISTRY, RECOMMENDED_PROVIDER_TYPES } from '@maka/core'; /** * First-run flow: a brand-new workspace (empty userData) must boot to the main @@ -7,7 +8,30 @@ import { test, expect } from './fixtures'; * E2E isolation seam (MAKA_E2E_USER_DATA_DIR) and the fake-backend switch * (MAKA_E2E) that the rest of the suite depends on. */ -test('boots to the main window on a fresh workspace', async ({ emptyWindow: page }) => { +test('boots to registry recommendations and browses the shared provider catalog', async ({ emptyWindow: page }) => { await expect(page).toHaveTitle('Maka'); await expect(page.locator('#root')).not.toBeEmpty(); + + const providerRows = page.locator('.maka-firstrun-row'); + await expect(providerRows).toHaveCount(RECOMMENDED_PROVIDER_TYPES.length); + await expect(providerRows).toContainText( + RECOMMENDED_PROVIDER_TYPES.map((type) => PROVIDER_REGISTRY[type].label), + ); + + await page.getByRole('button', { name: '浏览全部服务商' }).click(); + + await expect(page.getByLabel('设置内容')).toBeVisible(); + await expect(page.getByRole('heading', { name: '添加服务商' })).toBeVisible(); + await expect(page.getByRole('tablist', { name: '模型供应商分类' })).toBeVisible(); + await expect(page.getByPlaceholder('搜索服务商')).toBeVisible(); + + await page.getByRole('button', { name: '返回模型连接' }).click(); + await expect(page.getByRole('button', { name: '添加服务商' })).toBeVisible(); + + const settingsNav = page.locator('[aria-label="设置分组"]'); + await settingsNav.getByText('外观', { exact: true }).click(); + await settingsNav.getByText('模型', { exact: true }).click(); + + await expect(page.getByRole('button', { name: '添加服务商' })).toBeVisible(); + await expect(page.getByPlaceholder('搜索服务商')).toHaveCount(0); }); diff --git a/apps/desktop/src/main/__tests__/onboarding-hero-copy.test.ts b/apps/desktop/src/main/__tests__/onboarding-hero-copy.test.ts index 3f6711cd9f..49b8fe2574 100644 --- a/apps/desktop/src/main/__tests__/onboarding-hero-copy.test.ts +++ b/apps/desktop/src/main/__tests__/onboarding-hero-copy.test.ts @@ -361,6 +361,14 @@ describe('getOnboardingSetupSteps — first-run AI setup guide', () => { }); describe('OnboardingHero Quick Chat draft lifecycle', () => { + it('renders first-run provider recommendations from the shared registry', async () => { + const hero = await readFile(new URL('../../../src/renderer/OnboardingHero.tsx', import.meta.url), 'utf8'); + + assert.match(hero, /RECOMMENDED_PROVIDER_TYPES/); + assert.match(hero, /RECOMMENDED_PROVIDER_TYPES\.map\(\(type\) =>/); + assert.doesNotMatch(hero, /const FEATURED\s*=/, 'onboarding must not own a parallel provider list'); + }); + it('keeps first-run form controls on shared UI primitives', async () => { const hero = await readFile(new URL('../../../src/renderer/OnboardingHero.tsx', import.meta.url), 'utf8'); const checklist = await readFile(new URL('../../../src/renderer/FirstRunChecklist.tsx', import.meta.url), 'utf8'); diff --git a/apps/desktop/src/main/__tests__/provider-navigation-contract.test.ts b/apps/desktop/src/main/__tests__/provider-navigation-contract.test.ts index c9bd5f2a68..1e34617bdf 100644 --- a/apps/desktop/src/main/__tests__/provider-navigation-contract.test.ts +++ b/apps/desktop/src/main/__tests__/provider-navigation-contract.test.ts @@ -4,6 +4,7 @@ import { resolve } from 'node:path'; import { describe, test } from 'node:test'; const PANEL = resolve(import.meta.dirname, '../../../src/renderer/settings/ProvidersPanel.tsx'); +const SETTINGS_SURFACE = resolve(import.meta.dirname, '../../../src/renderer/settings/settings-surface.tsx'); const PROVIDER_CSS = resolve(import.meta.dirname, '../../../src/renderer/styles/settings/provider-editor.css'); describe('Settings model provider page hierarchy', () => { @@ -49,4 +50,21 @@ describe('Settings model provider page hierarchy', () => { 'catalog search must stay compact without collapsing below the standard control height', ); }); + + test('consumes an external catalog request after its first loaded model-page mount', async () => { + const source = await readFile(SETTINGS_SURFACE, 'utf8'); + + assert.match(source, /useState\(props\.openProviderCatalog === true\)/); + assert.match( + source, + /if \(!loading && section === 'models' && providerCatalogRequested\) \{\s*setProviderCatalogRequested\(false\);\s*\}/, + 'the first loaded model page must consume the one-shot catalog intent', + ); + assert.match(source, /openProviderCatalog=\{providerCatalogRequested\}/); + assert.doesNotMatch( + source, + /openProviderCatalog=\{props\.openProviderCatalog\}/, + 'the shell request must not be replayed every time the model page remounts', + ); + }); }); diff --git a/apps/desktop/src/renderer/OnboardingHero.tsx b/apps/desktop/src/renderer/OnboardingHero.tsx index f0fd7978ee..2699fc2137 100644 --- a/apps/desktop/src/renderer/OnboardingHero.tsx +++ b/apps/desktop/src/renderer/OnboardingHero.tsx @@ -21,7 +21,7 @@ import { ArrowRight, ArrowUp, ChevronRight, RotateCcw, Sparkles, KeyRound, Settings as SettingsIcon, Cpu, AlertCircle, FolderOpen, Paperclip, X } from '@maka/ui/icons'; import { Fragment, useCallback, useEffect, useRef, useState, type ClipboardEvent, type DragEvent, type KeyboardEvent } from 'react'; -import type { LlmConnection, OnboardingState, ProviderType, QuickChatMode, SettingsSection } from '@maka/core'; +import { RECOMMENDED_PROVIDER_TYPES, type LlmConnection, type OnboardingState, type QuickChatMode, type SettingsSection } from '@maka/core'; import { Button, Item, @@ -91,23 +91,12 @@ const READY_HERO_COPY_BY_LOCALE: Record = [ - { type: 'anthropic', tag: 'Claude · Anthropic', recommended: true }, - { type: 'openai', tag: 'OpenAI' }, - { type: 'zai-coding-plan', tag: 'GLM Coding Plan · Z.ai' }, - { type: 'MiniMax', tag: 'MiniMax M-series' }, - { type: 'kimi-coding-plan', tag: 'Kimi · Moonshot' }, - { type: 'deepseek', tag: 'DeepSeek' }, - { type: 'ollama', tag: 'Ollama' }, -]; - export interface OnboardingHeroProps { state: OnboardingState; /** Open Settings with a specific section preselected. */ onOpenSettings: (section?: SettingsSection) => void; + /** Open the shared Settings provider catalog. */ + onBrowseProviders: () => void; /** * Quick Chat submit handler (PR110b `quickChat:start`). Only * called from the `ready_empty` branch. The caller is responsible @@ -174,6 +163,7 @@ export function OnboardingHero(props: OnboardingHeroProps) { return ( void; + onBrowseProviders: () => void; onRefreshConnections?: () => void; refreshConnectionsPending?: boolean; onSkip?: () => Promise | void; @@ -284,10 +275,10 @@ function NeedsConnectionHero(props: { providers are added without pushing the footer off-screen. */}
    - {FEATURED.map((entry) => { - const display = providerDisplay(entry.type); + {RECOMMENDED_PROVIDER_TYPES.map((type) => { + const display = providerDisplay(type); return ( -
  • +
  • - + - - {entry.tag} - {entry.recommended && ( - 常用 - )} - + {display.name} {display.description} @@ -322,8 +308,11 @@ function NeedsConnectionHero(props: { {/* Designer audit P2-15: the footer's primary 打开设置·模型 button duplicated what clicking any provider row above already does (the list header even says 点一个进入设置). One affordance per action — - the footer keeps only the two genuinely distinct paths. */} + the footer keeps only genuinely distinct paths. */}
diff --git a/apps/desktop/stories/onboarding.stories.tsx b/apps/desktop/stories/onboarding.stories.tsx index 6deb9b61be..ee1100306a 100644 --- a/apps/desktop/stories/onboarding.stories.tsx +++ b/apps/desktop/stories/onboarding.stories.tsx @@ -69,6 +69,7 @@ function heroProps(state: OnboardingState) { return { state, onOpenSettings: (_section?: SettingsSection) => undefined, + onBrowseProviders: () => undefined, onQuickChatSubmit: async () => true, connections, onRefreshConnections: async () => undefined,