"use client";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Label } from "@kandev/ui/label";
import { Input } from "@kandev/ui/input";
import { Switch } from "@kandev/ui/switch";
import { Button } from "@kandev/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { useAppStore } from "@/components/state-provider";
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
import { ModelCombobox } from "@/components/settings/model-combobox";
import { ModeCombobox } from "@/components/settings/mode-combobox";
import { ModelFallbackFields } from "@/components/agent/cli-profile-fallback-fields";
import { CLIFlagsField } from "@/components/settings/cli-flags-field";
import {
createAgentAction,
createAgentProfileAction,
updateAgentProfileAction,
} from "@/app/actions/agents";
import { isHandledApiError } from "@/lib/api/client";
import type { Agent, AgentProfile, AvailableAgent, CLIFlag } from "@/lib/types/http";
import { seedDefaultCLIFlags } from "@/lib/cli-flags";
import {
buildDefaultPermissions,
PERMISSION_APPLY_AGENTCTL_AUTO_APPROVE,
} from "@/lib/agent-permissions";
import { t } from "@/lib/i18n";
export type CliProfileEditorMode = "create" | "edit";
export type CliProfileEditorProps = {
/** "create" mounts the inline create flow; "edit" patches an existing profile. */
mode: CliProfileEditorMode;
/** When `mode === "edit"`, the existing profile to patch. */
profile?: AgentProfile;
/** Default profile name suggestion in create mode. */
defaultProfileName?: string;
/** Called once a profile is successfully saved. Receives the saved profile. */
onSaved: (profile: AgentProfile) => void;
/** Called on cancel; only used by callers that wrap us in a dialog. */
onCancel?: () => void;
/**
* Show the advanced (permissions / passthrough) toggles. Defaults to
* collapsed; the wizard hides them entirely, the configuration tab
* exposes them inline.
*/
showAdvanced?: boolean;
/** Whether the editor exposes CLI passthrough. Office agents should keep ACP enabled. */
allowCliPassthrough?: boolean;
};
type FormState = {
agentName: string;
profileName: string;
model: string;
fallbackModel: string;
autoFallback: boolean;
requireExactModel: boolean;
mode: string;
cliFlags: CLIFlag[];
cliPassthrough: boolean;
allowIndexing: boolean;
autoApprove: boolean;
};
function pickDefaultAgent(available: AvailableAgent[]): AvailableAgent | undefined {
return available.find((a) => a.available) ?? available[0];
}
function fromExistingProfile(profile: AgentProfile): FormState {
return {
agentName: profile.agentId ?? "",
profileName: profile.name,
model: profile.model ?? "",
fallbackModel: profile.fallbackModel ?? "",
autoFallback: profile.autoFallback ?? false,
requireExactModel: profile.requireExactModel ?? false,
mode: profile.mode ?? "",
cliFlags: profile.cliFlags ?? [],
cliPassthrough: profile.cliPassthrough ?? false,
allowIndexing: profile.allowIndexing ?? false,
autoApprove: profile.autoApprove ?? false,
};
}
function fromDefaultAgent(
defaultName: string,
defaultAgent: AvailableAgent | undefined,
): FormState {
const cfg = defaultAgent?.model_config;
const permissionSettings = defaultAgent?.permission_settings ?? {};
const defaults = buildDefaultPermissions(permissionSettings);
return {
agentName: defaultAgent?.name ?? "",
profileName: defaultName,
model: cfg?.default_model ?? "",
fallbackModel: "",
autoFallback: false,
requireExactModel: false,
mode: cfg?.current_mode_id ?? "",
cliFlags: seedDefaultCLIFlags(permissionSettings),
cliPassthrough: false,
...defaults,
};
}
function initialState(
mode: CliProfileEditorMode,
profile: AgentProfile | undefined,
defaultName: string,
defaultAgent: AvailableAgent | undefined,
): FormState {
if (mode === "edit" && profile) return fromExistingProfile(profile);
return fromDefaultAgent(defaultName, defaultAgent);
}
function ProfileNameField({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
const { t } = useTranslation();
return (
onChange(e.target.value)}
placeholder={t("common:default")}
className="mt-1"
/>
);
}
export function CliProfileEditor({
mode,
profile,
defaultProfileName = "default",
onSaved,
onCancel,
showAdvanced = false,
allowCliPassthrough = true,
}: CliProfileEditorProps) {
const availableAgents = useAvailableAgents();
const settingsAgents = useAppStore((s) => s.settingsAgents.items);
const installed = useMemo(
() => availableAgents.items.filter((a) => a.available),
[availableAgents.items],
);
const [form, setForm] = useState(() =>
initialState(mode, profile, defaultProfileName, pickDefaultAgent(installed)),
);
const [saving, setSaving] = useState(false);
const [advancedOpen, setAdvancedOpen] = useState(showAdvanced);
const currentAgent = installed.find((a) => a.name === form.agentName);
const modelConfig = currentAgent?.model_config;
const permissionSettings = currentAgent?.permission_settings;
const patch = useCallback((p: Partial) => {
setForm((prev) => ({ ...prev, ...p }));
}, []);
const handleSave = useCallback(async () => {
if (!form.agentName || !form.profileName.trim()) return;
setSaving(true);
try {
const saved =
mode === "edit" && profile
? await saveExistingProfile(profile.id, form)
: await saveNewProfile(form, settingsAgents);
onSaved(saved);
} catch (error) {
if (isHandledApiError(error)) return;
throw error;
} finally {
setSaving(false);
}
}, [form, mode, profile, settingsAgents, onSaved]);
const canSave = form.agentName !== "" && form.profileName.trim() !== "";
return (
{mode === "create" && (
patch({
agentName: name,
model: installed.find((a) => a.name === name)?.model_config.default_model ?? "",
// A fallback model belongs to the previous client's model
// vocabulary; carrying it over would save a false gone-model
// configuration for the new client.
fallbackModel: "",
mode: installed.find((a) => a.name === name)?.model_config.current_mode_id ?? "",
cliFlags: seedDefaultCLIFlags(
installed.find((a) => a.name === name)?.permission_settings ?? {},
),
})
}
/>
)}
patch({ profileName })}
/>
setAdvancedOpen((o) => !o)}
cliPassthrough={form.cliPassthrough}
allowIndexing={form.allowIndexing}
autoApprove={form.autoApprove}
cliFlags={form.cliFlags}
permissionSettings={permissionSettings ?? {}}
allowCliPassthrough={allowCliPassthrough}
showAllowIndexing={Boolean(permissionSettings?.allow_indexing?.supported)}
onCliPassthroughChange={(v) => patch({ cliPassthrough: v })}
onAllowIndexingChange={(v) => patch({ allowIndexing: v })}
onAutoApproveChange={(v) => patch({ autoApprove: v })}
onCliFlagsChange={(v) => patch({ cliFlags: v })}
/>
);
}
function buttonLabel(mode: CliProfileEditorMode, saving: boolean): string {
if (saving) return mode === "edit" ? t("agents:savingProfile") : t("agents:creatingProfile");
return mode === "edit" ? t("agents:saveProfile") : t("agents:createProfile");
}
function EditorFooter({
mode,
saving,
canSave,
onSave,
onCancel,
}: {
mode: CliProfileEditorMode;
saving: boolean;
canSave: boolean;
onSave: () => void;
onCancel?: () => void;
}) {
const { t } = useTranslation();
return (
{onCancel && (
)}
);
}
function CliClientPicker({
installed,
value,
onChange,
}: {
installed: AvailableAgent[];
value: string;
onChange: (name: string) => void;
}) {
const { t } = useTranslation();
return (
{installed.length === 0 && (
{t("common:noCliClientsInstalledYetInstall")}
)}
);
}
// ModelModeFieldsBinding forwards the editor's form state to ModelModeFields.
// Extracted so CliProfileEditor stays under the linter's line budget.
function ModelModeFieldsBinding({
form,
patch,
modelConfig,
}: {
form: FormState;
patch: (p: Partial) => void;
modelConfig: NonNullable | null;
}) {
return (
patch({ model: v })}
onFallbackModelChange={(v) => patch({ fallbackModel: v })}
onAutoFallbackChange={(v) => patch({ autoFallback: v })}
onRequireExactModelChange={(v) => patch({ requireExactModel: v })}
onModeChange={(v) => patch({ mode: v })}
/>
);
}
type ModelModeFieldsProps = {
modelConfig: NonNullable | null;
model: string;
fallbackModel: string;
autoFallback: boolean;
requireExactModel: boolean;
mode: string;
onModelChange: (v: string) => void;
onFallbackModelChange: (v: string) => void;
onAutoFallbackChange: (v: boolean) => void;
onRequireExactModelChange: (v: boolean) => void;
onModeChange: (v: string) => void;
};
function ModelModeFields({
modelConfig,
model,
fallbackModel,
autoFallback,
requireExactModel,
mode,
onModelChange,
onFallbackModelChange,
onAutoFallbackChange,
onRequireExactModelChange,
onModeChange,
}: ModelModeFieldsProps) {
const { t } = useTranslation();
if (!modelConfig) {
return {t("common:pickACliClientToLoad")}
;
}
const availableModels = modelConfig.available_models ?? [];
const startModelGone = Boolean(model && !availableModels.some((m) => m.id === model));
const fallbackModelGone = Boolean(
fallbackModel && !availableModels.some((m) => m.id === fallbackModel),
);
return (
{(modelConfig.available_modes ?? []).length > 0 && (
)}
);
}
type AdvancedTogglesProps = {
open: boolean;
onToggle: () => void;
cliPassthrough: boolean;
allowIndexing: boolean;
autoApprove: boolean;
cliFlags: CLIFlag[];
permissionSettings: AvailableAgent["permission_settings"];
allowCliPassthrough: boolean;
showAllowIndexing: boolean;
onCliPassthroughChange: (v: boolean) => void;
onAllowIndexingChange: (v: boolean) => void;
onAutoApproveChange: (v: boolean) => void;
onCliFlagsChange: (v: CLIFlag[]) => void;
};
function AdvancedToggles({
open,
onToggle,
cliPassthrough,
allowIndexing,
autoApprove,
cliFlags,
permissionSettings,
allowCliPassthrough,
showAllowIndexing,
onCliPassthroughChange,
onAllowIndexingChange,
onAutoApproveChange,
onCliFlagsChange,
}: AdvancedTogglesProps) {
const { t } = useTranslation();
const autoSetting = permissionSettings?.auto_approve;
const showAgentctlAutoApprove = Boolean(
autoSetting?.supported && autoSetting.apply_method === PERMISSION_APPLY_AGENTCTL_AUTO_APPROVE,
);
return (
{open && (
{allowCliPassthrough && (
)}
{showAgentctlAutoApprove && autoSetting && (
)}
{showAllowIndexing && (
)}
)}
);
}
function ToggleRow({
id,
label,
description,
checked,
onChange,
}: {
id: string;
label: string;
description: string;
checked: boolean;
onChange: (v: boolean) => void;
}) {
return (
);
}
function AgentctlAutoApproveRow({
id,
setting,
checked,
onChange,
}: {
id: string;
setting: NonNullable["auto_approve"];
checked: boolean;
onChange: (v: boolean) => void;
}) {
const { t } = useTranslation();
return (
{setting?.description ?? t("common:kandevAllowsEveryAgentPermissionRequest")}
);
}
async function saveExistingProfile(id: string, form: FormState): Promise {
return updateAgentProfileAction(id, {
name: form.profileName.trim(),
model: form.model,
fallback_model: form.fallbackModel ?? "",
auto_fallback: form.autoFallback,
require_exact_model: form.requireExactModel,
mode: form.mode || undefined,
allow_indexing: form.allowIndexing,
auto_approve: form.autoApprove,
cli_flags: form.cliFlags,
cli_passthrough: form.cliPassthrough,
});
}
async function saveNewProfile(form: FormState, settingsAgents: Agent[]): Promise {
const existingAgent = settingsAgents.find((a) => a.name === form.agentName);
const profilePayload = {
name: form.profileName.trim(),
model: form.model,
fallback_model: form.fallbackModel ?? "",
auto_fallback: form.autoFallback,
require_exact_model: form.requireExactModel,
mode: form.mode || undefined,
allow_indexing: form.allowIndexing,
auto_approve: form.autoApprove,
cli_passthrough: form.cliPassthrough,
cli_flags: form.cliFlags,
};
if (existingAgent) {
return createAgentProfileAction(existingAgent.id, profilePayload);
}
// Otherwise create the kanban Agent row + the first profile in one POST.
const created = await createAgentAction({
name: form.agentName,
profiles: [profilePayload],
});
const newProfile = created.profiles.find((p) => p.name === profilePayload.name);
if (!newProfile) {
throw new Error(
`Created agent ${form.agentName} but the response did not include the new profile.`,
);
}
return {
...newProfile,
agentId: newProfile.agentId || created.id,
agentDisplayName: newProfile.agentDisplayName || created.name,
};
}