mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-04 10:21:02 -07:00
feat(profiles): refactor profiles to centralize logic in core
Centralized profile loading and management in packages/core. Updated CLI commands to use core ProfileManager. Implemented persistent profile selection in loadCliConfig. Standardized profile format as Markdown with YAML frontmatter. Verified multi-extension loading with 'coder' profile.
This commit is contained in:
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { ProfileManager } from '../../config/profile-manager.js';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
@@ -19,9 +18,7 @@ export const disableCommand: CommandModule = {
|
||||
handler: async () => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const manager = new ProfileManager(settings);
|
||||
|
||||
manager.disableProfile();
|
||||
settings.setValue(SettingScope.User, 'general.activeProfile', undefined);
|
||||
debugLogger.log('Profile disabled. Reverting to default behavior.');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Profile disabled. Reverting to default behavior.');
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { ProfileManager } from '../../config/profile-manager.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { Storage, ProfileManager, debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
/**
|
||||
@@ -25,9 +24,15 @@ export const enableCommand: CommandModule = {
|
||||
const name = String(argv['name']);
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const manager = new ProfileManager(settings);
|
||||
const profilesDir = Storage.getProfilesDir();
|
||||
const manager = new ProfileManager(profilesDir);
|
||||
await manager.load();
|
||||
|
||||
await manager.enableProfile(name);
|
||||
if (!manager.getProfile(name)) {
|
||||
throw new Error(`Profile "${name}" not found.`);
|
||||
}
|
||||
|
||||
settings.setValue(SettingScope.User, 'general.activeProfile', name);
|
||||
debugLogger.log(`Profile "${name}" successfully enabled.`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Profile "${name}" successfully enabled.`);
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { debugLogger, Storage, ProfileManager } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ProfileManager } from '../../config/profile-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface InstallArgs {
|
||||
@@ -18,9 +16,8 @@ interface InstallArgs {
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const profileManager = new ProfileManager(settings);
|
||||
const profile = await profileManager.installProfile(args.path);
|
||||
const manager = new ProfileManager(Storage.getProfilesDir());
|
||||
const profile = await manager.installProfile(args.path);
|
||||
debugLogger.log(
|
||||
chalk.green(`Profile "${profile.name}" installed successfully.`),
|
||||
);
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { debugLogger, Storage, ProfileManager } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ProfileManager } from '../../config/profile-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface LinkArgs {
|
||||
@@ -18,9 +16,8 @@ interface LinkArgs {
|
||||
|
||||
export async function handleLink(args: LinkArgs) {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const profileManager = new ProfileManager(settings);
|
||||
const profile = await profileManager.linkProfile(args.path);
|
||||
const manager = new ProfileManager(Storage.getProfilesDir());
|
||||
const profile = await manager.linkProfile(args.path);
|
||||
debugLogger.log(
|
||||
chalk.green(`Profile "${profile.name}" linked successfully.`),
|
||||
);
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { render, Box, Text } from 'ink';
|
||||
import { Storage, ProfileManager } from '@google/gemini-cli-core';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { ProfileManager } from '../../config/profile-manager.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
/**
|
||||
@@ -67,9 +67,11 @@ export const listCommand: CommandModule = {
|
||||
handler: async () => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const manager = new ProfileManager(settings);
|
||||
const profiles = await manager.listProfiles();
|
||||
const activeProfile = manager.getActiveProfileName();
|
||||
const profilesDir = Storage.getProfilesDir();
|
||||
const manager = new ProfileManager(profilesDir);
|
||||
await manager.load();
|
||||
const profiles = manager.getProfileNames();
|
||||
const activeProfile = settings.merged.general?.activeProfile;
|
||||
|
||||
const { waitUntilExit } = render(
|
||||
<ProfileListView profiles={profiles} activeProfile={activeProfile} />,
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { ProfileManager } from '../../config/profile-manager.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { Storage, ProfileManager, debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
/**
|
||||
@@ -24,8 +22,8 @@ export const uninstallCommand: CommandModule = {
|
||||
handler: async (argv) => {
|
||||
const name = String(argv['name']);
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const manager = new ProfileManager(settings);
|
||||
const manager = new ProfileManager(Storage.getProfilesDir());
|
||||
await manager.load();
|
||||
|
||||
await manager.uninstallProfile(name);
|
||||
debugLogger.log(`Profile "${name}" successfully uninstalled.`);
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
Storage as CoreStorage,
|
||||
ProfileManager as CoreProfileManager,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type Settings,
|
||||
@@ -62,7 +64,7 @@ import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensio
|
||||
import { requestConsentNonInteractive } from './extensions/consent.js';
|
||||
import { promptForSetting } from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { ProfileManager } from './profile-manager.js';
|
||||
// Removed legacy profile manager import
|
||||
import { runExitCleanup } from '../utils/cleanup.js';
|
||||
|
||||
export interface CliArgs {
|
||||
@@ -436,12 +438,15 @@ export async function loadCliConfig(
|
||||
const { cwd = process.cwd(), projectHooks } = options;
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
const loadedSettings = loadSettings(cwd);
|
||||
const profileManager = new ProfileManager(loadedSettings);
|
||||
const profilesDir = CoreStorage.getProfilesDir();
|
||||
const coreProfileManager = new CoreProfileManager(profilesDir);
|
||||
await coreProfileManager.load();
|
||||
const activeProfileName =
|
||||
argv.profiles || profileManager.getActiveProfileName();
|
||||
argv.profiles ||
|
||||
settings.general?.activeProfile ||
|
||||
coreProfileManager.getActiveProfileName();
|
||||
const profile = activeProfileName
|
||||
? await profileManager.getProfile(activeProfileName)
|
||||
? coreProfileManager.getProfile(activeProfileName)
|
||||
: null;
|
||||
|
||||
if (argv.sandbox) {
|
||||
@@ -493,7 +498,7 @@ export async function loadCliConfig(
|
||||
|
||||
let enabledExtensionOverrides = argv.extensions;
|
||||
if (enabledExtensionOverrides === undefined && profile) {
|
||||
const profileExtensions = profile.frontmatter.extensions;
|
||||
const profileExtensions = profile.extensions;
|
||||
if (profileExtensions !== undefined) {
|
||||
enabledExtensionOverrides =
|
||||
profileExtensions.length > 0 ? profileExtensions : ['none'];
|
||||
@@ -541,8 +546,8 @@ export async function loadCliConfig(
|
||||
filePaths = result.filePaths;
|
||||
}
|
||||
|
||||
if (profile?.context) {
|
||||
const profileContext = `Profile Context (${profile.name}):\n${profile.context}`;
|
||||
if (profile?.body) {
|
||||
const profileContext = `Profile Context (${profile.name}):\n${profile.body}`;
|
||||
if (typeof memoryContent === 'string') {
|
||||
memoryContent = profileContext + '\n\n' + memoryContent;
|
||||
} else {
|
||||
@@ -696,7 +701,7 @@ export async function loadCliConfig(
|
||||
const defaultModel = PREVIEW_GEMINI_MODEL_AUTO;
|
||||
const specifiedModel =
|
||||
argv.model ||
|
||||
profile?.frontmatter.default_model ||
|
||||
profile?.default_model ||
|
||||
process.env['GEMINI_MODEL'] ||
|
||||
settings.model?.name;
|
||||
|
||||
@@ -768,6 +773,8 @@ export async function loadCliConfig(
|
||||
extensionsEnabled,
|
||||
agents: settings.agents,
|
||||
adminSkillsEnabled,
|
||||
profilesDir,
|
||||
activeProfile: activeProfileName || undefined,
|
||||
allowedMcpServers: mcpEnabled
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
|
||||
: undefined,
|
||||
@@ -865,7 +872,7 @@ export async function loadCliConfig(
|
||||
hooks: settings.hooks || {},
|
||||
disabledHooks: settings.hooksConfig?.disabled || [],
|
||||
projectHooks: projectHooks || {},
|
||||
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
|
||||
onModelChange: (model: string) => saveModelChange(loadSettings(cwd), model),
|
||||
onReload: async () => {
|
||||
const refreshedSettings = loadSettings(cwd);
|
||||
return {
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { ProfileManager } from './profile-manager.js';
|
||||
import { type LoadedSettings } from './settings.js';
|
||||
|
||||
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const mockedOs = await importOriginal<typeof os>();
|
||||
return {
|
||||
...mockedOs,
|
||||
homedir: mockHomedir,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
homedir: mockHomedir,
|
||||
};
|
||||
});
|
||||
|
||||
describe('ProfileManager', () => {
|
||||
let tempHomeDir: string;
|
||||
let profilesDir: string;
|
||||
let mockSettings: LoadedSettings;
|
||||
let manager: ProfileManager;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-profile-test-'),
|
||||
);
|
||||
vi.stubEnv('GEMINI_CLI_HOME', tempHomeDir);
|
||||
|
||||
profilesDir = path.join(tempHomeDir, '.gemini', 'profiles');
|
||||
fs.mkdirSync(profilesDir, { recursive: true });
|
||||
|
||||
mockSettings = {
|
||||
merged: {
|
||||
general: {
|
||||
activeProfile: undefined,
|
||||
},
|
||||
},
|
||||
setValue: vi.fn(),
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
manager = new ProfileManager(mockSettings);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should list available profiles', async () => {
|
||||
fs.writeFileSync(path.join(profilesDir, 'coding.md'), '# Coding Profile');
|
||||
fs.writeFileSync(path.join(profilesDir, 'writing.md'), '# Writing Profile');
|
||||
fs.writeFileSync(path.join(profilesDir, 'not-a-profile.txt'), 'test');
|
||||
|
||||
const profiles = await manager.listProfiles();
|
||||
expect(profiles.sort()).toEqual(['coding', 'writing']);
|
||||
});
|
||||
|
||||
it('should return empty list if profiles directory does not exist', async () => {
|
||||
fs.rmSync(profilesDir, { recursive: true, force: true });
|
||||
const profiles = await manager.listProfiles();
|
||||
expect(profiles).toEqual([]);
|
||||
});
|
||||
|
||||
it('should ensure profiles directory exists', async () => {
|
||||
fs.rmSync(profilesDir, { recursive: true, force: true });
|
||||
expect(fs.existsSync(profilesDir)).toBe(false);
|
||||
await manager.ensureProfilesDir();
|
||||
expect(fs.existsSync(profilesDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('should get a profile with frontmatter and context', async () => {
|
||||
const content = `---
|
||||
name: coding
|
||||
description: For coding tasks
|
||||
extensions: [git, shell]
|
||||
default_model: gemini-2.0-flash
|
||||
---
|
||||
Use these instructions for coding.`;
|
||||
fs.writeFileSync(path.join(profilesDir, 'coding.md'), content);
|
||||
|
||||
const profile = await manager.getProfile('coding');
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile?.name).toBe('coding');
|
||||
expect(profile?.frontmatter.extensions).toEqual(['git', 'shell']);
|
||||
expect(profile?.frontmatter.default_model).toBe('gemini-2.0-flash');
|
||||
expect(profile?.context).toBe('Use these instructions for coding.');
|
||||
});
|
||||
|
||||
it('should throw if profile name does not match filename', async () => {
|
||||
const content = `---
|
||||
name: wrong-name
|
||||
extensions: []
|
||||
---`;
|
||||
fs.writeFileSync(path.join(profilesDir, 'test.md'), content);
|
||||
|
||||
await expect(manager.getProfile('test')).rejects.toThrow(
|
||||
/Profile name in frontmatter \(wrong-name\) must match filename \(test\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle optional extensions field', async () => {
|
||||
const content = `---
|
||||
name: test-no-ext
|
||||
---
|
||||
Body`;
|
||||
fs.writeFileSync(path.join(profilesDir, 'test-no-ext.md'), content);
|
||||
|
||||
const profile = await manager.getProfile('test-no-ext');
|
||||
expect(profile?.frontmatter.extensions).toBeUndefined();
|
||||
expect(profile?.context).toBe('Body');
|
||||
});
|
||||
|
||||
it('should throw if mandatory frontmatter is missing', async () => {
|
||||
const content = `Just some text without dashes`;
|
||||
fs.writeFileSync(path.join(profilesDir, 'no-fm.md'), content);
|
||||
await expect(manager.getProfile('no-fm')).rejects.toThrow(
|
||||
/missing mandatory YAML frontmatter/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if YAML is malformed', async () => {
|
||||
const content = `---
|
||||
name: [invalid yaml
|
||||
---
|
||||
Body`;
|
||||
fs.writeFileSync(path.join(profilesDir, 'bad-yaml.md'), content);
|
||||
await expect(manager.getProfile('bad-yaml')).rejects.toThrow(
|
||||
/Failed to parse profile/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if validation fails (invalid slug)', async () => {
|
||||
const content = `---
|
||||
name: Invalid Name
|
||||
---`;
|
||||
fs.writeFileSync(path.join(profilesDir, 'invalid-slug.md'), content);
|
||||
await expect(manager.getProfile('invalid-slug')).rejects.toThrow(
|
||||
/Validation failed.*name/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null for non-existent profile', async () => {
|
||||
const profile = await manager.getProfile('ghost');
|
||||
expect(profile).toBeNull();
|
||||
});
|
||||
|
||||
it('should uninstall a profile', async () => {
|
||||
const profilePath = path.join(profilesDir, 'coding.md');
|
||||
fs.writeFileSync(profilePath, '---\nname: coding\nextensions: []\n---');
|
||||
|
||||
await manager.uninstallProfile('coding');
|
||||
expect(fs.existsSync(profilePath)).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable profile before uninstalling if active', async () => {
|
||||
const profilePath = path.join(profilesDir, 'active.md');
|
||||
fs.writeFileSync(profilePath, '---\nname: active\nextensions: []\n---');
|
||||
|
||||
mockSettings.merged.general.activeProfile = 'active';
|
||||
|
||||
await manager.uninstallProfile('active');
|
||||
expect(fs.existsSync(profilePath)).toBe(false);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'general.activeProfile',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProfileManager Installation and Linking', () => {
|
||||
let tempHomeDir: string;
|
||||
let profilesDir: string;
|
||||
let mockSettings: LoadedSettings;
|
||||
let manager: ProfileManager;
|
||||
let sourceDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-profile-test-home-'),
|
||||
);
|
||||
sourceDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-profile-test-source-'),
|
||||
);
|
||||
vi.stubEnv('GEMINI_CLI_HOME', tempHomeDir);
|
||||
|
||||
profilesDir = path.join(tempHomeDir, '.gemini', 'profiles');
|
||||
fs.mkdirSync(profilesDir, { recursive: true });
|
||||
|
||||
mockSettings = {
|
||||
merged: { general: { activeProfile: undefined } },
|
||||
setValue: vi.fn(),
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
manager = new ProfileManager(mockSettings);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should install a profile by copying', async () => {
|
||||
const sourcePath = path.join(sourceDir, 'new-profile.md');
|
||||
const content = `---
|
||||
name: new-profile
|
||||
---
|
||||
Body`;
|
||||
fs.writeFileSync(sourcePath, content);
|
||||
|
||||
const profile = await manager.installProfile(sourcePath);
|
||||
expect(profile.name).toBe('new-profile');
|
||||
|
||||
const installedPath = path.join(profilesDir, 'new-profile.md');
|
||||
expect(fs.existsSync(installedPath)).toBe(true);
|
||||
expect(fs.readFileSync(installedPath, 'utf-8')).toBe(content);
|
||||
expect(fs.lstatSync(installedPath).isSymbolicLink()).toBe(false);
|
||||
});
|
||||
|
||||
it('should link a profile by creating a symlink', async () => {
|
||||
const sourcePath = path.join(sourceDir, 'linked-profile.md');
|
||||
const content = `---
|
||||
name: linked-profile
|
||||
---
|
||||
Body`;
|
||||
fs.writeFileSync(sourcePath, content);
|
||||
|
||||
const profile = await manager.linkProfile(sourcePath);
|
||||
expect(profile.name).toBe('linked-profile');
|
||||
|
||||
const linkedPath = path.join(profilesDir, 'linked-profile.md');
|
||||
expect(fs.existsSync(linkedPath)).toBe(true);
|
||||
expect(fs.lstatSync(linkedPath).isSymbolicLink()).toBe(true);
|
||||
expect(fs.readlinkSync(linkedPath)).toBe(sourcePath);
|
||||
});
|
||||
|
||||
it('should throw if installing when profile already exists', async () => {
|
||||
const sourcePath = path.join(sourceDir, 'existing.md');
|
||||
fs.writeFileSync(sourcePath, '---\nname: existing\n---');
|
||||
fs.writeFileSync(path.join(profilesDir, 'existing.md'), 'orig');
|
||||
|
||||
await expect(manager.installProfile(sourcePath)).rejects.toThrow(
|
||||
/Profile "existing" already exists/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if source file does not exist', async () => {
|
||||
await expect(manager.installProfile('/non/existent')).rejects.toThrow(
|
||||
/Source profile file not found/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if source file is invalid', async () => {
|
||||
const sourcePath = path.join(sourceDir, 'invalid.md');
|
||||
fs.writeFileSync(sourcePath, 'not a profile');
|
||||
|
||||
await expect(manager.installProfile(sourcePath)).rejects.toThrow(
|
||||
/missing mandatory YAML frontmatter/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if linking when profile already exists', async () => {
|
||||
const sourcePath = path.join(sourceDir, 'existing-link.md');
|
||||
fs.writeFileSync(sourcePath, '---\nname: existing-link\n---');
|
||||
fs.writeFileSync(path.join(profilesDir, 'existing-link.md'), 'orig');
|
||||
|
||||
await expect(manager.linkProfile(sourcePath)).rejects.toThrow(
|
||||
/Profile "existing-link" already exists/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should uninstall a linked profile (delete link but keep source)', async () => {
|
||||
const sourcePath = path.join(sourceDir, 'linked.md');
|
||||
fs.writeFileSync(sourcePath, '---\nname: linked\n---');
|
||||
await manager.linkProfile(sourcePath);
|
||||
|
||||
const linkPath = path.join(profilesDir, 'linked.md');
|
||||
expect(fs.existsSync(linkPath)).toBe(true);
|
||||
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
|
||||
|
||||
await manager.uninstallProfile('linked');
|
||||
expect(fs.existsSync(linkPath)).toBe(false);
|
||||
expect(fs.existsSync(sourcePath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw if source file is missing during linking', async () => {
|
||||
await expect(manager.linkProfile('/non/existent')).rejects.toThrow(
|
||||
/Source profile file not found/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,292 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { load } from 'js-yaml';
|
||||
import { z } from 'zod';
|
||||
import { Storage, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { type LoadedSettings, SettingScope } from './settings.js';
|
||||
|
||||
export const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---(?:\n([\s\S]*))?$/;
|
||||
|
||||
const profileFrontmatterSchema = z.object({
|
||||
name: z.string().regex(/^[a-z0-9-_]+$/, 'Name must be a valid slug'),
|
||||
description: z.string().optional(),
|
||||
extensions: z.array(z.string()).optional(),
|
||||
default_model: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ProfileFrontmatter = z.infer<typeof profileFrontmatterSchema>;
|
||||
|
||||
export interface Profile {
|
||||
name: string;
|
||||
frontmatter: ProfileFrontmatter;
|
||||
context: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of user profiles.
|
||||
* Profiles are stored as Markdown files with YAML frontmatter in ~/.gemini/profiles/.
|
||||
*/
|
||||
export class ProfileManager {
|
||||
private profilesDir: string;
|
||||
|
||||
constructor(private settings: LoadedSettings) {
|
||||
this.profilesDir = Storage.getProfilesDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the profiles directory exists.
|
||||
*/
|
||||
async ensureProfilesDir(): Promise<void> {
|
||||
try {
|
||||
if (!existsSync(this.profilesDir)) {
|
||||
await fs.mkdir(this.profilesDir, { recursive: true });
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to create profiles directory at ${this.profilesDir}: ${getErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the names of all available profiles.
|
||||
* @returns A list of profile names (filenames without .md extension).
|
||||
*/
|
||||
async listProfiles(): Promise<string[]> {
|
||||
try {
|
||||
if (!existsSync(this.profilesDir)) {
|
||||
return [];
|
||||
}
|
||||
const entries = await fs.readdir(this.profilesDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.map((entry) => path.basename(entry.name, '.md'));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list profiles: ${getErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and parses a profile by its name.
|
||||
* @param name The name of the profile to load.
|
||||
* @returns The parsed Profile object, or null if not found.
|
||||
* @throws Error if the profile exists but is malformed or invalid.
|
||||
*/
|
||||
async getProfile(name: string): Promise<Profile | null> {
|
||||
const filePath = path.join(this.profilesDir, `${name}.md`);
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
error.code === 'ENOENT'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to read profile "${name}": ${getErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const match = content.match(FRONTMATTER_REGEX);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Profile "${name}" is missing mandatory YAML frontmatter. Ensure it starts and ends with "---".`,
|
||||
);
|
||||
}
|
||||
|
||||
const frontmatterStr = match[1];
|
||||
const context = match[2]?.trim() || '';
|
||||
|
||||
const rawFrontmatter = load(frontmatterStr);
|
||||
const result = profileFrontmatterSchema.safeParse(rawFrontmatter);
|
||||
|
||||
if (!result.success) {
|
||||
// Collect and format validation errors for a better user experience
|
||||
const issues = result.error.issues
|
||||
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
||||
.join(', ');
|
||||
throw new Error(`Validation failed for profile "${name}": ${issues}`);
|
||||
}
|
||||
|
||||
const frontmatter = result.data;
|
||||
if (frontmatter.name !== name) {
|
||||
throw new Error(
|
||||
`Profile name in frontmatter (${frontmatter.name}) must match filename (${name}).`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
frontmatter,
|
||||
context,
|
||||
filePath,
|
||||
};
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes('Validation failed')
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to parse profile "${name}": ${getErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the currently active profile from settings.
|
||||
*/
|
||||
getActiveProfileName(): string | undefined {
|
||||
return this.settings.merged.general?.activeProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persistently enables a profile by updating user settings.
|
||||
* @param name The name of the profile to enable.
|
||||
* @throws Error if the profile does not exist.
|
||||
*/
|
||||
async enableProfile(name: string): Promise<void> {
|
||||
const profile = await this.getProfile(name);
|
||||
if (!profile) {
|
||||
throw new Error(`Profile "${name}" not found. Cannot enable.`);
|
||||
}
|
||||
this.settings.setValue(SettingScope.User, 'general.activeProfile', name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the currently active profile.
|
||||
*/
|
||||
disableProfile(): void {
|
||||
this.settings.setValue(
|
||||
SettingScope.User,
|
||||
'general.activeProfile',
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls (deletes) a profile.
|
||||
* If the profile is active, it will be disabled first.
|
||||
* @param name The name of the profile to uninstall.
|
||||
* @throws Error if the profile does not exist or deletion fails.
|
||||
*/
|
||||
async uninstallProfile(name: string): Promise<void> {
|
||||
const filePath = path.join(this.profilesDir, `${name}.md`);
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error(`Profile "${name}" not found. Cannot uninstall.`);
|
||||
}
|
||||
|
||||
if (this.getActiveProfileName() === name) {
|
||||
this.disableProfile();
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.rm(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to delete profile file for "${name}": ${getErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a profile by copying it from a source path to the profiles directory.
|
||||
* @param sourcePath Path to the profile file.
|
||||
* @returns The installed Profile.
|
||||
*/
|
||||
async installProfile(sourcePath: string): Promise<Profile> {
|
||||
const absoluteSource = path.resolve(sourcePath);
|
||||
if (!existsSync(absoluteSource)) {
|
||||
throw new Error(`Source profile file not found at ${absoluteSource}`);
|
||||
}
|
||||
|
||||
// Read and validate first
|
||||
const content = await fs.readFile(absoluteSource, 'utf-8');
|
||||
const match = content.match(FRONTMATTER_REGEX);
|
||||
if (!match) {
|
||||
throw new Error('Source file is missing mandatory YAML frontmatter.');
|
||||
}
|
||||
|
||||
const frontmatterStr = match[1];
|
||||
const rawFrontmatter = load(frontmatterStr);
|
||||
const result = profileFrontmatterSchema.safeParse(rawFrontmatter);
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Invalid profile frontmatter at ${absoluteSource}: ${result.error.issues.map((i) => i.message).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const name = result.data.name;
|
||||
const destPath = path.join(this.profilesDir, `${name}.md`);
|
||||
|
||||
if (existsSync(destPath)) {
|
||||
throw new Error(
|
||||
`Profile "${name}" already exists. Please uninstall it first or rename the source profile.`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.ensureProfilesDir();
|
||||
await fs.copyFile(absoluteSource, destPath);
|
||||
|
||||
return (await this.getProfile(name))!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a profile by creating a symlink in the profiles directory.
|
||||
* @param sourcePath Path to the profile file.
|
||||
* @returns The linked Profile.
|
||||
*/
|
||||
async linkProfile(sourcePath: string): Promise<Profile> {
|
||||
const absoluteSource = path.resolve(sourcePath);
|
||||
if (!existsSync(absoluteSource)) {
|
||||
throw new Error(`Source profile file not found at ${absoluteSource}`);
|
||||
}
|
||||
|
||||
// Read and validate first
|
||||
const content = await fs.readFile(absoluteSource, 'utf-8');
|
||||
const match = content.match(FRONTMATTER_REGEX);
|
||||
if (!match) {
|
||||
throw new Error('Source file is missing mandatory YAML frontmatter.');
|
||||
}
|
||||
|
||||
const frontmatterStr = match[1];
|
||||
const rawFrontmatter = load(frontmatterStr);
|
||||
const result = profileFrontmatterSchema.safeParse(rawFrontmatter);
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Invalid profile frontmatter at ${absoluteSource}: ${result.error.issues.map((i) => i.message).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const name = result.data.name;
|
||||
const destPath = path.join(this.profilesDir, `${name}.md`);
|
||||
|
||||
if (existsSync(destPath)) {
|
||||
throw new Error(
|
||||
`Profile "${name}" already exists. Please uninstall it first or rename the source profile.`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.ensureProfilesDir();
|
||||
await fs.symlink(absoluteSource, destPath);
|
||||
|
||||
return (await this.getProfile(name))!;
|
||||
}
|
||||
}
|
||||
@@ -44,11 +44,15 @@ describe('initializer', () => {
|
||||
getToolRegistry: ReturnType<typeof vi.fn>;
|
||||
getIdeMode: ReturnType<typeof vi.fn>;
|
||||
getGeminiMdFileCount: ReturnType<typeof vi.fn>;
|
||||
getProfileManager: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockSettings: LoadedSettings;
|
||||
let mockIdeClient: {
|
||||
connect: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockProfileManager: {
|
||||
load: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -56,7 +60,12 @@ describe('initializer', () => {
|
||||
getToolRegistry: vi.fn(),
|
||||
getIdeMode: vi.fn().mockReturnValue(false),
|
||||
getGeminiMdFileCount: vi.fn().mockReturnValue(5),
|
||||
getProfileManager: vi.fn(),
|
||||
};
|
||||
mockProfileManager = {
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockConfig.getProfileManager.mockReturnValue(mockProfileManager);
|
||||
mockSettings = {
|
||||
merged: {
|
||||
security: {
|
||||
|
||||
@@ -60,6 +60,11 @@ export async function initializeApp(
|
||||
logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
|
||||
}
|
||||
|
||||
// Load profiles
|
||||
const profilesHandle = startupProfiler.start('load_profiles');
|
||||
await config.getProfileManager().load();
|
||||
profilesHandle?.end();
|
||||
|
||||
return {
|
||||
authError,
|
||||
accountSuspensionInfo,
|
||||
|
||||
@@ -48,6 +48,7 @@ import { planCommand } from '../ui/commands/planCommand.js';
|
||||
import { policiesCommand } from '../ui/commands/policiesCommand.js';
|
||||
import { privacyCommand } from '../ui/commands/privacyCommand.js';
|
||||
import { profileCommand } from '../ui/commands/profileCommand.js';
|
||||
import { profilesCommand } from '../ui/commands/profilesCommand.js';
|
||||
import { quitCommand } from '../ui/commands/quitCommand.js';
|
||||
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
||||
import { resumeCommand } from '../ui/commands/resumeCommand.js';
|
||||
@@ -188,6 +189,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
policiesCommand,
|
||||
privacyCommand,
|
||||
...(isDevelopment ? [profileCommand] : []),
|
||||
profilesCommand,
|
||||
quitCommand,
|
||||
restoreCommand(this.config),
|
||||
{
|
||||
|
||||
147
packages/cli/src/ui/commands/profilesCommand.ts
Normal file
147
packages/cli/src/ui/commands/profilesCommand.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
type SlashCommandActionReturn,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import { type HistoryItemProfilesList, MessageType } from '../types.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import {
|
||||
listProfiles,
|
||||
switchProfile,
|
||||
getActiveProfile,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
async function listAction(
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<void | SlashCommandActionReturn> {
|
||||
const subArgs = args.trim().split(/\s+/);
|
||||
let useShowDescriptions = true;
|
||||
|
||||
for (const arg of subArgs) {
|
||||
if (arg === 'nodesc' || arg === '--nodesc') {
|
||||
useShowDescriptions = false;
|
||||
}
|
||||
}
|
||||
|
||||
const config = context.services.config;
|
||||
if (!config) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Could not retrieve configuration.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const profiles = listProfiles(config);
|
||||
const activeProfile = getActiveProfile(config);
|
||||
|
||||
const profilesListItem: HistoryItemProfilesList = {
|
||||
type: MessageType.PROFILES_LIST,
|
||||
profiles,
|
||||
activeProfileName: activeProfile?.name,
|
||||
showDescriptions: useShowDescriptions,
|
||||
};
|
||||
|
||||
context.ui.addItem(profilesListItem);
|
||||
}
|
||||
|
||||
async function switchAction(
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<void | SlashCommandActionReturn> {
|
||||
const profileName = args.trim();
|
||||
if (!profileName) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Please provide a profile name to switch to.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const config = context.services.config;
|
||||
if (!config) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Could not retrieve configuration.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await switchProfile(config, profileName);
|
||||
context.ui.addItem({
|
||||
type: MessageType.INFO,
|
||||
text: `Switched to profile: ${profileName}`,
|
||||
});
|
||||
} catch (error) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: `Failed to switch profile: ${getErrorMessage(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadAction(
|
||||
context: CommandContext,
|
||||
): Promise<void | SlashCommandActionReturn> {
|
||||
const config = context.services.config;
|
||||
if (!config) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Could not retrieve configuration.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await config.getProfileManager().load();
|
||||
context.ui.addItem({
|
||||
type: MessageType.INFO,
|
||||
text: 'Profiles reloaded successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: `Failed to reload profiles: ${getErrorMessage(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const profilesCommand: SlashCommand = {
|
||||
name: 'profiles',
|
||||
description:
|
||||
'List, switch, or reload Gemini CLI profiles. Usage: /profiles [list | switch <name> | reload]',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'list',
|
||||
description: 'List available profiles. Usage: /profiles list [nodesc]',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: listAction,
|
||||
},
|
||||
{
|
||||
name: 'switch',
|
||||
description:
|
||||
'Switch to a profile by name. Usage: /profiles switch <name>',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: switchAction,
|
||||
},
|
||||
{
|
||||
name: 'reload',
|
||||
description:
|
||||
'Reload the list of discovered profiles. Usage: /profiles reload',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: reloadAction,
|
||||
},
|
||||
],
|
||||
action: listAction,
|
||||
};
|
||||
@@ -35,6 +35,7 @@ import { ChatList } from './views/ChatList.js';
|
||||
import { ModelMessage } from './messages/ModelMessage.js';
|
||||
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||
import { HintMessage } from './messages/HintMessage.js';
|
||||
import { ProfilesList } from './views/ProfilesList.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
@@ -233,6 +234,13 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'chat_list' && (
|
||||
<ChatList chats={itemForDisplay.chats} />
|
||||
)}
|
||||
{itemForDisplay.type === 'profiles_list' && (
|
||||
<ProfilesList
|
||||
profiles={itemForDisplay.profiles}
|
||||
activeProfileName={itemForDisplay.activeProfileName}
|
||||
showDescriptions={itemForDisplay.showDescriptions}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
67
packages/cli/src/ui/components/views/ProfilesList.tsx
Normal file
67
packages/cli/src/ui/components/views/ProfilesList.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type ProfileDefinition } from '../../types.js';
|
||||
|
||||
interface ProfilesListProps {
|
||||
profiles: readonly ProfileDefinition[];
|
||||
activeProfileName?: string;
|
||||
showDescriptions: boolean;
|
||||
}
|
||||
|
||||
export const ProfilesList: React.FC<ProfilesListProps> = ({
|
||||
profiles,
|
||||
activeProfileName,
|
||||
showDescriptions,
|
||||
}) => {
|
||||
const sortedProfiles = profiles
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const renderProfile = (profile: ProfileDefinition) => {
|
||||
const isActive = profile.name === activeProfileName;
|
||||
|
||||
return (
|
||||
<Box key={profile.name} flexDirection="row">
|
||||
<Text color={theme.text.primary}>{' '}- </Text>
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row">
|
||||
<Text bold color={isActive ? theme.text.link : theme.text.primary}>
|
||||
{profile.name}
|
||||
</Text>
|
||||
{isActive && (
|
||||
<Text color={theme.status.success}>{' [Active]'}</Text>
|
||||
)}
|
||||
</Box>
|
||||
{showDescriptions && profile.description && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.text.secondary}>{profile.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{sortedProfiles.length > 0 ? (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Available Profiles:
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{sortedProfiles.map(renderProfile)}
|
||||
</Box>
|
||||
) : (
|
||||
<Text color={theme.text.primary}>No profiles available</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type ToolResultDisplay,
|
||||
type RetrieveUserQuotaResponse,
|
||||
type SkillDefinition,
|
||||
type ProfileDefinition,
|
||||
type AgentDefinition,
|
||||
type ApprovalMode,
|
||||
type Kind,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
import type { PartListUnion } from '@google/genai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export type { ThoughtSummary, SkillDefinition };
|
||||
export type { ThoughtSummary, SkillDefinition, ProfileDefinition };
|
||||
|
||||
export enum AuthState {
|
||||
// Attempting to authenticate or re-authenticate
|
||||
@@ -290,6 +291,13 @@ export type HistoryItemSkillsList = HistoryItemBase & {
|
||||
showDescriptions: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItemProfilesList = HistoryItemBase & {
|
||||
type: 'profiles_list';
|
||||
profiles: ProfileDefinition[];
|
||||
activeProfileName?: string;
|
||||
showDescriptions: boolean;
|
||||
};
|
||||
|
||||
export type AgentDefinitionJson = Pick<
|
||||
AgentDefinition,
|
||||
'name' | 'displayName' | 'description' | 'kind'
|
||||
@@ -376,6 +384,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemExtensionsList
|
||||
| HistoryItemToolsList
|
||||
| HistoryItemSkillsList
|
||||
| HistoryItemProfilesList
|
||||
| HistoryItemAgentsList
|
||||
| HistoryItemMcpStatus
|
||||
| HistoryItemChatList
|
||||
@@ -401,6 +410,7 @@ export enum MessageType {
|
||||
EXTENSIONS_LIST = 'extensions_list',
|
||||
TOOLS_LIST = 'tools_list',
|
||||
SKILLS_LIST = 'skills_list',
|
||||
PROFILES_LIST = 'profiles_list',
|
||||
AGENTS_LIST = 'agents_list',
|
||||
MCP_STATUS = 'mcp_status',
|
||||
CHAT_LIST = 'chat_list',
|
||||
|
||||
Reference in New Issue
Block a user