revert: revert KiloCode provider support

This reverts commit cc10485f0c.
This commit is contained in:
Илия 2026-05-25 21:19:56 +03:00 committed by GitHub
parent cc10485f0c
commit 8e0731f47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 55 additions and 964 deletions

View file

@ -1,11 +0,0 @@
import type {
CliProviderModelCatalog,
CliProviderModelCatalogItem,
CliProviderModelCatalogSource,
CliProviderModelCatalogStatus,
} from '@shared/types';
export type KilocodeModelCatalogDto = CliProviderModelCatalog;
export type KilocodeModelCatalogItemDto = CliProviderModelCatalogItem;
export type KilocodeModelCatalogSourceDto = CliProviderModelCatalogSource;
export type KilocodeModelCatalogStatusDto = CliProviderModelCatalogStatus;

View file

@ -1,6 +0,0 @@
export type {
KilocodeModelCatalogDto,
KilocodeModelCatalogItemDto,
KilocodeModelCatalogSourceDto,
KilocodeModelCatalogStatusDto,
} from './dto';

View file

@ -1,32 +0,0 @@
import type { KilocodeModelCatalogItemDto } from '../../contracts';
export function createStaticKilocodeModelCatalogModels(): KilocodeModelCatalogItemDto[] {
return [
{
id: 'claude-sonnet-4-5',
launchModel: 'claude-sonnet-4-5',
displayName: 'Claude Sonnet 4.5',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: true,
upgrade: false,
source: 'static-fallback',
},
{
id: 'claude-opus-4-5',
launchModel: 'claude-opus-4-5',
displayName: 'Claude Opus 4.5',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: false,
upgrade: false,
source: 'static-fallback',
},
];
}

View file

@ -1,8 +0,0 @@
export type {
KilocodeModelCatalogDto,
KilocodeModelCatalogItemDto,
KilocodeModelCatalogSourceDto,
KilocodeModelCatalogStatusDto,
} from './contracts';
export type { KilocodeModelCatalogFeatureFacade, KilocodeModelCatalogRequest } from './main';
export { createKilocodeModelCatalogFeature } from './main';

View file

@ -1,186 +0,0 @@
import { createHash } from 'node:crypto';
import { createStaticKilocodeModelCatalogModels } from '../../core/domain/kilocodeModelCatalogFallback';
import { InMemoryKilocodeModelCatalogCache } from '../infrastructure/InMemoryKilocodeModelCatalogCache';
import { KilocodeGatewayClient } from '../infrastructure/KilocodeGatewayClient';
import type { KilocodeModelCatalogDto, KilocodeModelCatalogItemDto } from '../../contracts';
import type { Logger } from '@shared/utils/logger';
type LoggerPort = Pick<Logger, 'warn'>;
const CATALOG_CACHE_TTL_MS = 10 * 60_000;
const CATALOG_STALE_TTL_MS = 24 * 60 * 60_000;
export interface KilocodeModelCatalogRequest {
apiKey?: string | null;
forceRefresh?: boolean;
}
export interface KilocodeModelCatalogFeatureFacade {
getCatalog(options?: KilocodeModelCatalogRequest): Promise<KilocodeModelCatalogDto>;
invalidate(): void;
}
function nowIso(): string {
return new Date().toISOString();
}
function staleAtIso(): string {
return new Date(Date.now() + CATALOG_CACHE_TTL_MS).toISOString();
}
function buildCacheKey(apiKey: string): string {
return `kilocode:${createHash('sha256').update(apiKey).digest('hex')}`;
}
function normalizeGatewayModels(
models: { id: string; displayName: string }[]
): KilocodeModelCatalogItemDto[] {
return models.map((model, index) => ({
id: model.id,
launchModel: model.id,
displayName: model.displayName,
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: index === 0,
upgrade: false,
source: 'app-server' as const,
}));
}
function createFallbackCatalog(options: {
message: string;
status?: KilocodeModelCatalogDto['status'];
appServerState: KilocodeModelCatalogDto['diagnostics']['appServerState'];
}): KilocodeModelCatalogDto {
const models = createStaticKilocodeModelCatalogModels();
const defaultModel = models.find((m) => m.isDefault) ?? models[0] ?? null;
return {
schemaVersion: 1,
providerId: 'kilocode',
source: 'static-fallback',
status: options.status ?? 'degraded',
fetchedAt: nowIso(),
staleAt: staleAtIso(),
defaultModelId: defaultModel?.id ?? null,
defaultLaunchModel: defaultModel?.launchModel ?? null,
models,
diagnostics: {
configReadState: 'skipped',
appServerState: options.appServerState,
message: options.message,
code: null,
},
};
}
export function createKilocodeModelCatalogFeature(options: {
logger: LoggerPort;
}): KilocodeModelCatalogFeatureFacade {
const cache = new InMemoryKilocodeModelCatalogCache();
const inFlightRefreshes = new Map<string, Promise<KilocodeModelCatalogDto>>();
const client = new KilocodeGatewayClient();
async function getCatalog(
request: KilocodeModelCatalogRequest = {}
): Promise<KilocodeModelCatalogDto> {
const apiKey = request.apiKey?.trim() || process.env.KILO_API_KEY?.trim() || null;
if (!apiKey) {
return createFallbackCatalog({
message: 'No KiloCode API key configured. Set KILO_API_KEY or configure an API key.',
appServerState: 'runtime-missing',
status: 'unavailable',
});
}
const cacheKey = buildCacheKey(apiKey);
if (request.forceRefresh !== true) {
const cached = cache.get(cacheKey, CATALOG_CACHE_TTL_MS);
if (cached) {
return cached;
}
}
const existing = inFlightRefreshes.get(cacheKey);
if (existing) {
return existing;
}
const refreshPromise = (async (): Promise<KilocodeModelCatalogDto> => {
try {
const gatewayModels = await client.listModels(apiKey);
const models = normalizeGatewayModels(gatewayModels);
if (models.length === 0) {
throw new Error('KiloCode gateway returned no models.');
}
const defaultModel = models[0] ?? null;
const catalog: KilocodeModelCatalogDto = {
schemaVersion: 1,
providerId: 'kilocode',
source: 'app-server',
status: 'ready',
fetchedAt: nowIso(),
staleAt: staleAtIso(),
defaultModelId: defaultModel?.id ?? null,
defaultLaunchModel: defaultModel?.launchModel ?? null,
models,
diagnostics: {
configReadState: 'skipped',
appServerState: 'healthy',
message: null,
code: null,
},
};
cache.set(cacheKey, catalog);
return catalog;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const stale = cache.getLatest(cacheKey);
if (stale && Date.parse(stale.fetchedAt) + CATALOG_STALE_TTL_MS > Date.now()) {
return {
...stale,
status: 'stale',
diagnostics: {
configReadState: 'skipped',
appServerState: 'degraded',
message,
code: null,
},
};
}
options.logger.warn('KiloCode model catalog refresh failed', { error: message });
return createFallbackCatalog({
message,
appServerState: 'degraded',
});
}
})();
inFlightRefreshes.set(cacheKey, refreshPromise);
try {
return await refreshPromise;
} finally {
if (inFlightRefreshes.get(cacheKey) === refreshPromise) {
inFlightRefreshes.delete(cacheKey);
}
}
}
return {
getCatalog,
invalidate: () => {
cache.clear();
inFlightRefreshes.clear();
},
};
}

View file

@ -1,5 +0,0 @@
export type {
KilocodeModelCatalogFeatureFacade,
KilocodeModelCatalogRequest,
} from './composition/createKilocodeModelCatalogFeature';
export { createKilocodeModelCatalogFeature } from './composition/createKilocodeModelCatalogFeature';

View file

@ -1,37 +0,0 @@
import type { KilocodeModelCatalogDto } from '../../contracts';
interface CacheEntry {
value: KilocodeModelCatalogDto;
observedAt: number;
}
export class InMemoryKilocodeModelCatalogCache {
private readonly entries = new Map<string, CacheEntry>();
get(key: string, maxAgeMs: number): KilocodeModelCatalogDto | null {
const entry = this.entries.get(key);
if (!entry) {
return null;
}
if (Date.now() - entry.observedAt > maxAgeMs) {
return null;
}
return structuredClone(entry.value);
}
getLatest(key: string): KilocodeModelCatalogDto | null {
const entry = this.entries.get(key);
return entry ? structuredClone(entry.value) : null;
}
set(key: string, value: KilocodeModelCatalogDto): void {
this.entries.set(key, {
value: structuredClone(value),
observedAt: Date.now(),
});
}
clear(): void {
this.entries.clear();
}
}

View file

