Skip to content
5 changes: 5 additions & 0 deletions packages/cli/src/runtime/runtimeSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,11 @@ export async function listSavedProfiles(): Promise<string[]> {
return manager.listProfiles();
}

export async function getProfileByName(profileName: string): Promise<Profile> {
const manager = new ProfileManager();
return manager.loadProfile(profileName);
}

export function getActiveProfileName(): string | null {
const { settingsService } = getCliRuntimeServices();
if (typeof settingsService.getCurrentProfileName === 'function') {
Expand Down
71 changes: 71 additions & 0 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { useProviderModelDialog } from './hooks/useProviderModelDialog.js';
import { useProviderDialog } from './hooks/useProviderDialog.js';
import { useLoadProfileDialog } from './hooks/useLoadProfileDialog.js';
import { useCreateProfileDialog } from './hooks/useCreateProfileDialog.js';
import { useProfileManagement } from './hooks/useProfileManagement.js';
import { useToolsDialog } from './hooks/useToolsDialog.js';
import {
shouldUpdateTokenMetrics,
Expand Down Expand Up @@ -1224,6 +1225,36 @@ export const AppContainer = (props: AppContainerProps) => {
appState,
});

const {
showListDialog: isProfileListDialogOpen,
showDetailDialog: isProfileDetailDialogOpen,
showEditorDialog: isProfileEditorDialogOpen,
profiles: profileListItems,
isLoading: profileDialogLoading,
selectedProfileName,
selectedProfile: selectedProfileData,
defaultProfileName,
activeProfileName,
profileError: profileDialogError,
openListDialog: openProfileListDialog,
closeListDialog: closeProfileListDialog,
viewProfileDetail,
closeDetailDialog: closeProfileDetailDialog,
loadProfile: loadProfileFromDetail,
deleteProfile: deleteProfileFromDetail,
setDefault: setProfileAsDefault,
openEditor: openProfileEditor,
closeEditor: closeProfileEditor,
saveProfile: saveProfileFromEditor,
} = useProfileManagement({
addMessage: (msg) =>
addItem(
{ type: msg.type as MessageType, text: msg.content },
msg.timestamp.getTime(),
),
appState,
});

const {
showDialog: isToolsDialogOpen,
openDialog: openToolsDialogRaw,
Expand Down Expand Up @@ -1413,6 +1444,9 @@ export const AppContainer = (props: AppContainerProps) => {
openProviderDialog,
openLoadProfileDialog,
openCreateProfileDialog,
openProfileListDialog,
viewProfileDetail,
openProfileEditor,
quit: setQuittingMessages,
setDebugMessage,
toggleCorgiMode,
Expand All @@ -1433,6 +1467,9 @@ export const AppContainer = (props: AppContainerProps) => {
openProviderDialog,
openLoadProfileDialog,
openCreateProfileDialog,
openProfileListDialog,
viewProfileDetail,
openProfileEditor,
setQuittingMessages,
setDebugMessage,
toggleCorgiMode,
Expand Down Expand Up @@ -2063,6 +2100,9 @@ export const AppContainer = (props: AppContainerProps) => {
isProviderModelDialogOpen,
isLoadProfileDialogOpen,
isCreateProfileDialogOpen,
isProfileListDialogOpen,
isProfileDetailDialogOpen,
isProfileEditorDialogOpen,
isToolsDialogOpen,
isFolderTrustDialogOpen,
showWorkspaceMigrationDialog,
Expand All @@ -2088,6 +2128,15 @@ export const AppContainer = (props: AppContainerProps) => {
subagentDialogInitialView,
subagentDialogInitialName,

// Profile management dialog data
profileListItems,
selectedProfileName,
selectedProfileData,
defaultProfileName,
activeProfileName,
profileDialogError,
profileDialogLoading,

// Confirmation requests
shellConfirmationRequest,
confirmationRequest,
Expand Down Expand Up @@ -2239,6 +2288,18 @@ export const AppContainer = (props: AppContainerProps) => {
openCreateProfileDialog,
exitCreateProfileDialog,

// Profile management dialogs
openProfileListDialog,
closeProfileListDialog,
viewProfileDetail,
closeProfileDetailDialog,
loadProfileFromDetail,
deleteProfileFromDetail,
setProfileAsDefault,
openProfileEditor,
closeProfileEditor,
saveProfileFromEditor,

// Tools dialog
openToolsDialog,
handleToolsSelect,
Expand Down Expand Up @@ -2339,6 +2400,16 @@ export const AppContainer = (props: AppContainerProps) => {
exitLoadProfileDialog,
openCreateProfileDialog,
exitCreateProfileDialog,
openProfileListDialog,
closeProfileListDialog,
viewProfileDetail,
closeProfileDetailDialog,
loadProfileFromDetail,
deleteProfileFromDetail,
setProfileAsDefault,
openProfileEditor,
closeProfileEditor,
saveProfileFromEditor,
openToolsDialog,
handleToolsSelect,
exitToolsDialog,
Expand Down
9 changes: 3 additions & 6 deletions packages/cli/src/ui/commands/profileCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,10 @@ describe('profileCommand', () => {
(cmd) => cmd?.name === 'list',
)!;

it('lists saved profiles', async () => {
it('opens the profile list dialog', async () => {
const result = await list.action!(context, '');
expect(runtimeMocks.listSavedProfiles).toHaveBeenCalled();
expect(result?.type).toBe('message');
expect(result).toBeDefined();
expect((result as { content: string }).content).toContain('alpha');
expect((result as { content: string }).content).toContain('beta');
expect(result?.type).toBe('dialog');
expect((result as { dialog: string }).dialog).toBe('profileList');
});
});

Expand Down
142 changes: 130 additions & 12 deletions packages/cli/src/ui/commands/profileCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,40 +893,154 @@ const createCommand: SlashCommand = {

/**
* Profile list subcommand
* Opens interactive profile list dialog
*/
const listCommand: SlashCommand = {
name: 'list',
description: 'list all saved profiles',
description: 'list all saved profiles (interactive)',
kind: CommandKind.BUILT_IN,
action: async (
_context: CommandContext,
_args: string,
): Promise<MessageActionReturn> => {
): Promise<MessageActionReturn | OpenDialogActionReturn> => {
// Open interactive profile list dialog
logger.log(() => 'list action returning profileList dialog');
return {
type: 'dialog',
dialog: 'profileList',
};
},
};

const profileShowSchema: CommandArgumentSchema = [
{
kind: 'value',
name: 'profile',
description: 'Select profile to view',
completer: profileNameCompleter,
},
];

/**
* Profile show subcommand
* Opens profile detail dialog directly for the specified profile
*/
const showCommand: SlashCommand = {
name: 'show',
description: 'view details of a specific profile',
kind: CommandKind.BUILT_IN,
schema: profileShowSchema,
action: async (
_context: CommandContext,
args: string,
): Promise<MessageActionReturn | OpenDialogActionReturn> => {
const trimmedArgs = args?.trim();

if (!trimmedArgs) {
return {
type: 'message',
messageType: 'error',
content: 'Usage: /profile show <profile-name>',
};
}

// Extract profile name - handle quoted names
const profileNameMatch = trimmedArgs.match(/^"([^"]+)"$/);
const profileName = profileNameMatch ? profileNameMatch[1] : trimmedArgs;

if (!profileName) {
return {
type: 'message',
messageType: 'error',
content: 'Usage: /profile show <profile-name>',
};
}

// Verify profile exists
try {
const profiles = await listProfiles();

if (profiles.length === 0) {
if (!profiles.includes(profileName)) {
return {
type: 'message',
messageType: 'info',
content:
'No profiles saved yet. Use /profile save "<name>" to create one.',
messageType: 'error',
content: `Profile '${profileName}' not found. Use /profile list to see available profiles.`,
};
}
} catch {
// Continue anyway, the dialog will show the error
}

return {
type: 'dialog',
dialog: 'profileDetail',
dialogData: { profileName },
};
},
};

const profileEditSchema: CommandArgumentSchema = [
{
kind: 'value',
name: 'profile',
description: 'Select profile to edit',
completer: profileNameCompleter,
},
];

/**
* Profile edit subcommand
* Opens profile editor dialog directly for the specified profile
*/
const editCommand: SlashCommand = {
name: 'edit',
description: 'edit a specific profile',
kind: CommandKind.BUILT_IN,
schema: profileEditSchema,
action: async (
_context: CommandContext,
args: string,
): Promise<MessageActionReturn | OpenDialogActionReturn> => {
const trimmedArgs = args?.trim();

const profileList = profiles.map((name) => ` • ${name}`).join('\n');
if (!trimmedArgs) {
return {
type: 'message',
messageType: 'info',
content: `Saved profiles:\n${profileList}`,
messageType: 'error',
content: 'Usage: /profile edit <profile-name>',
};
} catch (error) {
}

// Extract profile name - handle quoted names
const profileNameMatch = trimmedArgs.match(/^"([^"]+)"$/);
const profileName = profileNameMatch ? profileNameMatch[1] : trimmedArgs;

if (!profileName) {
return {
type: 'message',
messageType: 'error',
content: `Failed to list profiles: ${error instanceof Error ? error.message : String(error)}`,
content: 'Usage: /profile edit <profile-name>',
};
}

// Verify profile exists
try {
const profiles = await listProfiles();
if (!profiles.includes(profileName)) {
return {
type: 'message',
messageType: 'error',
content: `Profile '${profileName}' not found. Use /profile list to see available profiles.`,
};
}
} catch {
// Continue anyway, the dialog will show the error
}

return {
type: 'dialog',
dialog: 'profileEditor',
dialogData: { profileName },
};
},
};

Expand All @@ -944,6 +1058,8 @@ export const profileCommand: SlashCommand = {
deleteCommand,
setDefaultCommand,
listCommand,
showCommand,
editCommand,
],
action: async (
_context: CommandContext,
Expand All @@ -956,6 +1072,8 @@ export const profileCommand: SlashCommand = {
/profile save loadbalancer <lb-name> <roundrobin|failover> <profile1> <profile2> [...]
- Save a load balancer profile
/profile load <name> - Load a saved profile
/profile show <name> - View details of a specific profile
/profile edit <name> - Edit a specific profile
/profile create - Interactive wizard to create a profile
/profile delete <name> - Delete a saved profile
/profile set-default <name> - Set profile to load on startup (or "none")
Expand Down
18 changes: 16 additions & 2 deletions packages/cli/src/ui/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ export interface LoggingDialogData {
entries: unknown[];
}

/**
* Type-safe dialog data for profile dialogs.
*/
export interface ProfileDialogData {
/** Name of the profile to display/edit */
profileName?: string;
}

/** All supported dialog types */
export type DialogType =
| 'auth'
Expand All @@ -155,12 +163,17 @@ export type DialogType =
| 'loadProfile'
| 'createProfile'
| 'saveProfile'
| 'subagent';
| 'subagent'
| 'profileList'
| 'profileDetail'
| 'profileEditor';

/** Map dialog types to their associated data types for type-safe access */
export interface DialogDataMap {
subagent: SubagentDialogData;
logging: LoggingDialogData;
profileDetail: ProfileDialogData;
profileEditor: ProfileDialogData;
}

/**
Expand All @@ -174,9 +187,10 @@ export interface OpenDialogActionReturn {
* Dialog-specific data. Type depends on dialog:
* - 'subagent': SubagentDialogData
* - 'logging': LoggingDialogData
* - 'profileDetail'/'profileEditor': ProfileDialogData
* - others: undefined
*/
dialogData?: SubagentDialogData | LoggingDialogData;
dialogData?: SubagentDialogData | LoggingDialogData | ProfileDialogData;
}

/**
Expand Down
Loading
Loading