739eabc49a
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
550 lines
19 KiB
TypeScript
550 lines
19 KiB
TypeScript
import {
|
|
createAgentAction,
|
|
createAgentProfileAction,
|
|
deleteAgentProfileAction,
|
|
updateAgentAction,
|
|
updateAgentProfileAction,
|
|
updateAgentProfileMcpConfigAction,
|
|
} from "@/app/actions/agents";
|
|
import type {
|
|
Agent,
|
|
AgentProfile,
|
|
McpServerDef,
|
|
PermissionSetting,
|
|
ModelConfig,
|
|
ProfileEnvVar,
|
|
} from "@/lib/types/http";
|
|
import { arePermissionsDirty, permissionsToProfilePatch } from "@/lib/agent-permissions";
|
|
import { areCLIFlagsEqual } from "@/lib/cli-flags";
|
|
import { areConfigOptionsEqual } from "@/lib/config-options";
|
|
import { t } from "@/lib/i18n";
|
|
import { toAgentProfilePayload } from "@/lib/api/domains/agent-profile-normalize";
|
|
import type { ProfileFormData } from "@/components/settings/profile-form-fields";
|
|
|
|
// The JSON key the MCP editor validates against — an identifier, interpolated
|
|
// into the parse error rather than written into the catalog.
|
|
const MCP_SERVERS_KEY = "mcpServers";
|
|
|
|
/**
|
|
* Translates a ProfileFormData patch (snake_case form keys) into a
|
|
* Partial<AgentProfile> (camelCase). Profiles in client state use the
|
|
* canonical camelCase AgentProfile shape, so without this translation
|
|
* patches like { cli_passthrough: true } would land as a new snake_case
|
|
* key and the camelCase reader would never see them.
|
|
*/
|
|
export function toAgentProfilePatch(patch: Partial<ProfileFormData>): Partial<AgentProfile> {
|
|
const next: Partial<AgentProfile> = {};
|
|
if (patch.name !== undefined) next.name = patch.name;
|
|
if (patch.model !== undefined) next.model = patch.model;
|
|
if (patch.fallback_model !== undefined) next.fallbackModel = patch.fallback_model;
|
|
if (patch.auto_fallback !== undefined) next.autoFallback = patch.auto_fallback;
|
|
if (patch.require_exact_model !== undefined) next.requireExactModel = patch.require_exact_model;
|
|
if (patch.mode !== undefined) next.mode = patch.mode;
|
|
if (patch.config_options !== undefined) next.configOptions = patch.config_options;
|
|
if (patch.allow_indexing !== undefined) next.allowIndexing = patch.allow_indexing;
|
|
if (patch.auto_approve !== undefined) next.autoApprove = patch.auto_approve;
|
|
if (patch.cli_passthrough !== undefined) next.cliPassthrough = patch.cli_passthrough;
|
|
if (patch.cli_flags !== undefined) next.cliFlags = patch.cli_flags;
|
|
if (patch.command_prefix !== undefined) next.commandPrefix = patch.command_prefix;
|
|
return next;
|
|
}
|
|
|
|
function areEnvVarsEqual(a?: ProfileEnvVar[], b?: ProfileEnvVar[]): boolean {
|
|
const left = a ?? [];
|
|
const right = b ?? [];
|
|
if (left.length !== right.length) return false;
|
|
return left.every(
|
|
(ev, i) =>
|
|
ev.key === right[i]?.key &&
|
|
(ev.value ?? "") === (right[i]?.value ?? "") &&
|
|
(ev.secret_id ?? "") === (right[i]?.secret_id ?? ""),
|
|
);
|
|
}
|
|
|
|
type DraftMcpConfig = {
|
|
enabled: boolean;
|
|
servers: string;
|
|
dirty: boolean;
|
|
error: string | null;
|
|
};
|
|
|
|
/**
|
|
* Editable in-memory shape for an agent profile being created or edited
|
|
* in the settings UI. Mirrors the canonical (camelCase) `AgentProfile`
|
|
* with form-state extras. The save helpers translate this back to
|
|
* snake_case at the API boundary.
|
|
*
|
|
* `allow_indexing` is kept as a snake_case form key so the permissions
|
|
* map (which is keyed by snake_case agent metadata) flows through the
|
|
* draft unchanged.
|
|
*/
|
|
export type DraftProfile = AgentProfile & {
|
|
allow_indexing?: boolean;
|
|
auto_approve?: boolean;
|
|
isNew?: boolean;
|
|
mcp_config?: DraftMcpConfig;
|
|
};
|
|
|
|
export type DraftAgent = Omit<Agent, "profiles"> & { profiles: DraftProfile[]; isNew?: boolean };
|
|
|
|
function dynamicProfilePayload(profile: DraftProfile) {
|
|
if (profile.kind !== "dynamic" && !profile.dynamic) return undefined;
|
|
return toAgentProfilePayload(profile).dynamic;
|
|
}
|
|
|
|
export const parseProfileMcpServers = (raw: string): Record<string, McpServerDef> => {
|
|
if (!raw.trim()) return {};
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
throw new Error(t("agents:mcpConfigMustBeJsonObject"));
|
|
}
|
|
if (MCP_SERVERS_KEY in parsed) {
|
|
const nested = (parsed as { mcpServers?: unknown }).mcpServers;
|
|
if (!nested || typeof nested !== "object" || Array.isArray(nested)) {
|
|
throw new Error(t("agents:mcpKeyMustBeJsonObject", { key: MCP_SERVERS_KEY }));
|
|
}
|
|
return nested as Record<string, McpServerDef>;
|
|
}
|
|
return parsed as Record<string, McpServerDef>;
|
|
};
|
|
|
|
type SaveMcpForProfileParams = {
|
|
draftProfile: DraftProfile;
|
|
targetProfileId: string;
|
|
onToastError: (error: unknown) => void;
|
|
};
|
|
|
|
async function saveMcpForProfile({
|
|
draftProfile,
|
|
targetProfileId,
|
|
onToastError,
|
|
}: SaveMcpForProfileParams) {
|
|
if (!draftProfile.mcp_config?.dirty || !draftProfile.mcp_config.servers.trim()) return;
|
|
try {
|
|
const servers = parseProfileMcpServers(draftProfile.mcp_config.servers);
|
|
await updateAgentProfileMcpConfigAction(targetProfileId, {
|
|
enabled: draftProfile.mcp_config.enabled,
|
|
mcpServers: servers,
|
|
});
|
|
} catch (error) {
|
|
onToastError(error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function correlateCreatedProfiles(
|
|
submitted: DraftProfile[],
|
|
created: AgentProfile[],
|
|
): Map<string, string> {
|
|
const mappings = new Map<string, string>();
|
|
if (submitted.length === created.length) {
|
|
submitted.forEach((profile, index) => mappings.set(profile.id, created[index].id));
|
|
return mappings;
|
|
}
|
|
for (const profile of submitted) {
|
|
const match = created.find((candidate) => candidate.name === profile.name);
|
|
if (match) mappings.set(profile.id, match.id);
|
|
}
|
|
return mappings;
|
|
}
|
|
|
|
async function saveMcpForCreatedProfiles(
|
|
draftAgent: DraftAgent,
|
|
created: Agent,
|
|
onToastError: (error: unknown) => void,
|
|
) {
|
|
if (created.profiles.length === draftAgent.profiles.length) {
|
|
for (let index = 0; index < draftAgent.profiles.length; index += 1) {
|
|
await saveMcpForProfile({
|
|
draftProfile: draftAgent.profiles[index],
|
|
targetProfileId: created.profiles[index].id,
|
|
onToastError,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
for (const draftProfile of draftAgent.profiles) {
|
|
const createdProfile = created.profiles.find((profile) => profile.name === draftProfile.name);
|
|
if (!createdProfile) continue;
|
|
await saveMcpForProfile({
|
|
draftProfile,
|
|
targetProfileId: createdProfile.id,
|
|
onToastError,
|
|
});
|
|
}
|
|
}
|
|
|
|
function preservePendingMcpDrafts(draftAgent: DraftAgent, created: Agent): Agent {
|
|
const submittedById = new Map<string, DraftProfile>(
|
|
draftAgent.profiles.map((profile) => [profile.id, profile]),
|
|
);
|
|
const profileIds = correlateCreatedProfiles(draftAgent.profiles, created.profiles);
|
|
const submittedByCreatedId = new Map(
|
|
[...profileIds].map(([submittedId, createdId]) => [createdId, submittedById.get(submittedId)]),
|
|
);
|
|
return {
|
|
...created,
|
|
profiles: created.profiles.map((profile) => {
|
|
const pending = submittedByCreatedId.get(profile.id)?.mcp_config;
|
|
return pending ? { ...profile, mcp_config: pending } : profile;
|
|
}),
|
|
};
|
|
}
|
|
|
|
export type EnsureProfilesFn = (
|
|
agent: DraftAgent,
|
|
displayName: string,
|
|
defaultModel: string,
|
|
permissions?: Record<string, PermissionSetting>,
|
|
) => DraftAgent;
|
|
|
|
export type CloneAgentFn = (agent: Agent) => DraftAgent;
|
|
|
|
export type SaveAgentCallbacks = {
|
|
onToastError: (error: unknown) => void;
|
|
currentAgentModelConfig: ModelConfig;
|
|
permissionSettings: Record<string, PermissionSetting>;
|
|
resolveDisplayName: (name: string) => string;
|
|
upsertAgent: (agent: Agent) => void;
|
|
setDraftAgent: (agent: DraftAgent | ((current: DraftAgent) => DraftAgent)) => void;
|
|
ensureProfiles: EnsureProfilesFn;
|
|
cloneAgent: CloneAgentFn;
|
|
replaceRoute: (path: string) => void;
|
|
};
|
|
|
|
export async function saveNewAgent(draftAgent: DraftAgent, callbacks: SaveAgentCallbacks) {
|
|
let created = await createAgentAction({
|
|
name: draftAgent.name,
|
|
workspace_id: draftAgent.workspace_id,
|
|
profiles: draftAgent.profiles.map((profile) => ({
|
|
name: profile.name,
|
|
model: profile.model,
|
|
kind: profile.kind,
|
|
fallback_model: profile.fallbackModel ?? "",
|
|
auto_fallback: profile.autoFallback ?? false,
|
|
mode: profile.mode,
|
|
config_options: profile.configOptions ?? {},
|
|
...permissionsToProfilePatch(profile),
|
|
cli_passthrough: profile.cliPassthrough ?? false,
|
|
cli_flags: profile.cliFlags ?? [],
|
|
command_prefix: profile.commandPrefix ?? "",
|
|
env_vars: profile.envVars ?? [],
|
|
dynamic: dynamicProfilePayload(profile),
|
|
})),
|
|
});
|
|
|
|
try {
|
|
await saveMcpForCreatedProfiles(draftAgent, created, callbacks.onToastError);
|
|
} catch (error) {
|
|
const reconciled = preservePendingMcpDrafts(draftAgent, created);
|
|
callbacks.upsertAgent(reconciled);
|
|
const savedDraft = callbacks.ensureProfiles(
|
|
callbacks.cloneAgent(reconciled),
|
|
callbacks.resolveDisplayName(reconciled.name),
|
|
callbacks.currentAgentModelConfig.default_model,
|
|
callbacks.permissionSettings,
|
|
);
|
|
const profileIds = correlateCreatedProfiles(draftAgent.profiles, reconciled.profiles);
|
|
callbacks.setDraftAgent((current) =>
|
|
mergeSavedAgentDraft(current, draftAgent, savedDraft, profileIds),
|
|
);
|
|
callbacks.replaceRoute(`/settings/agents/${encodeURIComponent(reconciled.name)}`);
|
|
throw error;
|
|
}
|
|
|
|
if ((draftAgent.mcp_config_path ?? "") !== (created.mcp_config_path ?? "")) {
|
|
created = await updateAgentAction(created.id, {
|
|
mcp_config_path: draftAgent.mcp_config_path ?? "",
|
|
});
|
|
}
|
|
callbacks.upsertAgent(created);
|
|
const savedDraft = callbacks.ensureProfiles(
|
|
callbacks.cloneAgent(created),
|
|
callbacks.resolveDisplayName(created.name),
|
|
callbacks.currentAgentModelConfig.default_model,
|
|
callbacks.permissionSettings,
|
|
);
|
|
const profileIds = correlateCreatedProfiles(draftAgent.profiles, created.profiles);
|
|
callbacks.setDraftAgent((current) =>
|
|
mergeSavedAgentDraft(current, draftAgent, savedDraft, profileIds),
|
|
);
|
|
callbacks.replaceRoute(`/settings/agents/${encodeURIComponent(created.name)}`);
|
|
return savedDraft;
|
|
}
|
|
|
|
async function saveExistingAgentPatch(draftAgent: DraftAgent, savedAgent: Agent) {
|
|
const agentPatch: { workspace_id?: string | null; mcp_config_path?: string | null } = {};
|
|
if ((draftAgent.workspace_id ?? null) !== (savedAgent.workspace_id ?? null)) {
|
|
agentPatch.workspace_id = draftAgent.workspace_id ?? null;
|
|
}
|
|
if ((draftAgent.mcp_config_path ?? "") !== (savedAgent.mcp_config_path ?? "")) {
|
|
agentPatch.mcp_config_path = draftAgent.mcp_config_path ?? "";
|
|
}
|
|
if (Object.keys(agentPatch).length > 0) {
|
|
await updateAgentAction(savedAgent.id, agentPatch);
|
|
}
|
|
}
|
|
|
|
async function savePersistedProfile(
|
|
profile: DraftProfile,
|
|
savedProfile: AgentProfile,
|
|
onToastError: (error: unknown) => void,
|
|
): Promise<AgentProfile> {
|
|
await saveMcpForProfile({
|
|
draftProfile: profile,
|
|
targetProfileId: savedProfile.id,
|
|
onToastError,
|
|
});
|
|
if (isProfileDirty(profile, savedProfile)) {
|
|
return updateAgentProfileAction(profile.id, {
|
|
name: profile.name,
|
|
model: profile.model,
|
|
kind: profile.kind,
|
|
fallback_model: profile.fallbackModel ?? "",
|
|
auto_fallback: profile.autoFallback ?? false,
|
|
mode: profile.mode,
|
|
config_options: profile.configOptions ?? {},
|
|
...permissionsToProfilePatch(profile),
|
|
cli_passthrough: profile.cliPassthrough ?? false,
|
|
cli_flags: profile.cliFlags ?? [],
|
|
command_prefix: profile.commandPrefix ?? "",
|
|
env_vars: profile.envVars ?? [],
|
|
dynamic: dynamicProfilePayload(profile),
|
|
});
|
|
}
|
|
const { mcp_config: _pendingMcp, ...persistedProfile } = savedProfile as DraftProfile;
|
|
return persistedProfile;
|
|
}
|
|
|
|
async function saveExistingProfiles(
|
|
draftAgent: DraftAgent,
|
|
savedAgent: Agent,
|
|
isCreateMode: boolean,
|
|
onToastError: (error: unknown) => void,
|
|
): Promise<{ profiles: AgentProfile[]; profileIds: Map<string, string> }> {
|
|
const savedProfilesById = new Map(savedAgent.profiles.map((p) => [p.id, p]));
|
|
const nextProfiles: AgentProfile[] = isCreateMode ? [...savedAgent.profiles] : [];
|
|
const profileIds = new Map<string, string>();
|
|
const persistedProfiles: DraftProfile[] = [];
|
|
const persistedSubmittedIds = new Set<string>();
|
|
|
|
try {
|
|
for (const profile of draftAgent.profiles) {
|
|
const savedProfile = savedProfilesById.get(profile.id);
|
|
if (!savedProfile) {
|
|
const createdProfile = await createAgentProfileAction(savedAgent.id, {
|
|
name: profile.name,
|
|
model: profile.model,
|
|
kind: profile.kind,
|
|
fallback_model: profile.fallbackModel ?? "",
|
|
auto_fallback: profile.autoFallback ?? false,
|
|
mode: profile.mode,
|
|
config_options: profile.configOptions ?? {},
|
|
...permissionsToProfilePatch(profile),
|
|
cli_passthrough: profile.cliPassthrough ?? false,
|
|
cli_flags: profile.cliFlags ?? [],
|
|
command_prefix: profile.commandPrefix ?? "",
|
|
env_vars: profile.envVars ?? [],
|
|
dynamic: dynamicProfilePayload(profile),
|
|
});
|
|
profileIds.set(profile.id, createdProfile.id);
|
|
persistedSubmittedIds.add(profile.id);
|
|
persistedProfiles.push(
|
|
profile.mcp_config
|
|
? { ...createdProfile, mcp_config: profile.mcp_config }
|
|
: createdProfile,
|
|
);
|
|
await saveMcpForProfile({
|
|
draftProfile: profile,
|
|
targetProfileId: createdProfile.id,
|
|
onToastError,
|
|
});
|
|
persistedProfiles[persistedProfiles.length - 1] = createdProfile;
|
|
nextProfiles.push(createdProfile);
|
|
continue;
|
|
}
|
|
profileIds.set(profile.id, savedProfile.id);
|
|
const persistedProfile = await savePersistedProfile(profile, savedProfile, onToastError);
|
|
persistedSubmittedIds.add(profile.id);
|
|
persistedProfiles.push(persistedProfile);
|
|
nextProfiles.push(persistedProfile);
|
|
}
|
|
} catch (error) {
|
|
if (persistedProfiles.length > 0) {
|
|
throw new PartialProfileSaveError(
|
|
error,
|
|
persistedProfiles,
|
|
persistedSubmittedIds,
|
|
profileIds,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
return { profiles: nextProfiles, profileIds };
|
|
}
|
|
|
|
class PartialProfileSaveError extends Error {
|
|
constructor(
|
|
readonly original: unknown,
|
|
readonly persistedProfiles: DraftProfile[],
|
|
readonly persistedSubmittedIds: Set<string>,
|
|
readonly profileIds: Map<string, string>,
|
|
) {
|
|
super("Profile creation only partially completed");
|
|
}
|
|
}
|
|
|
|
function reconcilePartialProfileSave(
|
|
draftAgent: DraftAgent,
|
|
savedAgent: Agent,
|
|
partial: PartialProfileSaveError,
|
|
callbacks: SaveAgentCallbacks,
|
|
) {
|
|
const profilesById = new Map(savedAgent.profiles.map((profile) => [profile.id, profile]));
|
|
for (const profile of partial.persistedProfiles) profilesById.set(profile.id, profile);
|
|
const reconciled = {
|
|
...savedAgent,
|
|
profiles: [...profilesById.values()],
|
|
};
|
|
callbacks.upsertAgent(reconciled);
|
|
const submitted = {
|
|
...draftAgent,
|
|
profiles: draftAgent.profiles.filter((profile) =>
|
|
partial.persistedSubmittedIds.has(profile.id),
|
|
),
|
|
};
|
|
const persistedIds = new Set(partial.persistedProfiles.map((profile) => profile.id));
|
|
const savedDraft = callbacks.ensureProfiles(
|
|
{
|
|
...callbacks.cloneAgent(reconciled),
|
|
profiles: reconciled.profiles.filter((profile) => persistedIds.has(profile.id)),
|
|
},
|
|
callbacks.resolveDisplayName(reconciled.name),
|
|
callbacks.currentAgentModelConfig.default_model,
|
|
callbacks.permissionSettings,
|
|
);
|
|
callbacks.setDraftAgent((current) =>
|
|
mergeSavedAgentDraft(current, submitted, savedDraft, partial.profileIds),
|
|
);
|
|
}
|
|
|
|
async function deleteRemovedProfiles(draftAgent: DraftAgent, savedAgent: Agent) {
|
|
for (const savedProfile of savedAgent.profiles) {
|
|
const stillExists = draftAgent.profiles.some((p) => p.id === savedProfile.id);
|
|
if (!stillExists) {
|
|
await deleteAgentProfileAction(savedProfile.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function saveExistingAgent(
|
|
draftAgent: DraftAgent,
|
|
savedAgent: Agent,
|
|
isCreateMode: boolean,
|
|
callbacks: SaveAgentCallbacks,
|
|
) {
|
|
await saveExistingAgentPatch(draftAgent, savedAgent);
|
|
|
|
let savedProfiles: Awaited<ReturnType<typeof saveExistingProfiles>>;
|
|
try {
|
|
savedProfiles = await saveExistingProfiles(
|
|
draftAgent,
|
|
savedAgent,
|
|
isCreateMode,
|
|
callbacks.onToastError,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof PartialProfileSaveError) {
|
|
reconcilePartialProfileSave(draftAgent, savedAgent, error, callbacks);
|
|
throw error.original;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (!isCreateMode) {
|
|
await deleteRemovedProfiles(draftAgent, savedAgent);
|
|
}
|
|
|
|
const nextAgent = {
|
|
...savedAgent,
|
|
workspace_id: draftAgent.workspace_id ?? null,
|
|
mcp_config_path: draftAgent.mcp_config_path ?? "",
|
|
profiles: savedProfiles.profiles,
|
|
};
|
|
callbacks.upsertAgent(nextAgent);
|
|
const savedDraft = callbacks.ensureProfiles(
|
|
callbacks.cloneAgent(nextAgent),
|
|
callbacks.resolveDisplayName(nextAgent.name),
|
|
callbacks.currentAgentModelConfig.default_model,
|
|
callbacks.permissionSettings,
|
|
);
|
|
callbacks.setDraftAgent((current) =>
|
|
mergeSavedAgentDraft(current, draftAgent, savedDraft, savedProfiles.profileIds),
|
|
);
|
|
if (isCreateMode) {
|
|
callbacks.replaceRoute(`/settings/agents/${encodeURIComponent(savedAgent.name)}`);
|
|
}
|
|
return savedDraft;
|
|
}
|
|
|
|
export function mergeSavedAgentDraft(
|
|
current: DraftAgent,
|
|
submitted: DraftAgent,
|
|
saved: DraftAgent,
|
|
profileIds: ReadonlyMap<string, string> = new Map(),
|
|
): DraftAgent {
|
|
const currentById = new Map(current.profiles.map((profile) => [profile.id, profile]));
|
|
const submittedBySavedId = new Map(
|
|
submitted.profiles.map((profile) => [profileIds.get(profile.id) ?? profile.id, profile]),
|
|
);
|
|
const profiles = saved.profiles.map((savedProfile) => {
|
|
const submittedProfile = submittedBySavedId.get(savedProfile.id);
|
|
if (!submittedProfile) return savedProfile;
|
|
const currentProfile = currentById.get(submittedProfile.id);
|
|
if (!currentProfile || JSON.stringify(currentProfile) === JSON.stringify(submittedProfile)) {
|
|
return savedProfile;
|
|
}
|
|
return { ...savedProfile, ...currentProfile, id: savedProfile.id };
|
|
});
|
|
const submittedIds = new Set(submitted.profiles.map((profile) => profile.id));
|
|
profiles.push(...current.profiles.filter((profile) => !submittedIds.has(profile.id)));
|
|
return { ...saved, ...current, id: saved.id, name: saved.name, profiles };
|
|
}
|
|
|
|
function isProfileCliConfigDirty(draft: DraftProfile, saved: AgentProfile): boolean {
|
|
return (
|
|
draft.cliPassthrough !== saved.cliPassthrough ||
|
|
!areCLIFlagsEqual(draft.cliFlags ?? [], saved.cliFlags ?? []) ||
|
|
(draft.commandPrefix ?? "") !== (saved.commandPrefix ?? "") ||
|
|
!areEnvVarsEqual(draft.envVars, saved.envVars)
|
|
);
|
|
}
|
|
|
|
function isProfileIdentityDirty(draft: DraftProfile, saved: AgentProfile): boolean {
|
|
return [
|
|
(draft.kind ?? "concrete") !== (saved.kind ?? "concrete"),
|
|
JSON.stringify(draft.dynamic ?? null) !== JSON.stringify(saved.dynamic ?? null),
|
|
draft.name !== saved.name,
|
|
draft.model !== saved.model,
|
|
(draft.mode ?? "") !== (saved.mode ?? ""),
|
|
(draft.fallbackModel ?? "") !== (saved.fallbackModel ?? ""),
|
|
(draft.autoFallback ?? false) !== (saved.autoFallback ?? false),
|
|
].some(Boolean);
|
|
}
|
|
|
|
function isProfileSettingsDirty(draft: DraftProfile, saved: AgentProfile): boolean {
|
|
return (
|
|
areConfigOptionsEqual(draft.configOptions, saved.configOptions) === false ||
|
|
arePermissionsDirty(draft, saved)
|
|
);
|
|
}
|
|
|
|
export function isProfileDirty(draft: DraftProfile, saved?: AgentProfile): boolean {
|
|
if (!saved) return true;
|
|
return (
|
|
isProfileIdentityDirty(draft, saved) ||
|
|
isProfileSettingsDirty(draft, saved) ||
|
|
isProfileCliConfigDirty(draft, saved)
|
|
);
|
|
}
|