diff --git a/docs/claude-multimodel-integration-plan.md b/docs/claude-multimodel-integration-plan.md
new file mode 100644
index 00000000..e4d983bd
--- /dev/null
+++ b/docs/claude-multimodel-integration-plan.md
@@ -0,0 +1,825 @@
+# Claude Multimodel Integration Plan
+
+## Summary
+
+`claude_team` will integrate with a single CLI runtime: `claude-multimodel`.
+
+Inside that runtime, the app will track multiple provider states independently:
+
+- `anthropic`
+- `codex`
+
+The dashboard will show one grouped status banner for the runtime and its providers.
+That grouped banner should replace the current single-runtime dashboard banner, not sit beside a second provider banner.
+
+Provider and model selection will happen at process launch time, not in the banner.
+
+This avoids duplicating auth logic in `claude_team` and keeps `claude-multimodel` responsible
+for provider login, token storage, and provider capability reporting.
+
+## Core Decisions
+
+### 1. Runtime model
+
+There is one runtime:
+
+- `claude-multimodel`
+
+There are not multiple runtime binaries for this feature.
+`Anthropic` and `Codex` are providers inside the same runtime.
+
+Important consequence:
+
+- there is no separate `Codex` binary to install
+- there is no separate `Codex` version to display
+- there is no separate `Codex` updater flow
+
+### 2. Provider model
+
+Provider status must be tracked independently.
+
+Supported initial providers:
+
+- `anthropic`
+- `codex`
+
+The user may be authenticated with both at the same time.
+The UI must show both statuses independently without trying to choose a global active provider.
+
+This provider model must also remain compatible with future providers that expose
+multiple internal backend paths under one public provider id.
+
+Example:
+
+- public provider: `gemini`
+- internal runtime backend: `api` or `cli`
+
+In that scenario, `claude_team` should still treat `gemini` as one provider row.
+Backend choice is diagnostic/runtime metadata inside the provider, not a second provider id.
+
+Provider support must be treated per execution mode, not just globally.
+At minimum, the integration needs to distinguish:
+
+- interactive team launch / resume
+- scheduled one-shot execution
+
+Internal naming rule:
+
+- app-level provider id: `codex`
+- runtime/provider label in UI: `Codex`
+- this maps to the OpenAI/Codex path inside `claude-multimodel`
+- during migration, runtime-internal naming may still use `openai`; the bridge layer must normalize that to app-level `codex`
+
+Backward-compatibility rule:
+
+- existing persisted launches/schedules/teams that do not yet have `providerId` must default safely to `anthropic`
+- missing `providerId` in old metadata must never block resume, launch, or schedule execution
+
+### 3. Launch-time selection
+
+Banner state is informational only.
+
+Provider selection happens when launching a task/team/process:
+
+- selected provider
+- selected model
+
+Future extension:
+
+- per-teammate provider
+- per-teammate model
+
+Defaulting rule:
+
+- for backward compatibility, flows with no explicit provider should default to `anthropic`
+- provider defaulting must be explicit in code and persistence, not inferred from whichever UI tab/control happened to be last open
+- model defaults must be provider-aware rather than using a single global default such as `opus`
+- if a provider later adds internal backend selection, backend resolution must not silently change the app-level `providerId`
+
+## Goals
+
+- Show runtime status for `claude-multimodel`
+- Show separate provider status for `Anthropic` and `Codex`
+- Support login/logout/status checks through the runtime CLI
+- Reuse the existing binary resolver and terminal-based login modal flow where possible
+- Avoid reading runtime token/config internals directly from `claude_team`
+- Keep current team launch flow extensible for provider/model selection
+
+## Non-Goals
+
+- No separate `Codex` installer
+- No fake `Codex` download flow in the UI
+- No separate `Codex` runtime version
+- No full runtime abstraction layer right now
+- No per-teammate model/provider implementation in this phase
+
+## Architecture
+
+### Runtime ownership
+
+`claude-multimodel` owns:
+
+- provider auth flows
+- provider token storage
+- provider auth verification
+- provider model availability reporting
+
+`claude_team` owns:
+
+- UI rendering
+- caching and refreshing status
+- launch-time provider/model selection
+- mapping runtime status into app-friendly DTOs
+- reusing shared CLI probing helpers instead of duplicating binary/version logic
+- ensuring per-process provider env/args are isolated and do not leak across parallel launches
+- validating provider support for the concrete execution mode before spawn
+
+### Service boundary in `claude_team`
+
+Add a new main-process service:
+
+- `src/main/services/runtime/ClaudeMultimodelBridgeService.ts`
+
+Responsibilities:
+
+- resolve runtime binary path
+- get runtime version
+- get provider auth status
+- get provider model lists
+- aggregate banner DTO
+- trigger provider login/logout commands
+
+This service should be separate from `CliInstallerService`.
+Installer/update concerns and provider/auth concerns are different responsibilities.
+
+However, the implementation should reuse existing low-level pieces where possible:
+
+- `ClaudeBinaryResolver`
+- existing terminal modal login pattern
+- existing CLI version probing helpers, if they are extracted into a shared utility
+
+But existing Claude-specific auth/model logic must not be reused as-is for provider rows:
+
+- current installer auth probing is Anthropic-oriented
+- current model parsing and display utilities are Claude-oriented
+- provider rows must be driven by runtime provider data, not inferred from Claude-only helpers
+
+The service should also enforce bounded status-check behavior:
+
+- use timeouts for each runtime CLI command
+- keep partial results if one command fails
+- cache recent successful results briefly to avoid noisy repeated probes
+
+## Required CLI Contract in `claude-multimodel`
+
+The runtime should expose machine-readable commands.
+
+### 1. Runtime version
+
+Already available:
+
+```bash
+claude-multimodel --version
+```
+
+Used for:
+
+- runtime version display in banner header
+
+Version display rule:
+
+- the app should normalize dev/build suffixes before rendering the banner header
+- example:
+ - raw: `2.1.87-dev.20260401.t145625.sha38c09970 (Claude Code)`
+ - shown: `Claude 2.1.87`
+
+### 2. Provider auth status
+
+Add:
+
+```bash
+claude-multimodel auth status --json --provider all
+```
+
+Contract requirements:
+
+- non-interactive only
+- must never open a browser or prompt for user input
+- must be side-effect free:
+ - no provider switching
+ - no token mutation unless the runtime explicitly performs a safe read-only refresh internally
+- should return partial provider results when possible instead of failing the entire command on one provider-specific check
+
+Example response:
+
+```json
+{
+ "schemaVersion": 1,
+ "providers": {
+ "anthropic": {
+ "supported": true,
+ "authenticated": true,
+ "authMethod": "oauth",
+ "verificationState": "verified",
+ "canLoginFromUi": true,
+ "capabilities": {
+ "teamLaunch": true,
+ "oneShot": true
+ }
+ },
+ "codex": {
+ "supported": true,
+ "authenticated": false,
+ "authMethod": null,
+ "verificationState": "verified",
+ "canLoginFromUi": true,
+ "capabilities": {
+ "teamLaunch": true,
+ "oneShot": true
+ }
+ }
+ }
+}
+```
+
+Notes:
+
+- no global `active` provider state belongs in this contract
+- provider choice happens per launch request, not in global status
+- `authenticated` means the runtime considers usable credentials/session state present for that provider
+- `verificationState` must distinguish `verified` from `unknown` / `offline` / `error`
+- `supported` means the current runtime build knows how to use that provider
+- `capabilities` means the current runtime build knows which execution modes can use that provider
+
+### 3. Provider model lists
+
+Add:
+
+```bash
+claude-multimodel model list --json --provider all
+```
+
+Example response:
+
+```json
+{
+ "schemaVersion": 1,
+ "providers": {
+ "anthropic": {
+ "models": ["opus", "sonnet", "haiku"]
+ },
+ "codex": {
+ "models": ["gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini"]
+ }
+ }
+}
+```
+
+This is intentionally separate from auth status so auth and capability failures can be handled independently.
+
+Model listing should be available even when the provider is not currently authenticated, whenever the runtime can determine the model set statically.
+
+### 4. Provider login
+
+Add:
+
+```bash
+claude-multimodel auth login --provider anthropic
+claude-multimodel auth login --provider codex
+```
+
+```bash
+claude-multimodel auth logout --provider anthropic
+claude-multimodel auth logout --provider codex
+```
+
+This allows `claude_team` to reuse runtime-managed login flows instead of implementing OAuth directly.
+
+## Status Model in `claude_team`
+
+Recommended DTO:
+
+```ts
+type ProviderId = 'anthropic' | 'codex';
+
+interface ProviderStatus {
+ providerId: ProviderId;
+ displayName: string;
+ supported: boolean;
+ authenticated: boolean;
+ authMethod: 'oauth' | 'api_key' | 'external' | 'unknown' | null;
+ verificationState: 'verified' | 'unknown' | 'offline' | 'error';
+ statusMessage?: string | null;
+ models: string[];
+ canLoginFromUi: boolean;
+ capabilities: {
+ teamLaunch: boolean;
+ oneShot: boolean;
+ };
+ backend?: {
+ kind: string;
+ label: string;
+ endpointLabel?: string | null;
+ projectId?: string | null;
+ authMethodDetail?: string | null;
+ } | null;
+}
+
+interface ClaudeMultimodelDashboardStatus {
+ runtime: {
+ id: 'claude-multimodel';
+ installed: boolean;
+ version: string | null;
+ };
+ providers: ProviderStatus[];
+}
+```
+
+## Dashboard Banner
+
+One grouped banner should be shown.
+It should replace the current dashboard runtime banner, not add a second top-level banner.
+
+### Header
+
+- runtime name/version, for example: `Claude 2.1.87`
+- runtime auth-independent actions like `Extensions`
+
+For `claude-multimodel` mode:
+
+- keep the runtime version in the header
+- hide runtime self-update UI
+- hide binary path UI
+
+### Provider rows
+
+One row per provider:
+
+- `Anthropic`
+- `Codex`
+
+Each row may show:
+
+- authenticated state
+- auth method
+- verification state when status is uncertain
+- model list if available
+- backend diagnostics when the provider has internal backend resolution
+- actions:
+ - `Login` when unauthenticated
+ - `Logout` when authenticated
+ - `Re-check` always
+
+### Important UI rules
+
+- Do not show a fake `Codex version`
+- Do not show a separate `Codex installer`
+- Do not show a fake `Download Codex` action
+- Do not force a global provider choice from the banner
+- If a provider has multiple internal backends, show them as provider diagnostics, not as separate provider rows
+- Partial success must be visible:
+ - Anthropic ok, Codex missing
+ - Codex ok, Anthropic missing
+
+## Launch Flow Changes
+
+Current launch flow already supports a team-level `model`.
+Current UI also already has a provider affordance in [TeamModelSelector.tsx](../../src/renderer/components/team/dialogs/TeamModelSelector.tsx), but its logic is still placeholder/coming-soon. That placeholder currently uses `openai`; it should be normalized or mapped to the app-level provider id `codex` during implementation.
+
+Current code paths that must be kept in sync:
+
+- shared types in [team.ts](../../src/shared/types/team.ts)
+- HTTP launch parsing in [teams.ts](../../src/main/http/teams.ts)
+- IPC launch/create validation in [teams.ts](../../src/main/ipc/teams.ts)
+- persisted draft metadata in [TeamMetaStore.ts](../../src/main/services/team/TeamMetaStore.ts)
+- scheduled one-shot config in [schedule.ts](../../src/shared/types/schedule.ts)
+- scheduled execution in [ScheduledTaskExecutor.ts](../../src/main/services/schedule/ScheduledTaskExecutor.ts)
+- provider/model UI helpers in [TeamModelSelector.tsx](../../src/renderer/components/team/dialogs/TeamModelSelector.tsx)
+- model display parsing in [modelParser.ts](../../src/shared/utils/modelParser.ts)
+- launch dialog state persistence in [LaunchTeamDialog.tsx](../../src/renderer/components/team/dialogs/LaunchTeamDialog.tsx)
+- create dialog state persistence in [CreateTeamDialog.tsx](../../src/renderer/components/team/dialogs/CreateTeamDialog.tsx)
+- saved draft restore in [teams.ts](../../src/main/ipc/teams.ts)
+- launch param persistence in the renderer store
+- slash-command metadata in [slashCommands.ts](../../src/shared/utils/slashCommands.ts)
+
+Next step is to extend launch requests to include provider selection:
+
+```ts
+interface TeamLaunchRequest {
+ providerId?: 'anthropic' | 'codex';
+ model?: string;
+}
+```
+
+Future-compatible extension for providers with internal backend preference:
+
+```ts
+interface TeamLaunchRequest {
+ providerId?: 'anthropic' | 'codex' | 'gemini';
+ model?: string;
+ providerOptions?: {
+ geminiBackendPreference?: 'auto' | 'api' | 'cli';
+ };
+}
+```
+
+Provider selection must be applied only to the spawned child process environment/args.
+It must not mutate a global app-wide runtime provider state.
+
+This launch-time provider/model selection must be honored consistently across all spawn paths:
+
+- manual team launch
+- draft team launch that redirects into `createTeam`
+- scheduled/background execution
+- resume/retry/restart flows
+- any other non-interactive process launch path that currently inherits team/model settings
+
+Implementation rule:
+
+- provider choice must be expressed via child-process-scoped args/env only
+- conflicting provider env vars must be cleared for that child process
+- one run using `codex` must not affect another run using `anthropic`
+
+Future-compatible shape:
+
+```ts
+interface TeamLaunchRequest {
+ providerId?: 'anthropic' | 'codex';
+ model?: string;
+ memberProviders?: Record
@@ -162,22 +167,211 @@ interface InstalledBannerProps {
cliStatusLoading: boolean;
cliStatusError: string | null;
isBusy: boolean;
+ multimodelEnabled: boolean;
+ multimodelBusy: boolean;
onInstall: () => void;
onRefresh: () => void;
+ onMultimodelToggle: (enabled: boolean) => void;
+ onProviderLogin: (providerId: CliProviderId) => void;
+ onProviderLogout: (providerId: CliProviderId) => void;
variant: BannerVariant;
}
+function getProviderLabel(providerId: CliProviderId): string {
+ switch (providerId) {
+ case 'anthropic':
+ return 'Anthropic';
+ case 'codex':
+ return 'Codex';
+ case 'gemini':
+ return 'Gemini';
+ }
+}
+
+function getProviderTerminalCommand(providerId: CliProviderId): {
+ args: string[];
+ env?: Record
Failed to check for updates. Check your network connection and try again.
+ The configured free-code runtime was not found. +
+ )} ); } // Installed but not logged in — yellow warning banner - if (cliStatus.installed && !cliStatus.authLoggedIn) { + if (cliStatus.installed && cliStatus.flavor !== 'free-code' && !cliStatus.authLoggedIn) { if (isVerifyingAuth) { return (- Claude CLI is installed but you are not authenticated. Login is required for team - provisioning and AI features. + {cliStatus.displayName} is installed but you are not authenticated. Login is + required for team provisioning and AI features.
- claude auth status
+ {cliStatus.showBinaryPath && cliStatus.binaryPath
+ ? `"${cliStatus.binaryPath}" auth status`
+ : 'your configured CLI auth status command'}
{' '}
— check if it shows "Logged in"
- claude auth logout
+ {cliStatus.showBinaryPath && cliStatus.binaryPath
+ ? `"${cliStatus.binaryPath}" auth logout`
+ : 'the runtime logout command'}
{' '}
then{' '}
- claude auth login
+ {cliStatus.showBinaryPath && cliStatus.binaryPath
+ ? `"${cliStatus.binaryPath}" auth login`
+ : 'the runtime login command'}
{' '}
again
- claude
- {' '}
- in your terminal is the same binary the app uses
- {cliStatus.binaryPath && (
+ Make sure the CLI in your terminal is the same runtime the app uses
+ {cliStatus.showBinaryPath && cliStatus.binaryPath && (
:{' '}
@@ -682,7 +1090,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
{showLoginTerminal && cliStatus.binaryPath && (
{
@@ -719,14 +1127,45 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
// Installed — show version, path, update info
return (
-
+ <>
+ void handleMultimodelToggle(enabled)}
+ onProviderLogin={handleProviderLogin}
+ onProviderLogout={handleProviderLogout}
+ variant={variant}
+ />
+ {providerTerminal && cliStatus.binaryPath && (
+ {
+ setProviderTerminal(null);
+ recheckAuthState();
+ }}
+ onExit={() => {
+ recheckAuthState();
+ }}
+ autoCloseOnSuccessMs={3000}
+ successMessage={
+ providerTerminal.action === 'login' ? 'Authentication updated' : 'Provider logged out'
+ }
+ failureMessage={
+ providerTerminal.action === 'login' ? 'Authentication failed' : 'Logout failed'
+ }
+ />
+ )}
+ >
);
};
diff --git a/src/renderer/components/settings/hooks/useSettingsConfig.ts b/src/renderer/components/settings/hooks/useSettingsConfig.ts
index bc6b84f8..0ab735d2 100644
--- a/src/renderer/components/settings/hooks/useSettingsConfig.ts
+++ b/src/renderer/components/settings/hooks/useSettingsConfig.ts
@@ -29,6 +29,7 @@ export interface SafeConfig {
showDockIcon: boolean;
theme: 'dark' | 'light' | 'system';
defaultTab: 'dashboard' | 'last-session';
+ multimodelEnabled: boolean;
claudeRootPath: string | null;
agentLanguage: string;
autoExpandAIGroups: boolean;
@@ -169,6 +170,7 @@ export function useSettingsConfig(): UseSettingsConfigReturn {
showDockIcon: displayConfig?.general?.showDockIcon ?? true,
theme: displayConfig?.general?.theme ?? 'dark',
defaultTab: displayConfig?.general?.defaultTab ?? 'dashboard',
+ multimodelEnabled: displayConfig?.general?.multimodelEnabled ?? true,
claudeRootPath: displayConfig?.general?.claudeRootPath ?? null,
agentLanguage: displayConfig?.general?.agentLanguage ?? 'system',
autoExpandAIGroups: displayConfig?.general?.autoExpandAIGroups ?? false,
diff --git a/src/renderer/components/settings/hooks/useSettingsHandlers.ts b/src/renderer/components/settings/hooks/useSettingsHandlers.ts
index 76af24e2..18e964e5 100644
--- a/src/renderer/components/settings/hooks/useSettingsHandlers.ts
+++ b/src/renderer/components/settings/hooks/useSettingsHandlers.ts
@@ -314,6 +314,7 @@ export function useSettingsHandlers({
showDockIcon: true,
theme: 'dark',
defaultTab: 'dashboard',
+ multimodelEnabled: true,
claudeRootPath: null,
agentLanguage: 'system',
autoExpandAIGroups: false,
diff --git a/src/renderer/components/settings/sections/CliStatusSection.tsx b/src/renderer/components/settings/sections/CliStatusSection.tsx
index c351c371..8c00fabe 100644
--- a/src/renderer/components/settings/sections/CliStatusSection.tsx
+++ b/src/renderer/components/settings/sections/CliStatusSection.tsx
@@ -5,16 +5,22 @@
* Shows detection status, version info, download progress, and error states.
*/
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { isElectronMode } from '@renderer/api';
+import { confirm } from '@renderer/components/common/ConfirmDialog';
+import { SettingsToggle } from '@renderer/components/settings/components';
+import { TerminalModal } from '@renderer/components/terminal/TerminalModal';
import { useCliInstaller } from '@renderer/hooks/useCliInstaller';
+import { useStore } from '@renderer/store';
import { formatBytes } from '@renderer/utils/formatters';
import {
AlertTriangle,
CheckCircle,
Download,
Loader2,
+ LogIn,
+ LogOut,
Puzzle,
RefreshCw,
Terminal,
@@ -22,8 +28,132 @@ import {
import { SettingsSectionHeader } from '../components';
+import type { CliInstallationStatus, CliProviderId } from '@shared/types';
+
+function formatModelBadgeLabel(providerId: CliProviderId, model: string): string {
+ if (providerId === 'anthropic') {
+ return model.replace(/^claude-/, '');
+ }
+ if (providerId === 'codex') {
+ return model.replace(/^gpt-/, '');
+ }
+ if (providerId === 'gemini') {
+ return model.replace(/^gemini-/, '');
+ }
+ return model;
+}
+
+function ModelBadges({
+ providerId,
+ models,
+}: {
+ providerId: CliProviderId;
+ models: string[];
+}): React.JSX.Element {
+ return (
+
+ {models.map((model) => (
+
+ {formatModelBadgeLabel(providerId, model)}
+
+ ))}
+
+ );
+}
+
+function getProviderLabel(providerId: CliProviderId): string {
+ switch (providerId) {
+ case 'anthropic':
+ return 'Anthropic';
+ case 'codex':
+ return 'Codex';
+ case 'gemini':
+ return 'Gemini';
+ }
+}
+
+function getProviderTerminalCommand(providerId: CliProviderId): {
+ args: string[];
+ env?: Record;
+} {
+ if (providerId === 'gemini') {
+ return {
+ args: ['login'],
+ env: { CLAUDE_CODE_USE_GEMINI: '1' },
+ };
+ }
+
+ return {
+ args: ['auth', 'login', '--provider', providerId],
+ };
+}
+
+function getProviderTerminalLogoutCommand(providerId: CliProviderId): {
+ args: string[];
+ env?: Record;
+} {
+ if (providerId === 'gemini') {
+ return {
+ args: ['logout'],
+ env: { CLAUDE_CODE_USE_GEMINI: '1' },
+ };
+ }
+
+ return {
+ args: ['auth', 'logout', '--provider', providerId],
+ };
+}
+
+function createLoadingMultimodelStatus(): CliInstallationStatus {
+ const providers: Array<{ providerId: CliProviderId; displayName: string }> = [
+ { providerId: 'anthropic', displayName: 'Anthropic' },
+ { providerId: 'codex', displayName: 'Codex' },
+ { providerId: 'gemini', displayName: 'Gemini' },
+ ];
+
+ return {
+ flavor: 'free-code',
+ displayName: 'free-code-gemini-research',
+ supportsSelfUpdate: false,
+ showVersionDetails: false,
+ showBinaryPath: false,
+ installed: true,
+ installedVersion: null,
+ binaryPath: null,
+ latestVersion: null,
+ updateAvailable: false,
+ authLoggedIn: false,
+ authMethod: null,
+ providers: providers.map((provider) => ({
+ ...provider,
+ supported: false,
+ authenticated: false,
+ authMethod: null,
+ verificationState: 'unknown' as const,
+ statusMessage: 'Checking...',
+ models: [],
+ canLoginFromUi: true,
+ capabilities: {
+ teamLaunch: false,
+ oneShot: false,
+ },
+ backend: null,
+ })),
+ };
+}
+
export const CliStatusSection = (): React.JSX.Element | null => {
const isElectron = useMemo(() => isElectronMode(), []);
+ const appConfig = useStore((s) => s.appConfig);
+ const updateConfig = useStore((s) => s.updateConfig);
const {
cliStatus,
installerState,
@@ -36,7 +166,18 @@ export const CliStatusSection = (): React.JSX.Element | null => {
installCli,
isBusy,
cliStatusLoading,
+ invalidateCliStatus,
} = useCliInstaller();
+ const [providerTerminal, setProviderTerminal] = useState<{
+ providerId: CliProviderId;
+ action: 'login' | 'logout';
+ } | null>(null);
+ const [isSwitchingFlavor, setIsSwitchingFlavor] = useState(false);
+ const multimodelEnabled = appConfig?.general?.multimodelEnabled ?? true;
+ const effectiveCliStatus =
+ !cliStatus && cliStatusLoading && multimodelEnabled
+ ? createLoadingMultimodelStatus()
+ : cliStatus;
useEffect(() => {
if (isElectron) {
@@ -52,38 +193,118 @@ export const CliStatusSection = (): React.JSX.Element | null => {
void fetchCliStatus();
}, [fetchCliStatus]);
+ const handleProviderLogout = useCallback(async (providerId: CliProviderId) => {
+ const confirmed = await confirm({
+ title: `Logout from ${getProviderLabel(providerId)}?`,
+ message: 'This will remove the current provider session from the local Claude CLI runtime.',
+ confirmLabel: 'Logout',
+ cancelLabel: 'Cancel',
+ variant: 'danger',
+ });
+
+ if (!confirmed) {
+ return;
+ }
+
+ setProviderTerminal({
+ providerId,
+ action: 'logout',
+ });
+ }, []);
+
+ const recheckStatus = useCallback(() => {
+ void (async () => {
+ await invalidateCliStatus();
+ await fetchCliStatus();
+ })();
+ }, [fetchCliStatus, invalidateCliStatus]);
+
+ const handleMultimodelToggle = useCallback(
+ async (enabled: boolean) => {
+ setIsSwitchingFlavor(true);
+ try {
+ await updateConfig('general', { multimodelEnabled: enabled });
+ await invalidateCliStatus();
+ await fetchCliStatus();
+ } finally {
+ setIsSwitchingFlavor(false);
+ }
+ },
+ [fetchCliStatus, invalidateCliStatus, updateConfig]
+ );
+
if (!isElectron) return null;
+ const runtimeLabel =
+ effectiveCliStatus?.flavor === 'free-code'
+ ? null
+ : effectiveCliStatus &&
+ effectiveCliStatus.showVersionDetails &&
+ effectiveCliStatus.installedVersion
+ ? `${effectiveCliStatus.displayName} v${effectiveCliStatus.installedVersion ?? 'unknown'}`
+ : (effectiveCliStatus?.displayName ?? 'Claude CLI');
+
+ const providerTerminalCommand = providerTerminal
+ ? providerTerminal.action === 'login'
+ ? getProviderTerminalCommand(providerTerminal.providerId)
+ : getProviderTerminalLogoutCommand(providerTerminal.providerId)
+ : null;
+
return (
-
+
{/* Loading status */}
- {!cliStatus && installerState === 'idle' && (
+ {!effectiveCliStatus && installerState === 'idle' && (
- Checking CLI...
+ Checking AI Providers...
)}
{/* Status display */}
- {cliStatus && installerState === 'idle' && (
+ {effectiveCliStatus && installerState === 'idle' && (
- {cliStatus.installed ? (
+ {effectiveCliStatus.installed ? (
-
- Claude CLI v{cliStatus.installedVersion ?? 'unknown'}
-
+ {runtimeLabel && (
+ {runtimeLabel}
+ )}
+
+
+ Multimodel
+
+ {multimodelEnabled && (
+
+ Beta
+
+ )}
+ void handleMultimodelToggle(value)}
+ disabled={isBusy || cliStatusLoading || isSwitchingFlavor}
+ />
+
{/* Inline action buttons */}
- {cliStatus.updateAvailable ? (
+ {effectiveCliStatus.supportsSelfUpdate && effectiveCliStatus.updateAvailable ? (
- ) : (
+ ) : effectiveCliStatus.supportsSelfUpdate ? (
- )}
+ ) : null}
{/* Extensions button — right-aligned */}
- {cliStatus.binaryPath && (
+ {effectiveCliStatus.showBinaryPath && effectiveCliStatus.binaryPath && (
- {cliStatus.binaryPath}
+ {effectiveCliStatus.binaryPath}
)}
- {cliStatus.updateAvailable && cliStatus.latestVersion && (
-
-
- v{cliStatus.installedVersion} → v{cliStatus.latestVersion}
-
+ {effectiveCliStatus.supportsSelfUpdate &&
+ effectiveCliStatus.updateAvailable &&
+ effectiveCliStatus.latestVersion && (
+
+
+ v{effectiveCliStatus.installedVersion} → v
+ {effectiveCliStatus.latestVersion}
+
+
+ )}
+ {effectiveCliStatus.providers.length > 0 && (
+
+ {effectiveCliStatus.providers.map((provider) => (
+
+
+
+
+ {provider.displayName}
+
+
+ {provider.authenticated
+ ? provider.authMethod
+ ? `Authenticated via ${provider.authMethod}`
+ : 'Authenticated'
+ : provider.statusMessage || 'Not connected'}
+
+
+
+ {provider.backend?.label && (
+ Backend: {provider.backend.label}
+ )}
+ {provider.models.length === 0 && (
+ Models unavailable for this runtime build
+ )}
+
+
+
+ {provider.authenticated ? (
+
+ ) : provider.canLoginFromUi ? (
+
+ ) : null}
+
+ {provider.models.length > 0 && (
+
+
+
+ )}
+
+ ))}
)}
@@ -158,7 +475,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
)}
{/* Install button (CLI not installed) */}
- {!cliStatus.installed && (
+ {!effectiveCliStatus.installed && effectiveCliStatus.supportsSelfUpdate && (
)}
+ {!effectiveCliStatus.installed && !effectiveCliStatus.supportsSelfUpdate && (
+
+ The configured free-code runtime was not found.
+
+ )}
)}
@@ -270,6 +592,30 @@ export const CliStatusSection = (): React.JSX.Element | null => {
)}
+ {providerTerminal && cliStatus?.binaryPath && (
+ {
+ setProviderTerminal(null);
+ recheckStatus();
+ }}
+ onExit={() => {
+ recheckStatus();
+ }}
+ autoCloseOnSuccessMs={3000}
+ successMessage={
+ providerTerminal.action === 'login' ? 'Authentication updated' : 'Provider logged out'
+ }
+ failureMessage={
+ providerTerminal.action === 'login' ? 'Authentication failed' : 'Logout failed'
+ }
+ />
+ )}
);
};
diff --git a/src/renderer/components/terminal/EmbeddedTerminal.tsx b/src/renderer/components/terminal/EmbeddedTerminal.tsx
index 895cf062..78acc77d 100644
--- a/src/renderer/components/terminal/EmbeddedTerminal.tsx
+++ b/src/renderer/components/terminal/EmbeddedTerminal.tsx
@@ -16,6 +16,8 @@ interface EmbeddedTerminalProps {
args?: string[];
/** Working directory */
cwd?: string;
+ /** Environment variables merged into the PTY process env */
+ env?: Record;
/** Callback when PTY process exits */
onExit?: (exitCode: number) => void;
/** CSS class for container */
@@ -26,6 +28,7 @@ export const EmbeddedTerminal = ({
command,
args,
cwd,
+ env,
onExit,
className,
}: EmbeddedTerminalProps): React.JSX.Element => {
@@ -99,6 +102,7 @@ export const EmbeddedTerminal = ({
...(command ? { command } : {}),
...(args ? { args } : {}),
...(cwd ? { cwd } : {}),
+ ...(env ? { env } : {}),
cols: term.cols,
rows: term.rows,
};
diff --git a/src/renderer/components/terminal/TerminalModal.tsx b/src/renderer/components/terminal/TerminalModal.tsx
index a7c7f4e7..88c8dc37 100644
--- a/src/renderer/components/terminal/TerminalModal.tsx
+++ b/src/renderer/components/terminal/TerminalModal.tsx
@@ -14,6 +14,8 @@ interface TerminalModalProps {
args?: string[];
/** Working directory */
cwd?: string;
+ /** Environment variables merged into the PTY process env */
+ env?: Record;
/** Called when the modal should close */
onClose: () => void;
/** Called when the PTY process exits */
@@ -31,6 +33,7 @@ export function TerminalModal({
command,
args,
cwd,
+ env,
onClose,
onExit,
autoCloseOnSuccessMs = 0,
@@ -116,7 +119,7 @@ export function TerminalModal({
{/* Terminal area — always visible, status bar overlaid at bottom */}
-
+
{exited !== null && (
void {
cliInstallerError: progress.error ?? 'Unknown error',
});
break;
+ case 'status':
+ if (progress.status) {
+ useStore.setState({ cliStatus: progress.status });
+ }
+ break;
}
});
if (typeof cleanup === 'function') {
diff --git a/src/shared/types/cliInstaller.ts b/src/shared/types/cliInstaller.ts
index 9149ead1..00433e24 100644
--- a/src/shared/types/cliInstaller.ts
+++ b/src/shared/types/cliInstaller.ts
@@ -21,6 +21,40 @@ export type CliPlatform =
| 'win32-x64'
| 'win32-arm64';
+export type CliFlavor = 'claude' | 'free-code';
+
+export type CliProviderId = 'anthropic' | 'codex' | 'gemini';
+
+export interface CliProviderStatus {
+ providerId: CliProviderId;
+ displayName: string;
+ supported: boolean;
+ authenticated: boolean;
+ authMethod: string | null;
+ verificationState: 'verified' | 'unknown' | 'offline' | 'error';
+ statusMessage?: string | null;
+ models: string[];
+ canLoginFromUi: boolean;
+ capabilities: {
+ teamLaunch: boolean;
+ oneShot: boolean;
+ };
+ backend?: {
+ kind: string;
+ label: string;
+ endpointLabel?: string | null;
+ projectId?: string | null;
+ authMethodDetail?: string | null;
+ } | null;
+}
+
+export interface CliFlavorUiOptions {
+ displayName: string;
+ supportsSelfUpdate: boolean;
+ showVersionDetails: boolean;
+ showBinaryPath: boolean;
+}
+
// =============================================================================
// Installation Status
// =============================================================================
@@ -29,6 +63,16 @@ export type CliPlatform =
* Current CLI installation status returned by getStatus().
*/
export interface CliInstallationStatus {
+ /** Selected CLI runtime flavor */
+ flavor: CliFlavor;
+ /** Display label for the configured runtime */
+ displayName: string;
+ /** Whether this runtime should expose self-update/install actions in the UI */
+ supportsSelfUpdate: boolean;
+ /** Whether version text should be shown in the UI */
+ showVersionDetails: boolean;
+ /** Whether binary path should be shown in the UI */
+ showBinaryPath: boolean;
/** Whether CLI binary is found on the system */
installed: boolean;
/** Installed version string (e.g. "2.1.59"), null if not installed */
@@ -43,6 +87,8 @@ export interface CliInstallationStatus {
authLoggedIn: boolean;
/** Auth method if logged in (e.g. "oauth_token", "api_key"), null otherwise */
authMethod: string | null;
+ /** Provider-level runtime status when supported by the configured runtime */
+ providers: CliProviderStatus[];
}
// =============================================================================
@@ -54,7 +100,7 @@ export interface CliInstallationStatus {
*/
export interface CliInstallerProgress {
/** Current phase of the installation process */
- type: 'checking' | 'downloading' | 'verifying' | 'installing' | 'completed' | 'error';
+ type: 'checking' | 'downloading' | 'verifying' | 'installing' | 'completed' | 'error' | 'status';
/** Download progress 0-100, only present for 'downloading' */
percent?: number;
/** Bytes downloaded so far */
@@ -69,6 +115,8 @@ export interface CliInstallerProgress {
detail?: string;
/** Raw terminal output chunk (with ANSI codes), only for 'installing' */
rawChunk?: string;
+ /** Partial or full CLI status snapshot during status gathering. */
+ status?: CliInstallationStatus;
}
// =============================================================================
diff --git a/src/shared/types/notifications.ts b/src/shared/types/notifications.ts
index cd36ca7b..f273bf23 100644
--- a/src/shared/types/notifications.ts
+++ b/src/shared/types/notifications.ts
@@ -308,6 +308,8 @@ export interface AppConfig {
theme: 'dark' | 'light' | 'system';
/** Default tab to show on app launch */
defaultTab: 'dashboard' | 'last-session';
+ /** Whether to use the multimodel runtime instead of the stock Claude CLI */
+ multimodelEnabled: boolean;
/** Optional custom Claude root folder (auto-detected when null) */
claudeRootPath: string | null;
/** Agent communication language ('system' = use OS locale) */
diff --git a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts
new file mode 100644
index 00000000..6a45cd38
--- /dev/null
+++ b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts
@@ -0,0 +1,160 @@
+// @vitest-environment node
+import type { PathLike } from 'fs';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const execCliMock = vi.fn();
+const buildEnrichedEnvMock = vi.fn<(binaryPath: string) => NodeJS.ProcessEnv>();
+const getCachedShellEnvMock = vi.fn<() => NodeJS.ProcessEnv | null>();
+const getShellPreferredHomeMock = vi.fn<() => string>();
+const resolveInteractiveShellEnvMock = vi.fn<() => Promise>();
+const readFileMock = vi.fn<(path: PathLike, encoding: BufferEncoding) => Promise>();
+
+vi.mock('@main/utils/childProcess', () => ({
+ execCli: (...args: Parameters) => execCliMock(...args),
+}));
+
+vi.mock('@main/utils/cliEnv', () => ({
+ buildEnrichedEnv: (binaryPath: string) => buildEnrichedEnvMock(binaryPath),
+}));
+
+vi.mock('@main/utils/shellEnv', () => ({
+ getCachedShellEnv: () => getCachedShellEnvMock(),
+ getShellPreferredHome: () => getShellPreferredHomeMock(),
+ resolveInteractiveShellEnv: () => resolveInteractiveShellEnvMock(),
+}));
+
+vi.mock('fs', () => ({
+ default: {
+ promises: {
+ readFile: (filePath: PathLike, encoding: BufferEncoding) => readFileMock(filePath, encoding),
+ },
+ },
+ promises: {
+ readFile: (filePath: PathLike, encoding: BufferEncoding) => readFileMock(filePath, encoding),
+ },
+}));
+
+describe('ClaudeMultimodelBridgeService', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.clearAllMocks();
+ buildEnrichedEnvMock.mockReturnValue({});
+ getCachedShellEnvMock.mockReturnValue({});
+ getShellPreferredHomeMock.mockReturnValue('/Users/tester');
+ resolveInteractiveShellEnvMock.mockResolvedValue({});
+ readFileMock.mockImplementation(async (filePath) => {
+ if (String(filePath) === '/Users/tester/.claude.json') {
+ return JSON.stringify({
+ geminiResolvedBackend: 'cli',
+ geminiLastAuthMethod: 'cli_oauth_personal',
+ geminiProjectId: 'demo-project',
+ });
+ }
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
+ });
+ });
+
+ it('parses object-based model lists and exposes Gemini runtime status', async () => {
+ execCliMock.mockImplementation(async (_binaryPath, args, options) => {
+ const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
+ const env = options?.env ?? {};
+
+ if (normalizedArgs === 'auth status --json --provider all') {
+ return {
+ stdout: JSON.stringify({
+ providers: {
+ anthropic: {
+ supported: true,
+ authenticated: true,
+ authMethod: 'oauth_token',
+ verificationState: 'verified',
+ canLoginFromUi: true,
+ capabilities: { teamLaunch: true, oneShot: true },
+ backend: { kind: 'anthropic', label: 'Anthropic' },
+ },
+ codex: {
+ supported: true,
+ authenticated: false,
+ verificationState: 'verified',
+ canLoginFromUi: true,
+ statusMessage: 'Not connected',
+ capabilities: { teamLaunch: true, oneShot: true },
+ backend: { kind: 'openai', label: 'OpenAI' },
+ },
+ },
+ }),
+ stderr: '',
+ exitCode: 0,
+ };
+ }
+
+ if (normalizedArgs === 'model list --json --provider all' && env.CLAUDE_CODE_USE_GEMINI === '1') {
+ return {
+ stdout: JSON.stringify({
+ providers: {
+ gemini: {
+ models: [{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }],
+ },
+ },
+ }),
+ stderr: '',
+ exitCode: 0,
+ };
+ }
+
+ if (normalizedArgs === 'model list --json --provider all') {
+ return {
+ stdout: JSON.stringify({
+ providers: {
+ anthropic: {
+ models: [{ id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' }],
+ },
+ codex: {
+ models: [{ id: 'gpt-5-codex', label: 'GPT-5 Codex' }],
+ },
+ },
+ }),
+ stderr: '',
+ exitCode: 0,
+ };
+ }
+
+ throw new Error(`Unexpected execCli call: ${normalizedArgs}`);
+ });
+
+ const { ClaudeMultimodelBridgeService } = await import(
+ '@main/services/runtime/ClaudeMultimodelBridgeService'
+ );
+ const service = new ClaudeMultimodelBridgeService();
+
+ const providers = await service.getProviderStatuses('/mock/free-code');
+
+ expect(providers).toHaveLength(3);
+ expect(providers[0]).toMatchObject({
+ providerId: 'anthropic',
+ authenticated: true,
+ models: ['claude-sonnet-4-5'],
+ });
+ expect(providers[1]).toMatchObject({
+ providerId: 'codex',
+ authenticated: false,
+ models: ['gpt-5-codex'],
+ statusMessage: 'Not connected',
+ });
+ expect(providers[2]).toMatchObject({
+ providerId: 'gemini',
+ displayName: 'Gemini',
+ supported: true,
+ authenticated: true,
+ models: ['gemini-2.5-pro'],
+ canLoginFromUi: true,
+ authMethod: 'cli_oauth_personal',
+ backend: {
+ kind: 'cli',
+ label: 'Gemini CLI',
+ endpointLabel: 'Code Assist (cloudcode-pa.googleapis.com/v1internal)',
+ projectId: 'demo-project',
+ },
+ });
+ });
+});
diff --git a/test/main/services/runtime/providerRuntimeEnv.test.ts b/test/main/services/runtime/providerRuntimeEnv.test.ts
new file mode 100644
index 00000000..e7f4d92a
--- /dev/null
+++ b/test/main/services/runtime/providerRuntimeEnv.test.ts
@@ -0,0 +1,29 @@
+// @vitest-environment node
+import { describe, expect, it } from 'vitest';
+
+import {
+ applyProviderRuntimeEnv,
+ resolveTeamProviderId,
+} from '@main/services/runtime/providerRuntimeEnv';
+
+describe('providerRuntimeEnv', () => {
+ it('enables Gemini runtime mode and clears other third-party provider flags', () => {
+ const env: NodeJS.ProcessEnv = {
+ CLAUDE_CODE_USE_OPENAI: '1',
+ CLAUDE_CODE_USE_GEMINI: undefined,
+ CLAUDE_CODE_USE_BEDROCK: '1',
+ };
+
+ const result = applyProviderRuntimeEnv(env, 'gemini');
+
+ expect(result.CLAUDE_CODE_USE_GEMINI).toBe('1');
+ expect(result.CLAUDE_CODE_USE_OPENAI).toBeUndefined();
+ expect(result.CLAUDE_CODE_USE_BEDROCK).toBeUndefined();
+ });
+
+ it('preserves gemini as a valid team provider id', () => {
+ expect(resolveTeamProviderId('gemini')).toBe('gemini');
+ expect(resolveTeamProviderId('codex')).toBe('codex');
+ expect(resolveTeamProviderId(undefined)).toBe('anthropic');
+ });
+});
diff --git a/test/main/services/team/ClaudeBinaryResolver.test.ts b/test/main/services/team/ClaudeBinaryResolver.test.ts
new file mode 100644
index 00000000..b65a6098
--- /dev/null
+++ b/test/main/services/team/ClaudeBinaryResolver.test.ts
@@ -0,0 +1,94 @@
+// @vitest-environment node
+import type { PathLike } from 'fs';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const mockBuildMergedCliPath = vi.fn<(binaryPath: string | null) => string>();
+const mockGetShellPreferredHome = vi.fn<() => string>();
+const mockResolveInteractiveShellEnv = vi.fn<() => Promise>();
+const mockGetConfiguredCliFlavor = vi.fn<() => 'claude' | 'free-code'>();
+
+const accessMock = vi.fn<(filePath: PathLike, mode?: number) => Promise>();
+const statMock = vi.fn<
+ (filePath: PathLike) => Promise<{ isFile: () => boolean }>
+>();
+const readdirMock = vi.fn<(filePath: PathLike) => Promise>();
+
+vi.mock('@main/utils/cliPathMerge', () => ({
+ buildMergedCliPath: (binaryPath: string | null) => mockBuildMergedCliPath(binaryPath),
+}));
+
+vi.mock('@main/utils/shellEnv', () => ({
+ getShellPreferredHome: () => mockGetShellPreferredHome(),
+ resolveInteractiveShellEnv: () => mockResolveInteractiveShellEnv(),
+}));
+
+vi.mock('@main/services/team/cliFlavor', () => ({
+ getConfiguredCliFlavor: () => mockGetConfiguredCliFlavor(),
+}));
+
+vi.mock('fs', () => ({
+ default: {
+ constants: { X_OK: 1 },
+ promises: {
+ access: (filePath: PathLike, mode?: number) => accessMock(filePath, mode),
+ stat: (filePath: PathLike) => statMock(filePath),
+ readdir: (filePath: PathLike) => readdirMock(filePath),
+ },
+ },
+ constants: { X_OK: 1 },
+ promises: {
+ access: (filePath: PathLike, mode?: number) => accessMock(filePath, mode),
+ stat: (filePath: PathLike) => statMock(filePath),
+ readdir: (filePath: PathLike) => readdirMock(filePath),
+ },
+}));
+
+describe('ClaudeBinaryResolver', () => {
+ const originalPlatform = process.platform;
+ const originalCwd = process.cwd;
+ const workspaceRoot = '/Users/belief/dev/projects/claude/claude_team_freecode';
+
+ beforeEach(() => {
+ vi.resetModules();
+ vi.clearAllMocks();
+ mockBuildMergedCliPath.mockReturnValue('/usr/local/bin:/usr/bin');
+ mockGetShellPreferredHome.mockReturnValue('/Users/tester');
+ mockResolveInteractiveShellEnv.mockResolvedValue({});
+ mockGetConfiguredCliFlavor.mockReturnValue('free-code');
+ readdirMock.mockResolvedValue([]);
+ Object.defineProperty(process, 'platform', {
+ value: 'darwin',
+ configurable: true,
+ writable: true,
+ });
+ process.cwd = vi.fn(() => workspaceRoot);
+ delete process.env.CLAUDE_CLI_PATH;
+ });
+
+ afterEach(() => {
+ Object.defineProperty(process, 'platform', {
+ value: originalPlatform,
+ configurable: true,
+ writable: true,
+ });
+ process.cwd = originalCwd;
+ vi.unstubAllEnvs();
+ });
+
+ it('resolves free-code runtime from the free-code-gemini-research sibling repo', async () => {
+ const expectedBinary = '/Users/belief/dev/projects/claude/free-code-gemini-research/cli';
+
+ accessMock.mockImplementation(async (filePath) => {
+ if (filePath === expectedBinary) {
+ return;
+ }
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
+ });
+
+ const { ClaudeBinaryResolver } = await import('@main/services/team/ClaudeBinaryResolver');
+ ClaudeBinaryResolver.clearCache();
+
+ await expect(ClaudeBinaryResolver.resolve()).resolves.toBe(expectedBinary);
+ expect(accessMock).toHaveBeenCalledWith(expectedBinary, 1);
+ });
+});
diff --git a/test/main/services/team/cliFlavor.test.ts b/test/main/services/team/cliFlavor.test.ts
new file mode 100644
index 00000000..a4612c84
--- /dev/null
+++ b/test/main/services/team/cliFlavor.test.ts
@@ -0,0 +1,55 @@
+// @vitest-environment node
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+const getConfigMock = vi.fn();
+
+vi.mock('@main/services/infrastructure/ConfigManager', () => ({
+ configManager: {
+ getConfig: () => getConfigMock(),
+ },
+}));
+
+describe('cliFlavor', () => {
+ afterEach(() => {
+ delete process.env.CLAUDE_TEAM_CLI_FLAVOR;
+ vi.resetModules();
+ vi.clearAllMocks();
+ });
+
+ it('uses multimodel runtime by default when config enables it', async () => {
+ getConfigMock.mockReturnValue({
+ general: {
+ multimodelEnabled: true,
+ },
+ });
+
+ const { getConfiguredCliFlavor } = await import('@main/services/team/cliFlavor');
+
+ expect(getConfiguredCliFlavor()).toBe('free-code');
+ });
+
+ it('uses claude runtime when multimodel is disabled in config', async () => {
+ getConfigMock.mockReturnValue({
+ general: {
+ multimodelEnabled: false,
+ },
+ });
+
+ const { getConfiguredCliFlavor } = await import('@main/services/team/cliFlavor');
+
+ expect(getConfiguredCliFlavor()).toBe('claude');
+ });
+
+ it('lets env override the persisted config', async () => {
+ process.env.CLAUDE_TEAM_CLI_FLAVOR = 'claude';
+ getConfigMock.mockReturnValue({
+ general: {
+ multimodelEnabled: true,
+ },
+ });
+
+ const { getConfiguredCliFlavor } = await import('@main/services/team/cliFlavor');
+
+ expect(getConfiguredCliFlavor()).toBe('claude');
+ });
+});
diff --git a/test/renderer/store/cliInstallerSlice.test.ts b/test/renderer/store/cliInstallerSlice.test.ts
index d9dd9e92..7177eee6 100644
--- a/test/renderer/store/cliInstallerSlice.test.ts
+++ b/test/renderer/store/cliInstallerSlice.test.ts
@@ -83,6 +83,11 @@ describe('cliInstallerSlice', () => {
describe('fetchCliStatus', () => {
it('updates cliStatus from API', async () => {
const mockStatus: CliInstallationStatus = {
+ flavor: 'claude',
+ displayName: 'Claude CLI',
+ supportsSelfUpdate: true,
+ showVersionDetails: true,
+ showBinaryPath: true,
installed: true,
installedVersion: '2.1.59',
binaryPath: '/usr/local/bin/claude',
@@ -90,6 +95,7 @@ describe('cliInstallerSlice', () => {
updateAvailable: false,
authLoggedIn: false,
authMethod: null,
+ providers: [],
};
vi.mocked(api.cliInstaller.getStatus).mockResolvedValue(mockStatus);
@@ -109,6 +115,11 @@ describe('cliInstallerSlice', () => {
it('detects update available', async () => {
const mockStatus: CliInstallationStatus = {
+ flavor: 'claude',
+ displayName: 'Claude CLI',
+ supportsSelfUpdate: true,
+ showVersionDetails: true,
+ showBinaryPath: true,
installed: true,
installedVersion: '2.1.34',
binaryPath: '/usr/local/bin/claude',
@@ -116,6 +127,7 @@ describe('cliInstallerSlice', () => {
updateAvailable: true,
authLoggedIn: true,
authMethod: 'oauth_token',
+ providers: [],
};
vi.mocked(api.cliInstaller.getStatus).mockResolvedValue(mockStatus);