mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
refactor(desktop): localize settled TODO(i18n) literals (#57924)
The Capabilities/MCP/Hub/Skills UX has settled, so lift every `// TODO(i18n): literal until the UX settles` hardcoded English string into the typed i18n catalog and drop the comments. - New keys under `common` (expand, tryHint), `settings.mcp` (capability summary, status line, all-servers, auth flow, tool chip titles, log empty label), and `skills` (provenance, sort/bulk labels, empty states, editor actions). Full translations in en + zh; ja + zh-hant overrides added. - Module-level pure fns that had no `t` in scope now take the mcp translations (`capabilitySummary`/`statusLine`) or an `emptyLabel` prop (`McpLogs`); the archive toast takes `t`. - Shared `common.tryHint(term)` dedupes the "Try “…”" search hint across skills/messaging/cron/artifacts. No behavior or styling change — string lookups only. Zero TODO(i18n) remain.
This commit is contained in:
parent
20c83af664
commit
86518638a3
12 changed files with 244 additions and 93 deletions
|
|
@ -205,7 +205,6 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, .
|
|||
|
||||
// Rotating placeholder nudges from real data — search matches file paths and
|
||||
// session titles, not just labels; show it.
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
const searchHints = useMemo(() => {
|
||||
if (!artifacts?.length) {
|
||||
return undefined
|
||||
|
|
@ -217,10 +216,10 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, .
|
|||
|
||||
const titles = [...new Set(artifacts.map(artifact => artifact.sessionTitle).filter(Boolean))].slice(0, 2)
|
||||
|
||||
const hints = [...extensions.map(ext => `Try “.${ext}”`), ...titles.map(title => `Try “${title}”`)]
|
||||
const hints = [...extensions.map(ext => t.common.tryHint(`.${ext}`)), ...titles.map(title => t.common.tryHint(title))]
|
||||
|
||||
return hints.length > 0 ? hints : undefined
|
||||
}, [artifacts])
|
||||
}, [artifacts, t])
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const all = artifacts || []
|
||||
|
|
|
|||
|
|
@ -431,12 +431,11 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
|
|||
<PanelBody>
|
||||
<PanelList
|
||||
onSearchChange={setQuery}
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
searchHints={jobs
|
||||
.map(jobTitle)
|
||||
.filter(Boolean)
|
||||
.slice(0, 5)
|
||||
.map(title => `Try “${title}”`)}
|
||||
.map(title => t.common.tryHint(title))}
|
||||
searchLabel={c.search}
|
||||
searchPlaceholder={c.search}
|
||||
searchValue={query}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
|
||||
import { deleteLearningNode } from '@/hermes'
|
||||
import { type Translations, useI18n } from '@/i18n'
|
||||
import { notify } from '@/store/notifications'
|
||||
|
||||
export const ARCHIVE_SKILL_DESCRIPTION = 'The skill is archived and can be restored with `hermes curator restore`.'
|
||||
|
||||
export function notifySkillArchived(): void {
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
notify({ kind: 'success', message: 'Restorable via hermes curator restore.', title: 'Skill archived' })
|
||||
export function notifySkillArchived(t: Translations): void {
|
||||
notify({ kind: 'success', message: t.skills.skillArchivedMessage, title: t.skills.skillArchivedTitle })
|
||||
}
|
||||
|
||||
export async function archiveLearningSkill(id: string): Promise<void> {
|
||||
|
|
@ -46,6 +46,8 @@ export function ArchiveSkillConfirmDialog({
|
|||
skillId,
|
||||
skillName
|
||||
}: ArchiveSkillConfirmDialogProps) {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
confirmLabel="Archive"
|
||||
|
|
@ -58,7 +60,7 @@ export function ArchiveSkillConfirmDialog({
|
|||
|
||||
fireOptimistic(
|
||||
archiveLearningSkill(skillId).then(() => {
|
||||
notifySkillArchived()
|
||||
notifySkillArchived(t)
|
||||
onSuccess?.()
|
||||
}),
|
||||
rollback,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Codicon } from '@/components/ui/codicon'
|
|||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { RowButton } from '@/components/ui/row-button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $paneHeightOverride, $paneState, setPaneHeightOverride } from '@/store/panes'
|
||||
|
||||
|
|
@ -142,6 +143,7 @@ export function DetailPane({
|
|||
onClose?: () => void
|
||||
title: ReactNode
|
||||
}) {
|
||||
const { t } = useI18n()
|
||||
const override = useStore($paneHeightOverride(id))
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -196,8 +198,7 @@ export function DetailPane({
|
|||
{actions}
|
||||
<Button
|
||||
aria-expanded={!collapsed}
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
aria-label={collapsed ? 'Expand' : 'Collapse'}
|
||||
aria-label={collapsed ? t.common.expand : t.common.collapse}
|
||||
className={ICON_BUTTON}
|
||||
onClick={() => setPaneHeightOverride(id, collapsed ? undefined : 0)}
|
||||
size="icon"
|
||||
|
|
@ -206,8 +207,7 @@ export function DetailPane({
|
|||
<Codicon name={collapsed ? 'chevron-up' : 'chevron-down'} size="0.8125rem" />
|
||||
</Button>
|
||||
{onClose && (
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
<Button aria-label="Close" className={ICON_BUTTON} onClick={onClose} size="icon" variant="ghost">
|
||||
<Button aria-label={t.common.close} className={ICON_BUTTON} onClick={onClose} size="icon" variant="ghost">
|
||||
<Codicon name="close" size="0.8125rem" />
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -269,8 +269,7 @@ export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, .
|
|||
{...props}
|
||||
onSearchChange={setQuery}
|
||||
searchHidden={(platforms?.length ?? 0) === 0}
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
searchHints={platforms?.slice(0, 5).map(platform => `Try “${platform.name.toLowerCase()}”`)}
|
||||
searchHints={platforms?.slice(0, 5).map(platform => t.common.tryHint(platform.name.toLowerCase()))}
|
||||
searchPlaceholder={m.search}
|
||||
searchValue={query}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -102,13 +102,6 @@ const usageOf = (skill: SkillInfo): number => (typeof skill.usage === 'number' ?
|
|||
|
||||
const categoryFor = (skill: SkillInfo): string => asText(skill.category) || 'general'
|
||||
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
const PROVENANCE_LABEL: Record<NonNullable<SkillInfo['provenance']>, string> = {
|
||||
agent: 'Learned',
|
||||
bundled: 'Built-in',
|
||||
hub: 'Hub'
|
||||
}
|
||||
|
||||
// Row subtitle: category, with non-default origins badged.
|
||||
function skillSubtitle(skill: SkillInfo): React.ReactNode {
|
||||
const category = prettyName(categoryFor(skill))
|
||||
|
|
@ -295,7 +288,6 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
|
||||
// Rotating placeholder nudges from the user's own data — teach that search
|
||||
// understands categories and tool names, not just titles.
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
const searchHints = useMemo(() => {
|
||||
if (mode === 'skills' && skills?.length) {
|
||||
const counts = new Map<string, number>()
|
||||
|
|
@ -308,18 +300,18 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
return [...counts.entries()]
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([category]) => `Try “${category.toLowerCase()}”`)
|
||||
.map(([category]) => t.common.tryHint(category.toLowerCase()))
|
||||
}
|
||||
|
||||
if (mode === 'toolsets' && toolsets?.length) {
|
||||
return toolsets
|
||||
.filter(ts => isDesktopToolsetVisible(ts.name) && toolNames(ts).length > 0)
|
||||
.slice(0, 5)
|
||||
.map(ts => `Try “${toolNames(ts)[0]}”`)
|
||||
.map(ts => t.common.tryHint(toolNames(ts)[0]))
|
||||
}
|
||||
|
||||
return undefined
|
||||
}, [mode, skills, toolsets])
|
||||
}, [mode, skills, toolsets, t])
|
||||
|
||||
// Keep a valid selection: fall back to the first visible row when the
|
||||
// current selection is filtered out (or nothing is selected yet).
|
||||
|
|
@ -422,35 +414,32 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
)
|
||||
|
||||
// One switch line covering enable-all/disable-all.
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
const bulkSwitch = (allEnabled: boolean): ListStripMenuToggle => ({
|
||||
checked: allEnabled,
|
||||
disabled: bulkBusy,
|
||||
label: 'All',
|
||||
label: t.skills.all,
|
||||
onToggle: checked => void bulkToggle(checked)
|
||||
})
|
||||
|
||||
const allSkillsEnabled = bulkSkills.length > 0 && bulkSkills.every(s => s.enabled)
|
||||
const allToolsetsEnabled = bulkToolsets.length > 0 && bulkToolsets.every(ts => ts.enabled)
|
||||
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
const sortButton = (desc: boolean, flip: () => void) => (
|
||||
<ListStripButton onClick={flip}>{desc ? '↓ Most used' : '↑ Least used'}</ListStripButton>
|
||||
<ListStripButton onClick={flip}>{desc ? t.skills.sortMostUsedDesc : t.skills.sortLeastUsedAsc}</ListStripButton>
|
||||
)
|
||||
|
||||
// Full-bleed empty state, matching the MCP tab (spans both columns, not a
|
||||
// cramped note in the left rail). Query-aware, and says "tools" not the
|
||||
// internal "toolsets".
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
const capabilityEmpty = (noun: string) => {
|
||||
const q = query.trim()
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-1">
|
||||
<PanelEmpty
|
||||
description={q ? `Nothing matches “${q}”.` : `No ${noun} available yet.`}
|
||||
description={q ? t.skills.emptyNothingMatches(q) : t.skills.emptyNoneAvailable(noun)}
|
||||
icon="search"
|
||||
title={`No ${noun} found`}
|
||||
title={t.skills.emptyNoneFound(noun)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -503,8 +492,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
|
||||
try {
|
||||
await editLearningNode(skillEditor.name, skillDraft)
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
notify({ kind: 'success', title: 'Skill updated', message: t.skills.appliesToNewSessions(skillEditor.name) })
|
||||
notify({ kind: 'success', title: t.skills.skillUpdated, message: t.skills.appliesToNewSessions(skillEditor.name) })
|
||||
setSkillEditor(null)
|
||||
void refreshCapabilities()
|
||||
} catch (err) {
|
||||
|
|
@ -518,8 +506,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
<DetailPane
|
||||
actions={
|
||||
<Button disabled={skillSaving} onClick={() => void saveSkillEdit()} size="xs">
|
||||
{/* TODO(i18n): literal until the UX settles. */}
|
||||
{skillSaving ? t.common.saving : 'Save'}
|
||||
{skillSaving ? t.common.saving : t.common.save}
|
||||
</Button>
|
||||
}
|
||||
id="skill-editor"
|
||||
|
|
@ -590,7 +577,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
left={sortButton(skillsSortDesc, () => $skillsSortDesc.set(!$skillsSortDesc.get()))}
|
||||
right={
|
||||
<ListStripMenu
|
||||
items={[{ disabled: bulkBusy, label: 'Disable unused', onSelect: () => void disableUnused() }]}
|
||||
items={[{ disabled: bulkBusy, label: t.skills.disableUnused, onSelect: () => void disableUnused() }]}
|
||||
label={t.skills.tabSkills}
|
||||
toggle={bulkSwitch(allSkillsEnabled)}
|
||||
/>
|
||||
|
|
@ -613,8 +600,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
/>
|
||||
))}
|
||||
</ListColumn>
|
||||
{/* TODO(i18n): literal until the UX settles. */}
|
||||
<DetailColumn footer="Changes apply to new sessions.">
|
||||
<DetailColumn footer={t.skills.changesApplyNewSessions}>
|
||||
{activeSkill && (
|
||||
<SkillDetail
|
||||
onArchive={() => setArchiveTarget(activeSkill.name)}
|
||||
|
|
@ -665,8 +651,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
|
|||
)
|
||||
})}
|
||||
</ListColumn>
|
||||
{/* TODO(i18n): literal until the UX settles. */}
|
||||
<DetailColumn footer="Changes apply to new sessions.">
|
||||
<DetailColumn footer={t.skills.changesApplyNewSessions}>
|
||||
{activeToolset && (
|
||||
<ToolsetDetail onConfiguredChange={refreshToolsets} toolCalls={toolCalls ?? {}} toolset={activeToolset} />
|
||||
)}
|
||||
|
|
@ -737,7 +722,7 @@ function SkillDetail({ onArchive, onEdit, skill }: { onArchive: () => void; onEd
|
|||
<PanelPill>{prettyName(categoryFor(skill))}</PanelPill>
|
||||
{skill.provenance && skill.provenance !== 'bundled' && (
|
||||
<PanelPill tone={skill.provenance === 'agent' ? 'good' : 'muted'}>
|
||||
{PROVENANCE_LABEL[skill.provenance]}
|
||||
{t.skills.provenance[skill.provenance]}
|
||||
</PanelPill>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -746,12 +731,11 @@ function SkillDetail({ onArchive, onEdit, skill }: { onArchive: () => void; onEd
|
|||
/>
|
||||
{editable && (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* TODO(i18n): literals until the UX settles. */}
|
||||
<Button onClick={onEdit} size="xs" variant="text">
|
||||
Edit
|
||||
{t.skills.edit}
|
||||
</Button>
|
||||
<Button className="text-destructive hover:text-destructive" onClick={onArchive} size="xs" variant="text">
|
||||
Archive
|
||||
{t.skills.archive}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import {
|
|||
saveMcpServers,
|
||||
testMcpServer
|
||||
} from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { type Translations, useI18n } from '@/i18n'
|
||||
import { countEnabledTools, isToolEnabled, toggleToolInServer } from '@/lib/mcp-tool-filter'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
|
|
@ -168,8 +168,11 @@ const STATUS_DOT: Record<ServerStatus, string> = {
|
|||
// the capabilities the server actually has. When a `server` config is passed,
|
||||
// the tool count reflects the per-tool include/exclude filter (what's actually
|
||||
// registered), not the raw discovered count.
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
function capabilitySummary(probe: McpTestResult, server?: Record<string, unknown>): string {
|
||||
function capabilitySummary(
|
||||
m: Translations['settings']['mcp'],
|
||||
probe: McpTestResult,
|
||||
server?: Record<string, unknown>
|
||||
): string {
|
||||
const toolCount = server
|
||||
? countEnabledTools(
|
||||
server,
|
||||
|
|
@ -177,32 +180,30 @@ function capabilitySummary(probe: McpTestResult, server?: Record<string, unknown
|
|||
)
|
||||
: probe.tools.length
|
||||
|
||||
const parts = [
|
||||
`${toolCount} tools`,
|
||||
...(probe.prompts ? [`${probe.prompts} prompts`] : []),
|
||||
...(probe.resources ? [`${probe.resources} resources`] : [])
|
||||
]
|
||||
|
||||
return `${parts.join(', ')} enabled`
|
||||
return m.capabilitySummary(toolCount, probe.prompts ?? 0, probe.resources ?? 0)
|
||||
}
|
||||
|
||||
// TODO(i18n): literals until the UX settles.
|
||||
function statusLine(status: ServerStatus, probe: Probe | undefined, server?: Record<string, unknown>): string {
|
||||
function statusLine(
|
||||
m: Translations['settings']['mcp'],
|
||||
status: ServerStatus,
|
||||
probe: Probe | undefined,
|
||||
server?: Record<string, unknown>
|
||||
): string {
|
||||
switch (status) {
|
||||
case 'ok':
|
||||
return capabilitySummary(probe as McpTestResult, server)
|
||||
return capabilitySummary(m, probe as McpTestResult, server)
|
||||
|
||||
case 'probing':
|
||||
return 'Connecting…'
|
||||
return m.statusConnecting
|
||||
|
||||
case 'needs-auth':
|
||||
return 'Needs authentication'
|
||||
return m.statusNeedsAuth
|
||||
|
||||
case 'error':
|
||||
return 'Error'
|
||||
return m.statusError
|
||||
|
||||
case 'off':
|
||||
return 'Off'
|
||||
return m.statusOff
|
||||
|
||||
default:
|
||||
return ''
|
||||
|
|
@ -579,8 +580,11 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) {
|
|||
resetDraft(nextServers)
|
||||
}
|
||||
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
notify({ kind: 'success', title: 'Authenticated', message: `${serverName}: ${result.tools.length} tools` })
|
||||
notify({
|
||||
kind: 'success',
|
||||
title: m.authenticatedTitle,
|
||||
message: m.authenticatedMessage(serverName, result.tools.length)
|
||||
})
|
||||
void silentReload()
|
||||
} else if (result.error) {
|
||||
notifyError(new Error(result.error), serverName)
|
||||
|
|
@ -978,7 +982,7 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) {
|
|||
onSelect={() => focusServer(serverName)}
|
||||
onToggle={checked => void toggleServer(serverName, checked)}
|
||||
status={status}
|
||||
statusText={statusLine(status, probes[serverName], server)}
|
||||
statusText={statusLine(m, status, probes[serverName], server)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
|
@ -1014,8 +1018,7 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) {
|
|||
remountKey={docVersion}
|
||||
trailing={
|
||||
<Button disabled={saving || !dirty} onClick={() => void saveDoc()} size="xs">
|
||||
{/* TODO(i18n): literal until the UX settles. */}
|
||||
{saving ? t.common.saving : 'Save'}
|
||||
{saving ? t.common.saving : t.common.save}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
@ -1037,13 +1040,12 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) {
|
|||
defaultHeight={176}
|
||||
id="mcp-logs"
|
||||
title={
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
<span className="text-[0.68rem] font-normal text-muted-foreground/60">
|
||||
{selected && savedEntry ? selected : 'All servers'}
|
||||
{selected && savedEntry ? selected : m.allServers}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<McpLogs server={selected && savedEntry ? selected : null} source={logSource} />
|
||||
<McpLogs emptyLabel={m.noOutput} server={selected && savedEntry ? selected : null} source={logSource} />
|
||||
</DetailPane>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -1100,7 +1102,7 @@ function ServerConfig({
|
|||
!hasHeaderAuth &&
|
||||
(entry.auth === 'oauth' ? status === 'needs-auth' || status === 'error' : !entry.auth && status === 'needs-auth')
|
||||
|
||||
const summary = probe && probe !== 'probing' && probe.ok ? capabilitySummary(probe, entry) : null
|
||||
const summary = probe && probe !== 'probing' && probe.ok ? capabilitySummary(m, probe, entry) : null
|
||||
|
||||
return (
|
||||
// p-2 matches the list view's container so flipping list ⇄ config keeps
|
||||
|
|
@ -1112,12 +1114,11 @@ function ServerConfig({
|
|||
mt-2.5, h-4 switch → mt-3.5) no matter how tall the text column gets. */}
|
||||
<div className="flex items-start gap-2 pr-1.5">
|
||||
<Button
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
aria-label="All servers"
|
||||
aria-label={m.allServers}
|
||||
className={cn('mt-3', ICON_BUTTON)}
|
||||
onClick={onBack}
|
||||
size="icon"
|
||||
title="All servers"
|
||||
title={m.allServers}
|
||||
variant="ghost"
|
||||
>
|
||||
<Codicon name="chevron-left" size="0.8125rem" />
|
||||
|
|
@ -1161,15 +1162,11 @@ function ServerConfig({
|
|||
{canAuth && saved && (
|
||||
<div className="mt-3 flex justify-end">
|
||||
<Button disabled={authing} onClick={onAuthenticate} size="xs">
|
||||
{/* TODO(i18n): literals until the UX settles. */}
|
||||
{authing ? 'Waiting for browser…' : 'Authenticate'}
|
||||
{authing ? m.waitingForBrowser : m.authenticate}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!saved && (
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
<p className="mt-3 text-[0.68rem] text-muted-foreground/60">Unsaved — save mcp.json to connect.</p>
|
||||
)}
|
||||
{!saved && <p className="mt-3 text-[0.68rem] text-muted-foreground/60">{m.unsavedConnect}</p>}
|
||||
|
||||
{status === 'probing' && <PageLoader className="min-h-24" label={t.skills.loading} />}
|
||||
|
||||
|
|
@ -1181,8 +1178,7 @@ function ServerConfig({
|
|||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{/* Chip = a discovered tool; click to include/exclude it (struck
|
||||
through when excluded, so it won't register). The probe always
|
||||
lists every tool regardless of the filter.
|
||||
TODO(i18n): titles are literal until the UX settles. */}
|
||||
lists every tool regardless of the filter. */}
|
||||
{probe.tools.map(tool => {
|
||||
const on = isToolEnabled(entry, tool.name)
|
||||
|
||||
|
|
@ -1197,7 +1193,7 @@ function ServerConfig({
|
|||
disabled={!saved}
|
||||
key={tool.name}
|
||||
onClick={() => onToggleTool(tool.name)}
|
||||
title={on ? `Disable ${tool.name}` : `Enable ${tool.name}`}
|
||||
title={on ? m.disableTool(tool.name) : m.enableTool(tool.name)}
|
||||
type="button"
|
||||
>
|
||||
{tool.name}
|
||||
|
|
@ -1483,7 +1479,15 @@ function filterStdioSections(lines: string[], server: string): string[] {
|
|||
// editor. Scope follows the cursor-selected server (all servers otherwise);
|
||||
// source controls live in the pane header. Body is the app's tool-output
|
||||
// surface: CodeCardBody typography + the floating hover-reveal copy button.
|
||||
function McpLogs({ server, source }: { server: null | string; source: 'stdio' | 'agent' }) {
|
||||
function McpLogs({
|
||||
emptyLabel,
|
||||
server,
|
||||
source
|
||||
}: {
|
||||
emptyLabel: string
|
||||
server: null | string
|
||||
source: 'stdio' | 'agent'
|
||||
}) {
|
||||
const [lines, setLines] = useState<null | string[]>(null)
|
||||
// A profile switch reroutes getLogs to the new backend; keying the effect on
|
||||
// the active profile tears down the old poll (its `cancelled` flag blocks a
|
||||
|
|
@ -1518,8 +1522,7 @@ function McpLogs({ server, source }: { server: null | string; source: 'stdio' |
|
|||
}
|
||||
}, [server, source, activeProfile])
|
||||
|
||||
// TODO(i18n): literal until the UX settles.
|
||||
return <LogTail emptyLabel="No output yet." lines={lines} />
|
||||
return <LogTail emptyLabel={emptyLabel} lines={lines} />
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const en: Translations = {
|
|||
docs: 'Docs',
|
||||
done: 'Done',
|
||||
error: 'Error',
|
||||
expand: 'Expand',
|
||||
failed: 'Failed',
|
||||
formatJson: 'Format JSON',
|
||||
free: 'Free',
|
||||
|
|
@ -39,6 +40,7 @@ export const en: Translations = {
|
|||
set: 'Set',
|
||||
skip: 'Skip',
|
||||
update: 'Update',
|
||||
tryHint: term => `Try “${term}”`,
|
||||
on: 'On',
|
||||
off: 'Off'
|
||||
},
|
||||
|
|
@ -629,7 +631,22 @@ export const en: Translations = {
|
|||
catalogInstallStarted: name => `Installing ${name}... applies to new sessions when done.`,
|
||||
catalogInstallFailed: name => `Failed to install ${name}`,
|
||||
catalogEnvPrompt: name => `${name} requires credentials`,
|
||||
catalogEnvRequired: 'Fill in the required values before installing.'
|
||||
catalogEnvRequired: 'Fill in the required values before installing.',
|
||||
capabilitySummary: (tools, prompts, resources) =>
|
||||
`${[`${tools} tools`, ...(prompts ? [`${prompts} prompts`] : []), ...(resources ? [`${resources} resources`] : [])].join(', ')} enabled`,
|
||||
statusConnecting: 'Connecting…',
|
||||
statusNeedsAuth: 'Needs authentication',
|
||||
statusError: 'Error',
|
||||
statusOff: 'Off',
|
||||
allServers: 'All servers',
|
||||
authenticatedTitle: 'Authenticated',
|
||||
authenticatedMessage: (server, count) => `${server}: ${count} tools`,
|
||||
waitingForBrowser: 'Waiting for browser…',
|
||||
authenticate: 'Authenticate',
|
||||
unsavedConnect: 'Unsaved — save mcp.json to connect.',
|
||||
enableTool: tool => `Enable ${tool}`,
|
||||
disableTool: tool => `Disable ${tool}`,
|
||||
noOutput: 'No output yet.'
|
||||
},
|
||||
model: {
|
||||
loading: 'Loading model configuration...',
|
||||
|
|
@ -785,11 +802,28 @@ export const en: Translations = {
|
|||
failedToUpdate: name => `Failed to update ${name}`,
|
||||
sortMostUsed: 'Most used',
|
||||
sortAlpha: 'A–Z',
|
||||
sortMostUsedDesc: '↓ Most used',
|
||||
sortLeastUsedAsc: '↑ Least used',
|
||||
enableAll: 'Enable all',
|
||||
disableAll: 'Disable all',
|
||||
disableUnused: 'Disable unused',
|
||||
bulkUpdated: count => `Updated ${count} ${count === 1 ? 'item' : 'items'} for new sessions.`,
|
||||
bulkNoChange: 'Nothing to change.',
|
||||
usageCount: count => `used ${count}×`,
|
||||
provenance: {
|
||||
agent: 'Learned',
|
||||
bundled: 'Built-in',
|
||||
hub: 'Hub'
|
||||
},
|
||||
emptyNoneFound: noun => `No ${noun} found`,
|
||||
emptyNothingMatches: query => `Nothing matches “${query}”.`,
|
||||
emptyNoneAvailable: noun => `No ${noun} available yet.`,
|
||||
changesApplyNewSessions: 'Changes apply to new sessions.',
|
||||
skillUpdated: 'Skill updated',
|
||||
edit: 'Edit',
|
||||
archive: 'Archive',
|
||||
skillArchivedTitle: 'Skill archived',
|
||||
skillArchivedMessage: 'Restorable via hermes curator restore.',
|
||||
hub: {
|
||||
searchPlaceholder: 'Search the skill hub',
|
||||
search: 'Search',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const ja = defineLocale({
|
|||
docs: 'ドキュメント',
|
||||
done: '完了',
|
||||
error: 'エラー',
|
||||
expand: '展開',
|
||||
failed: '失敗',
|
||||
formatJson: 'JSON を整形',
|
||||
free: '無料',
|
||||
|
|
@ -39,6 +40,7 @@ export const ja = defineLocale({
|
|||
set: '設定',
|
||||
skip: 'スキップ',
|
||||
update: '更新',
|
||||
tryHint: term => `「${term}」を試す`,
|
||||
on: 'オン',
|
||||
off: 'オフ'
|
||||
},
|
||||
|
|
@ -725,7 +727,22 @@ export const ja = defineLocale({
|
|||
name: '名前',
|
||||
serverJson: 'サーバー JSON',
|
||||
remove: '削除',
|
||||
saveServer: 'サーバーを保存'
|
||||
saveServer: 'サーバーを保存',
|
||||
capabilitySummary: (tools, prompts, resources) =>
|
||||
`${[`ツール ${tools} 個`, ...(prompts ? [`プロンプト ${prompts} 個`] : []), ...(resources ? [`リソース ${resources} 個`] : [])].join('、')} を有効化`,
|
||||
statusConnecting: '接続中…',
|
||||
statusNeedsAuth: '認証が必要です',
|
||||
statusError: 'エラー',
|
||||
statusOff: 'オフ',
|
||||
allServers: 'すべてのサーバー',
|
||||
authenticatedTitle: '認証済み',
|
||||
authenticatedMessage: (server, count) => `${server}: ツール ${count} 個`,
|
||||
waitingForBrowser: 'ブラウザを待機中…',
|
||||
authenticate: '認証',
|
||||
unsavedConnect: '未保存 — 接続するには mcp.json を保存してください。',
|
||||
enableTool: tool => `${tool} を有効化`,
|
||||
disableTool: tool => `${tool} を無効化`,
|
||||
noOutput: 'まだ出力がありません。'
|
||||
},
|
||||
model: {
|
||||
loading: 'モデル設定を読み込み中...',
|
||||
|
|
@ -864,11 +881,28 @@ export const ja = defineLocale({
|
|||
failedToUpdate: name => `${name} の更新に失敗しました`,
|
||||
sortMostUsed: '使用頻度順',
|
||||
sortAlpha: 'A–Z',
|
||||
sortMostUsedDesc: '↓ 使用頻度順',
|
||||
sortLeastUsedAsc: '↑ 使用頻度が低い順',
|
||||
enableAll: 'すべて有効化',
|
||||
disableAll: 'すべて無効化',
|
||||
disableUnused: '未使用を無効化',
|
||||
bulkUpdated: count => `${count} 件を新しいセッション向けに更新しました。`,
|
||||
bulkNoChange: '変更するものはありません。',
|
||||
usageCount: count => `${count} 回使用`
|
||||
usageCount: count => `${count} 回使用`,
|
||||
provenance: {
|
||||
agent: '学習済み',
|
||||
bundled: '組み込み',
|
||||
hub: 'ハブ'
|
||||
},
|
||||
emptyNoneFound: noun => `${noun} が見つかりません`,
|
||||
emptyNothingMatches: query => `「${query}」に一致するものはありません。`,
|
||||
emptyNoneAvailable: noun => `利用可能な ${noun} はまだありません。`,
|
||||
changesApplyNewSessions: '変更は新しいセッションに適用されます。',
|
||||
skillUpdated: 'スキルを更新しました',
|
||||
edit: '編集',
|
||||
archive: 'アーカイブ',
|
||||
skillArchivedTitle: 'スキルをアーカイブしました',
|
||||
skillArchivedMessage: 'hermes curator restore で復元できます。'
|
||||
},
|
||||
|
||||
starmap: {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export interface Translations {
|
|||
docs: string
|
||||
done: string
|
||||
error: string
|
||||
expand: string
|
||||
failed: string
|
||||
formatJson: string
|
||||
free: string
|
||||
|
|
@ -84,6 +85,7 @@ export interface Translations {
|
|||
set: string
|
||||
skip: string
|
||||
update: string
|
||||
tryHint: (term: string) => string
|
||||
on: string
|
||||
off: string
|
||||
}
|
||||
|
|
@ -541,6 +543,20 @@ export interface Translations {
|
|||
catalogInstallFailed: (name: string) => string
|
||||
catalogEnvPrompt: (name: string) => string
|
||||
catalogEnvRequired: string
|
||||
capabilitySummary: (tools: number, prompts: number, resources: number) => string
|
||||
statusConnecting: string
|
||||
statusNeedsAuth: string
|
||||
statusError: string
|
||||
statusOff: string
|
||||
allServers: string
|
||||
authenticatedTitle: string
|
||||
authenticatedMessage: (server: string, count: number) => string
|
||||
waitingForBrowser: string
|
||||
authenticate: string
|
||||
unsavedConnect: string
|
||||
enableTool: (tool: string) => string
|
||||
disableTool: (tool: string) => string
|
||||
noOutput: string
|
||||
}
|
||||
model: {
|
||||
loading: string
|
||||
|
|
@ -682,11 +698,24 @@ export interface Translations {
|
|||
failedToUpdate: (name: string) => string
|
||||
sortMostUsed: string
|
||||
sortAlpha: string
|
||||
sortMostUsedDesc: string
|
||||
sortLeastUsedAsc: string
|
||||
enableAll: string
|
||||
disableAll: string
|
||||
disableUnused: string
|
||||
bulkUpdated: (count: number) => string
|
||||
bulkNoChange: string
|
||||
usageCount: (count: number | string) => string
|
||||
provenance: Record<'agent' | 'bundled' | 'hub', string>
|
||||
emptyNoneFound: (noun: string) => string
|
||||
emptyNothingMatches: (query: string) => string
|
||||
emptyNoneAvailable: (noun: string) => string
|
||||
changesApplyNewSessions: string
|
||||
skillUpdated: string
|
||||
edit: string
|
||||
archive: string
|
||||
skillArchivedTitle: string
|
||||
skillArchivedMessage: string
|
||||
hub: {
|
||||
searchPlaceholder: string
|
||||
search: string
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const zhHant = defineLocale({
|
|||
docs: '文件',
|
||||
done: '完成',
|
||||
error: '錯誤',
|
||||
expand: '展開',
|
||||
failed: '失敗',
|
||||
formatJson: '格式化 JSON',
|
||||
free: '免費',
|
||||
|
|
@ -39,6 +40,7 @@ export const zhHant = defineLocale({
|
|||
set: '設定',
|
||||
skip: '略過',
|
||||
update: '更新',
|
||||
tryHint: term => `試試「${term}」`,
|
||||
on: '開啟',
|
||||
off: '關閉'
|
||||
},
|
||||
|
|
@ -704,7 +706,22 @@ export const zhHant = defineLocale({
|
|||
name: '名稱',
|
||||
serverJson: '伺服器 JSON',
|
||||
remove: '移除',
|
||||
saveServer: '儲存伺服器'
|
||||
saveServer: '儲存伺服器',
|
||||
capabilitySummary: (tools, prompts, resources) =>
|
||||
`已啟用 ${[`${tools} 個工具`, ...(prompts ? [`${prompts} 個提示`] : []), ...(resources ? [`${resources} 個資源`] : [])].join('、')}`,
|
||||
statusConnecting: '連線中…',
|
||||
statusNeedsAuth: '需要驗證',
|
||||
statusError: '錯誤',
|
||||
statusOff: '關閉',
|
||||
allServers: '所有伺服器',
|
||||
authenticatedTitle: '已驗證',
|
||||
authenticatedMessage: (server, count) => `${server}:${count} 個工具`,
|
||||
waitingForBrowser: '等待瀏覽器…',
|
||||
authenticate: '驗證',
|
||||
unsavedConnect: '未儲存 — 儲存 mcp.json 以連線。',
|
||||
enableTool: tool => `啟用 ${tool}`,
|
||||
disableTool: tool => `停用 ${tool}`,
|
||||
noOutput: '尚無輸出。'
|
||||
},
|
||||
model: {
|
||||
loading: '正在載入模型設定...',
|
||||
|
|
@ -836,11 +853,28 @@ export const zhHant = defineLocale({
|
|||
failedToUpdate: name => `更新 ${name} 失敗`,
|
||||
sortMostUsed: '最常用',
|
||||
sortAlpha: 'A–Z',
|
||||
sortMostUsedDesc: '↓ 最常用',
|
||||
sortLeastUsedAsc: '↑ 最少用',
|
||||
enableAll: '全部啟用',
|
||||
disableAll: '全部停用',
|
||||
disableUnused: '停用未使用',
|
||||
bulkUpdated: count => `已為新工作階段更新 ${count} 項。`,
|
||||
bulkNoChange: '沒有需要變更的內容。',
|
||||
usageCount: count => `已使用 ${count} 次`
|
||||
usageCount: count => `已使用 ${count} 次`,
|
||||
provenance: {
|
||||
agent: '已學習',
|
||||
bundled: '內建',
|
||||
hub: '技能中心'
|
||||
},
|
||||
emptyNoneFound: noun => `找不到${noun}`,
|
||||
emptyNothingMatches: query => `沒有符合「${query}」的內容。`,
|
||||
emptyNoneAvailable: noun => `尚無可用的${noun}。`,
|
||||
changesApplyNewSessions: '變更將套用至新工作階段。',
|
||||
skillUpdated: '技能已更新',
|
||||
edit: '編輯',
|
||||
archive: '封存',
|
||||
skillArchivedTitle: '技能已封存',
|
||||
skillArchivedMessage: '可透過 hermes curator restore 還原。'
|
||||
},
|
||||
|
||||
starmap: {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const zh: Translations = {
|
|||
docs: '文档',
|
||||
done: '完成',
|
||||
error: '错误',
|
||||
expand: '展开',
|
||||
failed: '失败',
|
||||
formatJson: '格式化 JSON',
|
||||
free: '免费',
|
||||
|
|
@ -39,6 +40,7 @@ export const zh: Translations = {
|
|||
set: '设置',
|
||||
skip: '跳过',
|
||||
update: '更新',
|
||||
tryHint: term => `试试“${term}”`,
|
||||
on: '开',
|
||||
off: '关'
|
||||
},
|
||||
|
|
@ -818,7 +820,22 @@ export const zh: Translations = {
|
|||
catalogInstallStarted: name => `正在安装 ${name}… 完成后对新会话生效。`,
|
||||
catalogInstallFailed: name => `安装 ${name} 失败`,
|
||||
catalogEnvPrompt: name => `${name} 需要凭据`,
|
||||
catalogEnvRequired: '安装前请填写必需的值。'
|
||||
catalogEnvRequired: '安装前请填写必需的值。',
|
||||
capabilitySummary: (tools, prompts, resources) =>
|
||||
`已启用 ${[`${tools} 个工具`, ...(prompts ? [`${prompts} 个提示`] : []), ...(resources ? [`${resources} 个资源`] : [])].join('、')}`,
|
||||
statusConnecting: '连接中…',
|
||||
statusNeedsAuth: '需要认证',
|
||||
statusError: '错误',
|
||||
statusOff: '关闭',
|
||||
allServers: '所有服务器',
|
||||
authenticatedTitle: '已认证',
|
||||
authenticatedMessage: (server, count) => `${server}:${count} 个工具`,
|
||||
waitingForBrowser: '等待浏览器…',
|
||||
authenticate: '认证',
|
||||
unsavedConnect: '未保存 — 保存 mcp.json 以连接。',
|
||||
enableTool: tool => `启用 ${tool}`,
|
||||
disableTool: tool => `禁用 ${tool}`,
|
||||
noOutput: '暂无输出。'
|
||||
},
|
||||
model: {
|
||||
loading: '正在加载模型配置...',
|
||||
|
|
@ -969,11 +986,28 @@ export const zh: Translations = {
|
|||
failedToUpdate: name => `更新 ${name} 失败`,
|
||||
sortMostUsed: '最常用',
|
||||
sortAlpha: 'A–Z',
|
||||
sortMostUsedDesc: '↓ 最常用',
|
||||
sortLeastUsedAsc: '↑ 最少用',
|
||||
enableAll: '全部启用',
|
||||
disableAll: '全部停用',
|
||||
disableUnused: '禁用未使用',
|
||||
bulkUpdated: count => `已为新会话更新 ${count} 项。`,
|
||||
bulkNoChange: '没有需要更改的内容。',
|
||||
usageCount: count => `已使用 ${count} 次`,
|
||||
provenance: {
|
||||
agent: '习得',
|
||||
bundled: '内置',
|
||||
hub: '技能中心'
|
||||
},
|
||||
emptyNoneFound: noun => `未找到${noun}`,
|
||||
emptyNothingMatches: query => `没有匹配“${query}”的内容。`,
|
||||
emptyNoneAvailable: noun => `暂无可用的${noun}。`,
|
||||
changesApplyNewSessions: '更改将应用于新会话。',
|
||||
skillUpdated: '技能已更新',
|
||||
edit: '编辑',
|
||||
archive: '归档',
|
||||
skillArchivedTitle: '技能已归档',
|
||||
skillArchivedMessage: '可通过 hermes curator restore 恢复。',
|
||||
hub: {
|
||||
searchPlaceholder: '搜索技能中心',
|
||||
search: '搜索',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue