feat: /profile command (#203)

This commit is contained in:
Devansh Jain
2025-12-13 12:42:30 -08:00
committed by GitHub
parent 0dda14103a
commit d592fde824
7 changed files with 599 additions and 103 deletions

296
src/cli/commands/profile.ts Normal file
View File

@@ -0,0 +1,296 @@
// src/cli/commands/profile.ts
// Profile command handlers for managing local agent profiles
import { getClient } from "../../agent/client";
import { settingsManager } from "../../settings-manager";
import type { Buffers, Line } from "../helpers/accumulator";
import { formatErrorDetails } from "../helpers/errorFormatter";
// tiny helper for unique ids
function uid(prefix: string) {
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
// Helper type for command result
type CommandLine = Extract<Line, { kind: "command" }>;
// Context passed to profile handlers
export interface ProfileCommandContext {
buffersRef: { current: Buffers };
refreshDerived: () => void;
agentId: string;
setCommandRunning: (running: boolean) => void;
setAgentName: (name: string) => void;
}
// Helper to add a command result to buffers
export function addCommandResult(
buffersRef: { current: Buffers },
refreshDerived: () => void,
input: string,
output: string,
success: boolean,
phase: "running" | "finished" = "finished",
): string {
const cmdId = uid("cmd");
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
output,
phase,
...(phase === "finished" && { success }),
};
buffersRef.current.byId.set(cmdId, line);
buffersRef.current.order.push(cmdId);
refreshDerived();
return cmdId;
}
// Helper to update an existing command result
export function updateCommandResult(
buffersRef: { current: Buffers },
refreshDerived: () => void,
cmdId: string,
input: string,
output: string,
success: boolean,
phase: "running" | "finished" = "finished",
): void {
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
output,
phase,
...(phase === "finished" && { success }),
};
buffersRef.current.byId.set(cmdId, line);
refreshDerived();
}
// Get profiles from local settings
export function getProfiles(): Record<string, string> {
const localSettings = settingsManager.getLocalProjectSettings();
return localSettings.profiles || {};
}
// Check if a profile exists, returns error message if not found
export function validateProfileExists(
profileName: string,
profiles: Record<string, string>,
): string | null {
if (!profiles[profileName]) {
return `Profile "${profileName}" not found. Use /profile to list available profiles.`;
}
return null;
}
// Check if a profile name was provided, returns error message if not
export function validateProfileNameProvided(
profileName: string,
action: string,
): string | null {
if (!profileName) {
return `Please provide a profile name: /profile ${action} <name>`;
}
return null;
}
// /profile - list all profiles
export function handleProfileList(
ctx: ProfileCommandContext,
msg: string,
): void {
const profiles = getProfiles();
const profileNames = Object.keys(profiles);
let output: string;
if (profileNames.length === 0) {
output =
"No profiles saved. Use /profile save <name> to save the current agent.";
} else {
const lines = ["Saved profiles:"];
for (const name of profileNames.sort()) {
const profileAgentId = profiles[name];
const isCurrent = profileAgentId === ctx.agentId;
lines.push(
` ${name} -> ${profileAgentId}${isCurrent ? " (current)" : ""}`,
);
}
output = lines.join("\n");
}
addCommandResult(ctx.buffersRef, ctx.refreshDerived, msg, output, true);
}
// /profile save <name>
export async function handleProfileSave(
ctx: ProfileCommandContext,
msg: string,
profileName: string,
): Promise<void> {
const validationError = validateProfileNameProvided(profileName, "save");
if (validationError) {
addCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
msg,
validationError,
false,
);
return;
}
const cmdId = addCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
msg,
`Saving profile "${profileName}"...`,
false,
"running",
);
ctx.setCommandRunning(true);
try {
const client = await getClient();
// Update agent name via API
await client.agents.update(ctx.agentId, { name: profileName });
ctx.setAgentName(profileName);
// Save profile to local settings
const profiles = getProfiles();
const updatedProfiles = { ...profiles, [profileName]: ctx.agentId };
settingsManager.updateLocalProjectSettings({
profiles: updatedProfiles,
});
updateCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
cmdId,
msg,
`Saved profile "${profileName}" (agent ${ctx.agentId})`,
true,
);
} catch (error) {
const errorDetails = formatErrorDetails(error, ctx.agentId);
updateCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
cmdId,
msg,
`Failed: ${errorDetails}`,
false,
);
} finally {
ctx.setCommandRunning(false);
}
}
// Result from profile load validation
export interface ProfileLoadValidation {
targetAgentId: string | null;
needsConfirmation: boolean;
errorMessage: string | null;
}
// /profile load <name> - validation step (returns whether confirmation is needed)
export function validateProfileLoad(
ctx: ProfileCommandContext,
msg: string,
profileName: string,
): ProfileLoadValidation {
const nameError = validateProfileNameProvided(profileName, "load");
if (nameError) {
addCommandResult(ctx.buffersRef, ctx.refreshDerived, msg, nameError, false);
return {
targetAgentId: null,
needsConfirmation: false,
errorMessage: nameError,
};
}
const profiles = getProfiles();
const existsError = validateProfileExists(profileName, profiles);
if (existsError) {
addCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
msg,
existsError,
false,
);
return {
targetAgentId: null,
needsConfirmation: false,
errorMessage: existsError,
};
}
// We know the profile exists since validateProfileExists passed
const targetAgentId = profiles[profileName] as string;
// Check if current agent is saved to any profile
const currentAgentSaved = Object.values(profiles).includes(ctx.agentId);
if (!currentAgentSaved) {
return { targetAgentId, needsConfirmation: true, errorMessage: null };
}
return { targetAgentId, needsConfirmation: false, errorMessage: null };
}
// /profile delete <name>
export function handleProfileDelete(
ctx: ProfileCommandContext,
msg: string,
profileName: string,
): void {
const nameError = validateProfileNameProvided(profileName, "delete");
if (nameError) {
addCommandResult(ctx.buffersRef, ctx.refreshDerived, msg, nameError, false);
return;
}
const profiles = getProfiles();
const existsError = validateProfileExists(profileName, profiles);
if (existsError) {
addCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
msg,
existsError,
false,
);
return;
}
const { [profileName]: _, ...remainingProfiles } = profiles;
settingsManager.updateLocalProjectSettings({
profiles: remainingProfiles,
});
addCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
msg,
`Deleted profile "${profileName}"`,
true,
);
}
// Show usage help for unknown subcommand
export function handleProfileUsage(
ctx: ProfileCommandContext,
msg: string,
): void {
addCommandResult(
ctx.buffersRef,
ctx.refreshDerived,
msg,
"Usage: /profile [save|load|delete] <name>\n /profile - list profiles\n /profile save <name> - save current agent\n /profile load <name> - load a profile\n /profile delete <name> - delete a profile",
false,
);
}

View File

@@ -135,6 +135,13 @@ export const commands: Record<string, Command> = {
return "Opening message search...";
},
},
"/profile": {
desc: "Manage local profiles (save/load/delete)",
handler: () => {
// Handled specially in App.tsx for profile management
return "Managing profiles...";
},
},
};
/**