diff --git a/nanobot/webui/session_automations.py b/nanobot/webui/session_automations.py
index 308ec3aa..19340856 100644
--- a/nanobot/webui/session_automations.py
+++ b/nanobot/webui/session_automations.py
@@ -152,11 +152,11 @@ def _origin_payload(
job: CronJob,
session_manager: _SessionManagerLike | None,
) -> dict[str, Any] | None:
- session_key = job.payload.session_key
- if not session_key and job.payload.origin_channel and job.payload.origin_chat_id:
- session_key = f"{job.payload.origin_channel}:{job.payload.origin_chat_id}"
- if not session_key:
+ channel = job.payload.origin_channel
+ chat_id = job.payload.origin_chat_id
+ if not channel or not chat_id:
return None
+ session_key = f"{channel}:{chat_id}"
title = ""
preview = ""
@@ -166,7 +166,6 @@ def _origin_payload(
title = str(data.get("title") or "")
preview = _session_preview(data.get("messages"))
- channel, _, chat_id = session_key.partition(":")
return {
"session_key": session_key,
"channel": channel,
diff --git a/tests/channels/test_websocket_http_routes.py b/tests/channels/test_websocket_http_routes.py
index 4767f9dd..4ee76da9 100644
--- a/tests/channels/test_websocket_http_routes.py
+++ b/tests/channels/test_websocket_http_routes.py
@@ -826,6 +826,12 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
origin_channel="websocket",
origin_chat_id="abc",
)
+ legacy_job = cron.add_job(
+ name="english-quiz",
+ schedule=CronSchedule(kind="every", every_ms=3_600_000),
+ message="Practice English",
+ session_key="unified:default",
+ )
cron.register_system_job(
CronJob(
id="heartbeat",
@@ -862,6 +868,8 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
assert by_id[user_job.id]["state"]["run_history"] == []
assert by_id[user_job.id]["origin"]["session_key"] == "websocket:abc"
assert by_id[user_job.id]["origin"]["preview"] == "hi"
+ assert by_id[legacy_job.id]["payload"]["session_key"] == "unified:default"
+ assert by_id[legacy_job.id]["origin"] is None
assert by_id["heartbeat"]["protected"] is True
disabled = await _http_get(
diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx
index c8c4fd93..c9b3ef9d 100644
--- a/webui/src/components/settings/SettingsView.tsx
+++ b/webui/src/components/settings/SettingsView.tsx
@@ -3521,6 +3521,9 @@ function AutomationRow({
t(key, { defaultValue: fallback, ...(values ?? {}) });
const status = automationStatus(job, tx);
const origin = automationOriginLabel(job, tx);
+ const originHref = job.origin?.channel === "websocket"
+ ? `#/chat/${encodeURIComponent(job.origin.session_key)}`
+ : null;
const history = job.state.run_history ?? [];
const canManage = !job.protected;
const canRun = canManage && job.enabled && !job.state.pending;
@@ -3554,11 +3557,11 @@ function AutomationRow({
{formatAutomationLast(job, locale, tx)}
-
- {job.origin?.session_key ? (
+
+ {originHref ? (
{origin}
@@ -3751,7 +3754,10 @@ function automationOriginLabel(
): string {
if (job.protected) return tx("settings.automations.origin.system", "System");
const origin = job.origin;
- if (!origin) return tx("settings.automations.origin.unknown", "Unknown session");
+ if (!origin && job.payload.kind === "agent_turn") {
+ return tx("settings.automations.origin.legacy", "Recreate in target chat");
+ }
+ if (!origin) return tx("settings.automations.origin.unknown", "No linked chat");
return origin.title || origin.preview || origin.session_key;
}
diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json
index 04838947..89b3ec99 100644
--- a/webui/src/i18n/locales/en/common.json
+++ b/webui/src/i18n/locales/en/common.json
@@ -493,7 +493,7 @@
"schedule": "Schedule",
"next": "Next",
"last": "Last",
- "origin": "Origin"
+ "origin": "Linked chat"
},
"runNow": "Run now",
"pause": "Pause",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "System",
- "unknown": "Unknown session"
+ "unknown": "No linked chat",
+ "legacy": "Recreate in target chat"
},
"schedule": {
"at": "At {{time}}",
diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json
index c04429ee..c52a531c 100644
--- a/webui/src/i18n/locales/es/common.json
+++ b/webui/src/i18n/locales/es/common.json
@@ -493,7 +493,7 @@
"schedule": "Programación",
"next": "Siguiente",
"last": "Última",
- "origin": "Origen"
+ "origin": "Chat vinculado"
},
"runNow": "Ejecutar ahora",
"pause": "Pausar",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "Sistema",
- "unknown": "Sesión desconocida"
+ "unknown": "Sin chat vinculado",
+ "legacy": "Recréala en el chat de destino"
},
"schedule": {
"at": "A las {{time}}",
diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json
index 36fabcfe..6a6bb70e 100644
--- a/webui/src/i18n/locales/fr/common.json
+++ b/webui/src/i18n/locales/fr/common.json
@@ -493,7 +493,7 @@
"schedule": "Planning",
"next": "Prochaine",
"last": "Dernière",
- "origin": "Origine"
+ "origin": "Discussion liée"
},
"runNow": "Exécuter maintenant",
"pause": "Mettre en pause",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "Système",
- "unknown": "Session inconnue"
+ "unknown": "Aucune discussion liée",
+ "legacy": "Recréez-la dans la discussion cible"
},
"schedule": {
"at": "À {{time}}",
diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json
index 522735d6..9c49045b 100644
--- a/webui/src/i18n/locales/id/common.json
+++ b/webui/src/i18n/locales/id/common.json
@@ -493,7 +493,7 @@
"schedule": "Jadwal",
"next": "Berikutnya",
"last": "Terakhir",
- "origin": "Asal"
+ "origin": "Chat tertaut"
},
"runNow": "Jalankan sekarang",
"pause": "Jeda",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "Sistem",
- "unknown": "Sesi tidak dikenal"
+ "unknown": "Tidak ada chat tertaut",
+ "legacy": "Buat ulang di chat tujuan"
},
"schedule": {
"at": "Pada {{time}}",
diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json
index 57a3d0b1..34bc8262 100644
--- a/webui/src/i18n/locales/ja/common.json
+++ b/webui/src/i18n/locales/ja/common.json
@@ -493,7 +493,7 @@
"schedule": "スケジュール",
"next": "次回",
"last": "前回",
- "origin": "発生元"
+ "origin": "関連チャット"
},
"runNow": "今すぐ実行",
"pause": "一時停止",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "システム",
- "unknown": "不明なセッション"
+ "unknown": "関連チャットなし",
+ "legacy": "対象チャットで作り直してください"
},
"schedule": {
"at": "{{time}}",
diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json
index c1acc078..1580a0ef 100644
--- a/webui/src/i18n/locales/ko/common.json
+++ b/webui/src/i18n/locales/ko/common.json
@@ -493,7 +493,7 @@
"schedule": "일정",
"next": "다음",
"last": "마지막",
- "origin": "출처"
+ "origin": "연결된 채팅"
},
"runNow": "지금 실행",
"pause": "일시 중지",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "시스템",
- "unknown": "알 수 없는 세션"
+ "unknown": "연결된 채팅 없음",
+ "legacy": "대상 채팅에서 다시 만드세요"
},
"schedule": {
"at": "{{time}}",
diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json
index 1deddbac..1d41a3f8 100644
--- a/webui/src/i18n/locales/vi/common.json
+++ b/webui/src/i18n/locales/vi/common.json
@@ -493,7 +493,7 @@
"schedule": "Lịch",
"next": "Tiếp theo",
"last": "Lần trước",
- "origin": "Nguồn"
+ "origin": "Cuộc trò chuyện liên kết"
},
"runNow": "Chạy ngay",
"pause": "Tạm dừng",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "Hệ thống",
- "unknown": "Phiên không xác định"
+ "unknown": "Chưa liên kết cuộc trò chuyện",
+ "legacy": "Tạo lại trong cuộc trò chuyện đích"
},
"schedule": {
"at": "Vào {{time}}",
diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json
index d6300380..6d410477 100644
--- a/webui/src/i18n/locales/zh-CN/common.json
+++ b/webui/src/i18n/locales/zh-CN/common.json
@@ -493,7 +493,7 @@
"schedule": "计划",
"next": "下次",
"last": "上次",
- "origin": "来源"
+ "origin": "关联会话"
},
"runNow": "立即运行",
"pause": "暂停",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "系统",
- "unknown": "未知会话"
+ "unknown": "未关联会话",
+ "legacy": "请在目标会话中重新创建"
},
"schedule": {
"at": "在 {{time}}",
diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json
index 4d1bab31..8e59eeb0 100644
--- a/webui/src/i18n/locales/zh-TW/common.json
+++ b/webui/src/i18n/locales/zh-TW/common.json
@@ -493,7 +493,7 @@
"schedule": "排程",
"next": "下次",
"last": "上次",
- "origin": "來源"
+ "origin": "關聯會話"
},
"runNow": "立即執行",
"pause": "暫停",
@@ -512,7 +512,8 @@
},
"origin": {
"system": "系統",
- "unknown": "未知會話"
+ "unknown": "未關聯會話",
+ "legacy": "請在目標會話中重新建立"
},
"schedule": {
"at": "於 {{time}}",
diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx
index a52099de..1c9f04fb 100644
--- a/webui/src/tests/app-layout.test.tsx
+++ b/webui/src/tests/app-layout.test.tsx
@@ -365,6 +365,26 @@ describe("App layout", () => {
preview: "Check release blockers",
},
},
+ {
+ id: "legacy-quiz",
+ name: "english-quiz",
+ enabled: true,
+ protected: false,
+ delete_after_run: false,
+ schedule: { kind: "cron", expr: "30 9-23 * * *", tz: "Asia/Shanghai" },
+ payload: {
+ message: "Practice English",
+ kind: "agent_turn",
+ session_key: "unified:default",
+ },
+ state: {
+ next_run_at_ms: Date.UTC(2026, 3, 17, 11, 0, 0),
+ last_status: "ok",
+ pending: false,
+ run_history: [],
+ },
+ origin: null,
+ },
{
id: "heartbeat",
name: "heartbeat",
@@ -393,6 +413,9 @@ describe("App layout", () => {
expect(screen.getByText("Daily repo check")).toBeInTheDocument();
expect(screen.getByText("Check the repo status")).toBeInTheDocument();
expect(screen.getByText("Release prep")).toBeInTheDocument();
+ expect(screen.getByText("english-quiz")).toBeInTheDocument();
+ expect(screen.getByText("Recreate in target chat")).toBeInTheDocument();
+ expect(screen.queryByText("unified:default")).not.toBeInTheDocument();
expect(screen.getByText("heartbeat")).toBeInTheDocument();
expect(within(sidebar).getByRole("button", { name: "Automations" })).toHaveAttribute(
"aria-current",