From 6219144557ffaab863bbed481322c91ea8bdbd96 Mon Sep 17 00:00:00 2001
From: Neil
Date: Sat, 10 Oct 2026 15:25:18 -0700
Subject: [PATCH 1/2] feat(accounts): isolate ZCode sign-in terminal setup
---
.../settings/ZcodeCliSetupSection.test.tsx | 74 +++++++++++
.../settings/ZcodeCliSetupSection.tsx | 122 ++++++++++++++++++
.../ZcodePlanAccountsSection.test.tsx | 1 +
.../settings/ZcodePlanAccountsSection.tsx | 3 +
src/renderer/src/i18n/locales/en.json | 14 ++
src/renderer/src/i18n/locales/zh.json | 14 ++
tests/e2e/helpers/orca-app.ts | 2 +-
tests/e2e/zcode-cli-setup.spec.ts | 40 ++++++
8 files changed, 269 insertions(+), 1 deletion(-)
create mode 100644 src/renderer/src/components/settings/ZcodeCliSetupSection.test.tsx
create mode 100644 src/renderer/src/components/settings/ZcodeCliSetupSection.tsx
create mode 100644 tests/e2e/zcode-cli-setup.spec.ts
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 000000000000..b8f37d868839
--- /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 000000000000..922c926380db
--- /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.'
+ )}
+
+
+ setChoice('zai')}
+ >
+ {translate('auto.components.settings.ZcodeCliSetupSection.zai', 'Z.AI browser sign-in')}
+
+ setChoice('bigmodel')}
+ >
+ {translate(
+ 'auto.components.settings.ZcodeCliSetupSection.bigmodel',
+ 'BigModel browser sign-in'
+ )}
+
+ setChoice('api-key')}
+ >
+ {translate('auto.components.settings.ZcodeCliSetupSection.apiKey', 'ZCode API-key setup')}
+
+ {choice ? (
+ setChoice(null)}>
+ {translate(
+ 'auto.components.settings.ZcodeCliSetupSection.close',
+ 'Close setup terminal'
+ )}
+
+ ) : 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 ? (
({
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()
+ }
+})
From 3bc0200f9b390b1054303a5240007eeda0417b86 Mon Sep 17 00:00:00 2001
From: Neil
Date: Sat, 10 Oct 2026 15:39:10 -0700
Subject: [PATCH 2/2] test(composer): reuse fixtures to restore the main lint
gate
---
.../use-automations-page-setup-state.ts | 3 +-
...lder-workspace-composer-submit-fixtures.ts | 36 ++++++++++++++++++
...pace-composer-submit-host-platform.test.ts | 36 +-----------------
.../folder-workspace-composer-submit.test.ts | 37 +------------------
4 files changed, 39 insertions(+), 73 deletions(-)
create mode 100644 src/renderer/src/components/sidebar/folder-workspace-composer-submit-fixtures.ts
diff --git a/src/renderer/src/components/automations/use-automations-page-setup-state.ts b/src/renderer/src/components/automations/use-automations-page-setup-state.ts
index ae0939085fe3..5ada1d683d28 100644
--- a/src/renderer/src/components/automations/use-automations-page-setup-state.ts
+++ b/src/renderer/src/components/automations/use-automations-page-setup-state.ts
@@ -150,8 +150,7 @@ export function useAutomationsPageSetupState({
automationYamlHooksByRepoKey,
getAutomationHooksCacheKey,
repos,
- setAutomationYamlHooksByRepoKey,
- settings
+ setAutomationYamlHooksByRepoKey
]
)
const getDraftSetupDecisionDefaultSignature = useCallback(
diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit-fixtures.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit-fixtures.ts
new file mode 100644
index 000000000000..303445980ab1
--- /dev/null
+++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit-fixtures.ts
@@ -0,0 +1,36 @@
+import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
+import type { ProjectGroup } from '../../../../shared/project-group-types'
+
+export 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
+ }
+}
+
+export 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
+ }
+}
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 = {