Files
kandev/apps/web/components/settings/mcp-task-agent-profile-default-settings.tsx
T
2026-09-22 21:24:43 +01:00

223 lines
7.3 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { SettingsInfo } from "./settings-info";
import { CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
import { Label } from "@kandev/ui/label";
import { RadioGroup, RadioGroupItem } from "@kandev/ui/radio-group";
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
import { updateUserSettings } from "@/lib/api";
import type { MCPTaskAgentProfileDefault } from "@/lib/types/http";
import { SettingsCard } from "./settings-card";
import { SettingsRow, type SettingsPresentation } from "./settings-group";
import { GENERAL_SETTINGS_TARGETS } from "@/lib/settings-discovery/catalog/preferences";
import { useSettingsSaveContributor } from "./settings-save-provider";
import { Trans, useTranslation } from "react-i18next";
/**
* `value` is the persisted enum the API compares — never translated. The label
* and description hold catalog KEYS, resolved at render: a module-scope `t()`
* would freeze this copy at the boot locale (see docs/i18n.md).
*/
const PROFILE_LABEL_KEY = "settings:profileForTasksCreatedByAgents";
const OPTIONS: Array<{
value: MCPTaskAgentProfileDefault;
labelKey: string;
descriptionKey: string;
}> = [
{
value: "current_task",
labelKey: "settings:mcpTaskProfileCurrentTask",
descriptionKey: "settings:mcpTaskProfileCurrentTaskDescription",
},
{
value: "workspace_default",
labelKey: "settings:mcpTaskProfileWorkspaceDefault",
descriptionKey: "settings:mcpTaskProfileWorkspaceDefaultDescription",
},
];
function MCPTaskProfileScopeDescription() {
const { t } = useTranslation();
return (
<div className="space-y-2">
<p>{t("settings:useThisSettingWhenAnAgent")}</p>
<p>
<Trans i18nKey="settings:mcpCreateTaskScope">
<code className="rounded-sm bg-muted px-1 py-0.5 text-foreground">
create_task_kandev
</code>{" "}
creates new tasks and subtasks. This setting applies only when the call omits{" "}
<code className="rounded-sm bg-muted px-1 py-0.5 text-foreground">agent_profile_id</code>.
</Trans>
</p>
<p>
<Trans i18nKey="settings:mcpSpawnSessionScope">
<code className="rounded-sm bg-muted px-1 py-0.5 text-foreground">
spawn_session_kandev
</code>{" "}
and tasks you create yourself are not affected. An explicitly selected profile always
wins.
</Trans>
</p>
<p>
<Trans i18nKey="settings:mcpAffectedToolsHelp">
<code>create_task_kandev</code> creates a separate task. <code>spawn_session_kandev</code>{" "}
adds a session to the current task, so it does not use this preference.
</Trans>
</p>
{OPTIONS.map((option) => (
<p key={option.value}>
<strong>{t(option.labelKey)}: </strong>
{t(option.descriptionKey)}
</p>
))}
</div>
);
}
function MCPTaskProfileRadioGroup({
value,
isDirty,
onValueChange,
ariaDescribedBy,
}: {
value: MCPTaskAgentProfileDefault;
isDirty: boolean;
onValueChange: (value: MCPTaskAgentProfileDefault) => void;
ariaDescribedBy?: string;
}) {
const { t } = useTranslation();
return (
<RadioGroup
aria-label={t(PROFILE_LABEL_KEY)}
aria-describedby={ariaDescribedBy}
value={value}
onValueChange={(nextValue) => onValueChange(nextValue as MCPTaskAgentProfileDefault)}
data-settings-dirty={isDirty}
className="gap-3"
>
{OPTIONS.map((option) => {
const labelId = `mcp-task-profile-${option.value}-label`;
const descriptionId = `mcp-task-profile-${option.value}-description`;
return (
<Label
key={option.value}
htmlFor={`mcp-task-profile-${option.value}`}
className="flex min-h-11 w-full min-w-0 cursor-pointer items-start gap-3 rounded-md border p-3 hover:bg-muted/30"
>
<RadioGroupItem
id={`mcp-task-profile-${option.value}`}
value={option.value}
aria-labelledby={labelId}
aria-describedby={descriptionId}
className="mt-0.5"
/>
<span className="min-w-0 space-y-1">
<span id={labelId} className="block text-sm font-medium">
{t(option.labelKey)}
</span>
<span
id={descriptionId}
className="block whitespace-normal break-words text-xs text-muted-foreground"
>
{t(
option.value === "current_task"
? "settings:profileSessionShort"
: "settings:profileWorkspaceShort",
)}
</span>
</span>
</Label>
);
})}
</RadioGroup>
);
}
export function MCPTaskAgentProfileDefaultSettings({
presentation = "card",
}: {
presentation?: SettingsPresentation;
}) {
const { t } = useTranslation();
const preference = useAppStore((state) => state.userSettings.mcpTaskAgentProfileDefault);
const setUserSettings = useAppStore((state) => state.setUserSettings);
const storeApi = useAppStoreApi();
const [saved, setSaved] = useState(preference);
const [draft, setDraft] = useState(preference);
const draftRef = useRef(draft);
draftRef.current = draft;
const isDirty = draft !== saved;
useEffect(() => {
setSaved((previous) => {
if (draftRef.current === previous) setDraft(preference);
return preference;
});
}, [preference]);
useSettingsSaveContributor({
id: "general-mcp-task-agent-profile-default",
order: 10,
revision: draft,
isDirty,
save: async (revision) => {
const submitted = revision as MCPTaskAgentProfileDefault;
await updateUserSettings({ mcp_task_agent_profile_default: submitted });
setSaved(submitted);
setUserSettings({
...storeApi.getState().userSettings,
mcpTaskAgentProfileDefault: submitted,
});
},
discard: () => setDraft(saved),
});
if (presentation === "row") {
return (
<SettingsRow
label={t(PROFILE_LABEL_KEY)}
description={t("settings:agentProfileShort")}
info={
<SettingsInfo label={t(PROFILE_LABEL_KEY)}>
<MCPTaskProfileScopeDescription />
</SettingsInfo>
}
descriptionId="mcp-task-profile-description"
discoveryTargetId={GENERAL_SETTINGS_TARGETS.agentTaskProfile}
isDirty={isDirty}
control={
<MCPTaskProfileRadioGroup
value={draft}
isDirty={isDirty}
onValueChange={setDraft}
ariaDescribedBy="mcp-task-profile-description"
/>
}
/>
);
}
return (
<SettingsCard
isDirty={isDirty}
discoveryTargetId={GENERAL_SETTINGS_TARGETS.agentTaskProfile}
data-testid="mcp-task-profile-default-card"
>
<CardHeader>
<CardTitle className="text-base">
<h3>{t(PROFILE_LABEL_KEY)}</h3>
</CardTitle>
<SettingsInfo label={t(PROFILE_LABEL_KEY)}>
<MCPTaskProfileScopeDescription />
</SettingsInfo>
</CardHeader>
<CardContent>
<MCPTaskProfileRadioGroup value={draft} isDirty={isDirty} onValueChange={setDraft} />
</CardContent>
</SettingsCard>
);
}