Files
kandev/apps/web/components/settings/mcp-strategy-select.tsx
T
2026-09-04 12:16:13 +00:00

124 lines
4.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Label } from "@kandev/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { listMCPStrategies } from "@/lib/api/domains/settings-api";
import type { MCPStrategyOption } from "@/lib/types/http-agents";
/**
* Radix Select cannot hold an empty-string value, so the "off" choice needs a
* sentinel. Callers convert between it and "" at the API boundary via
* {@link toStrategyValue} / {@link fromStrategyValue}. It is a control value,
* never displayed and never translated.
*/
export const MCP_STRATEGY_NONE = "__none__";
/** Maps a stored strategy key ("" = off) to the Select's value. */
export function toStrategyValue(strategy: string | undefined): string {
return strategy ? strategy : MCP_STRATEGY_NONE;
}
/** Maps the Select's value back to a stored strategy key ("" = off). */
export function fromStrategyValue(value: string): string {
return value === MCP_STRATEGY_NONE ? "" : value;
}
/**
* Loads the selectable MCP strategies from the backend.
*
* The list is fetched rather than hardcoded so a strategy added in Go becomes
* selectable without a matching frontend edit — and so an option can never be
* offered that the backend would reject on save.
*/
export function useMCPStrategies(enabled: boolean) {
const [strategies, setStrategies] = useState<MCPStrategyOption[]>([]);
useEffect(() => {
if (!enabled) return;
let active = true;
listMCPStrategies()
.then((options) => {
if (active) setStrategies(options);
})
.catch(() => {
// Non-fatal: the picker falls back to the "off" choice only. Creating
// an agent without MCP still works, which is the pre-existing behavior.
if (active) setStrategies([]);
});
return () => {
active = false;
};
}, [enabled]);
return strategies;
}
type MCPStrategySelectProps = {
id: string;
/** Stored strategy key; "" or undefined means no injection. */
value: string | undefined;
/** Receives the stored strategy key ("" when the user picks "off"). */
onChange: (strategy: string) => void;
strategies: MCPStrategyOption[];
disabled?: boolean;
};
/**
* Picks how kandev injects its per-session MCP server into a custom TUI agent's
* wrapped CLI.
*
* This is an explicit choice rather than something inferred from the command,
* because the command is arbitrary (`zsh -ic "fuelclaude --model opus"` is a
* real example) and a wrong guess fails silently: kandev writes a config file,
* the CLI ignores it, and no error surfaces anywhere.
*/
export function MCPStrategySelect({
id,
value,
onChange,
strategies,
disabled,
}: MCPStrategySelectProps) {
const { t } = useTranslation();
return (
<div className="space-y-2">
<Label htmlFor={id}>{t("agents:mcpStrategy")}</Label>
<Select
value={toStrategyValue(value)}
onValueChange={(next) => onChange(fromStrategyValue(next))}
disabled={disabled}
>
<SelectTrigger
id={id}
className="w-full min-w-0 cursor-pointer [@media(pointer:coarse)]:min-h-11"
data-testid="mcp-strategy-select"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
value={MCP_STRATEGY_NONE}
className="cursor-pointer [@media(pointer:coarse)]:min-h-11"
>
{t("agents:mcpStrategyNone")}
</SelectItem>
{strategies.map((option) => (
<SelectItem
key={option.key}
value={option.key}
description={option.description}
className="cursor-pointer [@media(pointer:coarse)]:min-h-11"
>
{option.key}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">{t("agents:mcpStrategyHelp")}</p>
</div>
);
}