diff --git a/src/renderer/src/components/settings/ZcodeCliSetupSection.test.tsx b/src/renderer/src/components/settings/ZcodeCliSetupSection.test.tsx new file mode 100644 index 00000000000..b8f37d86883 --- /dev/null +++ b/src/renderer/src/components/settings/ZcodeCliSetupSection.test.tsx @@ -0,0 +1,74 @@ +// @vitest-environment happy-dom + +import '@testing-library/jest-dom/vitest' + +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ZcodeCliSetupSection } from './ZcodeCliSetupSection' + +const mocks = vi.hoisted(() => ({ settings: { agentCmdOverrides: { zcode: '' } } })) +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: unknown) => unknown) => selector(mocks) +})) +vi.mock('../onboarding/OnboardingInlineCommandTerminal', () => ({ + OnboardingInlineCommandTerminal: ({ + command, + description + }: { + command: string + description: string + }) => ( +
+ {command} +

{description}

+
+ ) +})) +vi.mock('./SearchableSetting', () => ({ + SearchableSetting: ({ children }: { children: React.ReactNode }) =>
{children}
+})) + +beforeEach(() => { + mocks.settings.agentCmdOverrides.zcode = '' +}) +afterEach(() => cleanup()) + +describe('ZCode CLI-owned sign-in setup', () => { + it('starts no terminal until the user opens a supported flow', () => { + render() + expect(screen.queryByTestId('setup-terminal')).not.toBeInTheDocument() + expect(screen.getByText(/ZCode’s masked API-key setup/)).toBeInTheDocument() + expect(screen.getByText(/use \/model to choose a GLM model/)).toBeInTheDocument() + }) + + it.each([ + ['Z.AI browser sign-in', 'zcode login zai --no-browser'], + ['BigModel browser sign-in', 'zcode login bigmodel --no-browser'], + ['ZCode API-key setup', 'zcode'] + ])('delegates %s to the supported upstream CLI command', async (label, command) => { + render() + fireEvent.click(screen.getByRole('button', { name: label })) + expect(await screen.findByTestId('setup-terminal')).toHaveTextContent(command) + expect(screen.getByRole('button', { name: 'Z.AI browser sign-in' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'BigModel browser sign-in' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'ZCode API-key setup' })).toBeDisabled() + if (label === 'ZCode API-key setup') { + expect(screen.getByText(/enter \/login.*masked prompt/)).toBeInTheDocument() + } else { + expect(screen.getByText(/authorization URL printed by ZCode/)).toBeInTheDocument() + } + fireEvent.click(screen.getByRole('button', { name: 'Close setup terminal' })) + expect(screen.queryByTestId('setup-terminal')).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: label })).toBeEnabled() + }) + + it('preserves the configured CLI executable instead of inventing model flags', async () => { + mocks.settings.agentCmdOverrides.zcode = 'node "C:\\ZCode CLI\\zcode.cjs"' + render() + fireEvent.click(screen.getByRole('button', { name: 'BigModel browser sign-in' })) + expect(await screen.findByTestId('setup-terminal')).toHaveTextContent( + 'node "C:\\ZCode CLI\\zcode.cjs" login bigmodel --no-browser' + ) + expect(screen.getByTestId('setup-terminal')).not.toHaveTextContent('--model') + }) +}) diff --git a/src/renderer/src/components/settings/ZcodeCliSetupSection.tsx b/src/renderer/src/components/settings/ZcodeCliSetupSection.tsx new file mode 100644 index 00000000000..922c926380d --- /dev/null +++ b/src/renderer/src/components/settings/ZcodeCliSetupSection.tsx @@ -0,0 +1,122 @@ +import { lazy, Suspense, useState } from 'react' +import { translate } from '@/i18n/i18n' +import { useAppStore } from '@/store' +import { Button } from '../ui/button' +import { SearchableSetting } from './SearchableSetting' + +const OnboardingInlineCommandTerminal = lazy(() => + import('../onboarding/OnboardingInlineCommandTerminal').then((module) => ({ + default: module.OnboardingInlineCommandTerminal + })) +) + +type SetupChoice = 'zai' | 'bigmodel' | 'api-key' + +export function ZcodeCliSetupSection(): React.JSX.Element { + const commandOverride = useAppStore((s) => s.settings?.agentCmdOverrides?.zcode) + const [choice, setChoice] = useState(null) + const executable = commandOverride?.trim() || 'zcode' + const command = choice === 'api-key' ? executable : `${executable} login ${choice} --no-browser` + + const title = translate( + 'auto.components.settings.ZcodeCliSetupSection.title', + 'Use GLM with ZCode' + ) + const description = translate( + 'auto.components.settings.ZcodeCliSetupSection.description', + 'Sign in to use GLM as a coding agent. The quota API key above only monitors usage; ZCode owns its own sign-in and model selection.' + ) + + return ( + +

{title}

+

{description}

+

+ {translate( + 'auto.components.settings.ZcodeCliSetupSection.help', + 'Choose browser sign-in for your plan site, or use ZCode’s masked API-key setup. After signing in, start ZCode and use /model to choose a GLM model.' + )} +

+
+ + + + {choice ? ( + + ) : null} +
+ {choice ? ( + + {translate( + 'auto.components.settings.ZcodeCliSetupSection.loading', + 'Loading sign-in terminal…' + )} +

+ } + > + +
+ ) : null} +
+ ) +} diff --git a/src/renderer/src/components/settings/ZcodePlanAccountsSection.tsx b/src/renderer/src/components/settings/ZcodePlanAccountsSection.tsx index 973396b9f88..c49f383f32c 100644 --- a/src/renderer/src/components/settings/ZcodePlanAccountsSection.tsx +++ b/src/renderer/src/components/settings/ZcodePlanAccountsSection.tsx @@ -18,6 +18,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '. import { SearchableSetting } from './SearchableSetting' import { collectZcodeUsageWindows, ZcodeUsageWindowView } from './zcode-plan-usage-windows' import { useZcodePlanCredentials } from './use-zcode-plan-credentials' +import { ZcodeCliSetupSection } from './ZcodeCliSetupSection' import { UnsealedCredentialNotice } from './UnsealedCredentialNotice' const SEARCH_KEYWORDS = [ @@ -330,6 +331,8 @@ export function ZcodePlanAccountsSection(): React.JSX.Element {