@ -1,101 +0,0 @@
import https from 'node:https';
const GATEWAY_BASE_URL = 'https://api.kilo.ai';
// KiloCode gateway endpoint: https://kilo.ai/docs/gateway/models-and-providers
const MODELS_PATH = '/api/gateway/models';
const REQUEST_TIMEOUT_MS = 8_000;
const ERROR_BODY_PREVIEW_LIMIT = 500;
interface GatewayModelObject {
id?: string;
object?: string;
created?: number;
owned_by?: string;
display_name?: string;
}
interface GatewayModelsResponse {
object?: string;
data?: GatewayModelObject[];
}
export interface KilocodeGatewayModel {
id: string;
displayName: string;
}
function sanitizeErrorBody(body: string): string {
const sanitized = body
.trim()
.replace(/Bearer\s+[A-Za-z0-9._~-]+/gi, 'Bearer [redacted]')
.replace(/sk-[A-Za-z0-9_-]+/g, '[redacted-api-key]');
if (!sanitized) {
return 'empty response body';
}
return sanitized.length > ERROR_BODY_PREVIEW_LIMIT
? `${sanitized.slice(0, ERROR_BODY_PREVIEW_LIMIT)}...`
: sanitized;
}
export class KilocodeGatewayClient {
async listModels(apiKey: string): Promise<KilocodeGatewayModel[]> {
const raw = await this.fetchModels(apiKey);
const items = raw.data ?? [];
return items
.filter(
(item): item is GatewayModelObject & { id: string } =>
typeof item.id === 'string' && item.id.trim().length > 0
)
.map((item) => ({
id: item.id.trim(),
displayName: (item.display_name ?? item.id).trim(),
}));
}
private fetchModels(apiKey: string): Promise<GatewayModelsResponse> {
return new Promise((resolve, reject) => {
const url = new URL(MODELS_PATH, GATEWAY_BASE_URL);
const options: https.RequestOptions = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method: 'GET',
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: 'application/json',
},
};
const req = https.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
reject(
new Error(
`KiloCode gateway responded with HTTP ${res.statusCode}: ${sanitizeErrorBody(body)}`
)
);
return;
}
try {
resolve(JSON.parse(body) as GatewayModelsResponse);
} catch {
reject(
new Error(`KiloCode gateway returned non-JSON response: ${sanitizeErrorBody(body)}`)
);
}
});
res.on('error', reject);
});
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
req.destroy(new Error(`KiloCode gateway request timed out after ${REQUEST_TIMEOUT_MS}ms`));
});
req.on('error', reject);
req.end();
});
}
}

View file

@ -20,7 +20,7 @@ export type MemberWorkSyncActionableWorkPriority =
| 'blocked' | 'blocked'
| 'needs_clarification'; | 'needs_clarification';
export type MemberWorkSyncProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode'; export type MemberWorkSyncProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type MemberWorkSyncReviewObligation = 'review_pickup_required' | 'review_in_progress'; export type MemberWorkSyncReviewObligation = 'review_pickup_required' | 'review_in_progress';

View file

