diff --git a/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx b/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx index 71c55544c7..afd9a1c615 100644 --- a/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx +++ b/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx @@ -1,7 +1,9 @@ import React from "react"; import { describe, it, expect, vi, afterEach } from "vitest"; import { render } from "ink-testing-library"; +import { I18nextProvider } from "react-i18next"; import { DashboardApp } from "../app.js"; +import { initCliI18n } from "../../../i18n/index.js"; import { DashboardTUI } from "../controller.js"; import { createInitialState } from "../state.js"; import type { ProjectItem, TaskItem, AgentItem, AgentDetailItem, ModelItem, SettingsValues, TaskDetailData } from "../state.js"; @@ -10,8 +12,17 @@ function newController(): DashboardTUI { return new DashboardTUI(); } +// Initialize the real CLI i18n instance so t() interpolation runs in tests — +// without a provider, react-i18next's fallback returns defaults with literal +// {{placeholders}}. Mirrors the production wrap in controller.render(). +const testI18n = initCliI18n("en"); + function renderDashboardAppNode(controller: DashboardTUI) { - return React.createElement(DashboardApp, { controller }); + return React.createElement( + I18nextProvider, + { i18n: testI18n }, + React.createElement(DashboardApp, { controller }), + ); } function makeSystemInfo() { diff --git a/packages/cli/src/commands/dashboard-tui/app.tsx b/packages/cli/src/commands/dashboard-tui/app.tsx index af48b5409c..27c28e612d 100644 --- a/packages/cli/src/commands/dashboard-tui/app.tsx +++ b/packages/cli/src/commands/dashboard-tui/app.tsx @@ -221,6 +221,7 @@ const LARGE_LOGO_MIN_COLS = 70; const LARGE_LOGO_MIN_ROWS = 16; function SplashScreen({ loadingStatus, updateStatus }: { loadingStatus: string; updateStatus: UpdateStatus | null }) { + const { t } = useTranslation("cli"); const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; const rows = stdout?.rows ?? 24; @@ -238,7 +239,7 @@ function SplashScreen({ loadingStatus, updateStatus }: { loadingStatus: string; {FUSION_URL} {`v${FUSION_VERSION}`} {updateStatus?.updateAvailable && ( - {`Update available: v${updateStatus.currentVersion} → v${updateStatus.latestVersion}. Run \`fn update\` for an installed CLI, or pull this source checkout.`} + {t("tui.updateAvailable", "Update available: v{{currentVersion}} → v{{latestVersion}}. Run `fn update` for an installed CLI, or pull this source checkout.", { currentVersion: updateStatus.currentVersion, latestVersion: updateStatus.latestVersion })} )} @@ -300,15 +301,16 @@ function Panel({ title, isFocused, children, flexGrow, flexShrink, width }: Pane // ── System panel ────────────────────────────────────────────────────────────── function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: boolean }) { + const { t } = useTranslation("cli"); const info = state.systemInfo; const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; // Watcher is the lowest-signal chip — drop it first when chips would wrap. const showWatcher = cols >= 100; return ( - + {!info ? ( - System information not available. + {t("tui.systemInfoUnavailable", "System information not available.")} ) : ( {/* Status chips wrap to multiple rows at narrow widths. */} @@ -345,7 +347,7 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b off-panel. The token in particular MUST always render in full so users can copy it (via [c]) or click-drag select it. */} {state.isReady && Number.isFinite(info.startupDurationMs) && ( - {`Ready in ${((info.startupDurationMs ?? 0) / 1000).toFixed(1)}s`} + {t("tui.readyIn", "Ready in {{secs}}s", { secs: ((info.startupDurationMs ?? 0) / 1000).toFixed(1) })} )} URL @@ -365,11 +367,11 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b token, regardless of split focus. */} - [Enter] open URL + [Enter] {t("tui.systemOpenUrl", "open URL")} {info.authToken ? ( - · [c] copy token · select token text to copy manually + · [c] {t("tui.systemCopyTokenHint", "copy token · select token text to copy manually")} ) : ( - · drag to select + · {t("tui.systemDragToSelect", "drag to select")} )} @@ -437,6 +439,7 @@ function StatRow({ label, children }: { label: string; children: React.ReactNode } function StatsPanel({ state, isFocused }: { state: DashboardState; isFocused: boolean }) { + const { t } = useTranslation("cli"); const sys = state.systemStats; const systemMemUsed = sys ? sys.systemTotalMem - sys.systemFreeMem : 0; const systemMemUsageColor = sys ? sysMemColor(systemMemUsed, sys.systemTotalMem) : undefined; @@ -446,7 +449,7 @@ function StatsPanel({ state, isFocused }: { state: DashboardState; isFocused: bo : null; return ( - + {sys ? ( <> @@ -484,7 +487,7 @@ function StatsPanel({ state, isFocused }: { state: DashboardState; isFocused: bo ) : ( - Stats not available. + {t("tui.statsUnavailable", "Stats not available.")} )} @@ -494,11 +497,12 @@ function StatsPanel({ state, isFocused }: { state: DashboardState; isFocused: bo // ── Settings panel (status mode) ────────────────────────────────────────────── function SettingsPanel({ state, isFocused }: { state: DashboardState; isFocused: boolean }) { + const { t } = useTranslation("cli"); const s = state.settings; return ( - + {!s ? ( - Settings not available. + {t("tui.settingsUnavailable", "Settings not available.")} ) : ( {( @@ -602,9 +606,10 @@ function LogsPanel({ const hiddenAbove = visibleStart; const hiddenBelow = entries.length - visibleEnd; + const { t } = useTranslation("cli"); const panelTitle = state.clipboardFlash - ? `Logs (${state.logEntries.length}/1000) · ${state.clipboardFlash.ok ? "✓ Copied!" : "✗ Copy failed"}` - : `Logs (${state.logEntries.length}/1000)`; + ? `${t("tui.logsPanelTitle", "Logs")} (${state.logEntries.length}/1000) · ${state.clipboardFlash.ok ? t("tui.copiedSuccess", "✓ Copied!") : t("tui.copyFailed", "✗ Copy failed")}` + : `${t("tui.logsPanelTitle", "Logs")} (${state.logEntries.length}/1000)`; return ( {logsExpandedMode && entries[cursor] ? ( @@ -615,9 +620,9 @@ function LogsPanel({ clipboardFlash={state.clipboardFlash} /> ) : entries.length === 0 ? ( - No log entries yet. + {t("tui.noLogEntries", "No log entries yet.")} ) : entries.length !== state.logEntries.length && entries.length === 0 ? ( - No entries match filter {logsSeverityFilter.toUpperCase()}. + {t("tui.noEntriesMatchFilter", "No entries match filter {{filter}}.", { filter: logsSeverityFilter.toUpperCase() })} ) : ( @@ -708,29 +713,30 @@ function ExpandedLog({ total: number; clipboardFlash: { ok: boolean; at: number } | null; }) { + const { t } = useTranslation("cli"); return ( - Entry {index + 1}/{total} · [Enter/Esc] close · [c] copy + {t("tui.expandedLogHeader", "Entry {{index}}/{{total}} · [Enter/Esc] close · [c] copy", { index: index + 1, total })} {clipboardFlash && ( - {clipboardFlash.ok ? "✓ Copied!" : "✗ Copy failed"} + {clipboardFlash.ok ? t("tui.copiedSuccess", "✓ Copied!") : t("tui.copyFailed", "✗ Copy failed")} )} - Time: + {t("tui.expandedLogTime", "Time:")} {formatTimestamp(entry.timestamp)} - Level: + {t("tui.expandedLogLevel", "Level:")} {entry.level.toUpperCase()} {entry.prefix && ( - Prefix: + {t("tui.expandedLogPrefix", "Prefix:")} {entry.prefix} )} @@ -743,19 +749,20 @@ function ExpandedLog({ // ── Utilities panel ─────────────────────────────────────────────────────────── function UtilitiesPanel({ state, isFocused }: { state: DashboardState; isFocused: boolean }) { + const { t } = useTranslation("cli"); const autoKill = state.autoKillVitestOnPressure; const thresholdPct = Math.round(state.vitestKillThreshold * 100); const actions: Array<{ key: string; label: string }> = [ - { key: "r", label: "Refresh Stats" }, - { key: "c", label: "Clear Logs" }, - { key: "t", label: "Toggle Engine Pause" }, - { key: "k", label: "Kill Vitest Processes" }, - { key: "v", label: `Auto-Kill Vitest >${thresholdPct}% Mem: ${autoKill ? "ON" : "OFF"}` }, - { key: "+/-", label: `Adjust Threshold (${thresholdPct}%)` }, - { key: "?", label: "Help" }, + { key: "r", label: t("tui.utilitiesRefreshStats", "Refresh Stats") }, + { key: "c", label: t("tui.utilitiesClearLogs", "Clear Logs") }, + { key: "t", label: t("tui.utilitiesToggleEnginePause", "Toggle Engine Pause") }, + { key: "k", label: t("tui.utilitiesKillVitest", "Kill Vitest Processes") }, + { key: "v", label: t("tui.utilitiesAutoKillVitest", "Auto-Kill Vitest >{{pct}}% Mem: {{state}}", { pct: thresholdPct, state: autoKill ? "ON" : "OFF" }) }, + { key: "+/-", label: t("tui.utilitiesAdjustThreshold", "Adjust Threshold ({{pct}}%)", { pct: thresholdPct }) }, + { key: "?", label: t("tui.utilitiesHelp", "Help") }, ]; return ( - + {actions.map((action) => ( @@ -1021,11 +1028,12 @@ function StatusModeSingle({ } function StatusBar({ state, controller: _controller }: { state: DashboardState; controller: DashboardTUI }) { + const { t } = useTranslation("cli"); const { systemInfo, updateStatus } = state; const hasUpdate = updateStatus?.updateAvailable === true; const uptime = systemInfo ? formatUptime(Date.now() - systemInfo.startTimeMs) : null; const url = systemInfo?.baseUrl ?? null; - const help = "Tab cycle panel · 1-5 jump"; + const help = t("tui.statusBarHelp", "Tab cycle panel · 1-5 jump"); // Single Text so Yoga truncates the tail (help text) when natural width // exceeds cols — guarantees a one-row footer with version/url preserved. @@ -1049,6 +1057,7 @@ function StatusBar({ state, controller: _controller }: { state: DashboardState; // ── Unified main header — used by both status and interactive modes ────────── function MainHeader({ state }: { state: DashboardState }) { + const { t } = useTranslation("cli"); const inInteractive = state.mode === "interactive"; const interactiveView = state.interactiveView; const { stdout } = useStdout(); @@ -1106,14 +1115,16 @@ function MainHeader({ state }: { state: DashboardState }) { {state.remoteStatus?.state === "running" && ( - ● tunnel{state.remoteStatus.url ? ` ${state.remoteStatus.url}` : ""}{cols >= 80 ? " [^Q] QR" : ""} + ● {t("tui.headerTunnelRunning", "tunnel")} + {state.remoteStatus.url ? ` ${state.remoteStatus.url}` : ""} + {cols >= 80 ? t("tui.headerTunnelQrHint", " [^Q] QR") : ""} )} {state.remoteStatus?.state === "starting" && ( - ● tunnel starting… + ● {t("tui.headerTunnelStarting", "tunnel starting…")} )} - {showHelpHint && [?] help [q] quit} + {showHelpHint && {t("tui.headerHelpQuitHint", "[?] help [q] quit")}} ); } @@ -1143,11 +1154,12 @@ function TaskCard({ selected: boolean; width: number; }) { + const { t } = useTranslation("cli"); const accent = COLUMN_COLORS[task.column] ?? "white"; const borderColor = selected ? "cyanBright" : "gray"; const titleColor = selected ? "whiteBright" : undefined; const shortId = task.id.length > 10 ? task.id.slice(0, 8) : task.id; - const title = task.title ?? task.description ?? "(untitled)"; + const title = task.title ?? task.description ?? t("tui.taskCardUntitled", "(untitled)"); return ( void; }) { + const { t } = useTranslation("cli"); const current = projects[selectedIndex] ?? null; if (!open) { return ( - Project: - {current?.name ?? "(none)"} - [p] change + {t("tui.projectSelectorLabel", "Project:")} + {current?.name ?? t("tui.projectSelectorNone", "(none)")} + {t("tui.projectSelectorChangeHint", "[p] change")} ); } @@ -1277,9 +1290,9 @@ function ProjectSelector({ backgroundColor="black" width={Math.max(30, ...projects.map((p) => p.name.length + 4))} > - Pick a project + {t("tui.projectSelectorPickTitle", "Pick a project")} {projects.length === 0 ? ( - (no projects registered) + {t("tui.projectSelectorNoProjects", "(no projects registered)")} ) : ( projects.map((p, i) => { const isSel = i === selectedIndex; @@ -1294,7 +1307,7 @@ function ProjectSelector({ }) )} - ↑↓ navigate · Enter select · Esc cancel + {t("tui.projectSelectorNavHints", "↑↓ navigate · Enter select · Esc cancel")} ); } @@ -1354,6 +1367,7 @@ function TaskDetailScreen({ interactiveData: DashboardState["interactiveData"]; controller: DashboardTUI; }) { + const { t } = useTranslation("cli"); const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; const isNarrow = cols < NARROW_THRESHOLD; @@ -1532,7 +1546,7 @@ function TaskDetailScreen({ ▶ {task.agentState} )} - [Esc] back + {t("tui.taskDetailBack", "[Esc] back")} @@ -1546,12 +1560,12 @@ function TaskDetailScreen({ {detail === null && ( - Loading task details… + {t("tui.taskDetailLoading", "Loading task details…")} )} {detail === "unavailable" && ( - Task no longer available — Esc to go back + {t("tui.taskDetailUnavailable", "Task no longer available — Esc to go back")} )} {detail && detail !== "unavailable" && ( @@ -1577,9 +1591,9 @@ function TaskDetailScreen({ {/* Steps section */} - ── Steps ────────────────────────────────────── + {t("tui.taskDetailStepsSectionHeader", "── Steps ──────────────────────────────────────")} {detail.steps.length === 0 ? ( - (no steps yet) + {t("tui.taskDetailNoSteps", "(no steps yet)")} ) : ( detail.steps.map((step) => { const icon = STEP_ICON[step.status] ?? "·"; @@ -1590,10 +1604,10 @@ function TaskDetailScreen({ let durationText = ""; if (isRunning && step.startedAt) { const elapsed = Date.now() - new Date(step.startedAt).getTime(); - durationText = ` (running — ${formatDurationMs(elapsed)})`; + durationText = t("tui.stepDurationRunning", " (running — {{duration}})", { duration: formatDurationMs(elapsed) }); } else if (isDone && step.startedAt && step.endedAt) { const elapsed = new Date(step.endedAt).getTime() - new Date(step.startedAt).getTime(); - durationText = ` (${step.status} — ${formatDurationMs(elapsed)})`; + durationText = t("tui.stepDurationDone", " ({{status}} — {{duration}})", { status: step.status, duration: formatDurationMs(elapsed) }); } return ( @@ -1614,14 +1628,14 @@ function TaskDetailScreen({ {/* Logs section — flexGrow so it fills remaining vertical space */} - ── Logs ─────────────────────────────────────── + {t("tui.taskDetailLogsSectionHeader", "── Logs ───────────────────────────────────────")} - {autoFollow ? "[live]" : "[paused]"} + {autoFollow ? t("tui.taskDetailLogsLive", "[live]") : t("tui.taskDetailLogsPaused", "[paused]")} {detail.recentLogs.length === 0 ? ( - (no log entries yet) + {t("tui.taskDetailNoLogEntries", "(no log entries yet)")} ) : ( {/* Compute the visible window from the bottom, offset by scroll position. */} @@ -1668,7 +1682,7 @@ function TaskDetailScreen({ )} - ↑↓/j/k scroll · PgUp/PgDn half-page · g top · G bottom · Esc back + {t("tui.taskDetailScrollHints", "↑↓/j/k scroll · PgUp/PgDn half-page · g top · G bottom · Esc back")} ); } @@ -1854,11 +1868,11 @@ function BoardView({ state, controller }: { state: DashboardState; controller: D const submitNewTask = async () => { const title = newTaskTitle.trim(); if (!title) { - setCreateError("Title cannot be empty"); + setCreateError(t("tui.boardCreateTaskTitleEmpty", "Title cannot be empty")); return; } if (!state.interactiveData || !selectedProject) { - setCreateError("No project selected"); + setCreateError(t("tui.boardCreateTaskNoProject", "No project selected")); return; } setCreating(true); @@ -1900,17 +1914,17 @@ function BoardView({ state, controller }: { state: DashboardState; controller: D paddingY={1} width={Math.min(80, Math.max(40, cols - 8))} > - New Task - Project: {selectedProject?.name ?? "(none)"} + {t("tui.boardNewTaskTitle", "New Task")} + {t("tui.boardNewTaskProject", "Project: {{name}}", { name: selectedProject?.name ?? "(none)" })} - Title + {t("tui.boardNewTaskTitleLabel", "Title")} ▸ void submitNewTask()} - placeholder="What needs doing?" + placeholder={t("tui.boardNewTaskPlaceholder", "What needs doing?")} /> @@ -1920,10 +1934,10 @@ function BoardView({ state, controller }: { state: DashboardState; controller: D {creating ? ( - Creating… + {t("tui.boardCreatingTask", "Creating…")} ) : ( - Enter to create · Esc to cancel + {t("tui.boardCreateTaskHints", "Enter to create · Esc to cancel")} )} @@ -2051,6 +2065,7 @@ function getRunLogLines(run: AgentRunItem): string[] { } function AgentsView({ state }: { state: DashboardState }) { + const { t } = useTranslation("cli"); const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; // Narrow mode: hide the inactive pane so list and detail don't overlap side-by-side. @@ -2125,7 +2140,7 @@ function AgentsView({ state }: { state: DashboardState }) { if (!data || !selectedAgent) { setSubView("list"); return; } data.deleteAgent(selectedAgent.id) .then(() => { - setStatusMsg(`Deleted agent ${selectedAgent.name}`); + setStatusMsg(t("tui.agentDeleted", "Deleted agent {{name}}", { name: selectedAgent.name })); return refreshList(); }) .catch((err: unknown) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)) @@ -2186,14 +2201,14 @@ function AgentsView({ state }: { state: DashboardState }) { if (input === "s") { if (!data || !selectedAgent) return; data.updateAgentState(selectedAgent.id, "active") - .then(() => { setStatusMsg("Agent started"); return refreshList(); }) + .then(() => { setStatusMsg(t("tui.agentStarted", "Agent started")); return refreshList(); }) .catch((err: unknown) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); return; } if (input === "x") { if (!data || !selectedAgent) return; data.updateAgentState(selectedAgent.id, "idle") - .then(() => { setStatusMsg("Agent stopped"); return refreshList(); }) + .then(() => { setStatusMsg(t("tui.agentStopped", "Agent stopped")); return refreshList(); }) .catch((err: unknown) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); return; } @@ -2211,12 +2226,12 @@ function AgentsView({ state }: { state: DashboardState }) { return ( - Delete agent? + {t("tui.agentDeleteConfirmTitle", "Delete agent?")} - Agent: {selectedAgent.name} - ID: {selectedAgent.id} + {t("tui.agentDeleteConfirmName", "Agent:")} {selectedAgent.name} + {t("tui.agentDeleteConfirmId", "ID:")} {selectedAgent.id} - [y] confirm delete [any other key] cancel + {t("tui.agentDeleteConfirmHints", "[y] confirm delete [any other key] cancel")} ); @@ -2243,12 +2258,12 @@ function AgentsView({ state }: { state: DashboardState }) { > - Agents ({agents.length}) + {t("tui.agentsListTitle", "Agents ({{count}})", { count: agents.length })} {agents.length === 0 ? ( - No agents found. + {t("tui.agentsNoAgents", "No agents found.")} ) : ( agents.map((agent, i) => { const isSel = i === selectedIndex; @@ -2289,19 +2304,19 @@ function AgentsView({ state }: { state: DashboardState }) { > - Agent Detail + {t("tui.agentDetailTitle", "Agent Detail")} {!selectedAgent ? ( - Select an agent from the list. + {t("tui.agentDetailSelectHint", "Select an agent from the list.")} ) : loadingDetail ? ( - Loading… + {t("tui.loading", "Loading…")} ) : !detail ? ( - Could not load agent detail. + {t("tui.agentDetailLoadError", "Could not load agent detail.")} ) : ( {detail.name} @@ -2310,49 +2325,49 @@ function AgentsView({ state }: { state: DashboardState }) { {showRunLogs && selectedRun ? ( <> - Run logs ({selectedRunIndex + 1}) - ID: {selectedRun.id} + {t("tui.agentRunLogsTitle", "Run logs ({{index}})", { index: selectedRunIndex + 1 })} + {t("tui.agentRunId", "ID:")} {selectedRun.id} {getRunLogLines(selectedRun).slice(0, 10).map((line, i) => ( {line} ))} - [Esc/q] back to runs + {t("tui.agentRunLogsBackHint", "[Esc/q] back to runs")} ) : ( <> - State: + {t("tui.agentDetailState", "State:")} {detail.state} - Role: + {t("tui.agentDetailRole", "Role:")} {detail.role} {detail.title && ( - Title: + {t("tui.agentDetailTitle2", "Title:")} {detail.title} )} {detail.taskId && ( - Task: + {t("tui.agentDetailTask", "Task:")} {detail.taskId} )} {detail.capabilities.length > 0 && ( - Caps: + {t("tui.agentDetailCaps", "Caps:")} {detail.capabilities.join(", ")} )} {recentRuns.length > 0 && ( <> - Run history (latest first): + {t("tui.agentRunHistory", "Run history (latest first):")} {recentRuns.slice(0, 5).map((run, i) => ( @@ -2363,7 +2378,7 @@ function AgentsView({ state }: { state: DashboardState }) { {run.triggerDetail && {run.triggerDetail}} ))} - [Enter] open logs + {t("tui.agentOpenLogsHint", "[Enter] open logs")} )} @@ -2377,9 +2392,9 @@ function AgentsView({ state }: { state: DashboardState }) { {/* Footer */} - [s] start [x] stop [D] delete [r] refresh [Tab] focus ↑↓ select + {t("tui.agentsFooterHints", "[s] start [x] stop [D] delete [r] refresh [Tab] focus ↑↓ select")} {isNarrow && ( - [narrow] {detailFocused ? "detail" : "list"} + {t("tui.narrowModeIndicator", "[narrow]")} {detailFocused ? t("tui.agentNarrowDetail", "detail") : t("tui.agentNarrowList", "list")} )} @@ -2411,6 +2426,7 @@ const SETTING_DEFS: SettingDef[] = [ ]; function SettingsInteractiveView({ state, controller }: { state: DashboardState; controller: DashboardTUI }) { + const { t } = useTranslation("cli"); const [selectedIndex, setSelectedIndex] = useState(0); const [localSettings, setLocalSettings] = useState(null); const [models, setModels] = useState([]); @@ -2466,7 +2482,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; const remoteStatus = data.remote ? await data.remote.getStatus().catch(() => null) : null; const remoteSettingsSnapshot = data.remote ? await data.remote.getSettings().catch(() => updated.remoteSettingsSnapshot) : undefined; setLocalSettings(remoteStatus ? { ...updated, remoteStatus, remoteSettingsSnapshot } : updated); - setStatusMsg("Saved"); + setStatusMsg(t("tui.settingsSaved", "Saved")); } catch (err) { setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`); } finally { @@ -2524,14 +2540,14 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; if (ttlInputMode) { if (key.escape) { setTtlInputMode(false); - setStatusMsg("Cancelled short-lived token input"); + setStatusMsg(t("tui.settingsCancelledTokenInput", "Cancelled short-lived token input")); } return; } if (inputUpper === "R") { void refreshRemoteStatus(); - setStatusMsg("Remote status refreshed"); + setStatusMsg(t("tui.settingsRemoteStatusRefreshed", "Remote status refreshed")); return; } @@ -2540,24 +2556,24 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; if (data?.remote && inputUpper === "C") { const provider = localSettings.remoteActiveProvider; if (!provider) { - setStatusMsg("Select a remote provider first"); + setStatusMsg(t("tui.settingsSelectProviderFirst", "Select a remote provider first")); } else { void data.remote.activateProvider(provider) .then(() => refreshRemoteStatus()) - .then(() => setStatusMsg(`Activated provider: ${provider}`)) + .then(() => setStatusMsg(t("tui.settingsActivatedProvider", "Activated provider: {{provider}}", { provider }))) .catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); } return; } if (data?.remote && inputUpper === "V") { - void data.remote.startTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg("Remote tunnel starting")) + void data.remote.startTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg(t("tui.settingsTunnelStarting", "Remote tunnel starting"))) .catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); return; } if (data?.remote && inputUpper === "X") { - void data.remote.stopTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg("Remote tunnel stopped")) + void data.remote.stopTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg(t("tui.settingsTunnelStopped", "Remote tunnel stopped"))) .catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); return; } @@ -2566,7 +2582,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; void data.remote.regeneratePersistentToken() .then((result) => { setPersistentMaskedToken(result.maskedToken ?? null); - setStatusMsg("Persistent token regenerated"); + setStatusMsg(t("tui.settingsPersistentTokenRegenerated", "Persistent token regenerated")); }) .catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); return; @@ -2575,20 +2591,20 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; if (data?.remote && inputUpper === "L") { setTtlInputValue(String(localSettings.remoteShortLivedTtlMs)); setTtlInputMode(true); - setStatusMsg("Enter TTL milliseconds and press Enter"); + setStatusMsg(t("tui.settingsEnterTtl", "Enter TTL milliseconds and press Enter")); return; } if (data?.remote && inputUpper === "U") { void handleFetchRemoteUrl("persistent") - .then(() => setStatusMsg("Remote URL fetched")) + .then(() => setStatusMsg(t("tui.settingsRemoteUrlFetched", "Remote URL fetched"))) .catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); return; } if (data?.remote && inputUpper === "K") { void handleFetchRemoteQr("persistent") - .then(() => setStatusMsg("QR payload fetched")) + .then(() => setStatusMsg(t("tui.settingsQrFetched", "QR payload fetched"))) .catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)); return; } @@ -2675,7 +2691,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; if (!data?.remote || !localSettings) return; const ttlMs = Number(value.trim()); if (!Number.isFinite(ttlMs) || ttlMs <= 0) { - setStatusMsg("TTL must be a positive number (ms)"); + setStatusMsg(t("tui.settingsTtlMustBePositive", "TTL must be a positive number (ms)")); return; } @@ -2688,7 +2704,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; setLocalSettings({ ...localSettings, remoteShortLivedTtlMs: ttlMs }); await saveField({ remoteShortLivedTtlMs: ttlMs }); await handleFetchRemoteUrl("short-lived", ttlMs); - setStatusMsg("Short-lived token generated"); + setStatusMsg(t("tui.settingsShortLivedTokenGenerated", "Short-lived token generated")); } catch (err) { setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`); } finally { @@ -2700,7 +2716,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; {statusMsg && ( - {saving ? "Saving…" : statusMsg} + {saving ? t("tui.settingsSavingInProgress", "Saving…") : statusMsg} )} @@ -2714,12 +2730,12 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; > - Settings + {t("tui.settingsInteractivePanelTitle", "Settings")} {!localSettings ? ( - Loading… + {t("tui.loading", "Loading…")} ) : ( SETTING_DEFS.map((def, i) => { const isSel = i === selectedIndex; @@ -2748,32 +2764,32 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; > - Edit / Models + {t("tui.settingsEditModelsTitle", "Edit / Models")} {!localSettings ? ( - Loading settings… + {t("tui.settingsLoadingSettings", "Loading settings…")} ) : !selectedDef ? null : ( <> {selectedDef.label} - Current: + {t("tui.settingsCurrentLabel", "Current:")} {renderValue(selectedDef, localSettings)} {selectedDef.type === "boolean" && ( - [Space] toggle + {t("tui.settingsBoolToggleHint", "[Space] toggle")} )} {selectedDef.type === "number" && ( - {selectedDef.key === "pollIntervalMs" ? "[+/-] adjust by 5000ms" : "[+/-] adjust by 1"} + {selectedDef.key === "pollIntervalMs" ? t("tui.settingsAdjust5000ms", "[+/-] adjust by 5000ms") : t("tui.settingsAdjust1", "[+/-] adjust by 1")} )} {selectedDef.type === "enum" && selectedDef.options && ( - [←/→] cycle options: + {t("tui.settingsEnumCycleHint", "[←/→] cycle options:")} {selectedDef.options.map((opt) => ( @@ -2786,31 +2802,31 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; )} - ──── Remote ──── + {t("tui.settingsRemoteHeader", "──── Remote ────")} - Provider: - {localSettings.remoteActiveProvider ?? "none"} - State: - {localSettings.remoteStatus?.state ?? "unknown"} + {t("tui.settingsRemoteProvider", "Provider:")} + {localSettings.remoteActiveProvider ?? t("tui.settingsRemoteProviderNone", "none")} + {t("tui.settingsRemoteState", "State:")} + {localSettings.remoteStatus?.state ?? t("tui.settingsRemoteStateUnknown", "unknown")} - Short-lived: - {localSettings.remoteSettingsSnapshot?.shortLivedEnabled ? "on" : "off"} + {t("tui.settingsRemoteShortLived", "Short-lived:")} + {localSettings.remoteSettingsSnapshot?.shortLivedEnabled ? t("tui.settingsRemoteOn", "on") : t("tui.settingsRemoteOff", "off")} {localSettings.remoteStatus?.url && ( - Tunnel URL: {localSettings.remoteStatus.url} + {t("tui.settingsRemoteTunnelUrl", "Tunnel URL:")} {localSettings.remoteStatus.url} )} {remoteUrl && ( - Auth URL: {remoteUrl} + {t("tui.settingsRemoteAuthUrl", "Auth URL:")} {remoteUrl} )} {remoteTokenMeta && ( - Token: {remoteTokenMeta} + {t("tui.settingsRemoteToken", "Token:")} {remoteTokenMeta} )} {persistentMaskedToken && ( - Persistent token: {persistentMaskedToken} + {t("tui.settingsRemotePersistentToken", "Persistent token:")} {persistentMaskedToken} )} {shortLivedExpiresAt && ( - Short-lived expires: {new Date(shortLivedExpiresAt).toLocaleString()} + {t("tui.settingsRemoteShortLivedExpires", "Short-lived expires:")} {new Date(shortLivedExpiresAt).toLocaleString()} )} {remoteQrDisplay && ( @@ -2824,7 +2840,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; )} {ttlInputMode && ( - TTL ms: + {t("tui.settingsTtlLabel", "TTL ms:")} - [Enter] generate [Esc] cancel + {t("tui.settingsTtlHints", "[Enter] generate [Esc] cancel")} )} - [C] activate provider [V] start [X] stop [P] persistent token [L] short-lived token - [U] URL hand-off [K] QR hand-off [R] refresh + {t("tui.settingsRemoteActions1", "[C] activate provider [V] start [X] stop [P] persistent token [L] short-lived token")} + {t("tui.settingsRemoteActions2", "[U] URL hand-off [K] QR hand-off [R] refresh")} {/* Models subsection */} {models.length > 0 && ( <> - ──── Available Models ──── - Configure default model in web dashboard + {t("tui.settingsAvailableModelsHeader", "──── Available Models ────")} + {t("tui.settingsConfigureModelInDashboard", "Configure default model in web dashboard")} {models.slice(0, 8).map((m) => ( @@ -2853,7 +2869,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; ))} {models.length > 8 && ( - … and {models.length - 8} more + {t("tui.settingsMoreModels", "… and {{count}} more", { count: models.length - 8 })} )} )} @@ -2864,7 +2880,7 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState; - [Tab] switch panel ↑↓ select setting [Space] toggle bool [+/-] adjust num [←/→] cycle enum [C/V/X/P/L/U/K/R] remote actions + {t("tui.settingsFooterHints", "[Tab] switch panel ↑↓ select setting [Space] toggle bool [+/-] adjust num [←/→] cycle enum [C/V/X/P/L/U/K/R] remote actions")} ); @@ -2921,6 +2937,7 @@ function PushModal({ onConfirm: () => void; onCancel: () => void; }) { + const { t } = useTranslation("cli"); useInput((_input, key) => { if (key.return) { onConfirm(); return; } if (key.escape) { onCancel(); return; } @@ -2937,18 +2954,18 @@ function PushModal({ paddingY={1} backgroundColor="black" > - Push to remote + {t("tui.gitPushModalTitle", "Push to remote")} - Branch: + {t("tui.gitPushModalBranch", "Branch:")} {status.branch} - ahead + {t("tui.gitPushModalAhead", "ahead")} {status.ahead} {toPush.length > 0 && ( <> - Commits to push (oldest→newest): + {t("tui.gitPushModalCommits", "Commits to push (oldest→newest):")} {[...toPush].reverse().map((c) => ( {c.shortSha} @@ -2958,12 +2975,13 @@ function PushModal({ )} - [Enter] push [Esc] cancel + {t("tui.gitPushModalHints", "[Enter] push [Esc] cancel")} ); } function GitView({ state, controller }: { state: DashboardState; controller: DashboardTUI }) { + const { t } = useTranslation("cli"); const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; @@ -3049,7 +3067,7 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das setPushModal({ phase: "pushing" }); if (data && projectPath) { data.git.push(projectPath).then((result) => { - setPushModal({ phase: "done", message: result.output || (result.success ? "Push successful" : "Push failed"), isError: !result.success }); + setPushModal({ phase: "done", message: result.output || (result.success ? t("tui.gitPushSuccessful", "Push successful") : t("tui.gitPushFailed", "Push failed")), isError: !result.success }); if (result.success) { setTimeout(() => { setPushModal(null); void refresh(); }, 2000); } @@ -3086,9 +3104,9 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das } if (input === "F" && data && projectPath) { - setStatusMsg("Fetching…"); + setStatusMsg(t("tui.gitFetching", "Fetching…")); data.git.fetch(projectPath).then((result) => { - setStatusMsg(result.success ? "Fetched" : `Fetch failed: ${result.output}`); + setStatusMsg(result.success ? t("tui.gitFetched", "Fetched") : t("tui.gitFetchFailed", "Fetch failed: {{output}}", { output: result.output })); void refresh(); }).catch((err: unknown) => { setStatusMsg(`Fetch error: ${err instanceof Error ? err.message : String(err)}`); @@ -3163,14 +3181,14 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das {/* Top bar: project selector + status */} - Project: - {selectedProject?.name ?? "(none)"} - [p] change + {t("tui.projectSelectorLabel", "Project:")} + {selectedProject?.name ?? t("tui.projectSelectorNone", "(none)")} + {t("tui.projectSelectorChangeHint", "[p] change")} {loading && ( - refreshing + {t("tui.gitRefreshing", "refreshing")} )} {statusMsg && {statusMsg}} @@ -3217,7 +3235,7 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das {!gitStatus ? ( - {projectPath ? "Loading…" : "No project"} + {projectPath ? t("tui.loading", "Loading…") : t("tui.gitNoProject", "No project")} ) : ( <> @@ -3330,7 +3348,7 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das {commits.length === 0 ? ( - {loading ? "Loading…" : "No commits"} + {loading ? t("tui.loading", "Loading…") : t("tui.gitNoCommits", "No commits")} ) : ( commits.map((c, i) => { const isSel = i === commitIndex; @@ -3438,7 +3456,7 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das ) : ( - Working tree clean + {t("tui.gitWorkingTreeClean", "Working tree clean")} )} @@ -3450,10 +3468,10 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das {/* Footer */} - [r] refresh {gitStatus && gitStatus.ahead > 0 ? "[P] push " : ""}[F] fetch [↑↓] rows [←→] status▸branches{worktrees.length > 1 ? "▸worktrees" : ""}▸commits▸changes [p] project [Esc/s] back + {t("tui.gitFooterHints", "[r] refresh {{push}}[F] fetch [↑↓] rows [←→] status▸branches{{worktrees}}▸commits▸changes [p] project [Esc/s] back", { push: gitStatus && gitStatus.ahead > 0 ? "[P] push " : "", worktrees: worktrees.length > 1 ? "▸worktrees" : "" })} {isNarrow && ( - [narrow] {activePane} + {t("tui.narrowModeIndicator", "[narrow]")} {activePane} )} @@ -3480,7 +3498,7 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das setPushModal({ phase: "pushing" }); if (data && projectPath) { data.git.push(projectPath).then((result) => { - setPushModal({ phase: "done", message: result.output || (result.success ? "Push successful" : "Push failed"), isError: !result.success }); + setPushModal({ phase: "done", message: result.output || (result.success ? t("tui.gitPushSuccessful", "Push successful") : t("tui.gitPushFailed", "Push failed")), isError: !result.success }); if (result.success) { setTimeout(() => { setPushModal(null); void refresh(); }, 2000); } @@ -3503,7 +3521,7 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das backgroundColor="black" > - Pushing to origin/{gitStatus?.branch ?? "…"} + {t("tui.gitPushingToOrigin", "Pushing to origin/{{branch}}", { branch: gitStatus?.branch ?? "…" })} )} {pushModal.phase === "done" && ( @@ -3518,7 +3536,7 @@ function GitView({ state, controller }: { state: DashboardState; controller: Das {pushModal.message} - {pushModal.isError && [Esc] dismiss} + {pushModal.isError && {t("tui.gitPushDismissHint", "[Esc] dismiss")}} )} @@ -3582,6 +3600,7 @@ function entriesToNodes(entries: FileEntry[], depth: number): TreeNode[] { } function FilesView({ state, controller }: { state: DashboardState; controller: DashboardTUI }) { + const { t } = useTranslation("cli"); const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; @@ -3900,10 +3919,10 @@ function FilesView({ state, controller }: { state: DashboardState; controller: D {treeLoading ? ( - Loading… + {t("tui.loading", "Loading…")} ) : flatNodes.length === 0 ? ( - (empty) + {t("tui.filesEmpty", "(empty)")} ) : ( flatNodes.map((node, i) => { const isSelected = focusedPane === "tree" && i === selectedIndex; @@ -3968,18 +3987,18 @@ function FilesView({ state, controller }: { state: DashboardState; controller: D {previewLoading ? ( - Loading… + {t("tui.loading", "Loading…")} ) : !previewEntry ? ( - Select a file to preview + {t("tui.filesSelectToPreview", "Select a file to preview")} ) : previewResult === null ? ( - Unable to read file + {t("tui.filesUnableToRead", "Unable to read file")} ) : previewResult.isBinary ? ( - [binary file, {formatFileSize(previewResult.size)}] + {t("tui.filesBinary", "[binary file, {{size}}]", { size: formatFileSize(previewResult.size) })} ) : previewResult.tooLarge ? ( - {formatFileSize(previewResult.size)} — [too large to preview] + {t("tui.filesTooLarge", "{{size}} — [too large to preview]", { size: formatFileSize(previewResult.size) })} ) : previewResult.content === "" ? ( - (empty file) + {t("tui.filesEmptyFile", "(empty file)")} ) : previewLines ? ( {previewLines.map((line, i) => { @@ -3994,7 +4013,7 @@ function FilesView({ state, controller }: { state: DashboardState; controller: D ); })} {totalLines > previewScroll + previewHeight && ( - … {totalLines - previewScroll - previewHeight} more lines + {t("tui.filesMoreLines", "… {{count}} more lines", { count: totalLines - previewScroll - previewHeight })} )} ) : null} @@ -4006,10 +4025,10 @@ function FilesView({ state, controller }: { state: DashboardState; controller: D {/* Footer hints */} - [Tab] switch pane [↑↓/jk] move [Enter] open [←/→] collapse/expand [.] hidden [w] wrap [p] project [r] reload + {t("tui.filesFooterHints", "[Tab] switch pane [↑↓/jk] move [Enter] open [←/→] collapse/expand [.] hidden [w] wrap [p] project [r] reload")} {isNarrow && ( - [narrow] {focusedPane} + {t("tui.narrowModeIndicator", "[narrow]")} {focusedPane} )} @@ -4024,7 +4043,7 @@ function FilesView({ state, controller }: { state: DashboardState; controller: D paddingY={1} backgroundColor="black" > - Select Project + {t("tui.filesSelectProject", "Select Project")} {projectsState.projects.map((proj, i) => ( @@ -4044,11 +4063,12 @@ function FilesView({ state, controller }: { state: DashboardState; controller: D // ── Interactive mode root ───────────────────────────────────────────────────── function InteractiveMode({ state, controller }: { state: DashboardState; controller: DashboardTUI }) { + const { t } = useTranslation("cli"); if (state.interactiveData === null) { return ( - Interactive mode unavailable — no data source + {t("tui.interactiveModeUnavailable", "Interactive mode unavailable — no data source")} ); @@ -4075,6 +4095,7 @@ interface DashboardAppProps { } export function DashboardApp({ controller }: DashboardAppProps) { + const { t } = useTranslation("cli"); const { exit } = useApp(); const { stdout } = useStdout(); @@ -4197,7 +4218,7 @@ export function DashboardApp({ controller }: DashboardAppProps) { const remote = state.interactiveData?.remote; if (!remote) return; if (state.remoteStatus?.state !== "running") { - setQrOverlay({ state: "error", message: "No remote tunnel is running. Start one in Settings (g)." }); + setQrOverlay({ state: "error", message: t("tui.qrNoTunnelRunning", "No remote tunnel is running. Start one in Settings (g).") }); return; } setQrOverlay({ state: "loading" }); @@ -4584,8 +4605,8 @@ export function DashboardApp({ controller }: DashboardAppProps) { )} {qrOverlay && ( - Remote Access — Scan to connect - {qrOverlay.state === "loading" && Generating QR…} + {t("tui.qrOverlayTitle", "Remote Access — Scan to connect")} + {qrOverlay.state === "loading" && {t("tui.qrGenerating", "Generating QR…")}} {qrOverlay.state === "error" && {qrOverlay.message}} {qrOverlay.state === "ready" && ( <> @@ -4599,7 +4620,7 @@ export function DashboardApp({ controller }: DashboardAppProps) { )} - [Esc] close + {t("tui.qrCloseHint", "[Esc] close")} )} diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index ad6ef1e9dd..db84a3f3fd 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -1,4 +1,5 @@ import { useState, useCallback, useEffect, useMemo, useRef, lazy, Suspense } from "react"; +import { useTranslation } from "react-i18next"; import { computeCapacityRisk, DEFAULT_CAPACITY_RISK_TODO_THRESHOLD, @@ -236,6 +237,7 @@ export function shouldShowFirstEverBootLoader(projectsLoading: boolean, projectC } function AppInner() { + const { t } = useTranslation("app"); const { toasts, addToast, removeToast } = useToast(); const { shellApi, state: shellState, ready: shellReady, openConnectionManagerSignal } = useShellConnection(); const shellHost = useShellHostContext(); @@ -1336,7 +1338,7 @@ function AppInner() { if (showBackendConnectionErrorPage) { return ( { diff --git a/packages/dashboard/app/components/ActiveAgentsPanel.tsx b/packages/dashboard/app/components/ActiveAgentsPanel.tsx index b42a835e89..109e702670 100644 --- a/packages/dashboard/app/components/ActiveAgentsPanel.tsx +++ b/packages/dashboard/app/components/ActiveAgentsPanel.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { Activity, FileText } from "lucide-react"; +import { useTranslation } from "react-i18next"; import type { Agent } from "../api"; import type { TaskDetail } from "@fusion/core"; import { fetchTaskDetail } from "../api"; @@ -17,6 +18,7 @@ interface LiveAgentCardProps { const TASK_STATUS_POLL_MS = 5000; function LiveAgentCard({ agent, projectId, onSelect, onOpenTaskLogs }: LiveAgentCardProps) { + const { t } = useTranslation("app"); const { entries, isConnected } = useLiveTranscript(agent.taskId, projectId); const [task, setTask] = useState(null); @@ -64,8 +66,8 @@ function LiveAgentCard({ agent, projectId, onSelect, onOpenTaskLogs }: LiveAgent const nextMs = new Date(agent.lastHeartbeatAt).getTime() + intervalMs; const deltaSec = Math.round((nextMs - Date.now()) / 1000); if (!Number.isFinite(deltaSec)) return null; - if (deltaSec <= 0) return `Heartbeat overdue ${formatElapsed(-deltaSec)}`; - return `Next heartbeat in ${formatElapsed(deltaSec)}`; + if (deltaSec <= 0) return t("agents.heartbeatOverdue", "Heartbeat overdue {{elapsed}}", { elapsed: formatElapsed(-deltaSec) }); + return t("agents.nextHeartbeat", "Next heartbeat in {{elapsed}}", { elapsed: formatElapsed(deltaSec) }); })(); const currentStep = task?.steps?.[task.currentStep ?? 0]; @@ -100,7 +102,7 @@ function LiveAgentCard({ agent, projectId, onSelect, onOpenTaskLogs }: LiveAgent onKeyDown={handleKeyDown} role="button" tabIndex={0} - aria-label={`Select agent ${agent.name}`} + aria-label={t("agents.selectAgent", "Select agent {{name}}", { name: agent.name })} >
@@ -122,12 +124,11 @@ function LiveAgentCard({ agent, projectId, onSelect, onOpenTaskLogs }: LiveAgent // SSE stream to attach to; useLiveTranscript bails out with // isConnected=false. Showing "Connecting..." here is misleading // — the agent is just idle. - {agent.state === "running" ? "Starting..." : "Idle — no task assigned"} + {agent.state === "running" ? t("agents.starting", "Starting...") : t("agents.idleNoTask", "Idle — no task assigned")} ) : currentStep ? ( <>
- Step {stepNumber} - {totalSteps ? `/${totalSteps}` : ""}: {currentStep.name} + {t("agents.step", "Step {{number}}{{total}}: {{name}}", { number: stepNumber, total: totalSteps ? `/${totalSteps}` : "", name: currentStep.name })}
{executorModel && (
@@ -135,11 +136,11 @@ function LiveAgentCard({ agent, projectId, onSelect, onOpenTaskLogs }: LiveAgent
)}
- {isConnected ? "Waiting for output..." : "Connecting to log stream..."} + {isConnected ? t("agents.waitingOutput", "Waiting for output...") : t("agents.connectingStream", "Connecting to log stream...")}
) : ( - {isConnected ? "Waiting for output..." : "Connecting..."} + {isConnected ? t("agents.waitingOutput", "Waiting for output...") : t("agents.connecting", "Connecting...")} )}
) : ( @@ -167,11 +168,11 @@ function LiveAgentCard({ agent, projectId, onSelect, onOpenTaskLogs }: LiveAgent type="button" className="live-agent-card-logs-btn" onClick={handleViewLogs} - title="View live run logs" - aria-label={`View live logs for ${agent.taskId}`} + title={t("agents.viewLiveLogs", "View live run logs")} + aria-label={t("agents.viewLogsFor", "View live logs for {{taskId}}", { taskId: agent.taskId })} > - Live logs + {t("agents.liveLogs", "Live logs")} )} {isConnected && } @@ -196,6 +197,7 @@ interface ActiveAgentsPanelProps { } export function ActiveAgentsPanel({ agents, projectId, onAgentSelect, onOpenTaskLogs, className = "" }: ActiveAgentsPanelProps) { + const { t } = useTranslation("app"); // Dedupe by id defensively. The store should return unique agents but a race // between the initial fetch and an SSE refresh can briefly surface the same // agent twice — without this guard React floods the console with duplicate @@ -208,7 +210,7 @@ export function ActiveAgentsPanel({ agents, projectId, onAgentSelect, onOpenTask
- Active Agents ({uniqueAgents.length}) + {t("agents.activeAgents", "Active Agents ({{count}})", { count: uniqueAgents.length })}
{uniqueAgents.map(agent => ( diff --git a/packages/dashboard/app/components/ActivityLogModal.tsx b/packages/dashboard/app/components/ActivityLogModal.tsx index 4af53d8f94..59ff1ac9e9 100644 --- a/packages/dashboard/app/components/ActivityLogModal.tsx +++ b/packages/dashboard/app/components/ActivityLogModal.tsx @@ -2,6 +2,7 @@ // in ScriptsModal.css. Until extracted, import that file so this eager modal is styled. import "./ScriptsModal.css"; import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { X, History, Trash2, Filter, RefreshCw, CheckCircle, XCircle, ArrowRight, Plus, Settings, AlertCircle, Loader2, Folder } from "lucide-react"; import { clearActivityLog, type ActivityLogEntry, type ActivityEventType, type ActivityFeedEntry } from "../api"; import { useActivityLog } from "../hooks/useActivityLog"; @@ -91,7 +92,7 @@ function formatTimestamp(timestamp: string): string { * - Event type filter * - Real-time updates via useActivityLog hook */ -export function ActivityLogModal({ +export function ActivityLogModal({ isOpen, onClose, tasks: _tasks, @@ -101,6 +102,7 @@ export function ActivityLogModal({ onProjectFilterChange, currentProject, }: ActivityLogModalProps) { + const { t } = useTranslation("app"); const [filteredType, setFilteredType] = useState("all"); const [filteredProjectId, setFilteredProjectId] = useState(projectId || "all"); const [showConfirmClear, setShowConfirmClear] = useState(false); @@ -209,7 +211,7 @@ export function ActivityLogModal({
- Activity Log + {t("activityLog.title", "Activity Log")}
{/* Project filter dropdown (when projects provided) */} @@ -222,7 +224,7 @@ export function ActivityLogModal({ className="activity-log-filter-select" data-testid="activity-project-filter" > - + {projects.map((project) => ( + {Object.entries(EVENT_TYPE_LABELS).map(([type, label]) => (
)} @@ -324,9 +326,9 @@ export function ActivityLogModal({

- {isFilterActive - ? "No activity matches the current filters" - : "No activity recorded yet"} + {isFilterActive + ? t("activityLog.noMatchingActivity", "No activity matches the current filters") + : t("activityLog.noActivityRecorded", "No activity recorded yet")}

{isFilterActive && ( )}
@@ -402,7 +404,7 @@ export function ActivityLogModal({ onClick={refresh} data-testid="activity-load-more" > - Load More + {t("activityLog.loadMore", "Load More")} )} @@ -417,20 +419,20 @@ export function ActivityLogModal({ {showConfirmClear && (
-

Clear Activity Log?

-

This will permanently delete all activity log entries. This action cannot be undone.

+

{t("activityLog.confirmClear", "Clear Activity Log?")}

+

{t("activityLog.confirmClearMessage", "This will permanently delete all activity log entries. This action cannot be undone.")}

diff --git a/packages/dashboard/app/components/AddNodeModal.tsx b/packages/dashboard/app/components/AddNodeModal.tsx index 457e62e370..3b2517e8a2 100644 --- a/packages/dashboard/app/components/AddNodeModal.tsx +++ b/packages/dashboard/app/components/AddNodeModal.tsx @@ -1,4 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; +import type { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import type { NodeProjectMappingInput, ProjectInfo, RemoteNodeDiscoveredProject, RemoteNodeProjectDiscoveryResult } from "../api"; import { validateProjectPath } from "../utils/projectDetection"; import type { ToastType } from "../hooks/useToast"; @@ -47,25 +49,25 @@ interface FormErrors { const MAX_CONCURRENT_MIN = 1; const MAX_CONCURRENT_MAX = 10; -function validateInput(input: AddNodeInput): FormErrors { +function validateInput(input: AddNodeInput, t: TFunction<"app">): FormErrors { const errors: FormErrors = { projectMappings: {} }; if (!input.name.trim()) { - errors.name = "Name is required"; + errors.name = t("nodes.nameRequired", "Name is required"); } if (input.type === "remote" && !input.url?.trim()) { - errors.url = "URL is required for remote nodes"; + errors.url = t("nodes.urlRequired", "URL is required for remote nodes"); } if (!Number.isFinite(input.maxConcurrent) || input.maxConcurrent < MAX_CONCURRENT_MIN || input.maxConcurrent > MAX_CONCURRENT_MAX) { - errors.maxConcurrent = `Concurrency must be between ${MAX_CONCURRENT_MIN} and ${MAX_CONCURRENT_MAX}`; + errors.maxConcurrent = t("nodes.concurrencyRange", "Concurrency must be between {{min}} and {{max}}", { min: MAX_CONCURRENT_MIN, max: MAX_CONCURRENT_MAX }); } for (const mapping of input.projectMappings) { const validation = validateProjectPath(mapping.path); if (!validation.valid) { - errors.projectMappings[mapping.projectId] = validation.error ?? "Path is invalid"; + errors.projectMappings[mapping.projectId] = validation.error ?? t("nodes.pathInvalid", "Path is invalid"); } } @@ -75,6 +77,7 @@ function validateInput(input: AddNodeInput): FormErrors { type DiscoveryState = "idle" | "loading" | "success" | "error"; export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjects, addToast, projects }: AddNodeModalProps) { + const { t } = useTranslation("app"); useMobileScrollLock(isOpen); const [name, setName] = useState(""); const [type, setType] = useState<"local" | "remote">("local"); @@ -143,7 +146,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec const trimmedUrl = url.trim(); if (!trimmedUrl) { - setErrors((current) => ({ ...current, url: "URL is required for remote nodes" })); + setErrors((current) => ({ ...current, url: t("nodes.urlRequired", "URL is required for remote nodes") })); return; } @@ -175,9 +178,9 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec } catch (error) { setDiscoveryState("error"); setDiscoveredProjects([]); - setDiscoveryError(error instanceof Error ? error.message : "Failed to discover remote projects"); + setDiscoveryError(error instanceof Error ? error.message : t("nodes.discoveryFailed", "Failed to discover remote projects")); } - }, [apiKey, apiKeyMode, discoveryState, isSubmitting, onDiscoverRemoteProjects, projects, url]); + }, [apiKey, apiKeyMode, discoveryState, isSubmitting, onDiscoverRemoteProjects, projects, t, url]); useEffect(() => { if (type !== "remote") { @@ -195,7 +198,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec const handleSubmit = useCallback(async () => { if (isSubmitting) return; - const validationErrors = validateInput(input); + const validationErrors = validateInput(input, t); setErrors(validationErrors); if ( @@ -208,7 +211,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec } if (input.type === "remote" && discoveryState !== "success") { - setDiscoveryError("Discover remote projects before adding this node."); + setDiscoveryError(t("nodes.discoverBeforeAdding", "Discover remote projects before adding this node.")); return; } @@ -216,15 +219,15 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec try { await onSubmit(input); - addToast(`Node "${input.name}" registered`, "success"); + addToast(t("nodes.registered", "Node \"{{name}}\" registered", { name: input.name }), "success"); closeModal(); } catch (error) { - const message = error instanceof Error ? error.message : "Failed to register node"; + const message = error instanceof Error ? error.message : t("nodes.registerFailed", "Failed to register node"); addToast(message, "error"); } finally { setIsSubmitting(false); } - }, [addToast, closeModal, discoveryState, input, isSubmitting, onSubmit]); + }, [addToast, closeModal, discoveryState, input, isSubmitting, onSubmit, t]); const toggleProjectSelection = (project: ProjectInfo) => { setSelectedProjectPaths((current) => { @@ -253,25 +256,25 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec return (
-
event.stopPropagation()} role="dialog" aria-modal="true" aria-label="Add Node"> +
event.stopPropagation()} role="dialog" aria-modal="true" aria-label={t("nodes.addNode", "Add Node")}>
-

Add Node

-
-

Register an existing Fusion node by providing its connection details and concurrency settings.

+

{t("nodes.description", "Register an existing Fusion node by providing its connection details and concurrency settings.")}

{type === "remote" && (
@@ -384,7 +387,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec )}
-

Attach Existing Projects

-

Select existing projects to run on this node and provide the node-specific absolute path for each one.

+

{t("nodes.attachProjects", "Attach Existing Projects")}

+

{t("nodes.attachProjectsHint", "Select existing projects to run on this node and provide the node-specific absolute path for each one.")}

{projects.length === 0 ? ( -

No projects are currently registered.

+

{t("nodes.noProjects", "No projects are currently registered.")}

) : (
{projects.map((project) => { @@ -422,18 +425,18 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec {selected && (
- +
diff --git a/packages/dashboard/app/components/AgentDetailView.tsx b/packages/dashboard/app/components/AgentDetailView.tsx index 20d1187e6c..94a89cf09c 100644 --- a/packages/dashboard/app/components/AgentDetailView.tsx +++ b/packages/dashboard/app/components/AgentDetailView.tsx @@ -1,6 +1,7 @@ import "./AgentDetailView.css"; import "./MailboxModal.css"; import { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Bot, Heart, Activity, Pause, Play, Square, Trash2, RefreshCw, Settings, FileText, ActivitySquare, X, Copy, @@ -109,18 +110,6 @@ const RUN_STATUS_ICONS: Record = { - "long-term": "Long-term", - daily: "Daily", - dreams: "Dreams", -}; - -const MEMORY_LAYER_DESCRIPTIONS: Record = { - "long-term": "Curated durable decisions, conventions, constraints, and pitfalls for this specific agent.", - daily: "Raw daily observations and open loops recorded by this agent.", - dreams: "Synthesized patterns and emerging themes distilled from this agent's daily memory.", -}; - const DEFAULT_HEARTBEAT_INTERVAL_LABEL = formatHeartbeatInterval(DEFAULT_HEARTBEAT_INTERVAL_MS); const CONFIG_AUTOSAVE_DEBOUNCE_MS = 700; @@ -135,6 +124,7 @@ function pickDefaultAgentMemoryPath(files: MemoryFileInfo[], currentPath: string } export function AgentDetailView({ agentId, projectId, onClose, addToast, onChildClick, inline = false, showInlineBackButton = false, initialTab, initialRunId, preferActiveRun = false, onMutationSuccess }: AgentDetailViewProps) { + const { t } = useTranslation("app"); const [agent, setAgent] = useState(null); const { confirm } = useConfirm(); const [logs, setLogs] = useState([]); @@ -511,11 +501,11 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild try { await updateAgentState(agentId, newState, projectId); - addToast(`Agent state updated to ${newState}`, "success"); + addToast(t("agents.stateUpdated", "Agent state updated to {{newState}}", { newState }), "success"); await handleSavedMutation(); } catch (err) { setAgent((prev) => (prev ? { ...prev, state: previousState } : prev)); - addToast(`Failed to update state: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.stateUpdateFailed", "Failed to update state: {{error}}", { error: getErrorMessage(err) }), "error"); } finally { setIsTransitioning(false); } @@ -537,13 +527,13 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild const skippedCount = nonEphemeralAgents.length - eligibleAgents.length; if (eligibleAgents.length === 0) { - addToast(`No agents eligible to ${targetState === "paused" ? "pause" : "resume"}`, "error"); + addToast(t("agents.bulkNoEligible", "No agents eligible to {{action}}", { action: targetState === "paused" ? t("agents.pause", "pause") : t("agents.resume", "resume") }), "error"); return; } const confirmed = await confirm({ - title: targetState === "paused" ? "Pause All Agents" : "Resume All Agents", - message: `${targetState === "paused" ? "Pause" : "Resume"} ${eligibleAgents.length} agent${eligibleAgents.length === 1 ? "" : "s"} in this project?`, + title: targetState === "paused" ? t("agents.pauseAllTitle", "Pause All Agents") : t("agents.resumeAllTitle", "Resume All Agents"), + message: t("agents.bulkConfirmMessage", "{{action}} {{count}} agent(s) in this project?", { action: targetState === "paused" ? t("agents.pauseAction", "Pause") : t("agents.resumeAction", "Resume"), count: eligibleAgents.length }), danger: targetState === "paused", }); if (!confirmed) return; @@ -558,21 +548,21 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild const successCount = results.length - failedResults.length; const failureCount = failedResults.length; - const baseSummary = `${targetState === "paused" ? "Paused" : "Resumed"} ${successCount} agent${successCount === 1 ? "" : "s"}; skipped ${skippedCount}`; + const baseSummary = t("agents.bulkResult", "{{action}} {{successCount}} agent(s); skipped {{skippedCount}}", { action: targetState === "paused" ? t("agents.pausedPast", "Paused") : t("agents.resumedPast", "Resumed"), successCount, skippedCount }); if (failureCount > 0) { const failureSummary = failedResults .slice(0, 3) .map(({ agent, result }) => `${agent.name || agent.id}: ${getErrorMessage(result.reason)}`) .join("; "); - addToast(`${baseSummary}; failed ${failureCount}${failureSummary ? ` (${failureSummary})` : ""}`, "error"); + addToast(t("agents.bulkResultWithFailures", "{{summary}}; failed {{failureCount}}{{detail}}", { summary: baseSummary, failureCount, detail: failureSummary ? ` (${failureSummary})` : "" }), "error"); } else { addToast(baseSummary, "success"); } await handleSavedMutation(); } catch (err) { - addToast(`Failed to ${targetState === "paused" ? "pause" : "resume"} agents: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.bulkActionFailed", "Failed to {{action}} agents: {{error}}", { action: targetState === "paused" ? t("agents.pause", "pause") : t("agents.resume", "resume"), error: getErrorMessage(err) }), "error"); } finally { setIsBulkActionRunning(false); } @@ -583,10 +573,10 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild setIsStartingRun(true); try { await startAgentRun(agentId, projectId, { source: "on_demand", triggerDetail: "Triggered from dashboard" }); - addToast(`Heartbeat run started for ${agent?.name ?? agentId}`, "success"); + addToast(t("agents.heartbeatStarted", "Heartbeat run started for {{name}}", { name: agent?.name ?? agentId }), "success"); setRunNowRefreshToken((prev) => prev + 1); } catch (err) { - addToast(`Failed to start heartbeat run: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.heartbeatStartFailed", "Failed to start heartbeat run: {{error}}", { error: getErrorMessage(err) }), "error"); } finally { setIsStartingRun(false); } @@ -595,18 +585,18 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild const handleDelete = async () => { if (!agent) return; const shouldDelete = await confirm({ - title: "Delete Agent", - message: `Delete agent "${agent.name}"? This cannot be undone.`, + title: t("agents.deleteTitle", "Delete Agent"), + message: t("agents.deleteConfirm", "Delete agent \"{{name}}\"? This cannot be undone.", { name: agent.name }), danger: true, }); if (!shouldDelete) return; try { await deleteAgent(agentId, projectId); - addToast(`Agent "${agent.name}" deleted`, "success"); + addToast(t("agents.deleted", "Agent \"{{name}}\" deleted", { name: agent.name }), "success"); await notifyMutationSuccess(true); onClose(); } catch (err) { - addToast(`Failed to delete agent: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.deleteFailed", "Failed to delete agent: {{error}}", { error: getErrorMessage(err) }), "error"); } }; @@ -627,7 +617,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild const copyAgentId = () => { if (agent) { navigator.clipboard.writeText(agent.id); - addToast("Agent ID copied to clipboard", "success"); + addToast(t("agents.idCopied", "Agent ID copied to clipboard"), "success"); } }; @@ -637,7 +627,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
- Loading agent... + {t("agents.loading", "Loading agent...")}
); @@ -657,7 +647,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
- Loading agent... + {t("agents.loading", "Loading agent...")}
@@ -692,10 +682,10 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild type="button" className="btn agent-detail-inline-back" onClick={onClose} - aria-label="Back to agents" + aria-label={t("agents.backToAgents", "Back to agents")} > - Agents + {t("agents.agentsLabel", "Agents")} ) : null}
@@ -726,65 +716,65 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild <> )} {agent.state === "active" && ( <> - - )} {agent.state === "paused" && ( <> - )} {agent.state === "running" && ( <> - - )} @@ -792,11 +782,11 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild <> - )} @@ -811,8 +801,8 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild onClick={() => setIsBulkMenuOpen((open) => !open)} aria-haspopup="menu" aria-expanded={isBulkMenuOpen} - aria-label="Bulk agent actions" - title="Bulk agent actions" + aria-label={t("agents.bulkActions", "Bulk agent actions")} + title={t("agents.bulkActions", "Bulk agent actions")} disabled={isTransitioning || isBulkActionRunning} > @@ -826,13 +816,13 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild onClick={() => void handleBulkStateChange("paused")} disabled={isPauseAllDisabled || isBulkActionRunning} > - Pause All Agents + {t("agents.pauseAll", "Pause All Agents")} {isBulkEligibilityLoading - ? "Loading eligible agents..." + ? t("agents.loadingEligible", "Loading eligible agents...") : isPauseAllDisabled - ? "No active agents eligible" - : `Pause ${bulkPauseEligibleCount} active/running agent${bulkPauseEligibleCount === 1 ? "" : "s"}`} + ? t("agents.noActiveEligible", "No active agents eligible") + : t("agents.pauseCountHint", "Pause {{count}} active/running agent(s)", { count: bulkPauseEligibleCount })}
)}
- {!inline && ( - )} @@ -868,16 +858,31 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild {/* Tabs */}
- {TABS.map(tab => ( - - ))} + {TABS.map(tab => { + const tabLabels: Record = { + dashboard: t("agents.tabDashboard", "Dashboard"), + logs: t("agents.tabLogs", "Logs"), + mail: t("agents.tabMail", "Mail"), + runs: t("agents.tabRuns", "Runs"), + tasks: t("agents.tabTasks", "Tasks"), + employees: t("agents.tabEmployees", "Employees"), + soul: t("agents.tabSoul", "Soul"), + instructions: t("agents.tabInstructions", "Instructions"), + memory: t("agents.tabMemory", "Agent Memory"), + reflections: t("agents.tabReflections", "Evaluation"), + config: t("agents.tabConfig", "Settings"), + }; + return ( + + ); + })}
{/* Tab Content */} @@ -896,7 +901,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild logs={logs} isStreaming={isStreaming} hasTask={!!agent.taskId || logs.length > 0 || latestRun !== null} - fallbackLabel={!agent.taskId && latestRun ? `Latest run · ${latestRun.id.slice(0, 8)}` : null} + fallbackLabel={!agent.taskId && latestRun ? t("agents.latestRunLabel", "Latest run · {{id}}", { id: latestRun.id.slice(0, 8) }) : null} /> )} @@ -996,7 +1001,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild {/* Footer with agent ID */} {!inline && (
- @@ -1005,7 +1010,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild {agent.taskId && ( <> | - Working on: + {t("agents.workingOn", "Working on:")} {agent.taskId} @@ -1021,17 +1026,18 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild // ── Dashboard Tab ─────────────────────────────────────────────────────────── -function DashboardTab({ - agent, +function DashboardTab({ + agent, health, onChildClick, projectId, -}: { - agent: AgentDetail; +}: { + agent: AgentDetail; health: AgentHealthStatus; onChildClick?: (childId: string) => void; projectId?: string; }) { + const { t } = useTranslation("app"); const stateStyle = STATE_COLORS[agent.state]; const [chainOfCommand, setChainOfCommand] = useState([]); const [isLoadingChainOfCommand, setIsLoadingChainOfCommand] = useState(true); @@ -1168,34 +1174,34 @@ function DashboardTab({ {budgetStatus?.isOverBudget && (
⚠️ - Budget Exhausted: This agent has exceeded its token budget and may operate with limited functionality. + {t("agents.budgetExhaustedTitle", "Budget Exhausted:")} {t("agents.budgetExhaustedBody", "This agent has exceeded its token budget and may operate with limited functionality.")}
)}
-

Overview

+

{t("agents.overview", "Overview")}

{agent.name} {agent.state}
{health.icon} {health.label} {(agent.pendingApprovalCount ?? 0) > 0 ? ( - + - {agent.pendingApprovalCount} pending approvals + {t("agents.pendingApprovalsCount", "{{count}} pending approvals", { count: agent.pendingApprovalCount })} ) : null} - Role: {agent.role} + {t("agents.roleLabel", "Role: {{role}}", { role: agent.role })} - {runtimeHint ? "Runtime" : "Model"} - {modelDisplay ?? "Auto"} + {runtimeHint ? t("agents.runtime", "Runtime") : t("agents.model", "Model")} + {modelDisplay ?? t("agents.auto", "Auto")} {agentSkills.length > 0 ? ( - Skills - + {t("agents.skills", "Skills")} + {agentSkills.map((skillId) => { const isSelected = selectedSkillId === skillId; return ( @@ -1206,7 +1212,7 @@ function DashboardTab({ title={skillId} onClick={() => handleSkillBadgeClick(skillId)} aria-expanded={isSelected} - aria-label={`View details for ${formatAgentSkillBadgeLabel(skillId)}`} + aria-label={t("agents.viewSkillDetails", "View details for {{skill}}", { skill: formatAgentSkillBadgeLabel(skillId) })} > {formatAgentSkillBadgeLabel(skillId)} @@ -1215,7 +1221,7 @@ function DashboardTab({ ) : ( - Skills: — + {t("agents.skillsNone", "Skills: —")} )}
{selectedSkillId ? ( @@ -1228,70 +1234,70 @@ function DashboardTab({ onClick={() => handleSkillBadgeClick(selectedSkillId)} > - Close + {t("common.close", "Close")}
{isLoadingSkillContent ? (
- Loading skill content... + {t("agents.loadingSkillContent", "Loading skill content...")}
) : skillContentError ? (
{skillContentError}
) : selectedSkillContent ? ( -
{selectedSkillContent.skillMd || "(No SKILL.md found)"}
+
{selectedSkillContent.skillMd || t("agents.noSkillMd", "(No SKILL.md found)")}
) : ( -
No skill content available
+
{t("agents.noSkillContent", "No skill content available")}
)}
) : null}
-

Heartbeat & Health

+

{t("agents.heartbeatAndHealth", "Heartbeat & Health")}

-

Last heartbeat

-

{agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "Never"}

+

{t("agents.lastHeartbeat", "Last heartbeat")}

+

{agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : t("agents.never", "Never")}

-

Next expected

-

{nextHeartbeatAt ? relativeTime(nextHeartbeatAt) : "Not scheduled"}

+

{t("agents.nextExpected", "Next expected")}

+

{nextHeartbeatAt ? relativeTime(nextHeartbeatAt) : t("agents.notScheduled", "Not scheduled")}

-

Interval

+

{t("agents.interval", "Interval")}

{formatHeartbeatInterval(heartbeatIntervalMs)}

-

Status

+

{t("agents.status", "Status")}

{health.label}{health.reason && ({health.reason})}

-

Current Work

+

{t("agents.currentWork", "Current Work")}

{agent.taskId ? (
) : ( -

No active assignment

+

{t("agents.noActiveAssignment", "No active assignment")}

)}
-

Recent Runs

-

{stats.successfulRuns}/{stats.totalRuns} successful ({stats.successRate}%)

+

{t("agents.recentRuns", "Recent Runs")}

+

{t("agents.runsSuccessRate", "{{successful}}/{{total}} successful ({{rate}}%)", { successful: stats.successfulRuns, total: stats.totalRuns, rate: stats.successRate })}

{recentRuns.length === 0 ? ( -

No runs yet

+

{t("agents.noRunsYet", "No runs yet")}

) : (
{recentRuns.map((run) => { @@ -1310,28 +1316,28 @@ function DashboardTab({
-

Throughput

+

{t("agents.throughput", "Throughput")}

-
{stats.totalRuns}
Total Runs
-
{stats.todayRuns}
Runs Today
-
{stats.successRate}%
Success Rate
+
{stats.totalRuns}
{t("agents.totalRuns", "Total Runs")}
+
{stats.todayRuns}
{t("agents.runsToday", "Runs Today")}
+
{stats.successRate}%
{t("agents.successRate", "Success Rate")}
-

Chain of Command

+

{t("agents.chainOfCommand", "Chain of Command")}

{isLoadingChainOfCommand ? ( -
Loading reporting chain...
+
{t("agents.loadingReportingChain", "Loading reporting chain...")}
) : chainOfCommand.length <= 1 ? ( -

No reporting chain

+

{t("agents.noReportingChain", "No reporting chain")}

) : ( -
+
{chainOfCommand.map((chainAgent, index) => { const isCurrent = index === chainOfCommand.length - 1; const isAncestor = !isCurrent; return (
- {!isCurrent && } @@ -1358,14 +1364,16 @@ function LogsTab({ hasTask: boolean; fallbackLabel?: string | null; }) { + const { t } = useTranslation("app"); + if (!hasTask) { return (
-

No activity yet

+

{t("agents.noActivityYet", "No activity yet")}

- Agent logs will appear here from the current task or most recent run + {t("agents.logsWillAppear", "Agent logs will appear here from the current task or most recent run")}

@@ -1375,23 +1383,23 @@ function LogsTab({ return (
- {logs.length} entries + {t("agents.logEntries", "{{count}} entries", { count: logs.length })} {fallbackLabel && ( {fallbackLabel} )} {isStreaming && ( - Live + {t("agents.live", "Live")} )}
{logs.length === 0 ? (
-

No log entries yet

+

{t("agents.noLogEntriesYet", "No log entries yet")}

- {isStreaming ? "Waiting for activity..." : "Logs will appear here when the agent is active"} + {isStreaming ? t("agents.waitingForActivity", "Waiting for activity...") : t("agents.logsWillAppearActive", "Logs will appear here when the agent is active")}

) : ( @@ -1448,6 +1456,7 @@ function MailTab({ addToast?: (message: string, type?: "success" | "error") => void; onRefresh: () => void; }) { + const { t } = useTranslation("app"); const [activeSubtab, setActiveSubtab] = useState<"inbox" | "outbox">("inbox"); const [knownAgents, setKnownAgents] = useState([]); @@ -1535,23 +1544,23 @@ function MailTab({ {activeSubtab === "inbox" ? ( {mailboxParticipantLabel(message.fromId, message.fromType, agentNamesById)} ) : ( - To: {mailboxParticipantLabel(message.toId, message.toType, agentNamesById)} + {t("agents.mailTo", "To: {{recipient}}", { recipient: mailboxParticipantLabel(message.toId, message.toType, agentNamesById) })} )} {formatMailboxTimestamp(message.createdAt)}
{message.content.slice(0, 80)}{message.content.length > 80 ? "…" : ""}
- {activeSubtab === "inbox" && !message.read ?
: null} + {activeSubtab === "inbox" && !message.read ?
: null} ); return (
-

{agent.name} Mail

+

{t("agents.agentMail", "{{name}} Mail", { name: agent.name })}

@@ -1561,7 +1570,7 @@ function MailTab({ onClick={() => setActiveSubtab("inbox")} > - Inbox + {t("agents.inbox", "Inbox")} {(mailbox?.unreadCount ?? 0) > 0 ? {mailbox?.unreadCount} : null}
{isLoading && !mailbox ? (
- Loading mailbox... + {t("agents.loadingMailbox", "Loading mailbox...")}
) : null} {!isLoading && error ? (
- Failed to load mailbox: {error} + {t("agents.mailboxLoadFailed", "Failed to load mailbox: {{error}}", { error })}
) : null} @@ -1597,27 +1606,27 @@ function MailTab({ onClick={() => setSelectedMessageId(null)} > - Back to {activeSubtab === "inbox" ? "Inbox" : "Outbox"} + {activeSubtab === "inbox" ? t("agents.backToInbox", "Back to Inbox") : t("agents.backToOutbox", "Back to Outbox")}
- From + {t("agents.mailFrom", "From")} {mailboxParticipantLabel(selectedMessage.fromId, selectedMessage.fromType, agentNamesById)}
- To + {t("agents.mailToLabel", "To")} {mailboxParticipantLabel(selectedMessage.toId, selectedMessage.toType, agentNamesById)}
- Type + {t("agents.mailType", "Type")} {selectedMessage.type}
- Sent + {t("agents.mailSent", "Sent")} {new Date(selectedMessage.createdAt).toLocaleString()}
{selectedMessage.metadata?.replyTo?.messageId ? ( -
↪ Replying to message {selectedMessage.metadata.replyTo.messageId}
+
{t("agents.replyingTo", "↪ Replying to message {{id}}", { id: selectedMessage.metadata.replyTo.messageId })}
) : null}
{selectedMessage.content}
@@ -1627,7 +1636,7 @@ function MailTab({ {messages.length === 0 ? (
{activeSubtab === "inbox" ? : } -

{activeSubtab === "inbox" ? "No received messages for this agent" : "No sent messages for this agent"}

+

{activeSubtab === "inbox" ? t("agents.noInboxMessages", "No received messages for this agent") : t("agents.noOutboxMessages", "No sent messages for this agent")}

) : ( messages.map(renderMessage) @@ -1656,7 +1665,7 @@ interface AgentTokenUsageSummary { allTime: AgentTokenUsageWindowSummary; } -function RunsTab({ +function RunsTab({ addToast, agentId, projectId, @@ -1666,7 +1675,7 @@ function RunsTab({ preferActiveRun, runNowRefreshToken, isEphemeral, -}: { +}: { addToast: (msg: string, type?: "success" | "error") => void; agentId: string; projectId?: string; @@ -1677,6 +1686,7 @@ function RunsTab({ runNowRefreshToken: number; isEphemeral: boolean; }) { + const { t } = useTranslation("app"); const [runs, setRuns] = useState([]); const { confirm } = useConfirm(); const [isLoadingRuns, setIsLoadingRuns] = useState(true); @@ -1810,7 +1820,7 @@ function RunsTab({ setRunLogs(logs); setDetailRun(detail); } catch (err) { - addToast(`Failed to load run details: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.runDetailsFailed", "Failed to load run details: {{error}}", { error: getErrorMessage(err) }), "error"); setRunLogs([]); setDetailRun(null); } finally { @@ -1840,8 +1850,8 @@ function RunsTab({ const handleStopRun = async () => { const shouldStop = await confirm({ - title: "Stop Active Run", - message: "Stop the active run? The agent's work will be interrupted.", + title: t("agents.stopRunTitle", "Stop Active Run"), + message: t("agents.stopRunConfirm", "Stop the active run? The agent's work will be interrupted."), danger: true, }); if (!shouldStop) { @@ -1850,11 +1860,11 @@ function RunsTab({ try { await stopAgentRun(agentId, projectId); - addToast("Run stopped", "success"); + addToast(t("agents.runStopped", "Run stopped"), "success"); setIsLoadingRuns(true); void loadRuns(); } catch (err) { - addToast(`Failed to stop run: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.stopRunFailed", "Failed to stop run: {{error}}", { error: getErrorMessage(err) }), "error"); } }; @@ -1863,7 +1873,7 @@ function RunsTab({
- Loading runs... + {t("agents.loadingRuns", "Loading runs...")}
); @@ -1874,8 +1884,8 @@ function RunsTab({
-

No runs yet

-

Heartbeat runs will appear here

+

{t("agents.noRunsYet", "No runs yet")}

+

{t("agents.heartbeatRunsWillAppear", "Heartbeat runs will appear here")}

); @@ -1892,10 +1902,10 @@ function RunsTab({ if (!usage) return null; return (
- Input: {usage.inputTokens.toLocaleString()} - Output: {usage.outputTokens.toLocaleString()} - {usage.cachedTokens > 0 && Cache read: {usage.cachedTokens.toLocaleString()}} - {(usage.cacheWriteTokens ?? 0) > 0 && Cache write: {(usage.cacheWriteTokens ?? 0).toLocaleString()}} + {t("agents.inputTokens", "Input: {{value}}", { value: usage.inputTokens.toLocaleString() })} + {t("agents.outputTokens", "Output: {{value}}", { value: usage.outputTokens.toLocaleString() })} + {usage.cachedTokens > 0 && {t("agents.cacheReadTokens", "Cache read: {{value}}", { value: usage.cachedTokens.toLocaleString() })}} + {(usage.cacheWriteTokens ?? 0) > 0 && {t("agents.cacheWriteTokens", "Cache write: {{value}}", { value: (usage.cacheWriteTokens ?? 0).toLocaleString() })}}
); }; @@ -1903,9 +1913,9 @@ function RunsTab({ const renderRunCard = (run: AgentHeartbeatRun, index: number, isActive: boolean) => { const statusInfo = RUN_STATUS_ICONS[run.status] || RUN_STATUS_ICONS.completed; const StatusIcon = statusInfo.icon; - const duration = run.endedAt + const duration = run.endedAt ? formatDuration(new Date(run.startedAt), new Date(run.endedAt)) - : "In progress"; + : t("agents.inProgress", "In progress"); const isSelected = selectedRunId === run.id; return ( @@ -1916,7 +1926,7 @@ function RunsTab({ role="button" tabIndex={0} aria-expanded={isSelected} - aria-label={`${isActive ? "Active" : ""} run ${run.id.slice(0, 8)}, ${run.status}`} + aria-label={t("agents.runAriaLabel", "{{active}}run {{id}}, {{status}}", { active: isActive ? t("agents.activePrefix", "Active ") : "", id: run.id.slice(0, 8), status: run.status })} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); @@ -1930,7 +1940,7 @@ function RunsTab({ {isActive ? ( - Live Run + {t("agents.liveRun", "Live Run")} ) : ( #{index + 1} {run.id.slice(0, 8)} @@ -1950,9 +1960,9 @@ function RunsTab({ e.stopPropagation(); void handleStopRun(); }} - aria-label="Stop active run" + aria-label={t("agents.stopActiveRun", "Stop active run")} > - Stop + {t("agents.stop", "Stop")} )} @@ -1961,13 +1971,13 @@ function RunsTab({ {run.heartbeatProcedureSource === "custom" && ( - Heartbeat: custom + {t("agents.heartbeatCustom", "Heartbeat: custom")} )}
- Started {relativeTime(run.startedAt)} + {t("agents.runStarted", "Started {{time}}", { time: relativeTime(run.startedAt) })} • {duration} {run.triggerDetail && ( @@ -1984,18 +1994,18 @@ function RunsTab({ {isLoadingDetail ? (
- Loading details... + {t("agents.loadingDetails", "Loading details...")}
) : detailRun && (
{/* System Prompt */}
- System Prompt + {t("agents.systemPrompt", "System Prompt")} {detailRun.systemPrompt ? (
{detailRun.systemPrompt}
) : ( -
System prompt not captured for this run
+
{t("agents.systemPromptNotCaptured", "System prompt not captured for this run")}
)}
@@ -2003,11 +2013,11 @@ function RunsTab({ {/* Execution Prompt */}
- Execution Prompt + {t("agents.executionPrompt", "Execution Prompt")} {detailRun.executionPrompt ? (
{detailRun.executionPrompt}
) : ( -
Execution prompt not captured for this run
+
{t("agents.executionPromptNotCaptured", "Execution prompt not captured for this run")}
)}
@@ -2015,7 +2025,7 @@ function RunsTab({ {/* Token Usage */} {detailRun.usageJson && (
-
Token Usage
+
{t("agents.tokenUsage", "Token Usage")}
{renderUsage(detailRun.usageJson)}
)} @@ -2023,7 +2033,7 @@ function RunsTab({ {/* Output */} {detailRun.stdoutExcerpt && (
-
Output
+
{t("agents.output", "Output")}
                       {detailRun.stdoutExcerpt.length > 2000
                         ? `${detailRun.stdoutExcerpt.slice(0, 2000)}\n\n... (truncated, ${detailRun.stdoutExcerpt.length} chars total)`
@@ -2035,7 +2045,7 @@ function RunsTab({
                 {/* Errors */}
                 {detailRun.stderrExcerpt && (
                   
-
Errors
+
{t("agents.errors", "Errors")}
-
Result
+
{t("agents.result", "Result")}
{JSON.stringify(detailRun.resultJson, null, 2)}
)} @@ -2063,7 +2073,7 @@ function RunsTab({ {/* Context */} {detailRun.contextSnapshot && Object.keys(detailRun.contextSnapshot).length > 0 && (
-
Context
+
{t("agents.context", "Context")}
{Object.entries(detailRun.contextSnapshot).map(([key, value]) => ( @@ -2077,21 +2087,21 @@ function RunsTab({ {/* No output state */} {!detailRun.stdoutExcerpt && !detailRun.stderrExcerpt && !detailRun.resultJson && ( -
No output captured
+
{t("agents.noOutputCaptured", "No output captured")}
)}
)} {/* Run Logs */}
-
Agent Logs
+
{t("agents.agentLogs", "Agent Logs")}
{isLoadingLogs ? (
- Loading logs... + {t("agents.loadingLogs", "Loading logs...")}
) : runLogs.length === 0 ? ( -
No logs available for this run
+
{t("agents.noLogsForRun", "No logs available for this run")}
) : ( )} @@ -2124,9 +2134,9 @@ function RunsTab({
{promptSizes.length > 0 && latestPrompt && (
-
Prompt Size
+
{t("agents.promptSize", "Prompt Size")}
- + @@ -2138,27 +2148,27 @@ function RunsTab({ )} {tokenUsageSummary && (
-
Cache hit ratio
+
{t("agents.cacheHitRatio", "Cache hit ratio")}
- {renderCacheWindow("Last 24h", tokenUsageSummary.last24h)} - {renderCacheWindow("Last 7d", tokenUsageSummary.last7d)} - {renderCacheWindow("All time", tokenUsageSummary.allTime)} + {renderCacheWindow(t("agents.last24h", "Last 24h"), tokenUsageSummary.last24h)} + {renderCacheWindow(t("agents.last7d", "Last 7d"), tokenUsageSummary.last7d)} + {renderCacheWindow(t("agents.allTime", "All time"), tokenUsageSummary.allTime)}
)}
- {runs.length} run{runs.length !== 1 ? "s" : ""} - {hasActiveRun && Live} + {t("agents.runsCount", { count: runs.length, defaultValue_one: "{{count}} run", defaultValue_other: "{{count}} runs" })} + {hasActiveRun && {t("agents.live", "Live")}}
{hasActiveRun && ( )}
@@ -2177,15 +2187,6 @@ function formatDuration(start: Date, end: Date): string { return `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`; } -const TASK_COLUMN_LABELS: Record = { - triage: "Triage", - todo: "Todo", - "in-progress": "In Progress", - "in-review": "In Review", - done: "Done", - archived: "Archived", -}; - function truncateTaskLabel(task: Task): string { const source = task.title?.trim() || task.description?.trim() || task.id; return source.length > 80 ? `${source.slice(0, 77)}...` : source; @@ -2200,6 +2201,7 @@ function TasksTab({ projectId?: string; addToast: (msg: string, type?: "success" | "error") => void; }) { + const { t } = useTranslation("app"); const [tasks, setTasks] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -2216,7 +2218,7 @@ function TasksTab({ .catch((err) => { if (!cancelled) { setTasks([]); - addToast(`Failed to load assigned tasks: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.loadTasksFailed", "Failed to load assigned tasks: {{error}}", { error: getErrorMessage(err) }), "error"); } }) .finally(() => { @@ -2234,7 +2236,7 @@ function TasksTab({ return (
-

Loading assigned tasks...

+

{t("agents.loadingTasks", "Loading assigned tasks...")}

); } @@ -2243,7 +2245,7 @@ function TasksTab({ return (
-

No tasks assigned to this agent

+

{t("agents.noTasksAssigned", "No tasks assigned to this agent")}

); } @@ -2254,13 +2256,22 @@ function TasksTab({
{task.id} - {TASK_COLUMN_LABELS[task.column]} + { + ({ + triage: t("board.triage", "Triage"), + todo: t("board.todo", "Todo"), + "in-progress": t("board.inProgress", "In Progress"), + "in-review": t("board.inReview", "In Review"), + done: t("board.done", "Done"), + archived: t("board.archived", "Archived"), + } as Record)[task.column] ?? task.column + }
{truncateTaskLabel(task)}
- {task.status ?? "idle"} · Updated {relativeTime(task.updatedAt)} + {task.status ?? "idle"} · {t("agents.taskUpdated", "Updated {{time}}", { time: relativeTime(task.updatedAt) })}
))} @@ -2368,6 +2379,7 @@ function SoulTab({ addToast: (message: string, type?: "success" | "error") => void; onSaved: () => Promise; }) { + const { t } = useTranslation("app"); const [soul, setSoul] = useState(agent.soul ?? ""); const [isSaving, setIsSaving] = useState(false); const [justSaved, setJustSaved] = useState(false); @@ -2392,14 +2404,14 @@ function SoulTab({ const handleSave = async () => { if (soul.length > 10000) { - addToast("Soul must be at most 10,000 characters", "error"); + addToast(t("agents.soulTooLong", "Soul must be at most 10,000 characters"), "error"); return; } setIsSaving(true); try { await updateAgentSoul(agent.id, soul, projectId); - addToast("Soul saved", "success"); + addToast(t("agents.soulSaved", "Soul saved"), "success"); setJustSaved(true); if (justSavedTimeoutRef.current) { clearTimeout(justSavedTimeoutRef.current); @@ -2407,7 +2419,7 @@ function SoulTab({ justSavedTimeoutRef.current = setTimeout(() => setJustSaved(false), 3000); await onSaved(); } catch (err) { - addToast(`Failed to save soul: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.soulSaveFailed", "Failed to save soul: {{error}}", { error: getErrorMessage(err) }), "error"); } finally { setIsSaving(false); } @@ -2416,33 +2428,33 @@ function SoulTab({ return (
-

Soul

+

{t("agents.soulTitle", "Soul")}

- Define this agent's personality and identity. + {t("agents.soulDescription", "Define this agent's personality and identity.")}

- +
@@ -2455,7 +2467,7 @@ function SoulTab({
) : (
- No soul defined yet. Switch to Edit mode to define the agent's personality. + {t("agents.soulEmptyPreview", "No soul defined yet. Switch to Edit mode to define the agent's personality.")}
) ) : ( @@ -2463,7 +2475,7 @@ function SoulTab({ id="agent-soul" className="input config-textarea-mono" rows={12} - placeholder="Describe this agent's personality, tone, and behavioral traits..." + placeholder={t("agents.soulPlaceholder", "Describe this agent's personality, tone, and behavioral traits...")} value={soul} onChange={(e) => { setSoul(e.target.value); @@ -2472,7 +2484,7 @@ function SoulTab({ /> )} {!showPreview && ( - Defines the agent's character and identity. Max 10,000 characters. + {t("agents.soulHint", "Defines the agent's character and identity. Max 10,000 characters.")} )}
@@ -2487,19 +2499,19 @@ function SoulTab({ {isSaving ? ( <> - Saving… + {t("common.saving", "Saving…")} ) : ( <> - Save Soul + {t("agents.saveSoul", "Save Soul")} )} {!hasChanges && justSaved && ( - Soul saved + {t("agents.soulSaved", "Soul saved")} )}
@@ -2520,6 +2532,7 @@ function MemoryTab({ addToast: (message: string, type?: "success" | "error") => void; onSaved: () => Promise; }) { + const { t } = useTranslation("app"); const [memory, setMemory] = useState(agent.memory ?? ""); const [isSaving, setIsSaving] = useState(false); const [justSaved, setJustSaved] = useState(false); @@ -2547,8 +2560,12 @@ function MemoryTab({ ); const selectedLayerDescription = selectedMemoryFile - ? MEMORY_LAYER_DESCRIPTIONS[selectedMemoryFile.layer] - : "Select a memory file to view or edit."; + ? ({ + "long-term": t("agents.memoryLayerLongTermDesc", "Curated durable decisions, conventions, constraints, and pitfalls for this specific agent."), + daily: t("agents.memoryLayerDailyDesc", "Raw daily observations and open loops recorded by this agent."), + dreams: t("agents.memoryLayerDreamsDesc", "Synthesized patterns and emerging themes distilled from this agent's daily memory."), + } as Record)[selectedMemoryFile.layer] ?? selectedMemoryFile.layer + : t("agents.selectMemoryFile", "Select a memory file to view or edit."); const loadSelectedMemoryFile = useCallback(async (path: string) => { setSelectedFileLoading(true); @@ -2559,7 +2576,7 @@ function MemoryTab({ setSelectedFileDirty(false); setSelectedFileJustSaved(false); } catch (err) { - addToast(`Failed to load agent memory file: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.memoryFileLoadFailed", "Failed to load agent memory file: {{error}}", { error: getErrorMessage(err) }), "error"); } finally { setSelectedFileLoading(false); } @@ -2581,7 +2598,7 @@ function MemoryTab({ const nextPath = pickDefaultAgentMemoryPath(files, preferredPath); await loadSelectedMemoryFile(nextPath); } catch (err) { - addToast(`Failed to load memory files: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.memoryFilesLoadFailed", "Failed to load memory files: {{error}}", { error: getErrorMessage(err) }), "error"); setMemoryFiles([]); setSelectedFilePath(""); setSelectedFileContent(""); @@ -2614,14 +2631,14 @@ function MemoryTab({ const handleSaveInlineMemory = async () => { if (memory.length > 50000) { - addToast("Memory must be at most 50,000 characters", "error"); + addToast(t("agents.memoryTooLong", "Memory must be at most 50,000 characters"), "error"); return; } setIsSaving(true); try { await updateAgentMemory(agent.id, memory, projectId); - addToast("Memory saved", "success"); + addToast(t("agents.memorySaved", "Memory saved"), "success"); setJustSaved(true); if (justSavedTimeoutRef.current) { clearTimeout(justSavedTimeoutRef.current); @@ -2629,7 +2646,7 @@ function MemoryTab({ justSavedTimeoutRef.current = setTimeout(() => setJustSaved(false), 3000); await onSaved(); } catch (err) { - addToast(`Failed to save memory: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.memorySaveFailed", "Failed to save memory: {{error}}", { error: getErrorMessage(err) }), "error"); } finally { setIsSaving(false); } @@ -2640,7 +2657,7 @@ function MemoryTab({ return; } if (selectedFileDirty) { - setFileSwitchHint("Save the current file before switching to another file."); + setFileSwitchHint(t("agents.saveBeforeSwitch", "Save the current file before switching to another file.")); return; } @@ -2664,10 +2681,10 @@ function MemoryTab({ selectedFileJustSavedTimeoutRef.current = setTimeout(() => setSelectedFileJustSaved(false), 3000); setFileSwitchHint(""); await loadMemoryFiles(selectedFilePath); - addToast("Agent memory file saved", "success"); + addToast(t("agents.memoryFileSaved", "Agent memory file saved"), "success"); await onSaved(); } catch (err) { - addToast(`Failed to save agent memory file: ${getErrorMessage(err)}`, "error"); + addToast(t("agents.memoryFileSaveFailed", "Failed to save agent memory file: {{error}}", { error: getErrorMessage(err) }), "error"); } finally { setSavingSelectedFile(false); } @@ -2676,21 +2693,21 @@ function MemoryTab({ return (
-

Agent Memory

+

{t("agents.memoryTitle", "Agent Memory")}

- Store context that belongs to this agent only. Workspace memory, daily notes, dreams, and qmd search live in project settings under Project Memory. + {t("agents.memoryDescription", "Store context that belongs to this agent only. Workspace memory, daily notes, dreams, and qmd search live in project settings under Project Memory.")}

{isReadOnly && (

- Read-only while this agent is running. + {t("agents.memoryReadOnly", "Read-only while this agent is running.")}

)}
- + - Short-form memory stored directly on the agent record and injected into prompts. + {t("agents.inlineMemoryHint", "Short-form memory stored directly on the agent record and injected into prompts.")}
@@ -2699,20 +2716,20 @@ function MemoryTab({ className={`btn btn-sm ${!showPreview ? "btn-primary" : ""}`} onClick={() => setShowPreview(false)} disabled={!showPreview} - aria-label="Edit mode" + aria-label={t("common.editMode", "Edit mode")} > - Edit + {t("common.edit", "Edit")} )}
@@ -2725,16 +2742,16 @@ function MemoryTab({
) : (
- No agent memory defined yet. Switch to Edit mode to add memory content. + {t("agents.memoryEmptyPreview", "No agent memory defined yet. Switch to Edit mode to add memory content.")}
) ) : (