+ {credentialEditable ? : null} + {usageWindows.length > 0 ? ( { + await waitForSessionReady(orcaPage) + await orcaPage.evaluate(() => { + const state = window.__store?.getState() + state?.openSettingsTarget({ pane: 'accounts', repoId: null, sectionId: 'accounts-zcode' }) + state?.openSettingsPage() + }) + const section = orcaPage.locator('#accounts-zcode') + await expect(section).toBeVisible({ timeout: 30_000 }) + await section.screenshot({ path: testInfo.outputPath('01-setup-buttons.png') }) + for (const [label, command] of [ + ['Z.AI browser sign-in', 'zcode login zai --no-browser'], + ['BigModel browser sign-in', 'zcode login bigmodel --no-browser'], + ['ZCode API-key setup', 'zcode'] + ] as const) { + await section.getByRole('button', { name: label, exact: true }).click() + const close = section.getByRole('button', { name: 'Close setup terminal', exact: true }) + await expect(close).toBeVisible({ timeout: 15_000 }) + const tabId = await section + .locator('[data-terminal-tab-id]') + .first() + .getAttribute('data-terminal-tab-id') + if (!tabId) { + throw new Error('Setup terminal did not mount') + } + await expectTerminalAccessibilityText(orcaPage, tabId, command) + await expect(section.getByRole('button', { name: label, exact: true })).toBeDisabled() + await section.screenshot({ path: testInfo.outputPath(`${label}.png`) }) + await close.click() + await expect(close).toHaveCount(0) + await expect(section.locator('[data-terminal-tab-id]')).toHaveCount(0) + await expect(section.getByRole('button', { name: label, exact: true })).toBeEnabled() + } +})