+ {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.test.tsx b/src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx
index 32c277b72ced..89f9c1f708e5 100644
--- a/src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx
+++ b/src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx
@@ -145,6 +145,7 @@ describe('ZcodePlanAccountsSection', () => {
render()
const input = await screen.findByPlaceholderText('Paste your GLM Coding Plan API key')
+ mocks.getStatus.mockResolvedValue({ apiKeyConfigured: true, zcodeCliConfigured: false })
fireEvent.change(input, { target: { value: ' glm-secret ' } })
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
diff --git a/src/renderer/src/components/settings/ZcodePlanAccountsSection.tsx b/src/renderer/src/components/settings/ZcodePlanAccountsSection.tsx
index be3d5e1010ef..7efad3c3a170 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 ? (
= {}): FolderWorkspace {
+ return {
+ id: 'folder-workspace-1',
+ projectGroupId: 'group-1',
+ name: 'hi',
+ folderPath: '/repo/platform/hi',
+ linkedTask: null,
+ comment: '',
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: 1,
+ createdAt: 1,
+ updatedAt: 1,
+ ...overrides
+ }
+}
diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts
index f1fcb5e6f008..95a01c86f40b 100644
--- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts
+++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts
@@ -1,8 +1,7 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
-import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
-import type { ProjectGroup } from '../../../../shared/project-group-types'
+import { makeProjectGroup, makeFolderWorkspace } from './folder-workspace-composer-submit-fixtures'
const mocks = vi.hoisted(() => ({ activateAndRevealFolderWorkspace: vi.fn() }))
@@ -14,39 +13,6 @@ vi.mock('@/lib/worktree-activation', async (importOriginal) => {
import { useAppStore } from '@/store'
import { submitFolderWorkspaceCreate } from './folder-workspace-composer-submit'
-function makeProjectGroup(): ProjectGroup {
- return {
- id: 'group-1',
- name: 'Platform',
- parentPath: '/repo/platform',
- parentGroupId: null,
- createdFrom: 'folder-scan',
- tabOrder: 0,
- isCollapsed: false,
- color: null,
- createdAt: 1,
- updatedAt: 1
- }
-}
-
-function makeFolderWorkspace(): FolderWorkspace {
- return {
- id: 'folder-workspace-1',
- projectGroupId: 'group-1',
- name: 'hi',
- folderPath: '/repo/platform/hi',
- linkedTask: null,
- comment: '',
- isArchived: false,
- isUnread: false,
- isPinned: false,
- sortOrder: 0,
- lastActivityAt: 1,
- createdAt: 1,
- updatedAt: 1
- }
-}
-
// Why: a folder group on an Orca server runs its agents there, so the server's OS decides quoting.
describe('submitFolderWorkspaceCreate on an Orca server', () => {
afterEach(() => {
diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts
index 85d5f2020be0..0964e076f87a 100644
--- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts
+++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts
@@ -1,8 +1,7 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
-import type { ProjectGroup } from '../../../../shared/project-group-types'
+import { makeProjectGroup, makeFolderWorkspace } from './folder-workspace-composer-submit-fixtures'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import type * as NewWorkspaceModule from '@/lib/new-workspace'
@@ -30,40 +29,6 @@ import {
submitFolderWorkspaceCreate
} from './folder-workspace-composer-submit'
-function makeProjectGroup(): ProjectGroup {
- return {
- id: 'group-1',
- name: 'Platform',
- parentPath: '/repo/platform',
- parentGroupId: null,
- createdFrom: 'folder-scan',
- tabOrder: 0,
- isCollapsed: false,
- color: null,
- createdAt: 1,
- updatedAt: 1
- }
-}
-
-function makeFolderWorkspace(overrides: Partial = {}): FolderWorkspace {
- return {
- id: 'folder-workspace-1',
- projectGroupId: 'group-1',
- name: 'hi',
- folderPath: '/repo/platform/hi',
- linkedTask: null,
- comment: '',
- isArchived: false,
- isUnread: false,
- isPinned: false,
- sortOrder: 0,
- lastActivityAt: 1,
- createdAt: 1,
- updatedAt: 1,
- ...overrides
- }
-}
-
describe('submitFolderWorkspaceCreate', () => {
it.each(['347', '002'])('keeps manual folder name %s with its linked source', async (name) => {
const linkedWorkItem = {
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 5b9378d6efea..1cc1400102e8 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -12632,6 +12632,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)",
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index a78155345c8d..ac3cb227baac 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -12246,6 +12246,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)",
diff --git a/tests/e2e/helpers/orca-app.ts b/tests/e2e/helpers/orca-app.ts
index e1d34d722293..edda9b620893 100644
--- a/tests/e2e/helpers/orca-app.ts
+++ b/tests/e2e/helpers/orca-app.ts
@@ -340,7 +340,7 @@ export const test = base.extend({
if (!store) {
return false
}
- await store.getState().fetchRepos()
+ await store.getState().fetchRepos({ runtimeEnvironmentId: null })
const repo = store.getState().repos.find((candidate) => candidate.id === repoId)
if (!repo) {
return false
diff --git a/tests/e2e/zcode-cli-setup.spec.ts b/tests/e2e/zcode-cli-setup.spec.ts
new file mode 100644
index 000000000000..7f92e3d956b1
--- /dev/null
+++ b/tests/e2e/zcode-cli-setup.spec.ts
@@ -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()
+ }
+})