diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ba94a0c3a9..a30bd5b7744 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -83,6 +83,7 @@ Docs: https://docs.openclaw.ai
- Cron/Failure alerts: add configurable repeated-failure alerting with per-job overrides and Web UI cron editor support (`inherit|disabled|custom` with threshold/cooldown/channel/target fields). (#24789) Thanks xbrak.
- Cron/Isolated model defaults: resolve isolated cron `subagents.model` (including object-form `primary`) through allowlist-aware model selection so isolated cron runs honor subagent model defaults unless explicitly overridden by job payload model. (#11474) Thanks @AnonO6.
- Cron/Isolated sessions list: persist the intended pre-run model/provider on isolated cron session entries so `sessions_list` reflects payload/session model overrides even when runs fail before post-run telemetry persistence. (#21279) Thanks @altaywtf.
+- Web UI/Chat sessions: add a cron-session visibility toggle in the session selector, fix cron-key detection across `cron:*` and `agent:*:cron:*` formats, and localize the new control labels/tooltips. (#26976) Thanks @ianderrington.
- Cron/One-shot reliability: retry transient one-shot failures with bounded backoff and configurable retry policy before disabling. (#24435) Thanks .
- Gateway/Cron auditability: add gateway info logs for successful cron create, update, and remove operations. (#25090) Thanks .
- Cron/Schedule errors: notify users when a job is auto-disabled after repeated schedule computation failures. (#29098) Thanks .
diff --git a/ui/src/i18n/locales/de.ts b/ui/src/i18n/locales/de.ts
index ed853e4e6c1..bbdf2bdb3b5 100644
--- a/ui/src/i18n/locales/de.ts
+++ b/ui/src/i18n/locales/de.ts
@@ -114,6 +114,9 @@ export const de: TranslationMap = {
refreshTitle: "Chat-Daten aktualisieren",
thinkingToggle: "Ausgabe des Assistenten ein-/ausblenden",
focusToggle: "Fokusmodus ein-/ausschalten (Seitenleiste + Kopfzeile ausblenden)",
+ hideCronSessions: "Cron-Sitzungen ausblenden",
+ showCronSessions: "Cron-Sitzungen anzeigen",
+ showCronSessionsHidden: "Cron-Sitzungen anzeigen ({count} ausgeblendet)",
onboardingDisabled: "Während der Einrichtung deaktiviert",
},
languages: {
diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts
index 83af74d2612..342ca8c85a5 100644
--- a/ui/src/i18n/locales/en.ts
+++ b/ui/src/i18n/locales/en.ts
@@ -111,6 +111,9 @@ export const en: TranslationMap = {
refreshTitle: "Refresh chat data",
thinkingToggle: "Toggle assistant thinking/working output",
focusToggle: "Toggle focus mode (hide sidebar + page header)",
+ hideCronSessions: "Hide cron sessions",
+ showCronSessions: "Show cron sessions",
+ showCronSessionsHidden: "Show cron sessions ({count} hidden)",
onboardingDisabled: "Disabled during onboarding",
},
languages: {
diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts
index 1c101272c50..7a973a13992 100644
--- a/ui/src/i18n/locales/pt-BR.ts
+++ b/ui/src/i18n/locales/pt-BR.ts
@@ -113,6 +113,9 @@ export const pt_BR: TranslationMap = {
refreshTitle: "Atualizar dados do chat",
thinkingToggle: "Alternar saída de pensamento/trabalho do assistente",
focusToggle: "Alternar modo de foco (ocultar barra lateral + cabeçalho da página)",
+ hideCronSessions: "Ocultar sessões de cron",
+ showCronSessions: "Mostrar sessões de cron",
+ showCronSessionsHidden: "Mostrar sessões de cron ({count} ocultas)",
onboardingDisabled: "Desativado durante a integração",
},
languages: {
diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts
index cd7273958b6..aad258d8bf4 100644
--- a/ui/src/i18n/locales/zh-CN.ts
+++ b/ui/src/i18n/locales/zh-CN.ts
@@ -110,6 +110,9 @@ export const zh_CN: TranslationMap = {
refreshTitle: "刷新聊天数据",
thinkingToggle: "切换助手思考/工作输出",
focusToggle: "切换专注模式 (隐藏侧边栏 + 页面页眉)",
+ hideCronSessions: "隐藏定时任务会话",
+ showCronSessions: "显示定时任务会话",
+ showCronSessionsHidden: "显示定时任务会话 (已隐藏 {count} 个)",
onboardingDisabled: "引导期间禁用",
},
languages: {
diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts
index 8e60f3c917e..1165d56fe4e 100644
--- a/ui/src/i18n/locales/zh-TW.ts
+++ b/ui/src/i18n/locales/zh-TW.ts
@@ -110,6 +110,9 @@ export const zh_TW: TranslationMap = {
refreshTitle: "刷新聊天數據",
thinkingToggle: "切換助手思考/工作輸出",
focusToggle: "切換專注模式 (隱藏側邊欄 + 頁面頁眉)",
+ hideCronSessions: "隱藏定時任務會話",
+ showCronSessions: "顯示定時任務會話",
+ showCronSessionsHidden: "顯示定時任務會話 (已隱藏 {count} 個)",
onboardingDisabled: "引導期間禁用",
},
languages: {
diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts
index 7bea77067ed..72f39209be3 100644
--- a/ui/src/ui/app-render.helpers.node.test.ts
+++ b/ui/src/ui/app-render.helpers.node.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
-import { parseSessionKey, resolveSessionDisplayName } from "./app-render.helpers.ts";
+import {
+ isCronSessionKey,
+ parseSessionKey,
+ resolveSessionDisplayName,
+} from "./app-render.helpers.ts";
import type { SessionsListResult } from "./types.ts";
type SessionRow = SessionsListResult["sessions"][number];
@@ -36,6 +40,10 @@ describe("parseSessionKey", () => {
prefix: "Cron:",
fallbackName: "Cron Job:",
});
+ expect(parseSessionKey("cron:daily-briefing-uuid")).toEqual({
+ prefix: "Cron:",
+ fallbackName: "Cron Job:",
+ });
});
it("identifies direct chat with known channel", () => {
@@ -261,3 +269,18 @@ describe("resolveSessionDisplayName", () => {
).toBe("Tyler");
});
});
+
+describe("isCronSessionKey", () => {
+ it("returns true for cron: prefixed keys", () => {
+ expect(isCronSessionKey("cron:abc-123")).toBe(true);
+ expect(isCronSessionKey("cron:weekly-agent-roundtable")).toBe(true);
+ expect(isCronSessionKey("agent:main:cron:abc-123")).toBe(true);
+ expect(isCronSessionKey("agent:main:cron:abc-123:run:run-1")).toBe(true);
+ });
+
+ it("returns false for non-cron keys", () => {
+ expect(isCronSessionKey("main")).toBe(false);
+ expect(isCronSessionKey("discord:group:eng")).toBe(false);
+ expect(isCronSessionKey("agent:main:slack:cron:job:run:uuid")).toBe(false);
+ });
+});
diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts
index d954147297b..68dfbe5e76d 100644
--- a/ui/src/ui/app-render.helpers.ts
+++ b/ui/src/ui/app-render.helpers.ts
@@ -82,12 +82,57 @@ export function renderTab(state: AppViewState, tab: Tab) {
`;
}
+function renderCronFilterIcon(hiddenCount: number) {
+ return html`
+
+
+ ${
+ hiddenCount > 0
+ ? html`${hiddenCount}`
+ : ""
+ }
+
+ `;
+}
+
export function renderChatControls(state: AppViewState) {
const mainSessionKey = resolveMainSessionKey(state.hello, state.sessionsResult);
+ const hideCron = state.sessionsHideCron ?? true;
+ const hiddenCronCount = hideCron
+ ? countHiddenCronSessions(state.sessionKey, state.sessionsResult)
+ : 0;
const sessionOptions = resolveSessionOptions(
state.sessionKey,
state.sessionsResult,
mainSessionKey,
+ hideCron,
);
const disableThinkingToggle = state.onboarding;
const disableFocusToggle = state.onboarding;
@@ -226,6 +271,22 @@ export function renderChatControls(state: AppViewState) {
>
${focusIcon}
+
`;
}
@@ -281,6 +342,8 @@ function capitalize(s: string): string {
* fallback display name. Exported for testing.
*/
export function parseSessionKey(key: string): SessionKeyInfo {
+ const normalized = key.toLowerCase();
+
// ── Main session ─────────────────────────────────
if (key === "main" || key === "agent:main:main") {
return { prefix: "", fallbackName: "Main Session" };
@@ -292,7 +355,7 @@ export function parseSessionKey(key: string): SessionKeyInfo {
}
// ── Cron job ─────────────────────────────────────
- if (key.includes(":cron:")) {
+ if (normalized.startsWith("cron:") || key.includes(":cron:")) {
return { prefix: "Cron:", fallbackName: "Cron Job:" };
}
@@ -349,10 +412,30 @@ export function resolveSessionDisplayName(
return fallbackName;
}
+export function isCronSessionKey(key: string): boolean {
+ const normalized = key.trim().toLowerCase();
+ if (!normalized) {
+ return false;
+ }
+ if (normalized.startsWith("cron:")) {
+ return true;
+ }
+ if (!normalized.startsWith("agent:")) {
+ return false;
+ }
+ const parts = normalized.split(":").filter(Boolean);
+ if (parts.length < 3) {
+ return false;
+ }
+ const rest = parts.slice(2).join(":");
+ return rest.startsWith("cron:");
+}
+
function resolveSessionOptions(
sessionKey: string,
sessions: SessionsListResult | null,
mainSessionKey?: string | null,
+ hideCron = false,
) {
const seen = new Set();
const options: Array<{ key: string; displayName?: string }> = [];
@@ -369,7 +452,8 @@ function resolveSessionOptions(
});
}
- // Add current session key next
+ // Add current session key next — always include it even if it's a cron session,
+ // so the active session is never silently dropped from the select.
if (!seen.has(sessionKey)) {
seen.add(sessionKey);
options.push({
@@ -378,10 +462,10 @@ function resolveSessionOptions(
});
}
- // Add sessions from the result
+ // Add sessions from the result, optionally filtering out cron sessions.
if (sessions?.sessions) {
for (const s of sessions.sessions) {
- if (!seen.has(s.key)) {
+ if (!seen.has(s.key) && !(hideCron && isCronSessionKey(s.key))) {
seen.add(s.key);
options.push({
key: s.key,
@@ -394,6 +478,15 @@ function resolveSessionOptions(
return options;
}
+/** Count sessions with a cron: key that would be hidden when hideCron=true. */
+function countHiddenCronSessions(sessionKey: string, sessions: SessionsListResult | null): number {
+ if (!sessions?.sessions) {
+ return 0;
+ }
+ // Don't count the currently active session even if it's a cron.
+ return sessions.sessions.filter((s) => isCronSessionKey(s.key) && s.key !== sessionKey).length;
+}
+
const THEME_ORDER: ThemeMode[] = ["system", "light", "dark"];
export function renderThemeToggle(state: AppViewState) {
diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts
index 03a47768864..362a9e332c3 100644
--- a/ui/src/ui/app-view-state.ts
+++ b/ui/src/ui/app-view-state.ts
@@ -163,6 +163,7 @@ export type AppViewState = {
sessionsFilterLimit: string;
sessionsIncludeGlobal: boolean;
sessionsIncludeUnknown: boolean;
+ sessionsHideCron: boolean;
usageLoading: boolean;
usageResult: SessionsUsageResult | null;
usageCostSummary: CostUsageSummary | null;
diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts
index af0f3b6538e..409697e785f 100644
--- a/ui/src/ui/app.ts
+++ b/ui/src/ui/app.ts
@@ -245,6 +245,7 @@ export class OpenClawApp extends LitElement {
@state() sessionsFilterLimit = "120";
@state() sessionsIncludeGlobal = true;
@state() sessionsIncludeUnknown = false;
+ @state() sessionsHideCron = true;
@state() usageLoading = false;
@state() usageResult: import("./types.js").SessionsUsageResult | null = null;