+
{t("detail.tabs.overview")}
diff --git a/ui/web/src/pages/cron/cron-detail/cron-header.tsx b/ui/web/src/pages/cron/cron-detail/cron-header.tsx
index 83f7fa6b..993103c0 100644
--- a/ui/web/src/pages/cron/cron-detail/cron-header.tsx
+++ b/ui/web/src/pages/cron/cron-detail/cron-header.tsx
@@ -6,6 +6,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { cn } from "@/lib/utils";
import type { CronJob } from "../hooks/use-cron";
import { formatSchedule } from "../cron-utils";
+import { useAgents } from "@/pages/agents/hooks/use-agents";
interface CronHeaderProps {
job: CronJob;
@@ -20,6 +21,9 @@ interface CronHeaderProps {
export function CronHeader({ job, isRunning, onBack, onRun, onAdvanced, onToggle, onDelete }: CronHeaderProps) {
const { t } = useTranslation("cron");
+ const { agents } = useAgents();
+ const agent = job.agentId ? agents.find((a) => a.id === job.agentId) : null;
+ const agentLabel = agent?.display_name || agent?.agent_key || job.agentId;
return (
@@ -61,7 +65,7 @@ export function CronHeader({ job, isRunning, onBack, onRun, onAdvanced, onToggle
{job.agentId && (
<>
·
- {job.agentId}
+ {agentLabel}
>
)}
diff --git a/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx b/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx
index 00a53cd3..7d8b214a 100644
--- a/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx
+++ b/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx
@@ -1,13 +1,18 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
+import { Calendar, Clock, AlertTriangle, Globe, Pencil } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { StickySaveBar } from "@/components/shared/sticky-save-bar";
+import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
import { formatDate } from "@/lib/format";
import type { CronJob } from "../hooks/use-cron";
+import { CronStatusBadge } from "../cron-utils";
+import { useAgents } from "@/pages/agents/hooks/use-agents";
interface CronOverviewTabProps {
job: CronJob;
@@ -23,17 +28,9 @@ function getEverySeconds(job: CronJob): string {
return "60";
}
-function InfoRow({ label, value }: { label: string; value: string }) {
- return (
-
- );
-}
-
export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
const { t } = useTranslation("cron");
+ const { agents } = useAgents();
const readonly = !onUpdate;
const [scheduleKind, setScheduleKind] = useState
(job.schedule.kind as ScheduleKind);
@@ -42,7 +39,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
const [message, setMessage] = useState(job.payload?.message ?? "");
const [agentId, setAgentId] = useState(job.agentId ?? "");
const [enabled, setEnabled] = useState(job.enabled);
-
+ const [editingMessage, setEditingMessage] = useState(false);
const [saving, setSaving] = useState(false);
const handleSave = async () => {
@@ -63,6 +60,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
agentId: agentId.trim() || undefined,
enabled,
});
+ setEditingMessage(false);
} catch {
// toast shown by hook
} finally {
@@ -136,20 +134,40 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
)}
- {/* Message section */}
+ {/* Message section — markdown preview or textarea */}
- {t("detail.messageSection")}
-
-
+
+
{t("detail.messageSection")}
+ {!readonly && (
+
+ )}
+
+
+ {editingMessage ? (
+ ) : (
+
+ {message ? (
+
+ ) : (
+
{t("detail.noMessage")}
+ )}
+
+ )}
{/* Agent & Status section */}
@@ -158,13 +176,19 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
- {/* Info rows */}
-
+ {/* Info grid */}
+
{job.state?.nextRunAtMs && (
-
+
+
+
+ {t("detail.infoRows.nextRun")}
+
+
{formatDate(new Date(job.state.nextRunAtMs))}
+
)}
{job.state?.lastRunAtMs && (
-
+
+
+
+ {t("detail.infoRows.lastRun")}
+
+
{formatDate(new Date(job.state.lastRunAtMs))}
+
)}
{job.state?.lastStatus && (
-
+
+
{t("detail.infoRows.lastStatus")}
+
+
)}
-
-
+
+
{t("detail.infoRows.created")}
+
{formatDate(new Date(job.createdAtMs))}
+
+
+
{t("detail.infoRows.updated")}
+
{formatDate(new Date(job.updatedAtMs))}
+
{job.schedule.tz && (
-
+
+
+
+ {t("detail.infoRows.timezone")}
+
+
{job.schedule.tz}
+
)}
-
- {/* Last error */}
- {job.state?.lastError && (
-
-
{t("detail.lastError")}
-
{job.state.lastError}
-
- )}
+ {/* Last error */}
+ {job.state?.lastError && (
+
+
+
+
{t("detail.lastError")}
+
+
+
+
+
+ )}
+
{!readonly && (
+ {/* Header row — always visible */}
+
+
+ {/* Expanded details */}
+ {expanded && (
+
+ {entry.summary && (
+
+
+
+ )}
+ {entry.error && (
+
+
+
+
{t("detail.lastError")}
+
+
+
+ )}
+ {/* Token details on mobile */}
+ {entry.inputTokens != null && entry.inputTokens > 0 && (
+
+ {t("detail.inOut", {
+ input: formatTokens(entry.inputTokens),
+ output: formatTokens(entry.outputTokens ?? 0),
+ })}
+
+ )}
+
+ )}
+
+ );
+}
+
export function CronRunHistoryTab({ job, getRunLog, onRefresh }: CronRunHistoryTabProps) {
const { t } = useTranslation("cron");
const [runLog, setRunLog] = useState([]);
@@ -48,7 +137,7 @@ export function CronRunHistoryTab({ job, getRunLog, onRefresh }: CronRunHistoryT
loadRunLog();
}, [loadRunLog]);
- // Poll while running to detect completion
+ // Poll while running
useEffect(() => {
if (!isRunning) return;
const interval = setInterval(onRefresh, 3000);
@@ -58,8 +147,15 @@ export function CronRunHistoryTab({ job, getRunLog, onRefresh }: CronRunHistoryT
return (
-
{t("detail.runHistory")}
-
@@ -74,51 +170,21 @@ export function CronRunHistoryTab({ job, getRunLog, onRefresh }: CronRunHistoryT
<>
{runLog.map((entry, i) => (
-
-
-
- {formatDate(new Date(entry.ts))}
- {entry.durationMs != null && entry.durationMs > 0 && (
- ({formatDuration(entry.durationMs)})
- )}
-
-
- {entry.inputTokens != null && entry.inputTokens > 0 && (
-
- {t("detail.inOut", {
- input: formatTokens(entry.inputTokens),
- output: formatTokens(entry.outputTokens ?? 0),
- })}
-
- )}
-
- {entry.status || "unknown"}
-
-
-
- {entry.summary && (
-
{entry.summary}
- )}
- {entry.error && (
-
{entry.error}
- )}
-
+
))}
-
{ setRunLogPage(p); loadRunLog(p); }}
- onPageSizeChange={(s) => { setRunLogPageSize(s); setRunLogPage(1); loadRunLog(1, s); }}
- pageSizes={[10, 20, 50]}
- />
+
+
{ setRunLogPage(p); loadRunLog(p); }}
+ onPageSizeChange={(s) => { setRunLogPageSize(s); setRunLogPage(1); loadRunLog(1, s); }}
+ pageSizes={[10, 20, 50]}
+ />
+
>
)}
diff --git a/ui/web/src/pages/cron/cron-list-row.tsx b/ui/web/src/pages/cron/cron-list-row.tsx
index 1ec3e2ac..16630c0e 100644
--- a/ui/web/src/pages/cron/cron-list-row.tsx
+++ b/ui/web/src/pages/cron/cron-list-row.tsx
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { CronJob } from "./hooks/use-cron";
import { formatSchedule, CronStatusBadge } from "./cron-utils";
+import { useAgents } from "@/pages/agents/hooks/use-agents";
interface CronListRowProps {
job: CronJob;
@@ -14,6 +15,8 @@ interface CronListRowProps {
export function CronListRow({ job, onClick, onRun, onDelete }: CronListRowProps) {
const { t } = useTranslation("cron");
+ const { agents } = useAgents();
+ const agent = job.agentId ? agents.find((a) => a.id === job.agentId) : null;
const isRunning = job.state?.lastStatus === "running";
return (
@@ -46,7 +49,7 @@ export function CronListRow({ job, onClick, onRun, onDelete }: CronListRowProps)
{/* Agent */}
- {job.agentId || t("card.defaultAgent")}
+ {agent?.display_name || agent?.agent_key || job.agentId || t("card.defaultAgent")}
{/* Message preview */}
diff --git a/ui/web/src/pages/cron/cron-page.tsx b/ui/web/src/pages/cron/cron-page.tsx
index fc573ec0..655546cc 100644
--- a/ui/web/src/pages/cron/cron-page.tsx
+++ b/ui/web/src/pages/cron/cron-page.tsx
@@ -31,26 +31,7 @@ export function CronPage() {
const [showCreate, setShowCreate] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null);
- // Detail routing
- const detailJob = detailId ? jobs.find((j) => j.id === detailId) : null;
- if (detailJob) {
- return (
- navigate("/cron")}
- onRun={runJob}
- onToggle={toggleJob}
- onDelete={async (id) => {
- await deleteJob(id);
- navigate("/cron");
- }}
- onUpdate={updateJob}
- getRunLog={getRunLog}
- onRefresh={refresh}
- />
- );
- }
-
+ // All hooks must be called before any conditional return
const filtered = useMemo(() => {
const q = search.toLowerCase();
if (!q) return jobs;
@@ -70,6 +51,26 @@ export function CronPage() {
resetPage();
}
+ // Detail routing — after all hooks
+ const detailJob = detailId ? jobs.find((j) => j.id === detailId) : null;
+ if (detailJob) {
+ return (
+ navigate("/cron")}
+ onRun={runJob}
+ onToggle={toggleJob}
+ onDelete={async (id) => {
+ await deleteJob(id);
+ navigate("/cron");
+ }}
+ onUpdate={updateJob}
+ getRunLog={getRunLog}
+ onRefresh={refresh}
+ />
+ );
+ }
+
return (
} />
} />
} />
-
} />
-
} />
+
} />
+
} />
{/* Admin-only pages */}
} />
} />