diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index 8c04338e..a39d48bc 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -3587,6 +3587,8 @@ function AutomationRow({ ? `#/chat/${encodeURIComponent(job.origin.session_key)}` : null; const history = job.state.run_history ?? []; + const latestRun = history[history.length - 1]; + const lastResult = automationLastResult(job, latestRun, locale, tx); const canManage = !job.protected; const canRun = canManage && job.enabled && !job.state.pending; const toggleAction: AutomationAction = job.enabled ? "disable" : "enable"; @@ -3594,11 +3596,11 @@ function AutomationRow({ const needsRecreation = automationNeedsRecreation(job); return ( -
-
+
+
-
- +
+ {job.name || job.id} {status.label} @@ -3606,11 +3608,11 @@ function AutomationRow({ {tx("settings.automations.oneShot", "One-time")} ) : null}
-

+

{job.payload.message || tx("settings.automations.systemTask", "System-managed automation")}

-
+
- {formatAutomationLast(job, locale, tx)} + + + {lastResult.primary} + {originHref ? ( @@ -3659,45 +3668,6 @@ function AutomationRow({
) : null} - {history.length ? ( -
-
- {tx("settings.automations.history.recent", "Recent runs")} -
-
- {history.slice(-4).map((record) => { - const statusLabel = automationRunStatusLabel(record.status, tx); - const duration = formatAutomationRunDuration(record.duration_ms, locale, tx); - const visibleLabel = record.status === "ok" - ? duration - : tx("settings.automations.history.statusWithDuration", "{{status}} · {{duration}}", { - status: statusLabel, - duration, - }); - const detail = record.error || fmtDateTime(record.run_at_ms, locale); - const accessibleLabel = `${statusLabel} · ${duration} · ${detail}`; - return ( - - {visibleLabel} - - ); - })} -
-
- ) : null} -
{job.created_at_ms ? ( @@ -3716,7 +3686,7 @@ function AutomationRow({
-
+
{canManage ? ( <> -
+
+
{label}
-
- {children} +
+
+ {children} +
+ {secondary ? ( +
+ {secondary} +
+ ) : null}
); @@ -4410,14 +4389,70 @@ function formatAutomationNextTitle( return fmtDateTime(job.state.next_run_at_ms, locale); } -function formatAutomationLast( +function automationLastResult( job: SessionAutomationJob, + latestRun: NonNullable[number] | undefined, locale: string, tx: (key: string, fallback: string, values?: Record) => string, -): string { - if (!job.state.last_run_at_ms) return tx("settings.automations.last.never", "Never"); - const status = automationRunStatusLabel(job.state.last_status, tx); - return `${fmtDateTime(job.state.last_run_at_ms, locale)} · ${status}`; +): { + primary: string; + secondary?: string; + title: string; + tone: "neutral" | "success" | "warning" | "danger"; +} { + if (job.state.pending) { + const secondary = job.state.last_run_at_ms + ? fmtDateTime(job.state.last_run_at_ms, locale) + : undefined; + return { + primary: tx("settings.automations.next.pending", "Running now"), + secondary, + title: secondary ?? tx("settings.automations.next.pending", "Running now"), + tone: "warning", + }; + } + if (latestRun) { + const status = automationRunStatusLabel(latestRun.status, tx); + const duration = latestRun.duration_ms === undefined + ? null + : formatAutomationRunDuration(latestRun.duration_ms, locale, tx); + const primary = duration + ? tx("settings.automations.history.statusWithDuration", "{{status}} · {{duration}}", { + status, + duration, + }) + : status; + const secondary = fmtDateTime(latestRun.run_at_ms, locale); + return { + primary, + secondary, + title: latestRun.error ? `${secondary} · ${latestRun.error}` : secondary, + tone: latestRun.status === "error" + ? "danger" + : latestRun.status === "skipped" + ? "warning" + : "success", + }; + } + if (job.state.last_run_at_ms) { + const status = automationRunStatusLabel(job.state.last_status, tx); + const secondary = fmtDateTime(job.state.last_run_at_ms, locale); + return { + primary: status, + secondary, + title: secondary, + tone: job.state.last_status === "error" ? "danger" : "success", + }; + } + const never = tx("settings.automations.last.never", "Never"); + return { primary: never, title: never, tone: "neutral" }; +} + +function automationResultDotClass(tone: "neutral" | "success" | "warning" | "danger"): string { + if (tone === "success") return "bg-emerald-500"; + if (tone === "warning") return "bg-amber-500"; + if (tone === "danger") return "bg-destructive"; + return "bg-muted-foreground/45"; } function automationRunStatusLabel( diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index dad2ca2f..ef3a9386 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -570,7 +570,7 @@ "ok": "Completed", "error": "Error", "skipped": "Skipped", - "recent": "Recent runs", + "recent": "Last result", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index d2b715ae..34f0e027 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -570,7 +570,7 @@ "ok": "Completada", "error": "Error", "skipped": "Omitida", - "recent": "Ejecuciones recientes", + "recent": "Último resultado", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index f321f48c..b27f7f59 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -570,7 +570,7 @@ "ok": "Terminée", "error": "Erreur", "skipped": "Ignorée", - "recent": "Exécutions récentes", + "recent": "Dernier résultat", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index 7915a01f..e1457b71 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -570,7 +570,7 @@ "ok": "Selesai", "error": "Error", "skipped": "Dilewati", - "recent": "Eksekusi terbaru", + "recent": "Hasil terakhir", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index 5d7ad950..75223b8c 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -570,7 +570,7 @@ "ok": "完了", "error": "エラー", "skipped": "スキップ", - "recent": "最近の実行", + "recent": "最新結果", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index a503c37f..44051a03 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -570,7 +570,7 @@ "ok": "완료", "error": "오류", "skipped": "건너뜀", - "recent": "최근 실행", + "recent": "최근 결과", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index 5855b04f..c7baf93f 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -570,7 +570,7 @@ "ok": "Hoàn tất", "error": "Lỗi", "skipped": "Đã bỏ qua", - "recent": "Lượt chạy gần đây", + "recent": "Kết quả gần nhất", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index 2c89c0a5..efdd240e 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -570,7 +570,7 @@ "ok": "完成", "error": "错误", "skipped": "已跳过", - "recent": "最近运行", + "recent": "最近结果", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index a943aa3b..befc58aa 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -570,7 +570,7 @@ "ok": "完成", "error": "錯誤", "skipped": "已略過", - "recent": "最近執行", + "recent": "最近結果", "statusWithDuration": "{{status}} · {{duration}}" }, "meta": { diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx index 07461bc3..53b3ab43 100644 --- a/webui/src/tests/app-layout.test.tsx +++ b/webui/src/tests/app-layout.test.tsx @@ -585,8 +585,8 @@ describe("App layout", () => { expect(screen.getByText("每日检查")).toBeInTheDocument(); expect(screen.getByText("检查仓库状态")).toBeInTheDocument(); expect(screen.getByText("每 1天")).toBeInTheDocument(); - expect(screen.getByText("最近运行")).toBeInTheDocument(); - expect(screen.getByText("不到 1 秒")).toBeInTheDocument(); + expect(screen.getByText("最近结果")).toBeInTheDocument(); + expect(screen.getByText("完成 · 不到 1 秒")).toBeInTheDocument(); expect(screen.queryByText("Workspace automations")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "刷新" })).not.toBeInTheDocument(); expect(document.title).toBe("自动任务 · nanobot");