diff --git a/nanobot/channels/websocket.py b/nanobot/channels/websocket.py index 1a048db8..3e5b6783 100644 --- a/nanobot/channels/websocket.py +++ b/nanobot/channels/websocket.py @@ -846,19 +846,6 @@ class WebSocketChannel(BaseChannel): self.logger.exception("send failed{}", label) raise - def _all_subscribed_connections(self) -> list[Any]: - """Return every live WebUI connection that is subscribed to at least one chat.""" - seen: set[int] = set() - conns: list[Any] = [] - for subscribers in self._subs.values(): - for connection in subscribers: - marker = id(connection) - if marker in seen: - continue - seen.add(marker) - conns.append(connection) - return conns - async def send(self, msg: OutboundMessage) -> None: if msg.metadata.get("_runtime_model_updated"): await self.send_runtime_model_updated( @@ -1161,7 +1148,7 @@ class WebSocketChannel(BaseChannel): async def send_session_updated(self, chat_id: str, *, scope: str | None = None) -> None: """Notify WebUI clients that a session row should refresh.""" - conns = self._all_subscribed_connections() + conns = list(self._conn_chats) if not conns: return body: dict[str, Any] = {"event": "session_updated", "chat_id": chat_id} diff --git a/nanobot/webui/session_automations.py b/nanobot/webui/session_automations.py index 0680dde7..55e748d6 100644 --- a/nanobot/webui/session_automations.py +++ b/nanobot/webui/session_automations.py @@ -5,7 +5,9 @@ from __future__ import annotations from collections.abc import Collection from typing import Any, Protocol +from nanobot.cron.session_turns import CRON_HISTORY_META from nanobot.cron.types import CronJob +from nanobot.session.manager import _message_preview_text class _CronServiceLike(Protocol): @@ -173,13 +175,21 @@ def _origin_payload( "preview": preview, } + def _session_preview(messages: Any) -> str: if not isinstance(messages, list): return "" + fallback_preview = "" for message in messages: if not isinstance(message, dict): continue - content = message.get("content") - if isinstance(content, str) and content.strip(): - return content.strip() - return "" + if message.get(CRON_HISTORY_META) is True: + continue + text = _message_preview_text(message) + if not text: + continue + if message.get("role") == "user": + return text + if not fallback_preview and message.get("role") == "assistant": + fallback_preview = text + return fallback_preview diff --git a/nanobot/webui/ws_http.py b/nanobot/webui/ws_http.py index 0cda45a1..9e2981f9 100644 --- a/nanobot/webui/ws_http.py +++ b/nanobot/webui/ws_http.py @@ -92,8 +92,6 @@ if TYPE_CHECKING: def _decode_api_key(raw_key: str) -> str | None: - from urllib.parse import unquote - key = unquote(raw_key) _api_key_re = re.compile(r"^[A-Za-z0-9_:.-]{1,128}$") if _api_key_re.match(key) is None: diff --git a/tests/channels/test_websocket_channel.py b/tests/channels/test_websocket_channel.py index a164796d..8fb80fcd 100644 --- a/tests/channels/test_websocket_channel.py +++ b/tests/channels/test_websocket_channel.py @@ -137,10 +137,6 @@ async def test_send_session_updated_broadcasts_to_other_webui_connections(bus) - channel._attach(other_conn, "chat-b") assert sorted(channel._subs) == ["chat-a", "chat-b"] assert sum(len(conns) for conns in channel._subs.values()) == 2 - assert {id(conn) for conn in channel._all_subscribed_connections()} == { - id(active_conn), - id(other_conn), - } await channel.send_session_updated("chat-a", scope="thread") diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index 7a42d8fd..32aab396 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -3870,8 +3870,10 @@ function AutomationActionGroup({ const tx = (key: string, fallback: string, values?: Record) => t(key, { defaultValue: fallback, ...(values ?? {}) }); const canManage = !job.protected; - const canRun = canManage && job.enabled && !job.state.pending; + const hasLinkedChat = Boolean(job.origin); + const canRun = canManage && hasLinkedChat && job.enabled && !job.state.pending; const toggleAction: AutomationAction = job.enabled ? "disable" : "enable"; + const canToggle = canManage && (job.enabled || hasLinkedChat); const toggleBusy = actionKey === `${toggleAction}:${job.id}`; if (!canManage) { @@ -3906,6 +3908,7 @@ function AutomationActionGroup({ : tx("settings.automations.resume", "Resume") } busy={toggleBusy} + disabled={!canToggle} onClick={() => void onAction(toggleAction, job)} > {job.enabled ? ( diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index c2628f6d..71acd9f6 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -472,14 +472,6 @@ "empty": "No apps match this filter." }, "automations": { - "kicker": "Workspace automations", - "title": "Automations", - "stats": { - "active": "Active", - "paused": "Paused", - "failed": "Needs attention", - "system": "System" - }, "filters": { "all": "All", "active": "Active", @@ -504,7 +496,6 @@ "labels": { "schedule": "Schedule", "next": "Next", - "last": "Last", "origin": "Linked chat", "created": "Created", "updated": "Updated" @@ -516,14 +507,12 @@ "delete": "Delete", "protected": "Protected", "editTitle": "Edit automation", - "editDescription": "Update the prompt and schedule. The linked chat stays unchanged.", "save": "Save", "deleteTitle": "Delete automation", "deleteDescription": "This removes {{name}} from the cron store. Past chat messages stay in the session.", "cancel": "Cancel", "status": { "system": "System", - "pending": "Pending", "running": "Running now", "paused": "Paused", "failed": "Failed", @@ -569,18 +558,10 @@ "pending": "Running now", "none": "No next run" }, - "last": { - "never": "Never", - "unknown": "unknown" - }, "message": { "showMore": "Show full message", "showLess": "Show less" }, - "meta": { - "created": "Created {{time}}", - "updated": "Updated {{time}}" - }, "fields": { "name": "Name", "message": "Message", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index 7f8fd4c5..6856a735 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -472,14 +472,6 @@ "empty": "Ninguna app coincide con este filtro." }, "automations": { - "kicker": "Automatizaciones del espacio", - "title": "Automatizaciones", - "stats": { - "active": "Activas", - "paused": "Pausadas", - "failed": "Requieren atención", - "system": "Sistema" - }, "filters": { "all": "Todas", "active": "Activas", @@ -504,7 +496,6 @@ "labels": { "schedule": "Programación", "next": "Siguiente", - "last": "Última", "origin": "Chat vinculado", "created": "Creada", "updated": "Actualizada" @@ -516,14 +507,12 @@ "delete": "Eliminar", "protected": "Protegida", "editTitle": "Editar automatización", - "editDescription": "Actualiza el prompt y la programación. El chat vinculado no cambia.", "save": "Guardar", "deleteTitle": "Eliminar automatización", "deleteDescription": "Esto elimina {{name}} del almacén cron. Los mensajes de chat anteriores permanecen en la sesión.", "cancel": "Cancelar", "status": { "system": "Sistema", - "pending": "Pendiente", "running": "Ejecutándose ahora", "paused": "Pausada", "failed": "Fallida", @@ -569,18 +558,10 @@ "pending": "Ejecutándose ahora", "none": "Sin próxima ejecución" }, - "last": { - "never": "Nunca", - "unknown": "desconocido" - }, "message": { "showMore": "Mostrar mensaje completo", "showLess": "Mostrar menos" }, - "meta": { - "created": "Creada {{time}}", - "updated": "Actualizada {{time}}" - }, "fields": { "name": "Nombre", "message": "Mensaje", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 8f46c49d..5139d5c5 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -472,14 +472,6 @@ "empty": "Aucune app ne correspond." }, "automations": { - "kicker": "Automatisations de l’espace", - "title": "Automatisations", - "stats": { - "active": "Actives", - "paused": "En pause", - "failed": "À traiter", - "system": "Système" - }, "filters": { "all": "Toutes", "active": "Actives", @@ -504,7 +496,6 @@ "labels": { "schedule": "Planning", "next": "Prochaine", - "last": "Dernière", "origin": "Discussion liée", "created": "Créée", "updated": "Modifiée" @@ -516,14 +507,12 @@ "delete": "Supprimer", "protected": "Protégée", "editTitle": "Modifier l’automatisation", - "editDescription": "Modifiez le prompt et le planning. La discussion liée ne change pas.", "save": "Enregistrer", "deleteTitle": "Supprimer l’automatisation", "deleteDescription": "Cela supprime {{name}} du stockage cron. Les anciens messages de chat restent dans la session.", "cancel": "Annuler", "status": { "system": "Système", - "pending": "En attente", "running": "En cours d’exécution", "paused": "En pause", "failed": "Échouée", @@ -569,18 +558,10 @@ "pending": "En cours d’exécution", "none": "Aucune prochaine exécution" }, - "last": { - "never": "Jamais", - "unknown": "inconnu" - }, "message": { "showMore": "Afficher le message complet", "showLess": "Afficher moins" }, - "meta": { - "created": "Créée {{time}}", - "updated": "Mise à jour {{time}}" - }, "fields": { "name": "Nom", "message": "Message", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index 6a6af3e8..884123b5 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -472,14 +472,6 @@ "empty": "Tidak ada aplikasi yang cocok." }, "automations": { - "kicker": "Otomasi ruang kerja", - "title": "Otomasi", - "stats": { - "active": "Aktif", - "paused": "Dijeda", - "failed": "Perlu ditangani", - "system": "Sistem" - }, "filters": { "all": "Semua", "active": "Aktif", @@ -504,7 +496,6 @@ "labels": { "schedule": "Jadwal", "next": "Berikutnya", - "last": "Terakhir", "origin": "Chat tertaut", "created": "Dibuat", "updated": "Diperbarui" @@ -516,14 +507,12 @@ "delete": "Hapus", "protected": "Terlindungi", "editTitle": "Edit otomasi", - "editDescription": "Perbarui prompt dan jadwal. Chat tertaut tidak berubah.", "save": "Simpan", "deleteTitle": "Hapus otomasi", "deleteDescription": "Ini menghapus {{name}} dari penyimpanan cron. Pesan chat sebelumnya tetap ada di sesi.", "cancel": "Batal", "status": { "system": "Sistem", - "pending": "Menunggu", "running": "Sedang berjalan", "paused": "Dijeda", "failed": "Gagal", @@ -569,18 +558,10 @@ "pending": "Sedang berjalan", "none": "Tidak ada jadwal berikutnya" }, - "last": { - "never": "Belum pernah", - "unknown": "tidak dikenal" - }, "message": { "showMore": "Tampilkan pesan lengkap", "showLess": "Tampilkan lebih sedikit" }, - "meta": { - "created": "Dibuat {{time}}", - "updated": "Diperbarui {{time}}" - }, "fields": { "name": "Nama", "message": "Pesan", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index d1c7dc63..a1b464f1 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -472,14 +472,6 @@ "empty": "一致するアプリはありません。" }, "automations": { - "kicker": "ワークスペースの自動タスク", - "title": "自動タスク", - "stats": { - "active": "実行中", - "paused": "一時停止", - "failed": "要対応", - "system": "システム" - }, "filters": { "all": "すべて", "active": "実行中", @@ -504,7 +496,6 @@ "labels": { "schedule": "スケジュール", "next": "次回", - "last": "前回", "origin": "関連チャット", "created": "作成", "updated": "更新" @@ -516,14 +507,12 @@ "delete": "削除", "protected": "保護済み", "editTitle": "自動タスクを編集", - "editDescription": "プロンプトとスケジュールを更新します。関連チャットは変更されません。", "save": "保存", "deleteTitle": "自動タスクを削除", "deleteDescription": "{{name}} を cron ストアから削除します。過去のチャットメッセージはセッションに残ります。", "cancel": "キャンセル", "status": { "system": "システム", - "pending": "待機中", "running": "実行中", "paused": "一時停止", "failed": "失敗", @@ -569,18 +558,10 @@ "pending": "実行中", "none": "次回実行なし" }, - "last": { - "never": "未実行", - "unknown": "不明" - }, "message": { "showMore": "メッセージ全文を表示", "showLess": "折りたたむ" }, - "meta": { - "created": "{{time}} 作成", - "updated": "{{time}} 更新" - }, "fields": { "name": "名前", "message": "メッセージ", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index e6313b7f..0a6cdab2 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -472,14 +472,6 @@ "empty": "일치하는 앱이 없습니다." }, "automations": { - "kicker": "작업 공간 자동화", - "title": "자동화", - "stats": { - "active": "활성", - "paused": "일시 중지", - "failed": "확인 필요", - "system": "시스템" - }, "filters": { "all": "전체", "active": "활성", @@ -504,7 +496,6 @@ "labels": { "schedule": "일정", "next": "다음", - "last": "마지막", "origin": "연결된 채팅", "created": "생성", "updated": "업데이트" @@ -516,14 +507,12 @@ "delete": "삭제", "protected": "보호됨", "editTitle": "자동화 편집", - "editDescription": "프롬프트와 일정을 업데이트합니다. 연결된 채팅은 변경되지 않습니다.", "save": "저장", "deleteTitle": "자동화 삭제", "deleteDescription": "{{name}}을 cron 저장소에서 삭제합니다. 이전 채팅 메시지는 세션에 남습니다.", "cancel": "취소", "status": { "system": "시스템", - "pending": "대기 중", "running": "실행 중", "paused": "일시 중지", "failed": "실패", @@ -569,18 +558,10 @@ "pending": "실행 중", "none": "다음 실행 없음" }, - "last": { - "never": "실행된 적 없음", - "unknown": "알 수 없음" - }, "message": { "showMore": "전체 메시지 보기", "showLess": "접기" }, - "meta": { - "created": "{{time}} 생성", - "updated": "{{time}} 업데이트" - }, "fields": { "name": "이름", "message": "메시지", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index 9e8f95e9..e5c82164 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -472,14 +472,6 @@ "empty": "Không có ứng dụng phù hợp." }, "automations": { - "kicker": "Tự động hóa không gian làm việc", - "title": "Tự động hóa", - "stats": { - "active": "Đang chạy", - "paused": "Đã tạm dừng", - "failed": "Cần xử lý", - "system": "Hệ thống" - }, "filters": { "all": "Tất cả", "active": "Đang chạy", @@ -504,7 +496,6 @@ "labels": { "schedule": "Lịch", "next": "Tiếp theo", - "last": "Lần trước", "origin": "Cuộc trò chuyện liên kết", "created": "Đã tạo", "updated": "Đã cập nhật" @@ -516,14 +507,12 @@ "delete": "Xóa", "protected": "Được bảo vệ", "editTitle": "Sửa tự động hóa", - "editDescription": "Cập nhật prompt và lịch. Cuộc trò chuyện liên kết không thay đổi.", "save": "Lưu", "deleteTitle": "Xóa tự động hóa", "deleteDescription": "Thao tác này xóa {{name}} khỏi kho cron. Tin nhắn chat trước đó vẫn ở trong phiên.", "cancel": "Hủy", "status": { "system": "Hệ thống", - "pending": "Đang chờ", "running": "Đang chạy", "paused": "Đã tạm dừng", "failed": "Thất bại", @@ -569,18 +558,10 @@ "pending": "Đang chạy", "none": "Không có lần chạy tiếp theo" }, - "last": { - "never": "Chưa từng chạy", - "unknown": "không xác định" - }, "message": { "showMore": "Hiển thị toàn bộ tin nhắn", "showLess": "Thu gọn" }, - "meta": { - "created": "Tạo lúc {{time}}", - "updated": "Cập nhật lúc {{time}}" - }, "fields": { "name": "Tên", "message": "Tin nhắn", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index 4ff4badb..cd816680 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -472,14 +472,6 @@ "empty": "没有匹配的应用。" }, "automations": { - "kicker": "工作区自动任务", - "title": "自动任务", - "stats": { - "active": "运行中", - "paused": "已暂停", - "failed": "异常", - "system": "系统" - }, "filters": { "all": "全部", "active": "运行中", @@ -504,7 +496,6 @@ "labels": { "schedule": "计划", "next": "下次", - "last": "上次", "origin": "关联会话", "created": "创建于", "updated": "更新于" @@ -516,14 +507,12 @@ "delete": "删除", "protected": "受保护", "editTitle": "编辑自动任务", - "editDescription": "更新提示词和计划;关联会话不会改变。", "save": "保存", "deleteTitle": "删除自动任务", "deleteDescription": "这会从 cron 存储中删除 {{name}},历史聊天消息会保留在会话中。", "cancel": "取消", "status": { "system": "系统", - "pending": "等待中", "running": "正在运行", "paused": "已暂停", "failed": "失败", @@ -569,18 +558,10 @@ "pending": "正在运行", "none": "没有下次运行" }, - "last": { - "never": "从未运行", - "unknown": "未知" - }, "message": { "showMore": "查看完整消息", "showLess": "收起消息" }, - "meta": { - "created": "创建于 {{time}}", - "updated": "更新于 {{time}}" - }, "fields": { "name": "名称", "message": "消息", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index fbbb0223..bd5ace9c 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -472,14 +472,6 @@ "empty": "沒有符合的應用。" }, "automations": { - "kicker": "工作區自動任務", - "title": "自動任務", - "stats": { - "active": "執行中", - "paused": "已暫停", - "failed": "異常", - "system": "系統" - }, "filters": { "all": "全部", "active": "執行中", @@ -504,7 +496,6 @@ "labels": { "schedule": "排程", "next": "下次", - "last": "上次", "origin": "關聯會話", "created": "建立於", "updated": "更新於" @@ -516,14 +507,12 @@ "delete": "刪除", "protected": "受保護", "editTitle": "編輯自動任務", - "editDescription": "更新提示詞和排程;關聯會話不會改變。", "save": "儲存", "deleteTitle": "刪除自動任務", "deleteDescription": "這會從 cron 儲存中刪除 {{name}},歷史聊天訊息會保留在會話中。", "cancel": "取消", "status": { "system": "系統", - "pending": "等待中", "running": "正在執行", "paused": "已暫停", "failed": "失敗", @@ -569,18 +558,10 @@ "pending": "正在執行", "none": "沒有下次執行" }, - "last": { - "never": "從未執行", - "unknown": "未知" - }, "message": { "showMore": "查看完整訊息", "showLess": "收起訊息" }, - "meta": { - "created": "建立於 {{time}}", - "updated": "更新於 {{time}}" - }, "fields": { "name": "名稱", "message": "訊息", diff --git a/webui/src/tests/i18n.test.tsx b/webui/src/tests/i18n.test.tsx index 6f084093..d037b2c6 100644 --- a/webui/src/tests/i18n.test.tsx +++ b/webui/src/tests/i18n.test.tsx @@ -48,7 +48,6 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [ "settings.nav.runtime", "settings.nav.advanced", "sidebar.automations", - "settings.automations.title", "settings.automations.filters.active", "settings.automations.queue", "settings.automations.empty",