diff --git a/apps/web/src/components/RoutinesSection.tsx b/apps/web/src/components/RoutinesSection.tsx index bcfd0325d..acff5c500 100644 --- a/apps/web/src/components/RoutinesSection.tsx +++ b/apps/web/src/components/RoutinesSection.tsx @@ -174,14 +174,56 @@ function formatRunTimestamp(ts: number): string { }); } -function runFailureReason(run: { - status: RoutineRun['status']; - error?: string | null; - summary?: string | null; -} | null | undefined): string | null { +// Maps known daemon-surfaced failure message prefixes onto i18n keys, so +// users running the app in a non-English locale see a translated reason +// instead of the raw English string the daemon ships to web (see e.g. +// `apps/daemon/src/server.ts` `createSseErrorPayload` callsites). We match +// on a stable prefix rather than the full string so any trailing context +// the daemon may append (e.g. `: `) is preserved verbatim +// after the localized prefix — losing it would hide debugging context the +// caller may rely on. Anything not in this list passes through untouched — +// untranslated failures continue to surface so behavior never regresses, +// they just stay in English until a matching i18n key is added here. +// Each entry is the *full current daemon message* (copied verbatim from +// the corresponding `createSseErrorPayload` call) paired with the i18n +// key that translates it. We match on full-string prefix so the current +// contract resolves to `t(key)` exactly; if the daemon later appends +// extra context (e.g. `: `), the extra text is +// preserved as a suffix on the translated message rather than dropped. +const KNOWN_DAEMON_ERROR_PREFIXES: ReadonlyArray = [ + [ + // Source: apps/daemon/src/server.ts `AGENT_EXECUTION_FAILED` payload. + 'Agent completed without producing any output. The model or provider may have returned an empty response — check the agent logs for upstream errors.', + 'routines.error.agentEmptyOutput', + ], +]; + +function localizeRoutineError(t: TranslateFn, raw: string): string { + for (const [prefix, key] of KNOWN_DAEMON_ERROR_PREFIXES) { + if (raw.startsWith(prefix)) { + // Preserve any suffix the daemon may have appended (e.g. provider + // detail after the known prefix). When the daemon emits exactly + // the prefix string today, `suffix` is empty and the result is + // just `t(key)`; nothing changes for the current contract. + const suffix = raw.slice(prefix.length); + return `${t(key)}${suffix}`; + } + } + return raw; +} + +function runFailureReason( + t: TranslateFn, + run: { + status: RoutineRun['status']; + error?: string | null; + summary?: string | null; + } | null | undefined, +): string | null { if (!run || run.status !== 'failed') return null; const reason = (run.error || run.summary || '').trim(); - return reason || null; + if (!reason) return null; + return localizeRoutineError(t, reason); } type FormState = { @@ -414,7 +456,7 @@ function RunHistory({ return (
    {runs.map((r) => { - const failureReason = runFailureReason(r); + const failureReason = runFailureReason(t, r); return (
  • @@ -763,7 +805,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) { : t('routines.targetCreate'); const isBusy = busyId === r.id; const isExpanded = expandedId === r.id; - const failureReason = runFailureReason(r.lastRun); + const failureReason = runFailureReason(t, r.lastRun); return (
  • diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index f2cd3cd78..ca04f8e51 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -967,6 +967,8 @@ export const en: Dict = { 'Delete this automation? Past runs and their projects are kept.', 'routines.errorPickProject': 'Pick a project to reuse, or switch to “Create new each run”', + 'routines.error.agentEmptyOutput': + 'Agent completed without producing any output. The model or provider may have returned an empty response — check the agent logs for upstream errors.', 'entry.helpAria': 'Help', 'entry.helpMenuAria': 'Help menu', 'entry.helpGetHelp': 'Get help on GitHub', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index 115f85b82..59e58f305 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -964,6 +964,8 @@ export const zhCN: Dict = { 'routines.status.canceled': '已取消', 'routines.confirmDelete': '删除此自动化?过往运行记录及其项目将予以保留。', 'routines.errorPickProject': '请选择要复用的项目,或切换为“每次运行新建项目”。', + 'routines.error.agentEmptyOutput': + '智能体执行完成但未产生任何输出。模型或服务商可能返回了空响应——请查看智能体日志以排查上游错误。', 'entry.helpAria': '帮助', 'entry.helpMenuAria': '帮助菜单', 'entry.helpGetHelp': '在 GitHub 上获取帮助', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index 644391e94..2fb18325d 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -698,6 +698,8 @@ export const zhTW: Dict = { 'routines.status.canceled': '已取消', 'routines.confirmDelete': '刪除此自動化?過往執行記錄及其專案將予以保留。', 'routines.errorPickProject': '請選擇要重複使用的專案,或切換為「每次執行建立新專案」。', + 'routines.error.agentEmptyOutput': + '代理執行完成但未產生任何輸出。模型或服務商可能傳回了空回應——請查看代理日誌以排查上游錯誤。', 'newproj.tabPrototype': '原型', 'newproj.tabLiveArtifact': '即時成品', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index 967c33ef3..84bd6e377 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -1279,6 +1279,12 @@ export interface Dict { 'routines.status.canceled': string; 'routines.confirmDelete': string; 'routines.errorPickProject': string; + // Routine run failure messages surfaced from the daemon. Each key + // corresponds to a well-known daemon-side error string we recognize + // and localize in `localizeRoutineError` (apps/web/src/components/ + // RoutinesSection.tsx). Unknown daemon errors keep their raw string + // so untranslated failures still surface, just in English. + 'routines.error.agentEmptyOutput': string; // Bottom-of-rail help menu 'entry.helpAria': string; 'entry.helpMenuAria': string;