feat(accounts): isolate ZCode sign-in terminal setup

This commit is contained in:
Neil
2026-10-10 16:04:51 -07:00
committed by Neil
parent 7dfcef4e10
commit 04f8f2b5d8
6 changed files with 267 additions and 0 deletions
@@ -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
}) => (
<div data-testid="setup-terminal">
<output>{command}</output>
<p>{description}</p>
</div>
)
}))
vi.mock('./SearchableSetting', () => ({
SearchableSetting: ({ children }: { children: React.ReactNode }) => <div>{children}</div>
}))
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(<ZcodeCliSetupSection />)
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(<ZcodeCliSetupSection />)
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(<ZcodeCliSetupSection />)
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')
})
})
@@ -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<SetupChoice | null>(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 (
<SearchableSetting
title={title}
description={description}
keywords={['zcode', 'glm', 'oauth', 'login', 'model', 'api key']}
className="space-y-2"
>
<h4 className="text-xs font-medium">{title}</h4>
<p className="text-xs text-muted-foreground">{description}</p>
<p className="text-xs text-muted-foreground">
{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.'
)}
</p>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="xs"
disabled={choice !== null}
onClick={() => setChoice('zai')}
>
{translate('auto.components.settings.ZcodeCliSetupSection.zai', 'Z.AI browser sign-in')}
</Button>
<Button
variant="outline"
size="xs"
disabled={choice !== null}
onClick={() => setChoice('bigmodel')}
>
{translate(
'auto.components.settings.ZcodeCliSetupSection.bigmodel',
'BigModel browser sign-in'
)}
</Button>
<Button
variant="outline"
size="xs"
disabled={choice !== null}
onClick={() => setChoice('api-key')}
>
{translate('auto.components.settings.ZcodeCliSetupSection.apiKey', 'ZCode API-key setup')}
</Button>
{choice ? (
<Button variant="ghost" size="xs" onClick={() => setChoice(null)}>
{translate(
'auto.components.settings.ZcodeCliSetupSection.close',
'Close setup terminal'
)}
</Button>
) : null}
</div>
{choice ? (
<Suspense
fallback={
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodeCliSetupSection.loading',
'Loading sign-in terminal…'
)}
</p>
}
>
<OnboardingInlineCommandTerminal
command={command}
title={translate(
'auto.components.settings.ZcodeCliSetupSection.terminalTitle',
'ZCode sign-in'
)}
ariaLabel={translate(
'auto.components.settings.ZcodeCliSetupSection.terminalLabel',
'ZCode sign-in terminal'
)}
description={
choice === 'api-key'
? translate(
'auto.components.settings.ZcodeCliSetupSection.apiKeyInstructions',
'Press Enter to start ZCode, then enter /login and choose the API-key option for your plan site. Enter the key in ZCode’s masked prompt.'
)
: translate(
'auto.components.settings.ZcodeCliSetupSection.oauthInstructions',
'Press Enter to run the sign-in command, then open the authorization URL printed by ZCode. Sign-in is saved on this terminal’s execution host.'
)
}
autoScrollIntoView={false}
terminalTopMarginPx={8}
worktreeId="zcode-sign-in-terminal"
/>
</Suspense>
) : null}
</SearchableSetting>
)
}
@@ -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 {
</p>
</SearchableSetting>
{credentialEditable ? <ZcodeCliSetupSection /> : null}
{usageWindows.length > 0 ? (
<SearchableSetting
title={translate(
+14
View File
@@ -12634,6 +12634,20 @@
"terminalsElsewhere_other": "Runs the relay while {{count}} terminals another Orca desktop or session opened on this host are running.",
"terminalsElsewhereUncounted": "Runs the relay while terminals another Orca desktop or session opened on this host are running."
},
"ZcodeCliSetupSection": {
"title": "Use GLM with ZCode",
"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.",
"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.",
"zai": "Z.AI browser sign-in",
"bigmodel": "BigModel browser sign-in",
"apiKey": "ZCode API-key setup",
"close": "Close setup terminal",
"loading": "Loading sign-in terminal…",
"terminalTitle": "ZCode sign-in",
"terminalLabel": "ZCode sign-in terminal",
"apiKeyInstructions": "Press Enter to start ZCode, then enter /login and choose the API-key option for your plan site. Enter the key in ZCode’s masked prompt.",
"oauthInstructions": "Press Enter to run the sign-in command, then open the authorization URL printed by ZCode. Sign-in is saved on this terminal’s execution host."
},
"ZcodePlanAccountsSection": {
"site": {
"bigmodel": "Zhipu · BigModel (open.bigmodel.cn)",
+14
View File
@@ -12248,6 +12248,20 @@
"usageLabel": "用量",
"usageTitle": "每月套餐用量"
},
"ZcodeCliSetupSection": {
"title": "通过 ZCode 使用 GLM",
"description": "登录后即可将 GLM 用作编程代理。上方的配额 API 密钥仅用于监控用量;ZCode 自行管理登录和模型选择。",
"help": "选择对应套餐站点的浏览器登录,或使用 ZCode 的隐藏输入 API 密钥设置。登录后,启动 ZCode 并使用 /model 选择 GLM 模型。",
"zai": "Z.AI 浏览器登录",
"bigmodel": "BigModel 浏览器登录",
"apiKey": "ZCode API 密钥设置",
"close": "关闭设置终端",
"loading": "正在加载登录终端…",
"terminalTitle": "ZCode 登录",
"terminalLabel": "ZCode 登录终端",
"apiKeyInstructions": "按 Enter 启动 ZCode,然后输入 /login 并选择套餐站点的 API 密钥选项。在 ZCode 的隐藏输入提示中输入密钥。",
"oauthInstructions": "按 Enter 运行登录命令,然后打开 ZCode 输出的授权网址。登录信息保存在此终端的执行主机上。"
},
"ZcodePlanAccountsSection": {
"site": {
"bigmodel": "智谱 · BigModel(open.bigmodel.cn)",
+40
View File
@@ -0,0 +1,40 @@
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
import { expectTerminalAccessibilityText } from './helpers/terminal-accessibility-tree'
test('Accounts prepares each ZCode sign-in command and closes its terminal', async ({
orcaPage
}, testInfo) => {
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()
}
})