diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index a1a7bdf945b..e8505830776 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -34,6 +34,7 @@ import { import { saveSettings, type UiSettings } from "./storage.ts"; import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition.ts"; import { resolveTheme, type ResolvedTheme, type ThemeMode, type ThemeName } from "./theme.ts"; +import { cleanupChatModuleState } from "./views/chat.ts"; let systemThemeCleanup: (() => void) | null = null; import type { AgentsListResult, AttentionItem } from "./types.ts"; @@ -365,9 +366,16 @@ function applyTabSelection( next: Tab, options: { refreshPolicy: "always" | "connected"; syncUrl?: boolean }, ) { + const prev = host.tab; if (host.tab !== next) { host.tab = next; } + + // Cleanup chat module state when navigating away from chat + if (prev === "chat" && next !== "chat") { + cleanupChatModuleState(); + } + if (next === "chat") { host.chatHasAutoScrolled = false; } diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index e92ad106939..79aae348697 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -501,12 +501,25 @@ function renderCollapsedToolCards( `; } +/** + * Max characters for auto-detecting and pretty-printing JSON. + * Prevents DoS from large JSON payloads in assistant/tool messages. + */ +const MAX_JSON_AUTOPARSE_CHARS = 20_000; + /** * Detect whether a trimmed string is a JSON object or array. * Must start with `{`/`[` and end with `}`/`]` and parse successfully. + * Size-capped to prevent render-loop DoS from large JSON messages. */ function detectJson(text: string): { parsed: unknown; pretty: string } | null { const t = text.trim(); + + // Enforce size cap to prevent UI freeze from multi-MB JSON payloads + if (t.length > MAX_JSON_AUTOPARSE_CHARS) { + return null; + } + if ((t.startsWith("{") && t.endsWith("}")) || (t.startsWith("[") && t.endsWith("]"))) { try { const parsed = JSON.parse(t); diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index 8048bc88670..fcf13c8047c 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -147,6 +147,28 @@ let searchOpen = false; let searchQuery = ""; let pinnedExpanded = false; +/** + * Cleanup module-level state when navigating away from chat view. + * Prevents STT recording from continuing after tab switch (which would + * send transcripts to the wrong session) and resets ephemeral UI state. + */ +export function cleanupChatModuleState() { + if (sttRecording) { + stopStt(); + sttRecording = false; + sttInterimText = ""; + } + slashMenuOpen = false; + slashMenuItems = []; + slashMenuIndex = 0; + slashMenuMode = "command"; + slashMenuCommand = null; + slashMenuArgItems = []; + searchOpen = false; + searchQuery = ""; + pinnedExpanded = false; +} + function adjustTextareaHeight(el: HTMLTextAreaElement) { el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 150)}px`; diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index 9335f66f9a5..ea052c019cc 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -6,6 +6,7 @@ import type { ConfigUiHints } from "../types.ts"; import { countSensitiveConfigValues, humanize, + isSensitiveConfigPath, pathKey, REDACTED_PLACEHOLDER, schemaType, @@ -501,6 +502,26 @@ function truncateValue(value: unknown, maxLen = 40): string { return str.slice(0, maxLen - 3) + "..."; } +/** + * Render diff value with redaction when in stream mode or path is sensitive. + * Prevents secrets from appearing in diff panel during screen sharing. + */ +function renderDiffValue( + path: string, + value: unknown, + streamMode: boolean, + uiHints: ConfigUiHints, +): string { + const hint = uiHints[path]; + const sensitive = hint?.sensitive ?? isSensitiveConfigPath(path); + + if (streamMode && sensitive) { + return REDACTED_PLACEHOLDER; + } + + return truncateValue(value); +} + type ThemeOption = { id: ThemeName; label: string; description: string; icon: TemplateResult }; const THEME_OPTIONS: ThemeOption[] = [ { id: "claw", label: "Claw", description: "Chroma family", icon: icons.zap }, @@ -909,11 +930,11 @@ export function renderConfig(props: ConfigProps) {