diff --git a/packages/desktop/src-tauri/src/commands/settings.rs b/packages/desktop/src-tauri/src/commands/settings.rs index 69ffec5f..65de2e85 100644 --- a/packages/desktop/src-tauri/src/commands/settings.rs +++ b/packages/desktop/src-tauri/src/commands/settings.rs @@ -289,6 +289,18 @@ fn sanitize_settings_update(payload: &Value) -> Value { if let Some(Value::Bool(b)) = obj.get("showReasoningTraces") { result_obj.insert("showReasoningTraces".to_string(), json!(b)); } + if let Some(Value::Bool(b)) = obj.get("showTextJustificationActivity") { + result_obj.insert("showTextJustificationActivity".to_string(), json!(b)); + } + if let Some(Value::Bool(b)) = obj.get("nativeNotificationsEnabled") { + result_obj.insert("nativeNotificationsEnabled".to_string(), json!(b)); + } + if let Some(Value::String(s)) = obj.get("notificationMode") { + let trimmed = s.trim(); + if trimmed == "always" || trimmed == "hidden-only" { + result_obj.insert("notificationMode".to_string(), json!(trimmed)); + } + } if let Some(Value::Bool(b)) = obj.get("autoDeleteEnabled") { result_obj.insert("autoDeleteEnabled".to_string(), json!(b)); } @@ -298,6 +310,12 @@ fn sanitize_settings_update(payload: &Value) -> Value { if let Some(Value::Bool(b)) = obj.get("autoCreateWorktree") { result_obj.insert("autoCreateWorktree".to_string(), json!(b)); } + if let Some(Value::String(s)) = obj.get("toolCallExpansion") { + let trimmed = s.trim(); + if trimmed == "collapsed" || trimmed == "activity" || trimmed == "detailed" { + result_obj.insert("toolCallExpansion".to_string(), json!(trimmed)); + } + } // Number fields if let Some(Value::Number(n)) = obj.get("autoDeleteAfterDays") { @@ -314,6 +332,47 @@ fn sanitize_settings_update(payload: &Value) -> Value { } } + if let Some(Value::Number(n)) = obj.get("fontSize") { + let parsed = n + .as_u64() + .or_else(|| n.as_i64().and_then(|v| if v >= 0 { Some(v as u64) } else { None })) + .or_else(|| n.as_f64().map(|v| v.round().max(0.0) as u64)); + if let Some(value) = parsed { + let clamped = value.max(50).min(200); + result_obj.insert("fontSize".to_string(), json!(clamped)); + } + } + if let Some(Value::Number(n)) = obj.get("padding") { + let parsed = n + .as_u64() + .or_else(|| n.as_i64().and_then(|v| if v >= 0 { Some(v as u64) } else { None })) + .or_else(|| n.as_f64().map(|v| v.round().max(0.0) as u64)); + if let Some(value) = parsed { + let clamped = value.max(50).min(200); + result_obj.insert("padding".to_string(), json!(clamped)); + } + } + if let Some(Value::Number(n)) = obj.get("cornerRadius") { + let parsed = n + .as_u64() + .or_else(|| n.as_i64().and_then(|v| if v >= 0 { Some(v as u64) } else { None })) + .or_else(|| n.as_f64().map(|v| v.round().max(0.0) as u64)); + if let Some(value) = parsed { + let clamped = value.max(0).min(32); + result_obj.insert("cornerRadius".to_string(), json!(clamped)); + } + } + if let Some(Value::Number(n)) = obj.get("inputBarOffset") { + let parsed = n + .as_u64() + .or_else(|| n.as_i64().and_then(|v| if v >= 0 { Some(v as u64) } else { None })) + .or_else(|| n.as_f64().map(|v| v.round().max(0.0) as u64)); + if let Some(value) = parsed { + let clamped = value.max(0).min(100); + result_obj.insert("inputBarOffset".to_string(), json!(clamped)); + } + } + // Memory limit fields if let Some(Value::Number(n)) = obj.get("memoryLimitHistorical") { let parsed = n @@ -355,6 +414,25 @@ fn sanitize_settings_update(payload: &Value) -> Value { } } + if let Some(Value::String(s)) = obj.get("diffLayoutPreference") { + let trimmed = s.trim(); + if trimmed == "dynamic" || trimmed == "inline" || trimmed == "side-by-side" { + result_obj.insert("diffLayoutPreference".to_string(), json!(trimmed)); + } + } + if let Some(Value::String(s)) = obj.get("diffViewMode") { + let trimmed = s.trim(); + if trimmed == "single" || trimmed == "stacked" { + result_obj.insert("diffViewMode".to_string(), json!(trimmed)); + } + } + if let Some(Value::Bool(b)) = obj.get("directoryShowHidden") { + result_obj.insert("directoryShowHidden".to_string(), json!(b)); + } + if let Some(Value::Bool(b)) = obj.get("filesViewShowGitignored") { + result_obj.insert("filesViewShowGitignored".to_string(), json!(b)); + } + // Array fields if let Some(arr) = obj.get("approvedDirectories") { result_obj.insert( diff --git a/packages/ui/src/components/chat/FileAttachment.tsx b/packages/ui/src/components/chat/FileAttachment.tsx index a4e9ca44..15d8886e 100644 --- a/packages/ui/src/components/chat/FileAttachment.tsx +++ b/packages/ui/src/components/chat/FileAttachment.tsx @@ -6,6 +6,7 @@ import { toast } from '@/components/ui'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs'; +import { useIsTextTruncated } from '@/hooks/useIsTextTruncated'; import type { ToolPopupContent } from './message/types'; export const FileAttachmentButton = memo(() => { @@ -125,6 +126,21 @@ interface FileChipProps { onRemove: () => void; } +const TruncatedMarquee = memo(({ text, title }: { text: string; title?: string }) => { + const labelRef = useRef(null); + const isTruncated = useIsTextTruncated(labelRef, [text]); + + return ( + + {text} + + ); +}); + const FileChip = memo(({ file, onRemove }: FileChipProps) => { const getFileIcon = () => { if (file.mimeType.startsWith('image/')) { @@ -169,9 +185,7 @@ const FileChip = memo(({ file, onRemove }: FileChipProps) => { {getFileIcon()}
- - {displayName} - +
({formatFileSize(file.size)}) @@ -292,9 +306,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup }: MessageFilesDis > {getFileIcon(file.mime)}
- - {extractFilename(file.filename)} - +
))} diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index 90e83e9d..65bfa57c 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -47,6 +47,7 @@ import { useConfigStore } from '@/stores/useConfigStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; import { useModelLists } from '@/hooks/useModelLists'; +import { useIsTextTruncated } from '@/hooks/useIsTextTruncated'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type IconComponent = ComponentType; @@ -1083,6 +1084,10 @@ export const ModelControls: React.FC = ({ className }) => { return getModelDisplayName(currentModel); }; + const currentModelDisplayName = getCurrentModelDisplayName(); + const modelLabelRef = React.useRef(null); + const isModelLabelTruncated = useIsTextTruncated(modelLabelRef, [currentModelDisplayName, isCompact]); + const getAgentDisplayName = () => { if (!uiAgentName) { const primaryAgents = agents.filter(agent => isPrimaryMode(agent.mode)); @@ -2122,19 +2127,20 @@ export const ModelControls: React.FC = ({ className }) => { ) : ( )} - - - {getCurrentModelDisplayName()} + + + {currentModelDisplayName} + - @@ -2246,13 +2252,14 @@ export const ModelControls: React.FC = ({ className }) => { )} - - {getCurrentModelDisplayName()} + + {currentModelDisplayName} diff --git a/packages/ui/src/components/terminal/TerminalViewport.tsx b/packages/ui/src/components/terminal/TerminalViewport.tsx index bb5b1f98..0a949afe 100644 --- a/packages/ui/src/components/terminal/TerminalViewport.tsx +++ b/packages/ui/src/components/terminal/TerminalViewport.tsx @@ -122,6 +122,55 @@ const TerminalViewport = React.forwardRef { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return; + } + const selection = window.getSelection(); + if (!selection) { + return; + } + const text = selection.toString(); + if (!text.trim()) { + return; + } + const container = containerRef.current; + if (!container) { + return; + } + const anchorNode = selection.anchorNode; + const focusNode = selection.focusNode; + if (anchorNode && !container.contains(anchorNode)) { + return; + } + if (focusNode && !container.contains(focusNode)) { + return; + } + + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + } catch { + // fall through to execCommand + } + + try { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } catch { + // ignore + } + }, []); + const resetWriteState = React.useCallback(() => { pendingWriteRef.current = ''; if (writeScheduledRef.current !== null && typeof window !== 'undefined') { @@ -782,6 +831,12 @@ const TerminalViewport = React.forwardRef { + void copySelectionToClipboard(); + }} + onTouchEnd={() => { + void copySelectionToClipboard(); + }} > {enableTouchScroll ? (