mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
feat(accounts): isolate ZCode sign-in terminal setup
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user