@ -384,7 +384,6 @@ const BRAND_ALIASES: Record<string, string> = {
'gitlab-duo': 'gitlab-duo', 'gitlab-duo': 'gitlab-duo',
'google-vertex': 'google-vertex', 'google-vertex': 'google-vertex',
'hugging-face': 'huggingface', 'hugging-face': 'huggingface',
kilocode: 'kilo',
'mistral-ai': 'mistral', 'mistral-ai': 'mistral',
'ollama-cloud': 'ollama-cloud', 'ollama-cloud': 'ollama-cloud',
'opencode-zen': 'opencode', 'opencode-zen': 'opencode',

View file

@ -1,10 +1,4 @@
export type WorkspaceTrustProvider = export type WorkspaceTrustProvider = 'claude' | 'anthropic' | 'codex' | 'gemini' | 'opencode';
| 'claude'
| 'anthropic'
| 'codex'
| 'gemini'
| 'opencode'
| 'kilocode';
export type WorkspaceTrustWorkspaceSource = export type WorkspaceTrustWorkspaceSource =
| 'team-root' | 'team-root'

View file

@ -32,10 +32,6 @@ import {
type CodexModelCatalogFeatureFacade, type CodexModelCatalogFeatureFacade,
createCodexModelCatalogFeature, createCodexModelCatalogFeature,
} from '@features/codex-model-catalog/main'; } from '@features/codex-model-catalog/main';
import {
type KilocodeModelCatalogFeatureFacade,
createKilocodeModelCatalogFeature,
} from '@features/kilocode-model-catalog/main';
import { import {
createMemberLogStreamFeature, createMemberLogStreamFeature,
registerMemberLogStreamIpc, registerMemberLogStreamIpc,
@ -903,7 +899,6 @@ let updaterService: UpdaterService;
let sshConnectionManager: SshConnectionManager; let sshConnectionManager: SshConnectionManager;
let codexAccountFeature: CodexAccountFeatureFacade | null = null; let codexAccountFeature: CodexAccountFeatureFacade | null = null;
let codexModelCatalogFeature: CodexModelCatalogFeatureFacade | null = null; let codexModelCatalogFeature: CodexModelCatalogFeatureFacade | null = null;
let kilocodeModelCatalogFeature: KilocodeModelCatalogFeatureFacade | null = null;
let recentProjectsFeature: RecentProjectsFeatureFacade; let recentProjectsFeature: RecentProjectsFeatureFacade;
let runtimeProviderManagementFeature: RuntimeProviderManagementFeatureFacade; let runtimeProviderManagementFeature: RuntimeProviderManagementFeatureFacade;
let memberWorkSyncFeature: MemberWorkSyncFeatureFacade | null = null; let memberWorkSyncFeature: MemberWorkSyncFeatureFacade | null = null;
@ -2108,10 +2103,6 @@ async function initializeServices(): Promise<void> {
codexAccountFeature, codexAccountFeature,
}); });
providerConnectionService.setCodexModelCatalogFeature(codexModelCatalogFeature); providerConnectionService.setCodexModelCatalogFeature(codexModelCatalogFeature);
kilocodeModelCatalogFeature = createKilocodeModelCatalogFeature({
logger: createLogger('Feature:KilocodeModelCatalog'),
});
providerConnectionService.setKilocodeModelCatalogFeature(kilocodeModelCatalogFeature);
// startProcessHealthPolling() is deferred to after window creation // startProcessHealthPolling() is deferred to after window creation
// (did-finish-load handler) to avoid thread pool contention at startup. // (did-finish-load handler) to avoid thread pool contention at startup.
@ -2364,13 +2355,10 @@ async function shutdownServices(): Promise<void> {
await runShutdownStep('skills watcher stop', () => skillsWatcherService?.stopAll()); await runShutdownStep('skills watcher stop', () => skillsWatcherService?.stopAll());
await runShutdownStep('provider connection feature detach', () => { await runShutdownStep('provider connection feature detach', () => {
providerConnectionService.setCodexModelCatalogFeature(null); providerConnectionService.setCodexModelCatalogFeature(null);
providerConnectionService.setKilocodeModelCatalogFeature(null);
providerConnectionService.setCodexAccountFeature(null); providerConnectionService.setCodexAccountFeature(null);
}); });
await runShutdownStep('Codex model catalog dispose', () => codexModelCatalogFeature?.dispose()); await runShutdownStep('Codex model catalog dispose', () => codexModelCatalogFeature?.dispose());
codexModelCatalogFeature = null; codexModelCatalogFeature = null;
kilocodeModelCatalogFeature?.invalidate();
kilocodeModelCatalogFeature = null;
await runShutdownStep('Codex account dispose', () => codexAccountFeature?.dispose()); await runShutdownStep('Codex account dispose', () => codexAccountFeature?.dispose());
codexAccountFeature = null; codexAccountFeature = null;
await runShutdownStep('member work sync dispose', () => memberWorkSyncFeature?.dispose()); await runShutdownStep('member work sync dispose', () => memberWorkSyncFeature?.dispose());

View file

@ -44,12 +44,7 @@ const cachedStatus = new Map<
>(); >();
let statusCacheGeneration = 0; let statusCacheGeneration = 0;
const STATUS_CACHE_TTL_MS = 5_000; const STATUS_CACHE_TTL_MS = 5_000;
const FRONTEND_MULTIMODEL_PROVIDER_IDS = new Set<CliProviderId>([ const FRONTEND_MULTIMODEL_PROVIDER_IDS = new Set<CliProviderId>(['anthropic', 'codex', 'opencode']);
'anthropic',
'codex',
'opencode',
'kilocode',
]);
function isFrontendMultimodelProviderId(providerId: CliProviderId): boolean { function isFrontendMultimodelProviderId(providerId: CliProviderId): boolean {
return FRONTEND_MULTIMODEL_PROVIDER_IDS.has(providerId); return FRONTEND_MULTIMODEL_PROVIDER_IDS.has(providerId);

View file

@ -45,7 +45,6 @@ import {
type ProviderModelAvailabilityContext, type ProviderModelAvailabilityContext,
type ProviderModelAvailabilitySnapshot, type ProviderModelAvailabilitySnapshot,
} from '../runtime/CliProviderModelAvailabilityService'; } from '../runtime/CliProviderModelAvailabilityService';
import { providerConnectionService } from '../runtime/ProviderConnectionService';
import { ClaudeBinaryResolver } from '../team/ClaudeBinaryResolver'; import { ClaudeBinaryResolver } from '../team/ClaudeBinaryResolver';
import { getCliFlavorUiOptions, getConfiguredCliFlavor } from '../team/cliFlavor'; import { getCliFlavorUiOptions, getConfiguredCliFlavor } from '../team/cliFlavor';
@ -72,12 +71,7 @@ const GCS_BASE =
'https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases'; 'https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases';
const CLI_INSTALLER_PROGRESS_CHANNEL = 'cliInstaller:progress'; const CLI_INSTALLER_PROGRESS_CHANNEL = 'cliInstaller:progress';
const FRONTEND_MULTIMODEL_PROVIDER_IDS: CliProviderId[] = [ const FRONTEND_MULTIMODEL_PROVIDER_IDS: CliProviderId[] = ['anthropic', 'codex', 'opencode'];
'anthropic',
'codex',
'opencode',
'kilocode',
];
const FRONTEND_MULTIMODEL_PROVIDER_ID_SET = new Set<CliProviderId>( const FRONTEND_MULTIMODEL_PROVIDER_ID_SET = new Set<CliProviderId>(
FRONTEND_MULTIMODEL_PROVIDER_IDS FRONTEND_MULTIMODEL_PROVIDER_IDS
); );
@ -92,8 +86,6 @@ function getProviderDisplayName(providerId: CliProviderId): string {
return 'Gemini'; return 'Gemini';
case 'opencode': case 'opencode':
return 'OpenCode (200+ models)'; return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
} }
} }
@ -927,10 +919,6 @@ export class CliInstallerService {
background: false, background: false,
}); });
if (providerId === 'kilocode') {
return this.resolveKilocodeProviderStatus();
}
const binaryPath = await ClaudeBinaryResolver.resolve(); const binaryPath = await ClaudeBinaryResolver.resolve();
if (!binaryPath) { if (!binaryPath) {
return null; return null;
@ -971,10 +959,6 @@ export class CliInstallerService {
background: false, background: false,
}); });
if (providerId === 'kilocode') {
return this.resolveKilocodeProviderStatus();
}
const binaryPath = await ClaudeBinaryResolver.resolve(); const binaryPath = await ClaudeBinaryResolver.resolve();
if (!binaryPath) { if (!binaryPath) {
return null; return null;
@ -1220,53 +1204,6 @@ export class CliInstallerService {
result.authMethod = null; result.authMethod = null;
} }
private async resolveKilocodeProviderStatus(): Promise<CliProviderStatus> {
const baseStatus: CliProviderStatus = {
providerId: 'kilocode',
displayName: 'KiloCode',
supported: false,
authenticated: false,
authMethod: null,
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: false,
oneShot: false,
extensions: createDefaultCliExtensionCapabilities(),
},
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: null,
modelCatalog: null,
runtimeCapabilities: null,
subscriptionRateLimits: null,
};
// enrichProviderStatus checks both the app key store and process.env for KILO_API_KEY
const enriched = await providerConnectionService.enrichProviderStatus(baseStatus);
const hasApiKey = Boolean(enriched.connection?.apiKeyConfigured);
const status: CliProviderStatus = {
...enriched,
supported: hasApiKey,
authenticated: hasApiKey,
authMethod: hasApiKey ? 'api_key' : null,
statusMessage: hasApiKey ? null : 'Configure KILO_API_KEY to use KiloCode.',
capabilities: {
...enriched.capabilities,
teamLaunch: hasApiKey,
},
};
this.updateLatestProviderStatus(status);
return status;
}
private markProvidersDeferred(result: CliInstallationStatus): void { private markProvidersDeferred(result: CliInstallationStatus): void {
if (result.flavor !== 'agent_teams_orchestrator') { if (result.flavor !== 'agent_teams_orchestrator') {
return; return;
@ -1304,38 +1241,13 @@ export class CliInstallerService {
if (result.flavor === 'agent_teams_orchestrator') { if (result.flavor === 'agent_teams_orchestrator') {
result.authStatusChecking = true; result.authStatusChecking = true;
let statusTarget = result; let statusTarget = result;
const buildFrontendProviders = async ( const applyProviders = (providersSnapshot: CliProviderStatus[], final: boolean): void => {
providersSnapshot: CliProviderStatus[]
): Promise<CliProviderStatus[]> => {
const providersById = new Map(providersSnapshot.map((provider) => [provider.providerId, provider]));
const frontendProviders: CliProviderStatus[] = [];
for (const providerId of FRONTEND_MULTIMODEL_PROVIDER_IDS) {
if (providerId === 'kilocode') {
frontendProviders.push(await this.resolveKilocodeProviderStatus());
continue;
}
const provider = providersById.get(providerId);
if (provider) {
frontendProviders.push(provider);
}
}
return frontendProviders;
};
const applyProviders = async (
providersSnapshot: CliProviderStatus[],
final: boolean
): Promise<void> => {
if (generation !== this.statusGatherGeneration) { if (generation !== this.statusGatherGeneration) {
return; return;
} }
const target = statusTarget; const target = statusTarget;
const frontendProviders = await buildFrontendProviders( const frontendProviders = filterFrontendMultimodelProviders(providersSnapshot);
filterFrontendMultimodelProviders(providersSnapshot)
);
if (generation !== this.statusGatherGeneration) {
return;
}
target.providers = frontendProviders; target.providers = frontendProviders;
target.authLoggedIn = hasFrontendAuthenticatedProvider(frontendProviders); target.authLoggedIn = hasFrontendAuthenticatedProvider(frontendProviders);
target.authMethod = getFrontendAuthenticatedProvider(frontendProviders)?.authMethod ?? null; target.authMethod = getFrontendAuthenticatedProvider(frontendProviders)?.authMethod ?? null;
@ -1348,10 +1260,10 @@ export class CliInstallerService {
const completion = this.multimodelBridgeService const completion = this.multimodelBridgeService
.getProviderStatuses(binaryPath, (providersSnapshot) => { .getProviderStatuses(binaryPath, (providersSnapshot) => {
void applyProviders(providersSnapshot, false); applyProviders(providersSnapshot, false);
}) })
.then((providers) => { .then((providers) => {
return applyProviders(providers, true); applyProviders(providers, true);
}) })
.catch((error) => { .catch((error) => {
if (generation !== this.statusGatherGeneration) { if (generation !== this.statusGatherGeneration) {

View file

@ -335,8 +335,6 @@ function getProviderDisplayName(providerId: CliProviderId): string {
return 'Gemini'; return 'Gemini';
case 'opencode': case 'opencode':
return 'OpenCode (200+ models)'; return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
} }
} }
@ -423,14 +421,15 @@ function mapRuntimeExtensionCapabilities(
const defaults = capabilities const defaults = capabilities
? createDefaultCliExtensionCapabilities() ? createDefaultCliExtensionCapabilities()
: createLegacyRuntimeFallbackCliExtensionCapabilities(); : createLegacyRuntimeFallbackCliExtensionCapabilities();
const isExternalRuntime = providerId === 'opencode' || providerId === 'kilocode'; const pluginStatus =
const pluginStatus = isExternalRuntime providerId === 'opencode'
? 'unsupported' ? 'unsupported'
: (capabilities?.plugins?.status ?? defaults.plugins.status); : (capabilities?.plugins?.status ?? defaults.plugins.status);
const pluginReason = isExternalRuntime const pluginReason =
? (capabilities?.plugins?.reason ?? providerId === 'opencode'
`${getProviderDisplayName(providerId)} does not support plugin management from Agent Teams.`) ? (capabilities?.plugins?.reason ??
: (capabilities?.plugins?.reason ?? defaults.plugins.reason); 'OpenCode does not support plugin management from Agent Teams.')
: (capabilities?.plugins?.reason ?? defaults.plugins.reason);
return { return {
plugins: { plugins: {

View file

@ -23,7 +23,6 @@ import type {
CodexModelCatalogFeatureFacade, CodexModelCatalogFeatureFacade,
CodexModelCatalogRequest, CodexModelCatalogRequest,
} from '@features/codex-model-catalog/main'; } from '@features/codex-model-catalog/main';
import type { KilocodeModelCatalogFeatureFacade } from '@features/kilocode-model-catalog/main';
import type { import type {
CliProviderAuthMode, CliProviderAuthMode,
CliProviderConnectionInfo, CliProviderConnectionInfo,
@ -66,18 +65,12 @@ const PROVIDER_CAPABILITIES: Record<
supportsApiKey: false, supportsApiKey: false,
configurableAuthModes: [], configurableAuthModes: [],
}, },
kilocode: {
supportsOAuth: false,
supportsApiKey: true,
configurableAuthModes: ['api_key'],
},
}; };
const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = { const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = {
anthropic: 'ANTHROPIC_API_KEY', anthropic: 'ANTHROPIC_API_KEY',
codex: 'OPENAI_API_KEY', codex: 'OPENAI_API_KEY',
gemini: 'GEMINI_API_KEY', gemini: 'GEMINI_API_KEY',
kilocode: 'KILO_API_KEY',
}; };
const ANTHROPIC_BASE_URL_ENV_VAR = 'ANTHROPIC_BASE_URL'; const ANTHROPIC_BASE_URL_ENV_VAR = 'ANTHROPIC_BASE_URL';
@ -371,10 +364,6 @@ export class ProviderConnectionService {
private codexAccountFeature: CodexAccountSnapshotReader | null = null; private codexAccountFeature: CodexAccountSnapshotReader | null = null;
private codexModelCatalogFeature: Pick<CodexModelCatalogFeatureFacade, 'getCatalog'> | null = private codexModelCatalogFeature: Pick<CodexModelCatalogFeatureFacade, 'getCatalog'> | null =
null; null;
private kilocodeModelCatalogFeature: Pick<
KilocodeModelCatalogFeatureFacade,
'getCatalog'
> | null = null;
private readonly anthropicApiKeyVerificationCache = new Map< private readonly anthropicApiKeyVerificationCache = new Map<
string, string,
{ result: AnthropicApiKeyVerificationResult; at: number } { result: AnthropicApiKeyVerificationResult; at: number }
@ -402,12 +391,6 @@ export class ProviderConnectionService {
this.codexModelCatalogFeature = feature; this.codexModelCatalogFeature = feature;
} }
setKilocodeModelCatalogFeature(
feature: Pick<KilocodeModelCatalogFeatureFacade, 'getCatalog'> | null
): void {
this.kilocodeModelCatalogFeature = feature;
}
async getCodexModelCatalog( async getCodexModelCatalog(
request: CodexModelCatalogRequest = {} request: CodexModelCatalogRequest = {}
): Promise<CodexModelCatalogDto | null> { ): Promise<CodexModelCatalogDto | null> {
@ -435,10 +418,6 @@ export class ProviderConnectionService {
return this.configManager.getConfig().providerConnections.codex.preferredAuthMode; return this.configManager.getConfig().providerConnections.codex.preferredAuthMode;
} }
if (providerId === 'kilocode') {
return 'api_key';
}
return null; return null;
} }
@ -620,16 +599,6 @@ export class ProviderConnectionService {
return env; return env;
} }
if (providerId === 'kilocode') {
const apiKey = await this.resolveProviderApiKeyForEnv(env, 'kilocode', options);
if (apiKey) {
env.KILO_API_KEY = apiKey;
} else if (typeof env.KILO_API_KEY === 'string' && !env.KILO_API_KEY.trim()) {
delete env.KILO_API_KEY;
}
return env;
}
if (providerId !== 'codex') { if (providerId !== 'codex') {
return env; return env;
} }
@ -676,7 +645,7 @@ export class ProviderConnectionService {
options?: StoredApiKeyAccessOptions options?: StoredApiKeyAccessOptions
): Promise<NodeJS.ProcessEnv> { ): Promise<NodeJS.ProcessEnv> {
let nextEnv = env; let nextEnv = env;
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode', 'kilocode'] as const) { for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId, undefined, options); nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
} }
return nextEnv; return nextEnv;
@ -712,14 +681,6 @@ export class ProviderConnectionService {
return env; return env;
} }
if (providerId === 'kilocode') {
const apiKey = await this.resolveProviderApiKeyForEnv(env, 'kilocode', options);
if (apiKey) {
env.KILO_API_KEY = apiKey;
}
return env;
}
if (providerId !== 'codex') { if (providerId !== 'codex') {
return env; return env;
} }
@ -761,7 +722,7 @@ export class ProviderConnectionService {
options?: StoredApiKeyAccessOptions options?: StoredApiKeyAccessOptions
): Promise<NodeJS.ProcessEnv> { ): Promise<NodeJS.ProcessEnv> {
let nextEnv = env; let nextEnv = env;
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode', 'kilocode'] as const) { for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId, undefined, options); nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
} }
return nextEnv; return nextEnv;
@ -804,22 +765,6 @@ export class ProviderConnectionService {
); );
} }
if (providerId === 'kilocode') {
if (typeof env.KILO_API_KEY === 'string' && env.KILO_API_KEY.trim()) {
return null;
}
if (await this.hasStoredApiKey('KILO_API_KEY')) {
return null;
}
if (this.getExternalCredential('kilocode')?.value.trim()) {
return null;
}
return 'KiloCode API key is not configured. Set KILO_API_KEY or add it in Provider Settings.';
}
if (providerId !== 'codex') { if (providerId !== 'codex') {
return null; return null;
} }
@ -902,13 +847,7 @@ export class ProviderConnectionService {
async getConfiguredConnectionIssues( async getConfiguredConnectionIssues(
env: NodeJS.ProcessEnv, env: NodeJS.ProcessEnv,
providerIds: readonly CliProviderId[] = [ providerIds: readonly CliProviderId[] = ['anthropic', 'codex', 'gemini', 'opencode'],
'anthropic',
'codex',
'gemini',
'opencode',
'kilocode',
],
runtimeBackendOverrides?: Partial<Record<CliProviderId, string>> runtimeBackendOverrides?: Partial<Record<CliProviderId, string>>
): Promise<Partial<Record<CliProviderId, string>>> { ): Promise<Partial<Record<CliProviderId, string>>> {
const issues: Partial<Record<CliProviderId, string>> = {}; const issues: Partial<Record<CliProviderId, string>> = {};
@ -977,10 +916,6 @@ export class ProviderConnectionService {
return this.enrichAnthropicProviderStatus(withConnection); return this.enrichAnthropicProviderStatus(withConnection);
} }
if (provider.providerId === 'kilocode') {
return this.enrichKilocodeProviderStatus(withConnection);
}
if (provider.providerId !== 'codex') { if (provider.providerId !== 'codex') {
return withConnection; return withConnection;
} }
@ -1045,33 +980,6 @@ export class ProviderConnectionService {
} }
} }
private async enrichKilocodeProviderStatus(
provider: CliProviderStatus
): Promise<CliProviderStatus> {
if (!this.kilocodeModelCatalogFeature || !provider.connection?.apiKeyConfigured) {
return provider;
}
try {
const catalog = await this.kilocodeModelCatalogFeature.getCatalog({
apiKey: await this.resolveStoredOrExternalProviderApiKey('kilocode'),
});
if (catalog.status === 'unavailable' || catalog.models.length === 0) {
return provider;
}
const models = catalog.models
.filter((m) => !m.hidden)
.map((m) => m.launchModel.trim())
.filter(Boolean);
return {
...provider,
models: models.length > 0 ? models : provider.models,
modelCatalog: catalog,
};
} catch {
return provider;
}
}
private async enrichAnthropicProviderStatus( private async enrichAnthropicProviderStatus(
provider: CliProviderStatus provider: CliProviderStatus
): Promise<CliProviderStatus> { ): Promise<CliProviderStatus> {
@ -1288,46 +1196,6 @@ export class ProviderConnectionService {
return this.apiKeyService.lookupPreferred(envVarName); return this.apiKeyService.lookupPreferred(envVarName);
} }
private async resolveStoredOrExternalProviderApiKey(
providerId: CliProviderId,
options?: StoredApiKeyAccessOptions
): Promise<string | null> {
const envVarName = PROVIDER_API_KEY_ENV_VARS[providerId];
if (!envVarName) {
return null;
}
const storedKey = await this.lookupStoredApiKeyValue(envVarName, options);
if (storedKey?.value.trim()) {
return storedKey.value.trim();
}
return this.getExternalCredential(providerId)?.value.trim() || null;
}
private async resolveProviderApiKeyForEnv(
env: NodeJS.ProcessEnv,
providerId: CliProviderId,
options?: StoredApiKeyAccessOptions
): Promise<string | null> {
const envVarName = PROVIDER_API_KEY_ENV_VARS[providerId];
if (!envVarName) {
return null;
}
const storedKey = await this.lookupStoredApiKeyValue(envVarName, options);
if (storedKey?.value.trim()) {
return storedKey.value.trim();
}
const existingValue = env[envVarName];
if (typeof existingValue === 'string' && existingValue.trim()) {
return existingValue.trim();
}
return this.getExternalCredential(providerId)?.value.trim() || null;
}
private getConfiguredCodexRuntimeBackend(runtimeBackendOverride?: string | null): 'codex-native' { private getConfiguredCodexRuntimeBackend(runtimeBackendOverride?: string | null): 'codex-native' {
if (runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID) { if (runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID) {
return runtimeBackendOverride; return runtimeBackendOverride;
@ -1509,16 +1377,6 @@ export class ProviderConnectionService {
} }
} }
if (providerId === 'kilocode') {
const apiKey = this.getExternalEnvValue('KILO_API_KEY');
if (apiKey) {
return {
label: 'Detected from KILO_API_KEY',
value: apiKey,
};
}
}
return null; return null;
} }

View file

@ -103,12 +103,7 @@ export function applyProviderRuntimeEnv(
export function resolveRuntimeProviderId( export function resolveRuntimeProviderId(
providerId: RuntimeEnvProviderId | undefined providerId: RuntimeEnvProviderId | undefined
): CliProviderId { ): CliProviderId {
if ( if (providerId === 'codex' || providerId === 'gemini' || providerId === 'opencode') {
providerId === 'codex' ||
providerId === 'gemini' ||
providerId === 'opencode' ||
providerId === 'kilocode'
) {
return providerId; return providerId;
} }
@ -116,10 +111,7 @@ export function resolveRuntimeProviderId(
} }
export function resolveTeamProviderId(providerId: TeamProviderId | undefined): TeamProviderId { export function resolveTeamProviderId(providerId: TeamProviderId | undefined): TeamProviderId {
return providerId === 'codex' || return providerId === 'codex' || providerId === 'gemini' || providerId === 'opencode'
providerId === 'gemini' ||
providerId === 'opencode' ||
providerId === 'kilocode'
? providerId ? providerId
: 'anthropic'; : 'anthropic';
} }

View file

@ -1311,8 +1311,6 @@ function getProviderRuntimeFailureLabel(providerId: TeamProviderId): string {
return 'Gemini runtime'; return 'Gemini runtime';
case 'opencode': case 'opencode':
return 'OpenCode runtime'; return 'OpenCode runtime';
case 'kilocode':
return 'KiloCode runtime';
} }
} }
@ -16571,17 +16569,6 @@ export class TeamProvisioningService {
? Array.from(new Set(providerModelChecks.map((check) => check.modelId))) ? Array.from(new Set(providerModelChecks.map((check) => check.modelId)))
: selectedModelIds; : selectedModelIds;
if (providerId === 'kilocode') {
const kilocodeConnection =
await this.providerConnectionService.getConnectionInfo('kilocode');
if (!kilocodeConnection.apiKeyConfigured) {
blockingMessages.push(
'KiloCode: API key not configured. Set KILO_API_KEY or add it in Provider Settings.'
);
}
continue;
}
if (providerId === 'opencode') { if (providerId === 'opencode') {
const adapter = this.getOpenCodeRuntimeAdapter(); const adapter = this.getOpenCodeRuntimeAdapter();
if (!adapter) { if (!adapter) {

View file

@ -14,13 +14,7 @@ import type {
TeamProvisioningSupportDiagnostic, TeamProvisioningSupportDiagnostic,
} from '@shared/types'; } from '@shared/types';
export const TEAM_RUNTIME_PROVIDER_IDS = [ export const TEAM_RUNTIME_PROVIDER_IDS = ['anthropic', 'codex', 'gemini', 'opencode'] as const;
'anthropic',
'codex',
'gemini',
'opencode',
'kilocode',
] as const;
export type TeamRuntimeProviderId = (typeof TEAM_RUNTIME_PROVIDER_IDS)[number]; export type TeamRuntimeProviderId = (typeof TEAM_RUNTIME_PROVIDER_IDS)[number];

View file

@ -202,7 +202,5 @@ export const ProviderBrandLogo = ({
return <GeminiBrandLogo className={className} />; return <GeminiBrandLogo className={className} />;
case 'opencode': case 'opencode':
return <OpenCodeBrandLogo className={className} />; return <OpenCodeBrandLogo className={className} />;
case 'kilocode':
return <OpenCodeBrandLogo className={className} />;
} }
}; };

View file

@ -443,8 +443,6 @@ function getProviderLabel(providerId: CliProviderId): string {
return 'Gemini'; return 'Gemini';
case 'opencode': case 'opencode':
return 'OpenCode (200+ models)'; return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
} }
} }

View file

@ -288,7 +288,6 @@ export function getDashboardRateLimitsForProvider(
return getAnthropicDashboardRateLimits(provider); return getAnthropicDashboardRateLimits(provider);
case 'gemini': case 'gemini':
case 'opencode': case 'opencode':
case 'kilocode':
return null; return null;
} }
} }

View file

@ -62,7 +62,7 @@ const ProviderCapabilityCardSkeleton = ({
providerId, providerId,
displayName, displayName,
}: { }: {
providerId: 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode'; providerId: 'anthropic' | 'codex' | 'gemini' | 'opencode';
displayName: string; displayName: string;
}): React.JSX.Element => { }): React.JSX.Element => {
const { t } = useAppTranslation('extensions'); const { t } = useAppTranslation('extensions');

View file

@ -68,7 +68,7 @@ import type { CodexRuntimeStatus } from '@features/codex-runtime-installer/contr
import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@shared/types'; import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@shared/types';
import type { ApiKeyEntry } from '@shared/types/extensions'; import type { ApiKeyEntry } from '@shared/types/extensions';
type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini' | 'kilocode'; type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini';
type PendingConnectionAction = 'auto' | 'oauth' | 'chatgpt' | 'api_key' | 'compatible' | null; type PendingConnectionAction = 'auto' | 'oauth' | 'chatgpt' | 'api_key' | 'compatible' | null;
interface ConnectionMethodCardOption { interface ConnectionMethodCardOption {
@ -98,7 +98,7 @@ interface Props {
const API_KEY_PROVIDER_CONFIG: Record< const API_KEY_PROVIDER_CONFIG: Record<
ApiKeyProviderId, ApiKeyProviderId,
{ {
envVarName: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY' | 'GEMINI_API_KEY' | 'KILO_API_KEY'; envVarName: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY' | 'GEMINI_API_KEY';
name: string; name: string;
title: string; title: string;
description: string; description: string;
@ -129,14 +129,6 @@ const API_KEY_PROVIDER_CONFIG: Record<
'Use `GEMINI_API_KEY` for the Gemini API backend. CLI SDK and ADC do not require it.', 'Use `GEMINI_API_KEY` for the Gemini API backend. CLI SDK and ADC do not require it.',
placeholder: 'AIza...', placeholder: 'AIza...',
}, },
kilocode: {
envVarName: 'KILO_API_KEY',
name: 'KiloCode API Key',
title: 'API key',
description:
'Use your KiloCode API key to authenticate with the KiloCode gateway and load available models.',
placeholder: 'kc-...',
},
}; };
const API_KEY_PROVIDER_TRANSLATION_KEYS = { const API_KEY_PROVIDER_TRANSLATION_KEYS = {
@ -159,7 +151,7 @@ const API_KEY_PROVIDER_TRANSLATION_KEYS = {
placeholder: 'providerRuntime.apiKey.providers.gemini.placeholder', placeholder: 'providerRuntime.apiKey.providers.gemini.placeholder',
}, },
} as const satisfies Record< } as const satisfies Record<
Exclude<ApiKeyProviderId, 'kilocode'>, ApiKeyProviderId,
{ {
name: string; name: string;
title: string; title: string;
@ -173,12 +165,7 @@ const ANTHROPIC_COMPATIBLE_AUTH_TOKEN_NAME = 'Anthropic-compatible Auth Token';
const FIRST_PARTY_ANTHROPIC_HOSTS = new Set(['api.anthropic.com', 'api-staging.anthropic.com']); const FIRST_PARTY_ANTHROPIC_HOSTS = new Set(['api.anthropic.com', 'api-staging.anthropic.com']);
function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProviderId { function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProviderId {
return ( return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini';
providerId === 'anthropic' ||
providerId === 'codex' ||
providerId === 'gemini' ||
providerId === 'kilocode'
);
} }
function isCodexRuntimeInstalling( function isCodexRuntimeInstalling(
@ -256,8 +243,6 @@ function getConnectionDescription(
return t('providerRuntime.connection.descriptions.gemini'); return t('providerRuntime.connection.descriptions.gemini');
case 'opencode': case 'opencode':
return t('providerRuntime.connection.descriptions.opencode'); return t('providerRuntime.connection.descriptions.opencode');
case 'kilocode':
return 'KiloCode uses an API key for authentication with the KiloCode gateway.';
} }
} }
@ -274,8 +259,6 @@ function getRuntimeDescription(
return t('providerRuntime.runtime.descriptions.gemini'); return t('providerRuntime.runtime.descriptions.gemini');
case 'opencode': case 'opencode':
return t('providerRuntime.runtime.descriptions.opencode'); return t('providerRuntime.runtime.descriptions.opencode');
case 'kilocode':
return 'KiloCode uses its own managed runtime host. Configure an API key to use the KiloCode gateway.';
} }
} }
@ -308,10 +291,6 @@ function getAuthModeDescription(
} }
} }
if (providerId === 'kilocode' && authMode === 'api_key') {
return 'Use a KiloCode API key for gateway access.';
}
return ''; return '';
} }
@ -1047,10 +1026,9 @@ export const ProviderRuntimeSettingsDialog = ({
? selectedProvider.providerId ? selectedProvider.providerId
: null; : null;
const apiKeyConfig = apiKeyProviderId ? API_KEY_PROVIDER_CONFIG[apiKeyProviderId] : null; const apiKeyConfig = apiKeyProviderId ? API_KEY_PROVIDER_CONFIG[apiKeyProviderId] : null;
const apiKeyTranslationKeys = const apiKeyTranslationKeys = apiKeyProviderId
apiKeyProviderId && apiKeyProviderId !== 'kilocode' ? API_KEY_PROVIDER_TRANSLATION_KEYS[apiKeyProviderId]
? API_KEY_PROVIDER_TRANSLATION_KEYS[apiKeyProviderId] : null;
: null;
const apiKeyDisplayConfig = apiKeyTranslationKeys const apiKeyDisplayConfig = apiKeyTranslationKeys
? { ? {
title: t(apiKeyTranslationKeys.title), title: t(apiKeyTranslationKeys.title),
@ -1058,7 +1036,7 @@ export const ProviderRuntimeSettingsDialog = ({
name: t(apiKeyTranslationKeys.name), name: t(apiKeyTranslationKeys.name),
placeholder: t(apiKeyTranslationKeys.placeholder), placeholder: t(apiKeyTranslationKeys.placeholder),
} }
: apiKeyConfig; : null;
const showApiKeyForm = const showApiKeyForm =
selectedProvider && selectedProvider &&
isApiKeyProviderId(selectedProvider.providerId) && isApiKeyProviderId(selectedProvider.providerId) &&

View file

@ -124,8 +124,6 @@ function getProviderLabel(providerId: CliProviderId): string {
return 'Gemini'; return 'Gemini';
case 'opencode': case 'opencode':
return 'OpenCode (200+ models)'; return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
} }
} }

View file

@ -411,7 +411,6 @@ export const DateGroupedSessions = memo((): React.JSX.Element => {
codex: 0, codex: 0,
gemini: 0, gemini: 0,
opencode: 0, opencode: 0,
kilocode: 0,
}; };
for (const session of searchedSessions) { for (const session of searchedSessions) {

View file

@ -65,7 +65,6 @@ const PROVIDER_LABELS: Record<TeamProviderId, string> = {
codex: 'Codex', codex: 'Codex',
gemini: 'Gemini', gemini: 'Gemini',
opencode: 'OpenCode', opencode: 'OpenCode',
kilocode: 'KiloCode',
}; };
function getProviderLabel(providerId: TeamProviderId): string { function getProviderLabel(providerId: TeamProviderId): string {

View file

@ -28,24 +28,14 @@ const CODEX_PROVIDER_INSTALL_REFRESH_ATTEMPTS = 3;
const CODEX_PROVIDER_INSTALL_REFRESH_RETRY_DELAY_MS = 700; const CODEX_PROVIDER_INSTALL_REFRESH_RETRY_DELAY_MS = 700;
export const MULTIMODEL_PROVIDER_IDS: CliProviderId[] = isGeminiUiFrozen() export const MULTIMODEL_PROVIDER_IDS: CliProviderId[] = isGeminiUiFrozen()
? ['anthropic', 'codex', 'opencode', 'kilocode']
: ['anthropic', 'codex', 'gemini', 'opencode', 'kilocode'];
const MULTIMODEL_PROVIDER_HYDRATION_IDS: CliProviderId[] = isGeminiUiFrozen()
? ['anthropic', 'codex', 'opencode'] ? ['anthropic', 'codex', 'opencode']
: ['anthropic', 'codex', 'gemini', 'opencode']; : ['anthropic', 'codex', 'gemini', 'opencode'];
const MULTIMODEL_PROVIDER_ID_SET = new Set<CliProviderId>(MULTIMODEL_PROVIDER_IDS); const MULTIMODEL_PROVIDER_ID_SET = new Set<CliProviderId>(MULTIMODEL_PROVIDER_IDS);
const MULTIMODEL_PROVIDER_HYDRATION_ID_SET = new Set<CliProviderId>(
MULTIMODEL_PROVIDER_HYDRATION_IDS
);
function isActiveMultimodelProviderId(providerId: CliProviderId): boolean { function isActiveMultimodelProviderId(providerId: CliProviderId): boolean {
return MULTIMODEL_PROVIDER_ID_SET.has(providerId); return MULTIMODEL_PROVIDER_ID_SET.has(providerId);
} }
function isHydratableMultimodelProviderId(providerId: CliProviderId): boolean {
return MULTIMODEL_PROVIDER_HYDRATION_ID_SET.has(providerId);
}
export function createLoadingMultimodelCliStatus(): CliInstallationStatus { export function createLoadingMultimodelCliStatus(): CliInstallationStatus {
const providers: CliProviderStatus[] = MULTIMODEL_PROVIDER_IDS.map((providerId) => ({ const providers: CliProviderStatus[] = MULTIMODEL_PROVIDER_IDS.map((providerId) => ({
providerId, providerId,
@ -269,7 +259,7 @@ export function getIncompleteMultimodelProviderIds(
return status.providers return status.providers
.filter( .filter(
(provider) => (provider) =>
isHydratableMultimodelProviderId(provider.providerId) && isActiveMultimodelProviderId(provider.providerId) &&
!isHydratedMultimodelProviderStatus(provider) !isHydratedMultimodelProviderStatus(provider)
) )
.map((provider) => provider.providerId); .map((provider) => provider.providerId);
@ -285,7 +275,7 @@ export function getModelOnlyFallbackProviderIds(
return status.providers return status.providers
.filter( .filter(
(provider) => (provider) =>
isHydratableMultimodelProviderId(provider.providerId) && isActiveMultimodelProviderId(provider.providerId) &&
isModelOnlyFallbackProviderStatus(provider) isModelOnlyFallbackProviderStatus(provider)
) )
.map((provider) => provider.providerId); .map((provider) => provider.providerId);
@ -303,7 +293,7 @@ export function reconcileMultimodelProviderLoading(
const providersById = new Map( const providersById = new Map(
status.providers.map((provider) => [provider.providerId, provider]) status.providers.map((provider) => [provider.providerId, provider])
); );
return MULTIMODEL_PROVIDER_HYDRATION_IDS.reduce<Partial<Record<CliProviderId, boolean>>>( return MULTIMODEL_PROVIDER_IDS.reduce<Partial<Record<CliProviderId, boolean>>>(
(nextLoading, providerId) => { (nextLoading, providerId) => {
const provider = providersById.get(providerId); const provider = providersById.get(providerId);
return { return {
@ -575,9 +565,7 @@ function isMultimodelCliStatus(
function hasActiveProviderStatusLoading( function hasActiveProviderStatusLoading(
providerLoading: Partial<Record<CliProviderId, boolean>> providerLoading: Partial<Record<CliProviderId, boolean>>
): boolean { ): boolean {
return MULTIMODEL_PROVIDER_HYDRATION_IDS.some( return MULTIMODEL_PROVIDER_IDS.some((providerId) => providerLoading[providerId] === true);
(providerId) => providerLoading[providerId] === true
);
} }
function getAuthenticatedProvider(providers: CliProviderStatus[]): CliProviderStatus | null { function getAuthenticatedProvider(providers: CliProviderStatus[]): CliProviderStatus | null {
@ -616,8 +604,6 @@ function getProviderDisplayName(providerId: CliProviderId): string {
return 'Gemini'; return 'Gemini';
case 'opencode': case 'opencode':
return 'OpenCode (200+ models)'; return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
} }
} }
@ -771,7 +757,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
: createLoadingMultimodelCliStatus(); : createLoadingMultimodelCliStatus();
const shouldMarkIncompleteProvidersLoading = hydrateProviders || providerStatusMode === 'defer'; const shouldMarkIncompleteProvidersLoading = hydrateProviders || providerStatusMode === 'defer';
const providerLoading = Object.fromEntries( const providerLoading = Object.fromEntries(
MULTIMODEL_PROVIDER_HYDRATION_IDS.map((providerId) => [ MULTIMODEL_PROVIDER_IDS.map((providerId) => [
providerId, providerId,
shouldMarkIncompleteProvidersLoading && shouldMarkIncompleteProvidersLoading &&
initialStatus.installed && initialStatus.installed &&
@ -822,14 +808,14 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
const nextCliStatus = mergeCliStatusPreservingHydratedProviders(state.cliStatus, metadata); const nextCliStatus = mergeCliStatusPreservingHydratedProviders(state.cliStatus, metadata);
const nextProviderLoading = Object.fromEntries( const nextProviderLoading = Object.fromEntries(
MULTIMODEL_PROVIDER_HYDRATION_IDS.map((providerId) => [ MULTIMODEL_PROVIDER_IDS.map((providerId) => [
providerId, providerId,
!isHydratedMultimodelProviderStatus( !isHydratedMultimodelProviderStatus(
nextCliStatus.providers.find((provider) => provider.providerId === providerId) nextCliStatus.providers.find((provider) => provider.providerId === providerId)
), ),
]) ])
) as Partial<Record<CliProviderId, boolean>>; ) as Partial<Record<CliProviderId, boolean>>;
pendingProviderIds = MULTIMODEL_PROVIDER_HYDRATION_IDS.filter( pendingProviderIds = MULTIMODEL_PROVIDER_IDS.filter(
(providerId) => nextProviderLoading[providerId] === true (providerId) => nextProviderLoading[providerId] === true
); );
const nextAuthState = isMultimodelCliStatus(nextCliStatus) const nextAuthState = isMultimodelCliStatus(nextCliStatus)
@ -881,7 +867,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
try { try {
if (hydrateProviders) { if (hydrateProviders) {
await Promise.allSettled( await Promise.allSettled(
MULTIMODEL_PROVIDER_HYDRATION_IDS.map((providerId) => MULTIMODEL_PROVIDER_IDS.map((providerId) =>
get().fetchCliProviderStatus(providerId, { get().fetchCliProviderStatus(providerId, {
silent: false, silent: false,
epoch, epoch,
@ -925,7 +911,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
}); });
if (status.installed) { if (status.installed) {
for (const provider of status.providers) { for (const provider of status.providers) {
if (!isHydratableMultimodelProviderId(provider.providerId)) { if (!isActiveMultimodelProviderId(provider.providerId)) {
continue; continue;
} }
void get().fetchCliProviderStatus(provider.providerId, { void get().fetchCliProviderStatus(provider.providerId, {

View file

@ -42,7 +42,6 @@ const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
codex: 'Codex', codex: 'Codex',
gemini: 'Gemini', gemini: 'Gemini',
opencode: 'OpenCode', opencode: 'OpenCode',
kilocode: 'KiloCode',
}; };
const ANTHROPIC_ALIAS_LABELS = { const ANTHROPIC_ALIAS_LABELS = {
@ -141,7 +140,6 @@ const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProv
}, },
], ],
opencode: [{ value: '', label: 'Default', badgeLabel: 'Default' }], opencode: [{ value: '', label: 'Default', badgeLabel: 'Default' }],
kilocode: [{ value: '', label: 'Default', badgeLabel: 'Default' }],
}; };
const TEAM_PROVIDER_MODEL_ORDER: Record<SupportedProviderId, Map<string, number>> = { const TEAM_PROVIDER_MODEL_ORDER: Record<SupportedProviderId, Map<string, number>> = {
@ -151,9 +149,6 @@ const TEAM_PROVIDER_MODEL_ORDER: Record<SupportedProviderId, Map<string, number>
opencode: new Map( opencode: new Map(
TEAM_PROVIDER_MODEL_OPTIONS.opencode.map((option, index) => [option.value, index]) TEAM_PROVIDER_MODEL_OPTIONS.opencode.map((option, index) => [option.value, index])
), ),
kilocode: new Map(
TEAM_PROVIDER_MODEL_OPTIONS.kilocode.map((option, index) => [option.value, index])
),
}; };
function getKnownTeamProviderModelOption( function getKnownTeamProviderModelOption(
@ -342,9 +337,6 @@ export function getTeamModelBadgeLabel(
if (providerId === 'opencode') { if (providerId === 'opencode') {
return getTeamModelLabel(trimmed) ?? trimmed; return getTeamModelLabel(trimmed) ?? trimmed;
} }
if (providerId === 'kilocode') {
return getTeamModelLabel(trimmed) ?? trimmed;
}
return trimmed; return trimmed;
} }

View file

@ -33,7 +33,7 @@ export type CliPlatform =
export type CliFlavor = 'claude' | 'agent_teams_orchestrator'; export type CliFlavor = 'claude' | 'agent_teams_orchestrator';
export type CliProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode'; export type CliProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type CliProviderAuthMode = 'auto' | 'oauth' | 'chatgpt' | 'api_key'; export type CliProviderAuthMode = 'auto' | 'oauth' | 'chatgpt' | 'api_key';
export const CLI_PROVIDER_STATUS_DEFERRED_MESSAGE = 'Provider status will refresh when needed.'; export const CLI_PROVIDER_STATUS_DEFERRED_MESSAGE = 'Provider status will refresh when needed.';

View file

@ -966,7 +966,7 @@ export interface TeamViewSnapshot {
} }
export type EffortLevel = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'; export type EffortLevel = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode'; export type TeamProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type TeamProviderBackendId = export type TeamProviderBackendId =
| 'auto' | 'auto'
| 'adapter' | 'adapter'

View file

@ -3,13 +3,7 @@ import { parseOpenCodeQualifiedModelRef } from './opencodeModelRef';
import type { TeamProviderId } from '@shared/types'; import type { TeamProviderId } from '@shared/types';
export function isTeamProviderId(value: unknown): value is TeamProviderId { export function isTeamProviderId(value: unknown): value is TeamProviderId {
return ( return value === 'anthropic' || value === 'codex' || value === 'gemini' || value === 'opencode';
value === 'anthropic' ||
value === 'codex' ||
value === 'gemini' ||
value === 'opencode' ||
value === 'kilocode'
);
} }
export function normalizeOptionalTeamProviderId(value: unknown): TeamProviderId | undefined { export function normalizeOptionalTeamProviderId(value: unknown): TeamProviderId | undefined {
@ -39,15 +33,6 @@ export function inferTeamProviderIdFromModel(
return 'opencode'; return 'opencode';
} }
if (
normalized.startsWith('kilocode/') ||
normalizedWithoutExtendedContextSuffix.startsWith('kilocode/') ||
normalized.startsWith('kilo/') ||
normalizedWithoutExtendedContextSuffix.startsWith('kilo/')
) {
return 'kilocode';
}
if ( if (
normalized.startsWith('gpt-') || normalized.startsWith('gpt-') ||
normalized.startsWith('codex') || normalized.startsWith('codex') ||

View file

@ -239,7 +239,6 @@ describe('CliInstallerService', () => {
'anthropic', 'anthropic',
'codex', 'codex',
'opencode', 'opencode',
'kilocode',
]); ]);
expect(openCodeStatus).toMatchObject({ expect(openCodeStatus).toMatchObject({
displayName: 'OpenCode (200+ models)', displayName: 'OpenCode (200+ models)',
@ -336,7 +335,6 @@ describe('CliInstallerService', () => {
'anthropic', 'anthropic',
'codex', 'codex',
'opencode', 'opencode',
'kilocode',
]); ]);
expect(status.authLoggedIn).toBe(false); expect(status.authLoggedIn).toBe(false);
expect(status.authMethod).toBeNull(); expect(status.authMethod).toBeNull();
@ -382,7 +380,7 @@ describe('CliInstallerService', () => {
expect(resolveInteractiveShellEnvBestEffortMock).not.toHaveBeenCalled(); expect(resolveInteractiveShellEnvBestEffortMock).not.toHaveBeenCalled();
expect(status.authStatusChecking).toBe(false); expect(status.authStatusChecking).toBe(false);
expect(status.authLoggedIn).toBe(false); expect(status.authLoggedIn).toBe(false);
expect(status.providers).toHaveLength(4); expect(status.providers).toHaveLength(3);
expect( expect(
status.providers.every( status.providers.every(
(provider) => provider.statusMessage === 'Provider status will refresh when needed.' (provider) => provider.statusMessage === 'Provider status will refresh when needed.'
@ -1215,12 +1213,13 @@ describe('CliInstallerService', () => {
createTestProviderStatus('codex', false, null), createTestProviderStatus('codex', false, null),
createTestProviderStatus('opencode', false, null), createTestProviderStatus('opencode', false, null),
]); ]);
await vi.waitFor(() => { await Promise.resolve();
const latest = service.getLatestStatusSnapshot(); await Promise.resolve();
expect(latest?.authStatusChecking).toBe(false);
expect(latest?.authLoggedIn).toBe(true); const latest = service.getLatestStatusSnapshot();
expect(latest?.authMethod).toBe('oauth_token'); expect(latest?.authStatusChecking).toBe(false);
}); expect(latest?.authLoggedIn).toBe(true);
expect(latest?.authMethod).toBe('oauth_token');
expect(status.authStatusChecking).toBe(true); expect(status.authStatusChecking).toBe(true);
expect(status.authLoggedIn).toBe(false); expect(status.authLoggedIn).toBe(false);
expect(status.providers.every((provider) => provider.statusMessage === 'Checking...')).toBe( expect(status.providers.every((provider) => provider.statusMessage === 'Checking...')).toBe(

View file

@ -40,7 +40,6 @@ describe('ProviderConnectionService', () => {
const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY; const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY;
const originalAnthropicAuthToken = process.env.ANTHROPIC_AUTH_TOKEN; const originalAnthropicAuthToken = process.env.ANTHROPIC_AUTH_TOKEN;
const originalAnthropicBaseUrl = process.env.ANTHROPIC_BASE_URL; const originalAnthropicBaseUrl = process.env.ANTHROPIC_BASE_URL;
const originalKiloApiKey = process.env.KILO_API_KEY;
function createConfig( function createConfig(
authMode: 'auto' | 'oauth' | 'api_key' = 'auto', authMode: 'auto' | 'oauth' | 'api_key' = 'auto',
@ -137,7 +136,6 @@ describe('ProviderConnectionService', () => {
delete process.env.ANTHROPIC_API_KEY; delete process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_AUTH_TOKEN; delete process.env.ANTHROPIC_AUTH_TOKEN;
delete process.env.ANTHROPIC_BASE_URL; delete process.env.ANTHROPIC_BASE_URL;
delete process.env.KILO_API_KEY;
}); });
afterEach(() => { afterEach(() => {
@ -170,12 +168,6 @@ describe('ProviderConnectionService', () => {
} else { } else {
process.env.ANTHROPIC_BASE_URL = originalAnthropicBaseUrl; process.env.ANTHROPIC_BASE_URL = originalAnthropicBaseUrl;
} }
if (originalKiloApiKey === undefined) {
delete process.env.KILO_API_KEY;
} else {
process.env.KILO_API_KEY = originalKiloApiKey;
}
}); });
it('removes Anthropic environment credentials when OAuth mode is selected', async () => { it('removes Anthropic environment credentials when OAuth mode is selected', async () => {
@ -647,126 +639,6 @@ describe('ProviderConnectionService', () => {
expect(result.GEMINI_API_KEY).toBe('gemini-stored-key'); expect(result.GEMINI_API_KEY).toBe('gemini-stored-key');
}); });
it('injects stored KiloCode API keys for runtime launches', async () => {
const lookupPreferred = vi.fn().mockResolvedValue({
envVarName: 'KILO_API_KEY',
value: 'kilo-stored-key',
});
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred,
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
const result = await service.applyConfiguredConnectionEnv({}, 'kilocode');
expect(lookupPreferred).toHaveBeenCalledWith('KILO_API_KEY');
expect(result.KILO_API_KEY).toBe('kilo-stored-key');
await expect(service.getConfiguredConnectionIssue(result, 'kilocode')).resolves.toBeNull();
});
it('reports a missing KiloCode API key before runtime launches', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
const issue = await service.getConfiguredConnectionIssue({}, 'kilocode');
expect(issue).toContain('KiloCode API key is not configured');
});
it('passes stored KiloCode API keys to catalog hydration', async () => {
const lookupPreferred = vi.fn().mockResolvedValue({
envVarName: 'KILO_API_KEY',
value: 'kilo-stored-key',
});
const getCatalog = vi.fn().mockResolvedValue({
schemaVersion: 1,
providerId: 'kilocode',
source: 'app-server',
status: 'ready',
fetchedAt: '2026-05-25T00:00:00.000Z',
staleAt: '2026-05-25T00:10:00.000Z',
defaultModelId: 'kilo/test',
defaultLaunchModel: 'kilo/test',
models: [
{
id: 'kilo/test',
launchModel: 'kilo/test',
displayName: 'Kilo Test',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: true,
upgrade: false,
source: 'app-server',
},
],
diagnostics: {
configReadState: 'skipped',
appServerState: 'healthy',
message: null,
code: null,
},
});
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred,
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
service.setKilocodeModelCatalogFeature({ getCatalog });
const enriched = await service.enrichProviderStatus({
providerId: 'kilocode',
displayName: 'KiloCode',
supported: true,
authenticated: true,
authMethod: 'api_key',
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: true,
oneShot: false,
extensions: {
plugins: { supported: false, status: 'unsupported' },
mcp: { supported: false, status: 'unsupported' },
skills: { supported: false, status: 'unsupported' },
apiKeys: { supported: true, status: 'supported' },
},
},
backend: null,
} as never);
expect(getCatalog).toHaveBeenCalledWith({ apiKey: 'kilo-stored-key' });
expect(enriched.models).toEqual(['kilo/test']);
});
it('reports a missing Anthropic API key when api_key mode is selected', async () => { it('reports a missing Anthropic API key when api_key mode is selected', async () => {
const { ProviderConnectionService } = const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService'); await import('@main/services/runtime/ProviderConnectionService');

View file

@ -2696,7 +2696,7 @@ describe('CLI status visibility during completed install state', () => {
await Promise.resolve(); await Promise.resolve();
}); });
expect(host.textContent).toContain('Providers: 1/4 connected'); expect(host.textContent).toContain('Providers: 1/3 connected');
expect(host.textContent).toContain('5h left'); expect(host.textContent).toContain('5h left');
expect(host.textContent).toContain('1w left'); expect(host.textContent).toContain('1w left');
expect(host.textContent).toContain('resets'); expect(host.textContent).toContain('resets');

View file

@ -1,12 +1,11 @@
import React, { act } from 'react'; import React, { act } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts'; import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
import type { CliInstallationStatus } from '@shared/types'; import type { CliInstallationStatus } from '@shared/types';
import type { SkillCatalogItem } from '@shared/types/extensions'; import type { SkillCatalogItem } from '@shared/types/extensions';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
interface StoreState { interface StoreState {
fetchSkillsCatalog: ReturnType<typeof vi.fn>; fetchSkillsCatalog: ReturnType<typeof vi.fn>;
@ -560,7 +559,7 @@ describe('SkillsPanel', () => {
}); });
expect(host.textContent).toContain( expect(host.textContent).toContain(
'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, OpenCode (200+ models), and KiloCode.' 'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, and OpenCode (200+ models).'
); );
expect(host.textContent).toContain('Codex only'); expect(host.textContent).toContain('Codex only');

View file

@ -62,49 +62,6 @@ vi.mock('@sentry/electron/main', () => sentryNoOp);
vi.mock('@sentry/electron/renderer', () => sentryNoOp); vi.mock('@sentry/electron/renderer', () => sentryNoOp);
vi.mock('@sentry/react', () => sentryNoOp); vi.mock('@sentry/react', () => sentryNoOp);
function createInMemoryStorage(): Storage {
const values = new Map<string, string>();
return {
get length() {
return values.size;
},
clear() {
values.clear();
},
getItem(key: string) {
return values.get(key) ?? null;
},
key(index: number) {
return Array.from(values.keys())[index] ?? null;
},
removeItem(key: string) {
values.delete(key);
},
setItem(key: string, value: string) {
values.set(key, String(value));
},
};
}
function hasStorageApi(value: unknown): value is Storage {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as Storage).getItem === 'function' &&
typeof (value as Storage).setItem === 'function' &&
typeof (value as Storage).removeItem === 'function' &&
typeof (value as Storage).clear === 'function'
);
}
if (!hasStorageApi(globalThis.localStorage)) {
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
value: createInMemoryStorage(),
});
}
// Mock HOME for tests that need a predictable home path. It must be writable: // Mock HOME for tests that need a predictable home path. It must be writable:
// some services persist state in best-effort background writes after a test has // some services persist state in best-effort background writes after a test has
// already reset path overrides. // already reset path overrides.