f09e938673
Migrates the eleven remaining files that `i18next/no-literal-string` flagged outside `app/office`, `app/demo` and the task-create surfaces owned by #2326: the session prepare panel, the onboarding agents step, the host system-metrics readout, the system-health issues dialog, the shared panel search bar, the mermaid diagram block, the settings agent card, both release-notes surfaces, and the shared data-table pair. Findings drop 884 -> 826 (-58) and the guard allowlist grows 442 -> 453, file by file rather than by directory since every one of these directories still holds un-migrated files. Defects repaired beyond the codemod's output: - `data-table-pagination.tsx` rendered `{count} results` as a bare JSX fragment, invisible to both plural gates. It is now one interpolated key with `_one`/`_other`, as are the health-dialog issue count and the release-notes "N new releases" helper. - Two mixed literal+expression blocks were half-converted, rendering identically in English while being structurally untranslatable. Both were rebuilt as a single message. - The codemod re-sorted `settings.json` into `localeCompare` order and re-encoded every non-ASCII character to `\uXXXX` under a comment claiming it matched the existing files. No catalog is stored escaped and only `common` is in that order, so both halves were reverted; all four catalogs are additions-only. - Copy in default parameters and plain helpers is invisible to the lint rule, so `Search…`, the six prepare-panel header labels, the setup-script step name and error, the metric labels, `lastUpdatedText` and `buildDescription` were migrated by hand. `task:preparingEnvironment` was already taken by #2326 holding "Preparing environment" without the ellipsis this surface renders, so the prepare panel uses `preparingEnvironmentEllipsis` rather than silently changing its copy. Units, wire ids, the `{workspaceId}` URL token, the `"ellipsis"` page sentinel and every mermaid theme/CSS value stay English as protocol.
306 lines
10 KiB
TypeScript
306 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import {
|
|
IconAlertTriangle,
|
|
IconCheck,
|
|
IconClipboard,
|
|
IconChevronDown,
|
|
IconLoader2,
|
|
IconDownload,
|
|
IconExternalLink,
|
|
IconLock,
|
|
} from "@tabler/icons-react";
|
|
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@kandev/ui/collapsible";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
|
import { AgentLogo } from "@/components/agent-logo";
|
|
import { ProfileFormFields, type ProfileFormData } from "@/components/settings/profile-form-fields";
|
|
import type { AvailableAgent, ToolStatus } from "@/lib/types/http";
|
|
import { copyToClipboard } from "@/lib/utils/copy-to-clipboard";
|
|
import { Trans, useTranslation } from "react-i18next";
|
|
|
|
export type AgentSetting = {
|
|
profileId: string;
|
|
formData: ProfileFormData;
|
|
dirty: boolean;
|
|
};
|
|
|
|
function CopyButton({ text }: { text: string }) {
|
|
const [copied, setCopied] = useState(false);
|
|
const handleCopy = useCallback(async () => {
|
|
if (await copyToClipboard(text)) {
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
}
|
|
}, [text]);
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
className="p-0.5 cursor-pointer hover:text-foreground text-muted-foreground transition-colors shrink-0"
|
|
>
|
|
{copied ? (
|
|
<IconCheck className="h-3 w-3 text-green-500" />
|
|
) : (
|
|
<IconClipboard className="h-3 w-3" />
|
|
)}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function StatusPill({ status, error }: { status: string; error?: string }) {
|
|
const { t } = useTranslation();
|
|
switch (status) {
|
|
case "auth_required": {
|
|
const pill = (
|
|
<span className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
|
<IconLock className="h-3.5 w-3.5" />
|
|
{t("common:noAuth")}
|
|
</span>
|
|
);
|
|
if (!error) return pill;
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>{pill}</TooltipTrigger>
|
|
<TooltipContent className="max-w-sm">{error}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
case "not_installed":
|
|
return (
|
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
{t("common:notInstalled")}
|
|
</span>
|
|
);
|
|
case "failed": {
|
|
const pill = (
|
|
<span className="flex items-center gap-1 text-xs text-red-600 dark:text-red-400">
|
|
<IconAlertTriangle className="h-3.5 w-3.5" />
|
|
{t("common:error")}
|
|
</span>
|
|
);
|
|
if (!error) return pill;
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>{pill}</TooltipTrigger>
|
|
<TooltipContent className="max-w-sm">{error}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
case "probing":
|
|
return (
|
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<IconLoader2 className="h-3.5 w-3.5 animate-spin" />
|
|
{t("common:probing")}
|
|
</span>
|
|
);
|
|
case "not_configured":
|
|
return (
|
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
{t("common:pending")}
|
|
</span>
|
|
);
|
|
default:
|
|
return (
|
|
<span className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
|
<IconCheck className="h-3.5 w-3.5" />
|
|
{t("common:installed")}
|
|
</span>
|
|
);
|
|
}
|
|
}
|
|
|
|
function InstalledAgentRow({
|
|
agent,
|
|
settings,
|
|
isOpen,
|
|
onToggle,
|
|
onUpdateSetting,
|
|
}: {
|
|
agent: AvailableAgent;
|
|
settings: AgentSetting | undefined;
|
|
isOpen: boolean;
|
|
onToggle: (open: boolean) => void;
|
|
onUpdateSetting: (agentName: string, formPatch: Partial<ProfileFormData>) => void;
|
|
}) {
|
|
const currentModel = settings?.formData.model || agent.model_config.default_model;
|
|
const modelName =
|
|
agent.model_config.available_models.find((m) => m.id === currentModel)?.name ?? currentModel;
|
|
const status = agent.model_config.status ?? "ok";
|
|
const showModelPill = status === "ok" && !!modelName;
|
|
|
|
return (
|
|
<Collapsible open={isOpen} onOpenChange={onToggle}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex w-full items-center gap-3 rounded-lg border p-2 text-left cursor-pointer hover:bg-muted/50 transition-colors group"
|
|
>
|
|
<AgentLogo agentName={agent.name} size={28} className="shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{agent.display_name}</p>
|
|
</div>
|
|
{showModelPill && (
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium truncate max-w-[120px]">
|
|
{modelName}
|
|
</span>
|
|
)}
|
|
<StatusPill status={status} error={agent.model_config.error} />
|
|
<IconChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="border border-t-0 rounded-b-lg px-3 pb-3 pt-2">
|
|
{settings && (
|
|
<ProfileFormFields
|
|
variant="compact"
|
|
hideNameField
|
|
hideCustomCLIFlags
|
|
profile={settings.formData}
|
|
onChange={(patch) => onUpdateSetting(agent.name, patch)}
|
|
modelConfig={agent.model_config}
|
|
permissionSettings={agent.permission_settings ?? {}}
|
|
passthroughConfig={agent.passthrough_config ?? null}
|
|
agentName={agent.name}
|
|
/>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
}
|
|
|
|
function NotInstalledItems({
|
|
agents,
|
|
tools,
|
|
showLabel,
|
|
}: {
|
|
agents: AvailableAgent[];
|
|
tools: ToolStatus[];
|
|
showLabel: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
if (agents.length === 0 && tools.length === 0) return null;
|
|
return (
|
|
<>
|
|
{showLabel && (
|
|
<p className="text-xs text-muted-foreground mt-1">{t("common:notYetInstalled")}</p>
|
|
)}
|
|
{agents.map((agent) => (
|
|
<div
|
|
key={agent.name}
|
|
className="flex w-full items-center gap-3 rounded-lg border border-dashed p-2"
|
|
>
|
|
<AgentLogo agentName={agent.name} size={28} className="opacity-60 shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-muted-foreground truncate">
|
|
{agent.display_name}
|
|
</p>
|
|
{agent.install_script && (
|
|
<div className="flex items-center gap-1 mt-0.5 min-w-0">
|
|
<code className="text-[10px] text-muted-foreground font-mono truncate min-w-0">
|
|
{agent.install_script}
|
|
</code>
|
|
<CopyButton text={agent.install_script} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<IconDownload className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
</div>
|
|
))}
|
|
{tools.map((tool) => (
|
|
<div
|
|
key={tool.name}
|
|
className="flex w-full items-center gap-3 rounded-lg border border-dashed p-2"
|
|
>
|
|
<IconDownload className="h-7 w-7 p-1 text-muted-foreground opacity-60 shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-muted-foreground truncate">
|
|
{tool.display_name}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground break-words">{tool.description}</p>
|
|
{tool.install_script && (
|
|
<div className="flex items-center gap-1 mt-0.5 min-w-0">
|
|
<code className="text-[10px] text-muted-foreground font-mono truncate min-w-0">
|
|
{tool.install_script}
|
|
</code>
|
|
<CopyButton text={tool.install_script} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
{tool.info_url && (
|
|
<a
|
|
href={tool.info_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-muted-foreground hover:text-foreground shrink-0 cursor-pointer"
|
|
>
|
|
<IconExternalLink className="h-3.5 w-3.5" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function StepAgents({
|
|
availableAgents,
|
|
tools,
|
|
agentSettings,
|
|
loading,
|
|
onUpdateSetting,
|
|
}: {
|
|
availableAgents: AvailableAgent[];
|
|
tools: ToolStatus[];
|
|
agentSettings: Record<string, AgentSetting>;
|
|
loading: boolean;
|
|
onUpdateSetting: (agentName: string, formPatch: Partial<ProfileFormData>) => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [openAgent, setOpenAgent] = useState<string | null>(null);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-full gap-3 text-sm text-muted-foreground">
|
|
<IconLoader2 className="h-6 w-6 animate-spin" />
|
|
{t("common:discoveringAgents")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const installedAgents = availableAgents.filter((a) => a.available);
|
|
const notInstalledAgents = availableAgents.filter((a) => !a.available && a.install_script);
|
|
const notInstalledTools = tools.filter((t) => !t.available);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-1 gap-2 max-h-[320px] overflow-y-auto">
|
|
{installedAgents.map((agent) => (
|
|
<InstalledAgentRow
|
|
key={agent.name}
|
|
agent={agent}
|
|
settings={agentSettings[agent.name]}
|
|
isOpen={openAgent === agent.name}
|
|
onToggle={(isOpen) => setOpenAgent(isOpen ? agent.name : null)}
|
|
onUpdateSetting={onUpdateSetting}
|
|
/>
|
|
))}
|
|
<NotInstalledItems
|
|
agents={notInstalledAgents}
|
|
tools={notInstalledTools}
|
|
showLabel={installedAgents.length > 0}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">{t("common:onboardingAgentsHelp")}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
<Trans i18nKey="common:onboardingAgentsAutoApproveWarning">
|
|
<span className="text-yellow-500 font-medium">Careful:</span> The default Agent Profiles
|
|
run with Auto Approve enabled (YOLO mode).
|
|
</Trans>
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|