From 5c72ca38ab83c06c625905850f3928a0bb7f0b85 Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Sun, 25 Jan 2026 19:42:06 +0000 Subject: [PATCH 1/2] fix(tts): generate audio when block streaming drops final reply When block streaming succeeds, final replies are dropped but TTS was only applied to final replies. Fix by accumulating block text during streaming and generating TTS-only audio after streaming completes. Also: - Change truncate vs skip behavior when summary OFF (now truncates) - Align TTS limits with Telegram max (4096 chars) - Improve /tts command help messages with examples - Add newline separator between accumulated blocks --- src/auto-reply/reply/commands-tts.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auto-reply/reply/commands-tts.ts b/src/auto-reply/reply/commands-tts.ts index 04b60a4e9..bba7e2b02 100644 --- a/src/auto-reply/reply/commands-tts.ts +++ b/src/auto-reply/reply/commands-tts.ts @@ -25,11 +25,11 @@ type ParsedTtsCommand = { }; function parseTtsCommand(normalized: string): ParsedTtsCommand | null { - // Accept `/tts` and `/tts [args]` as a single control surface. - if (normalized === "/tts") return { action: "status", args: "" }; + // Accept `/tts [args]` - return null for `/tts` alone to trigger inline menu. + if (normalized === "/tts") return null; if (!normalized.startsWith("/tts ")) return null; const rest = normalized.slice(5).trim(); - if (!rest) return { action: "status", args: "" }; + if (!rest) return null; const [action, ...tail] = rest.split(/\s+/); return { action: action.toLowerCase(), args: tail.join(" ").trim() }; } From 1353a848d78f96147114e332b9e5964f6d7837c8 Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Mon, 26 Jan 2026 04:21:11 +0000 Subject: [PATCH 2/2] fix(ui): visually distinguish system messages in webchat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add distinct styling for system role messages in the webchat UI. System messages now appear centered with muted styling, clearly differentiated from user and assistant messages. - Add 'system' roleClass to renderMessageGroup and renderAvatar - Add CSS styles for .chat-group.system and .chat-avatar.system - Use info icon (ℹ) for system message avatars Closes #2001 --- ui/src/styles/chat/grouped.css | 34 ++++++++++++++++++++++++++++++++ ui/src/ui/chat/grouped-render.ts | 18 ++++++++++++----- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index 39b641826..16ee6d872 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -34,6 +34,34 @@ justify-content: flex-end; } +/* System messages centered with muted styling */ +.chat-group.system { + justify-content: center; + margin-left: 16px; +} + +.chat-group.system .chat-group-messages { + align-items: center; + max-width: min(600px, calc(100% - 60px)); +} + +.chat-group.system .chat-bubble { + background: var(--secondary); + border: 1px solid var(--border); + font-size: 13px; + color: var(--muted); + padding: 8px 12px; +} + +.chat-group.system .chat-group-footer { + justify-content: center; +} + +.chat-group.system .chat-sender-name { + color: var(--muted); + opacity: 0.8; +} + /* Footer at bottom of message group (role + time) */ .chat-group-footer { display: flex; @@ -89,6 +117,12 @@ color: var(--muted); } +.chat-avatar.system { + background: var(--border); + color: var(--muted); + font-size: 16px; +} + /* Image avatar support */ img.chat-avatar { display: block; diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 4a9ccec14..e18014a0b 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -120,13 +120,17 @@ export function renderMessageGroup( ? "You" : normalizedRole === "assistant" ? assistantName - : normalizedRole; + : normalizedRole === "system" + ? "System" + : normalizedRole; const roleClass = normalizedRole === "user" ? "user" : normalizedRole === "assistant" ? "assistant" - : "other"; + : normalizedRole === "system" + ? "system" + : "other"; const timestamp = new Date(group.timestamp).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", @@ -173,15 +177,19 @@ function renderAvatar( ? assistantName.charAt(0).toUpperCase() || "A" : normalized === "tool" ? "⚙" - : "?"; + : normalized === "system" + ? "ℹ" + : "?"; const className = normalized === "user" ? "user" : normalized === "assistant" ? "assistant" - : normalized === "tool" + : normalized === "tool" ? "tool" - : "other"; + : normalized === "system" + ? "system" + : "other"; if (assistantAvatar && normalized === "assistant") { if (isAvatarUrl(assistantAvatar)) {