i18n(skills): localise SkillsSection strings

SkillsSection had nine hardcoded English strings (header, filter
placeholder/aria-label, empty-state copy, button labels). Move them to
skills.* keys in en.ts/zh.ts and swap the component to useT().
This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 23:35:21 -04:00
1 parent 2bd47a601a
commit d513497341
5 files changed
+51 -524

No files matched your search

@@ -1,12 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Robot } from "@phosphor-icons/react";
import { DEPARTMENT_OPTIONS } from "@shared/constants";
import type { AgentRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
import { isConnectorExecutionReady } from "@shared/connector-policy";
import { unwrap } from "../../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "../ui";
import { useT } from "../../i18n";
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
import { AgentForm } from "./agent-form";
export function AgentCreator({
companyId,
@@ -21,188 +14,14 @@ export function AgentCreator({
agents: AgentRecord[];
onSaved: () => Promise<void>;
}) {
const executionReadyConnectors = useMemo(
() => connectors.filter((connector) => isConnectorExecutionReady(connector)),
[connectors],
);
const [name, setName] = useState("");
const [role, setRole] = useState("");
const [title, setTitle] = useState("");
const [department, setDepartment] = useState<DepartmentId | "">("");
const [connectorId, setConnectorId] = useState(executionReadyConnectors[0]?.id ?? "");
const [model, setModel] = useState("");
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
const [workspaceId, setWorkspaceId] = useState(workspaces[0]?.id ?? "");
const [budget, setBudget] = useState("120");
const [capabilities, setCapabilities] = useState("");
const [reportsTo, setReportsTo] = useState("");
const [thinkingEffort, setThinkingEffort] = useState("");
const [requestError, setRequestError] = useState<string | null>(null);
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
const selectedConnector = executionReadyConnectors.find((connector) => connector.id === connectorId) ?? null;
useEffect(() => {
if (!connectorId) {
setAvailableModels([]);
setModel("");
return;
}
void (async () => {
const result = await window.agentCompany.listConnectorModels(connectorId);
if (result.ok) {
setAvailableModels(result.data);
setModel(result.data[0]?.id ?? "");
}
})();
if (connectorId !== "claude_local" && connectorId !== "codex_local") {
setThinkingEffort("");
}
}, [connectorId]);
useEffect(() => {
if (!connectorId && executionReadyConnectors[0]) {
setConnectorId(executionReadyConnectors[0].id);
return;
}
if (connectorId && !executionReadyConnectors.some((connector) => connector.id === connectorId)) {
setConnectorId(executionReadyConnectors[0]?.id ?? "");
}
}, [executionReadyConnectors, connectorId]);
const t = useT();
const [showAdvanced, setShowAdvanced] = useState(false);
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
return (
<div className="grid min-w-0 gap-4">
<Input label={t("org.agentName")} value={name} onChange={setName} placeholder={t("org.agentName")} hint="Display name shown in org chart and messages" />
<Input label={t("agent.role")} value={role} onChange={setRole} placeholder="e.g. Engineer, PM, CEO" hint="Determines which tasks the agent can handle" />
<Input label={t("agent.title")} value={title} onChange={setTitle} placeholder={t("agent.title")} hint="Job title shown under the name" />
<Select
label={t("org.connector")}
value={connectorId}
onChange={(value) => setConnectorId(value as ConnectorRecord["id"])}
options={executionReadyConnectors.map((connector) => ({ value: connector.id, label: connector.id.startsWith("claude_compat") && connector.notes ? connector.notes : connector.label }))}
/>
{executionReadyConnectors.length === 0 ? (
<p className="text-[12px] text-warning">
{t("org.noConnectorAvailable")}
</p>
) : null}
{availableModels.length > 0 ? (
<Select
label={t("org.model")}
value={model}
onChange={setModel}
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
/>
) : null}
{supportsEffort ? (
<Select
label={t("org.thinkingEffort")}
value={thinkingEffort}
onChange={setThinkingEffort}
options={[
{ value: "", label: t("org.default") },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
]}
/>
) : null}
<button type="button" onClick={() => setShowAdvanced(!showAdvanced)} className="flex items-center gap-1.5 text-[12px] font-medium text-muted-foreground transition hover:text-foreground">
<svg width="10" height="10" viewBox="0 0 10 10" className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}><path d="M3 1l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" /></svg>
{t("org.advancedSettings")}
</button>
<AnimatePresence>
{showAdvanced ? (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }} className="grid gap-4 overflow-hidden">
<Select
label={t("comm.department")}
value={department}
onChange={(value) => setDepartment(value as DepartmentId | "")}
options={DEPARTMENT_OPTIONS.map((option) => ({
value: option.value,
label: option.value === "" ? t("comm.selectDepartment") : option.label,
}))}
/>
<Select
label={t("agent.workspace")}
value={workspaceId}
onChange={setWorkspaceId}
options={[
{ value: "", label: t("agent.noWorkspace") },
...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name })),
]}
/>
<Select
label={t("agent.reportsTo")}
value={reportsTo}
onChange={setReportsTo}
options={[
{ value: "", label: t("agent.noManager") },
...agents.map((a) => ({ value: a.id, label: a.name })),
]}
/>
<Input label={t("org.monthlyBudget")} value={budget} onChange={setBudget} placeholder="120" type="number" hint="Spending cap per month; agent pauses when exceeded" />
<TextArea
label={t("agent.capabilities")}
value={capabilities}
onChange={setCapabilities}
placeholder="What this agent does"
hint="Skills and domain knowledge injected into the agent's system prompt"
/>
</motion.div>
) : null}
</AnimatePresence>
<p className="text-[12px] text-muted-foreground">
{t("org.hireRequestNote")}
</p>
<ActionButton
label={t("org.submitHireRequest")}
tone="accent"
onClick={() =>
void (async () => {
try {
setRequestError(null);
if (!selectedConnector) {
setRequestError(t("org.selectConnectorFirst"));
return;
}
const meta: Record<string, string> = {};
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
unwrap(await window.agentCompany.requestHire({
companyId,
requestedByAgentId: null,
name: name.trim(),
role: role.trim(),
title: title.trim(),
department: selectedDepartment,
connectorId: connectorId as ConnectorRecord["id"],
workspaceId: workspaceId || null,
reportsTo: reportsTo || null,
model: model || null,
metadataJson: JSON.stringify(meta),
capabilities: capabilities.trim(),
budgetMonthlyUsd: Number(budget || 0),
}));
setName("");
setRole("");
setTitle("");
setDepartment("");
setCapabilities("");
setReportsTo("");
setRequestError(null);
await onSaved();
} catch (error) {
setRequestError(error instanceof Error ? error.message : t("org.hireRequestFailed"));
}
})()
}
icon={Robot}
disabled={!selectedConnector}
/>
{requestError ? <p className="text-[12px] text-destructive">{requestError}</p> : null}
</div>
<AgentForm
mode="create"
companyId={companyId}
connectors={connectors}
workspaces={workspaces}
agents={agents}
onSaved={onSaved}
/>
);
}
@@ -1,16 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Pencil, Trash } from "@phosphor-icons/react";
import { DEPARTMENT_OPTIONS } from "@shared/constants";
import type { AgentRecord, BudgetPolicyRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import { unwrap } from "../../lib/desktop";
import { safeJson } from "../../lib/safe-json";
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
import { useAppStore } from "../../state/store";
import { useT } from "../../i18n";
import { ActionButton, Input, Select, TextArea } from "../ui";
import { SkillAgentToggles } from "../skills/SkillAgentToggles";
import { ConfirmDelete, HeartbeatSection } from "./helpers";
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
import { AgentForm } from "./agent-form";
export function AgentEditor({
agent,
@@ -29,317 +18,16 @@ export function AgentEditor({
onSaved: () => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const t = useT();
const [name, setName] = useState(agent.name);
const [role, setRole] = useState(agent.role);
const [title, setTitle] = useState(agent.title);
const [department, setDepartment] = useState<DepartmentId | "">(agent.department ?? "");
const [status, setStatus] = useState<AgentRecord["status"]>(agent.status);
const [connectorId, setConnectorId] = useState<ConnectorRecord["id"]>(agent.connectorId);
const [model, setModel] = useState(agent.model ?? "");
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
const [workspaceId, setWorkspaceId] = useState(agent.workspaceId ?? "");
const [budget, setBudget] = useState(String(agent.budgetMonthlyUsd));
const [capabilities, setCapabilities] = useState(agent.capabilities);
const [reportsTo, setReportsTo] = useState(agent.reportsTo ?? "");
const [confirmingDelete, setConfirmingDelete] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const existingMeta = useMemo(
() => safeJson<Record<string, unknown>>(agent.metadataJson, {}),
[agent.metadataJson],
);
const [thinkingEffort, setThinkingEffort] = useState<string>(
typeof existingMeta.thinkingEffort === "string" ? existingMeta.thinkingEffort : "",
);
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
const pendingApproval = agent.status === "pending_approval";
const executionReadyConnectors = useMemo(
() => connectors.filter((connector) => isConnectorExecutionReady(connector)),
[connectors],
);
const selectedConnectorRecord = connectors.find((connector) => connector.id === connectorId) ?? null;
const selectedConnectorIssue = selectedConnectorRecord ? getConnectorExecutionReadinessIssue(selectedConnectorRecord) : "Select a connector.";
const persistedConnectorRecord = connectors.find((connector) => connector.id === agent.connectorId) ?? null;
const persistedConnectorIssue = persistedConnectorRecord ? getConnectorExecutionReadinessIssue(persistedConnectorRecord) : "Current connector is unavailable.";
const connectorOptions = useMemo(() => {
const getDisplayLabel = (c: typeof connectors[number]) =>
c.id.startsWith("claude_compat") && c.notes ? c.notes : c.label;
const readyOptions = executionReadyConnectors.map((connector) => ({ value: connector.id, label: getDisplayLabel(connector) }));
if (!selectedConnectorRecord || isConnectorExecutionReady(selectedConnectorRecord)) {
return readyOptions;
}
return [
{ value: selectedConnectorRecord.id, label: `${getDisplayLabel(selectedConnectorRecord)} (current, blocked)` },
...readyOptions.filter((option) => option.value !== selectedConnectorRecord.id),
];
}, [executionReadyConnectors, selectedConnectorRecord]);
useEffect(() => {
if (connectorId.startsWith("claude_compat")) {
// Parse comma-separated user-configured models
const compat = connectors.find((c) => c.id === connectorId);
const raw = compat?.model ?? "";
const models = raw.split(",").map((m) => m.trim()).filter(Boolean).map((m) => ({ id: m, label: m }));
setAvailableModels(models);
if (connectorId !== agent.connectorId && models.length > 0) {
setModel(models[0].id);
}
} else {
void (async () => {
const result = await window.agentCompany.listConnectorModels(connectorId);
if (result.ok) {
setAvailableModels(result.data);
if (connectorId !== agent.connectorId) {
setModel(result.data[0]?.id ?? "");
}
}
})();
}
if (connectorId !== "claude_local" && !connectorId.startsWith("claude_compat") && connectorId !== "codex_local") {
setThinkingEffort("");
}
}, [connectorId, agent.connectorId, connectors]);
const skills = useAppStore((s) => s.skills);
const setSkills = useAppStore((s) => s.setSkills);
useEffect(() => {
if (skills.length === 0 && companyId) {
window.agentCompany.listSkills(companyId).then((result) => {
if (result.ok) setSkills(result.data);
});
}
}, [companyId, skills.length, setSkills]);
// Load agent's budget policy (D-11: read from budget_policies)
const [agentPolicy, setAgentPolicy] = useState<BudgetPolicyRecord | null>(null);
useEffect(() => {
void (async () => {
const result = await window.agentCompany.listBudgetPolicies(companyId);
if (result.ok) {
const policy = result.data
.filter((p) => p.scopeType === "agent" && p.scopeId === agent.id)
.sort((left, right) => {
if (left.isActive !== right.isActive) return left.isActive ? -1 : 1;
return right.updatedAt.localeCompare(left.updatedAt);
})[0];
if (policy) {
setAgentPolicy(policy);
setBudget(getBudgetPolicyAmountDollars(policy));
}
}
})();
}, [companyId, agent.id]);
const managerCandidates = agents.filter((a) => a.id !== agent.id);
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
async function persistAgentDraft() {
const meta = { ...existingMeta };
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
else delete meta.thinkingEffort;
const budgetDollars = Number(budget || 0);
unwrap(await window.agentCompany.saveAgent({
id: agent.id,
companyId,
name: name.trim(),
role: role.trim(),
title: title.trim(),
department: selectedDepartment,
status,
connectorId,
workspaceId: workspaceId || null,
reportsTo: reportsTo || null,
model: model || null,
metadataJson: JSON.stringify(meta),
budgetMonthlyUsd: budgetDollars,
capabilities: capabilities.trim(),
}));
if (budgetDollars > 0 && agent.id) {
const payload = buildBudgetPolicyInput({
policyId: agentPolicy?.id ?? null,
companyId,
scopeType: "agent",
scopeId: agent.id,
amountDollars: String(budgetDollars),
warnPercent: String(agentPolicy?.warnPercent ?? 80),
hardStopEnabled: agentPolicy?.hardStopEnabled ?? true,
notifyEnabled: agentPolicy?.notifyEnabled ?? true,
});
if (!payload) {
throw new Error("Agent budget policy is invalid.");
}
unwrap(await window.agentCompany.saveBudgetPolicy(payload));
}
}
async function handleWakeNow() {
setSaveError(null);
try {
await persistAgentDraft();
unwrap(await window.agentCompany.triggerHeartbeat({ agentId: agent.id, companyId, trigger: "manual" }));
await onSaved();
} catch (error) {
setSaveError(error instanceof Error ? error.message : "Failed to wake agent.");
}
}
return (
<div className="grid min-w-0 gap-4">
<Input label={t("agent.name")} value={name} onChange={setName} placeholder={t("agent.name")} readOnly={pendingApproval} disabled={pendingApproval} />
<Input label={t("agent.role")} value={role} onChange={setRole} placeholder={t("agent.role")} readOnly={pendingApproval} disabled={pendingApproval} />
<Input label={t("agent.title")} value={title} onChange={setTitle} placeholder={t("agent.title")} readOnly={pendingApproval} disabled={pendingApproval} />
<Select
label={t("comm.department")}
value={department}
onChange={(value) => setDepartment(value as DepartmentId | "")}
disabled={pendingApproval}
options={DEPARTMENT_OPTIONS.map((option) => ({
value: option.value,
label: option.value === "" ? t("comm.selectDepartment") : option.label,
}))}
/>
<Select
label={t("agent.status")}
value={status}
onChange={(v) => setStatus(v as AgentRecord["status"])}
disabled={pendingApproval}
options={pendingApproval
? [{ value: "pending_approval", label: t("agent.pendingApproval") }]
: [
{ value: "idle", label: "Idle" },
{ value: "active", label: "Active" },
{ value: "running", label: "Running" },
{ value: "paused", label: "Paused" },
{ value: "error", label: "Error" },
{ value: "terminated", label: "Terminated" },
]}
/>
{pendingApproval ? (
<p className="text-[12px] text-muted-foreground">
{t("agent.pendingHireNote")}
</p>
) : null}
<Select
label={t("agent.connector")}
value={connectorId}
onChange={(v) => setConnectorId(v as ConnectorRecord["id"])}
disabled={pendingApproval}
options={connectorOptions}
/>
{selectedConnectorIssue ? (
<p className="text-[12px] text-warning">
{selectedConnectorIssue}
</p>
) : null}
{availableModels.length > 0 ? (
<Select
label={t("agent.model")}
value={model}
onChange={setModel}
disabled={pendingApproval}
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
/>
) : null}
{supportsEffort ? (
<Select
label={t("agent.thinkingEffort")}
value={thinkingEffort}
onChange={setThinkingEffort}
disabled={pendingApproval}
options={[
{ value: "", label: "Default" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
]}
/>
) : null}
<Select
label={t("agent.workspace")}
value={workspaceId}
onChange={setWorkspaceId}
disabled={pendingApproval}
options={[
{ value: "", label: t("agent.noWorkspace") },
...workspaces.map((w) => ({ value: w.id, label: w.name })),
]}
/>
<Select
label={t("agent.reportsTo")}
value={reportsTo}
onChange={setReportsTo}
disabled={pendingApproval}
options={[
{ value: "", label: t("agent.noManager") },
...managerCandidates.map((a) => ({ value: a.id, label: a.name })),
]}
/>
<Input label={t("agent.budget")} value={budget} onChange={setBudget} placeholder="120" type="number" readOnly={pendingApproval} disabled={pendingApproval} />
<TextArea label={t("agent.capabilities")} value={capabilities} onChange={setCapabilities} placeholder={t("agent.capabilities")} readOnly={pendingApproval} disabled={pendingApproval} />
<HeartbeatSection
agent={agent}
companyId={companyId}
pendingApproval={pendingApproval}
persistedConnectorIssue={selectedConnectorIssue ?? persistedConnectorIssue}
onSaved={onSaved}
onWakeNow={() => void handleWakeNow()}
/>
{/* Skills section (D-04 dual access) */}
<div className="mt-6 border-t border-border pt-4">
<h3 className="text-[13px] font-semibold uppercase tracking-[0.08em] text-primary mb-3">{t("agent.skills")}</h3>
<SkillAgentToggles mode="agent" companyId={companyId} agentId={agent.id} skills={skills} />
</div>
<div className="flex items-center gap-3">
<ActionButton
label={t("agent.saveChanges")}
tone="accent"
icon={Pencil}
disabled={pendingApproval || !!selectedConnectorIssue}
onClick={() =>
void (async () => {
setSaveError(null);
try {
await persistAgentDraft();
await onSaved();
} catch (error) {
setSaveError(error instanceof Error ? error.message : "Failed to save agent.");
}
})()
}
/>
{confirmingDelete ? (
<ConfirmDelete
entityLabel={agent.name}
onConfirm={() =>
void (async () => {
await window.agentCompany.deleteAgent({ id: agent.id, companyId });
await onDeleted();
})()
}
onCancel={() => setConfirmingDelete(false)}
/>
) : (
<ActionButton
label="Delete"
tone="danger"
icon={Trash}
disabled={pendingApproval}
onClick={() => setConfirmingDelete(true)}
/>
)}
</div>
{pendingApproval ? (
<p className="text-[12px] text-muted-foreground">
Pending hires are read-only here. Use Hiring or Approvals to approve or reject the request before editing the agent profile.
</p>
) : null}
{saveError ? <p className="text-[12px] text-destructive">{saveError}</p> : null}
</div>
<AgentForm
mode="edit"
companyId={companyId}
connectors={connectors}
workspaces={workspaces}
agents={agents}
agent={agent}
onSaved={onSaved}
onDeleted={onDeleted}
/>
);
}
@@ -9,6 +9,7 @@ import { ActionButton, EmptyState } from "../ui";
import { SkillList } from "./SkillList";
import { SkillImportModal } from "./SkillImportModal";
import { SkillDetail } from "./SkillDetail";
import { useT } from "../../i18n";
export function SkillsSection({
companyId,
@@ -19,6 +20,7 @@ export function SkillsSection({
agents: AgentRecord[];
onRefresh: () => void;
}) {
const t = useT();
const skills = useAppStore((s) => s.skills);
const setSkills = useAppStore((s) => s.setSkills);
const selectedSkillId = useAppStore((s) => s.selectedSkillId);
@@ -64,7 +66,7 @@ export function SkillsSection({
{/* Header */}
<div className="flex shrink-0 items-center gap-3 px-4 py-3">
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-foreground">
Skills
{t("skills.title")}
</h2>
{skills.length > 0 && (
<span className="inline-flex h-[20px] min-w-[20px] items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground">
@@ -75,7 +77,7 @@ export function SkillsSection({
type="button"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ml-auto flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-muted hover:text-foreground"
onClick={() => setSkillImportModalOpen(true)}
aria-label="Import skill"
aria-label={t("skills.importAria")}
>
<Plus size={16} weight="bold" />
</button>
@@ -93,8 +95,8 @@ export function SkillsSection({
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border border-border bg-card py-2 pl-8 pr-3 text-[13px] text-foreground placeholder:text-muted-foreground"
value={skillFilter}
onChange={(e) => setSkillFilter(e.target.value)}
placeholder="Filter skills..."
aria-label="Filter skills"
placeholder={t("skills.filterPlaceholder")}
aria-label={t("skills.filterAria")}
/>
</div>
</div>
@@ -103,11 +105,11 @@ export function SkillsSection({
<div className="flex-1 overflow-y-auto">
{skills.length === 0 ? (
<EmptyState
title="No skills imported"
detail="Import skills from GitHub, a local path, or skills.sh to get started."
title={t("skills.emptyTitle")}
detail={t("skills.emptyDetail")}
action={
<ActionButton
label="Import Skill"
label={t("skills.importButton")}
icon={Plus}
tone="accent"
onClick={() => setSkillImportModalOpen(true)}
@@ -138,8 +140,8 @@ export function SkillsSection({
) : (
<div className="flex flex-1 items-center justify-center">
<EmptyState
title="Select a skill to view details"
detail="Choose a skill from the list to see its content, file tree, and configuration."
title={t("skills.selectToViewTitle")}
detail={t("skills.selectToViewDetail")}
/>
</div>
)}
+9
View File
@@ -190,6 +190,15 @@ export const en: Record<string, string> = {
"agent.saveChanges": "Save changes",
"agent.delete": "Delete agent",
"agent.skills": "Skills",
"skills.title": "Skills",
"skills.importAria": "Import skill",
"skills.filterPlaceholder": "Filter skills...",
"skills.filterAria": "Filter skills",
"skills.emptyTitle": "No skills imported",
"skills.emptyDetail": "Import skills from GitHub, a local path, or skills.sh to get started.",
"skills.importButton": "Import Skill",
"skills.selectToViewTitle": "Select a skill to view details",
"skills.selectToViewDetail": "Choose a skill from the list to see its content, file tree, and configuration.",
"agent.pendingApproval": "Pending Approval",
"agent.pendingHireNote": "Pending hires must be approved or rejected from Hiring or Approvals.",
+9
View File
@@ -190,6 +190,15 @@ export const zh: Record<string, string> = {
"agent.saveChanges": "保存",
"agent.delete": "删除",
"agent.skills": "技能",
"skills.title": "技能",
"skills.importAria": "导入技能",
"skills.filterPlaceholder": "过滤技能...",
"skills.filterAria": "过滤技能",
"skills.emptyTitle": "尚未导入技能",
"skills.emptyDetail": "从 GitHub、本地路径或 skills.sh 导入技能即可开始。",
"skills.importButton": "导入技能",
"skills.selectToViewTitle": "选择一个技能查看详情",
"skills.selectToViewDetail": "从列表中选择一项,查看其内容、文件树与配置。",
"agent.pendingApproval": "等待审批",
"agent.pendingHireNote": "待审批成员需在招聘或审批页面处理。",