feat: add text truncation hook and marquee support (#217)
Introduce a hook to detect text truncation for dynamic UI updates Replace static marquee spans with a reusable marquee component in files and labels Enable copying of terminal selection on mouse up and touch end
This commit is contained in:
committed by
GitHub
parent
f34027df10
commit
efe9ad27b7
@@ -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(
|
||||
|
||||
@@ -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<HTMLSpanElement>(null);
|
||||
const isTruncated = useIsTextTruncated(labelRef, [text]);
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={labelRef}
|
||||
className={cn('marquee-text', isTruncated && 'marquee-text--active')}
|
||||
title={title ?? text}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
const FileChip = memo(({ file, onRemove }: FileChipProps) => {
|
||||
const getFileIcon = () => {
|
||||
if (file.mimeType.startsWith('image/')) {
|
||||
@@ -169,9 +185,7 @@ const FileChip = memo(({ file, onRemove }: FileChipProps) => {
|
||||
</div>
|
||||
{getFileIcon()}
|
||||
<div className="overflow-hidden max-w-[200px]">
|
||||
<span className="marquee-text" title={file.serverPath || displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
<TruncatedMarquee text={displayName} title={file.serverPath || displayName} />
|
||||
</div>
|
||||
<span className="text-muted-foreground flex-shrink-0">
|
||||
({formatFileSize(file.size)})
|
||||
@@ -292,9 +306,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup }: MessageFilesDis
|
||||
>
|
||||
{getFileIcon(file.mime)}
|
||||
<div className="overflow-hidden max-w-[200px]">
|
||||
<span className="marquee-text">
|
||||
{extractFilename(file.filename)}
|
||||
</span>
|
||||
<TruncatedMarquee text={extractFilename(file.filename)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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<any>;
|
||||
@@ -1083,6 +1084,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
return getModelDisplayName(currentModel);
|
||||
};
|
||||
|
||||
const currentModelDisplayName = getCurrentModelDisplayName();
|
||||
const modelLabelRef = React.useRef<HTMLSpanElement>(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<ModelControlsProps> = ({ className }) => {
|
||||
) : (
|
||||
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
|
||||
)}
|
||||
<span
|
||||
key={`${currentProviderId}-${currentModelId}`}
|
||||
className={cn(
|
||||
'model-controls__model-label overflow-hidden',
|
||||
controlTextSize,
|
||||
'font-medium whitespace-nowrap text-foreground min-w-0',
|
||||
'max-w-[260px]'
|
||||
)}
|
||||
>
|
||||
<span className="marquee-text">
|
||||
{getCurrentModelDisplayName()}
|
||||
<span
|
||||
ref={modelLabelRef}
|
||||
key={`${currentProviderId}-${currentModelId}`}
|
||||
className={cn(
|
||||
'model-controls__model-label overflow-hidden',
|
||||
controlTextSize,
|
||||
'font-medium whitespace-nowrap text-foreground min-w-0',
|
||||
'max-w-[260px]'
|
||||
)}
|
||||
>
|
||||
<span className={cn('marquee-text', isModelLabelTruncated && 'marquee-text--active')}>
|
||||
{currentModelDisplayName}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
@@ -2246,13 +2252,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
|
||||
)}
|
||||
<span
|
||||
ref={modelLabelRef}
|
||||
className={cn(
|
||||
'model-controls__model-label typography-micro font-medium overflow-hidden min-w-0',
|
||||
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
||||
)}
|
||||
>
|
||||
<span className="marquee-text">
|
||||
{getCurrentModelDisplayName()}
|
||||
<span className={cn('marquee-text', isModelLabelTruncated && 'marquee-text--active')}>
|
||||
{currentModelDisplayName}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -122,6 +122,55 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
}
|
||||
}, []);
|
||||
|
||||
const copySelectionToClipboard = React.useCallback(async () => {
|
||||
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<TerminalController, TerminalViewportPr
|
||||
terminalRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
void copySelectionToClipboard();
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
void copySelectionToClipboard();
|
||||
}}
|
||||
>
|
||||
{enableTouchScroll ? (
|
||||
<textarea
|
||||
|
||||
@@ -49,6 +49,7 @@ type GitViewSnapshot = {
|
||||
directory?: string;
|
||||
selectedPaths: string[];
|
||||
commitMessage: string;
|
||||
generatedHighlights: string[];
|
||||
};
|
||||
|
||||
type GitmojiEntry = {
|
||||
@@ -179,7 +180,7 @@ const matchGitmojiFromSubject = (subject: string, gitmojis: GitmojiEntry[]): Git
|
||||
return null;
|
||||
};
|
||||
|
||||
let gitViewSnapshot: GitViewSnapshot | null = null;
|
||||
const gitViewSnapshots = new Map<string, GitViewSnapshot>();
|
||||
|
||||
const useEffectiveDirectory = () => {
|
||||
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore();
|
||||
@@ -226,9 +227,8 @@ export const GitView: React.FC = () => {
|
||||
} = useGitStore();
|
||||
|
||||
const initialSnapshot = React.useMemo(() => {
|
||||
if (!gitViewSnapshot) return null;
|
||||
if (gitViewSnapshot.directory !== currentDirectory) return null;
|
||||
return gitViewSnapshot;
|
||||
if (!currentDirectory) return null;
|
||||
return gitViewSnapshots.get(currentDirectory) ?? null;
|
||||
}, [currentDirectory]);
|
||||
|
||||
const settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled);
|
||||
@@ -273,7 +273,9 @@ export const GitView: React.FC = () => {
|
||||
const [hasUserAdjustedSelection, setHasUserAdjustedSelection] = React.useState(false);
|
||||
const [revertingPaths, setRevertingPaths] = React.useState<Set<string>>(new Set());
|
||||
const [isGeneratingMessage, setIsGeneratingMessage] = React.useState(false);
|
||||
const [generatedHighlights, setGeneratedHighlights] = React.useState<string[]>([]);
|
||||
const [generatedHighlights, setGeneratedHighlights] = React.useState<string[]>(
|
||||
initialSnapshot?.generatedHighlights ?? []
|
||||
);
|
||||
const clearGeneratedHighlights = React.useCallback(() => {
|
||||
setGeneratedHighlights([]);
|
||||
}, []);
|
||||
@@ -346,19 +348,14 @@ export const GitView: React.FC = () => {
|
||||
}, [expandedCommitHashes, currentDirectory, git, commitFilesMap, loadingCommitHashes]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (!currentDirectory) {
|
||||
gitViewSnapshot = null;
|
||||
return;
|
||||
}
|
||||
|
||||
gitViewSnapshot = {
|
||||
directory: currentDirectory,
|
||||
selectedPaths: Array.from(selectedPaths),
|
||||
commitMessage,
|
||||
};
|
||||
};
|
||||
}, [commitMessage, currentDirectory, selectedPaths]);
|
||||
if (!currentDirectory) return;
|
||||
gitViewSnapshots.set(currentDirectory, {
|
||||
directory: currentDirectory,
|
||||
selectedPaths: Array.from(selectedPaths),
|
||||
commitMessage,
|
||||
generatedHighlights,
|
||||
});
|
||||
}, [commitMessage, currentDirectory, selectedPaths, generatedHighlights]);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadProfiles();
|
||||
|
||||
@@ -44,6 +44,7 @@ const MODIFIER_ARROW_SUFFIX: Record<Modifier, string> = {
|
||||
cmd: '3',
|
||||
};
|
||||
|
||||
|
||||
const STREAM_OPTIONS = {
|
||||
retry: {
|
||||
maxRetries: 3,
|
||||
@@ -429,6 +430,7 @@ export const TerminalView: React.FC = () => {
|
||||
}
|
||||
}, [clearBuffer, effectiveDirectory, setConnectionError, terminal]);
|
||||
|
||||
|
||||
const handleViewportInput = React.useCallback(
|
||||
(data: string) => {
|
||||
if (!data) {
|
||||
|
||||
@@ -60,6 +60,15 @@ const branchToTitle = (branch: string): string => {
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
type PullRequestDraftSnapshot = {
|
||||
title: string;
|
||||
body: string;
|
||||
draft: boolean;
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
const pullRequestDraftSnapshots = new Map<string, PullRequestDraftSnapshot>();
|
||||
|
||||
const openExternal = async (url: string) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const desktop = (window as typeof window & { opencodeDesktop?: { openExternal?: (url: string) => Promise<unknown> } }).opencodeDesktop;
|
||||
@@ -96,14 +105,20 @@ export const PullRequestSection: React.FC<{
|
||||
setSettingsDialogOpen(true);
|
||||
}, [setSettingsDialogOpen, setSidebarSection]);
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(true);
|
||||
const snapshotKey = React.useMemo(() => `${directory}::${branch}`, [directory, branch]);
|
||||
const initialSnapshot = React.useMemo(
|
||||
() => pullRequestDraftSnapshots.get(snapshotKey) ?? null,
|
||||
[snapshotKey]
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(initialSnapshot?.isOpen ?? true);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [status, setStatus] = React.useState<GitHubPullRequestStatus | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [title, setTitle] = React.useState(() => branchToTitle(branch));
|
||||
const [body, setBody] = React.useState('');
|
||||
const [draft, setDraft] = React.useState(false);
|
||||
const [title, setTitle] = React.useState(() => initialSnapshot?.title ?? branchToTitle(branch));
|
||||
const [body, setBody] = React.useState(() => initialSnapshot?.body ?? '');
|
||||
const [draft, setDraft] = React.useState(() => initialSnapshot?.draft ?? false);
|
||||
const [mergeMethod, setMergeMethod] = React.useState<MergeMethod>('squash');
|
||||
|
||||
const [isGenerating, setIsGenerating] = React.useState(false);
|
||||
@@ -359,11 +374,25 @@ export const PullRequestSection: React.FC<{
|
||||
}, [branch, canShow, directory, github]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTitle(branchToTitle(branch));
|
||||
setBody('');
|
||||
setDraft(false);
|
||||
const snapshot = pullRequestDraftSnapshots.get(snapshotKey) ?? null;
|
||||
setTitle(snapshot?.title ?? branchToTitle(branch));
|
||||
setBody(snapshot?.body ?? '');
|
||||
setDraft(snapshot?.draft ?? false);
|
||||
setIsOpen(snapshot?.isOpen ?? true);
|
||||
void refresh();
|
||||
}, [branch, refresh]);
|
||||
}, [branch, refresh, snapshotKey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!directory || !branch) {
|
||||
return;
|
||||
}
|
||||
pullRequestDraftSnapshots.set(snapshotKey, {
|
||||
title,
|
||||
body,
|
||||
draft,
|
||||
isOpen,
|
||||
});
|
||||
}, [snapshotKey, title, body, draft, isOpen, directory, branch]);
|
||||
|
||||
const generateDescription = React.useCallback(async () => {
|
||||
if (isGenerating) return;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
||||
|
||||
export const useIsTextTruncated = <T extends HTMLElement>(
|
||||
ref: React.RefObject<T | null>,
|
||||
deps: React.DependencyList = []
|
||||
): boolean => {
|
||||
const [isTruncated, setIsTruncated] = React.useState(false);
|
||||
|
||||
const checkTruncation = React.useCallback(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const next = element.scrollWidth > element.clientWidth + 1;
|
||||
setIsTruncated(next);
|
||||
}, [ref]);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
checkTruncation();
|
||||
}, [checkTruncation, ...deps]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element || typeof ResizeObserver === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const observer = new ResizeObserver(() => {
|
||||
checkTruncation();
|
||||
});
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, [checkTruncation, ref]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const handleResize = () => checkTruncation();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [checkTruncation]);
|
||||
|
||||
return isTruncated;
|
||||
};
|
||||
@@ -644,7 +644,7 @@ html:not(.dark) .chat-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group:hover .marquee-text,
|
||||
.marquee-text:hover {
|
||||
.group:hover .marquee-text--active,
|
||||
.marquee-text--active:hover {
|
||||
animation: marquee-scroll 5s linear infinite alternate;
|
||||
}
|
||||
|
||||
@@ -395,10 +395,22 @@ export interface SettingsPayload {
|
||||
securityScopedBookmarks?: string[];
|
||||
pinnedDirectories?: string[];
|
||||
showReasoningTraces?: boolean;
|
||||
showTextJustificationActivity?: boolean;
|
||||
nativeNotificationsEnabled?: boolean;
|
||||
notificationMode?: 'always' | 'hidden-only';
|
||||
autoDeleteEnabled?: boolean;
|
||||
autoDeleteAfterDays?: number;
|
||||
queueModeEnabled?: boolean;
|
||||
gitmojiEnabled?: boolean;
|
||||
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed';
|
||||
fontSize?: number;
|
||||
padding?: number;
|
||||
cornerRadius?: number;
|
||||
inputBarOffset?: number;
|
||||
diffLayoutPreference?: 'dynamic' | 'inline' | 'side-by-side';
|
||||
diffViewMode?: 'single' | 'stacked';
|
||||
directoryShowHidden?: boolean;
|
||||
filesViewShowGitignored?: boolean;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,18 @@ import type { DesktopSettings } from '@/lib/desktop';
|
||||
|
||||
type AppearanceSlice = {
|
||||
showReasoningTraces: boolean;
|
||||
showTextJustificationActivity: boolean;
|
||||
nativeNotificationsEnabled: boolean;
|
||||
notificationMode: 'always' | 'hidden-only';
|
||||
autoDeleteEnabled: boolean;
|
||||
autoDeleteAfterDays: number;
|
||||
toolCallExpansion: 'collapsed' | 'activity' | 'detailed';
|
||||
fontSize: number;
|
||||
padding: number;
|
||||
cornerRadius: number;
|
||||
inputBarOffset: number;
|
||||
diffLayoutPreference: 'dynamic' | 'inline' | 'side-by-side';
|
||||
diffViewMode: 'single' | 'stacked';
|
||||
};
|
||||
|
||||
let initialized = false;
|
||||
@@ -19,8 +29,18 @@ export const startAppearanceAutoSave = (): void => {
|
||||
|
||||
let previous: AppearanceSlice = {
|
||||
showReasoningTraces: useUIStore.getState().showReasoningTraces,
|
||||
showTextJustificationActivity: useUIStore.getState().showTextJustificationActivity,
|
||||
nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled,
|
||||
notificationMode: useUIStore.getState().notificationMode,
|
||||
autoDeleteEnabled: useUIStore.getState().autoDeleteEnabled,
|
||||
autoDeleteAfterDays: useUIStore.getState().autoDeleteAfterDays,
|
||||
toolCallExpansion: useUIStore.getState().toolCallExpansion,
|
||||
fontSize: useUIStore.getState().fontSize,
|
||||
padding: useUIStore.getState().padding,
|
||||
cornerRadius: useUIStore.getState().cornerRadius,
|
||||
inputBarOffset: useUIStore.getState().inputBarOffset,
|
||||
diffLayoutPreference: useUIStore.getState().diffLayoutPreference,
|
||||
diffViewMode: useUIStore.getState().diffViewMode,
|
||||
};
|
||||
|
||||
let pending: Partial<DesktopSettings> | null = null;
|
||||
@@ -46,8 +66,18 @@ export const startAppearanceAutoSave = (): void => {
|
||||
useUIStore.subscribe((state) => {
|
||||
const current: AppearanceSlice = {
|
||||
showReasoningTraces: state.showReasoningTraces,
|
||||
showTextJustificationActivity: state.showTextJustificationActivity,
|
||||
nativeNotificationsEnabled: state.nativeNotificationsEnabled,
|
||||
notificationMode: state.notificationMode,
|
||||
autoDeleteEnabled: state.autoDeleteEnabled,
|
||||
autoDeleteAfterDays: state.autoDeleteAfterDays,
|
||||
toolCallExpansion: state.toolCallExpansion,
|
||||
fontSize: state.fontSize,
|
||||
padding: state.padding,
|
||||
cornerRadius: state.cornerRadius,
|
||||
inputBarOffset: state.inputBarOffset,
|
||||
diffLayoutPreference: state.diffLayoutPreference,
|
||||
diffViewMode: state.diffViewMode,
|
||||
};
|
||||
|
||||
const diff: Partial<DesktopSettings> = {};
|
||||
@@ -55,12 +85,42 @@ export const startAppearanceAutoSave = (): void => {
|
||||
if (current.showReasoningTraces !== previous.showReasoningTraces) {
|
||||
diff.showReasoningTraces = current.showReasoningTraces;
|
||||
}
|
||||
if (current.showTextJustificationActivity !== previous.showTextJustificationActivity) {
|
||||
diff.showTextJustificationActivity = current.showTextJustificationActivity;
|
||||
}
|
||||
if (current.nativeNotificationsEnabled !== previous.nativeNotificationsEnabled) {
|
||||
diff.nativeNotificationsEnabled = current.nativeNotificationsEnabled;
|
||||
}
|
||||
if (current.notificationMode !== previous.notificationMode) {
|
||||
diff.notificationMode = current.notificationMode;
|
||||
}
|
||||
if (current.autoDeleteEnabled !== previous.autoDeleteEnabled) {
|
||||
diff.autoDeleteEnabled = current.autoDeleteEnabled;
|
||||
}
|
||||
if (current.autoDeleteAfterDays !== previous.autoDeleteAfterDays) {
|
||||
diff.autoDeleteAfterDays = current.autoDeleteAfterDays;
|
||||
}
|
||||
if (current.toolCallExpansion !== previous.toolCallExpansion) {
|
||||
diff.toolCallExpansion = current.toolCallExpansion;
|
||||
}
|
||||
if (current.fontSize !== previous.fontSize) {
|
||||
diff.fontSize = current.fontSize;
|
||||
}
|
||||
if (current.padding !== previous.padding) {
|
||||
diff.padding = current.padding;
|
||||
}
|
||||
if (current.cornerRadius !== previous.cornerRadius) {
|
||||
diff.cornerRadius = current.cornerRadius;
|
||||
}
|
||||
if (current.inputBarOffset !== previous.inputBarOffset) {
|
||||
diff.inputBarOffset = current.inputBarOffset;
|
||||
}
|
||||
if (current.diffLayoutPreference !== previous.diffLayoutPreference) {
|
||||
diff.diffLayoutPreference = current.diffLayoutPreference;
|
||||
}
|
||||
if (current.diffViewMode !== previous.diffViewMode) {
|
||||
diff.diffViewMode = current.diffViewMode;
|
||||
}
|
||||
|
||||
previous = current;
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ export type DesktopSettings = {
|
||||
pinnedDirectories?: string[];
|
||||
showReasoningTraces?: boolean;
|
||||
showTextJustificationActivity?: boolean;
|
||||
nativeNotificationsEnabled?: boolean;
|
||||
notificationMode?: 'always' | 'hidden-only';
|
||||
autoDeleteEnabled?: boolean;
|
||||
autoDeleteAfterDays?: number;
|
||||
defaultModel?: string; // format: "provider/model"
|
||||
@@ -61,6 +63,15 @@ export type DesktopSettings = {
|
||||
autoCreateWorktree?: boolean;
|
||||
queueModeEnabled?: boolean;
|
||||
gitmojiEnabled?: boolean;
|
||||
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed';
|
||||
fontSize?: number;
|
||||
padding?: number;
|
||||
cornerRadius?: number;
|
||||
inputBarOffset?: number;
|
||||
diffLayoutPreference?: 'dynamic' | 'inline' | 'side-by-side';
|
||||
diffViewMode?: 'single' | 'stacked';
|
||||
directoryShowHidden?: boolean;
|
||||
filesViewShowGitignored?: boolean;
|
||||
|
||||
// Memory limits for message viewport management
|
||||
memoryLimitHistorical?: number; // Default fetch limit when loading/syncing (default: 90)
|
||||
@@ -320,4 +331,3 @@ export const restartToApplyUpdate = async (): Promise<boolean> => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
|
||||
const SHOW_HIDDEN_STORAGE_KEY = 'directoryTreeShowHidden';
|
||||
const SHOW_HIDDEN_EVENT = 'directory-show-hidden-change';
|
||||
|
||||
const readStoredShowHidden = (): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const stored = getSafeStorage().getItem(SHOW_HIDDEN_STORAGE_KEY);
|
||||
if (stored === null) {
|
||||
return true;
|
||||
}
|
||||
return stored === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,7 +27,10 @@ export const notifyDirectoryShowHiddenChanged = () => {
|
||||
window.dispatchEvent(new Event(SHOW_HIDDEN_EVENT));
|
||||
};
|
||||
|
||||
export const setDirectoryShowHidden = (value: boolean) => {
|
||||
export const setDirectoryShowHidden = (
|
||||
value: boolean,
|
||||
options: { persist?: boolean } = {}
|
||||
) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
@@ -33,6 +40,10 @@ export const setDirectoryShowHidden = (value: boolean) => {
|
||||
} catch {
|
||||
// ignore storage errors
|
||||
}
|
||||
|
||||
if (options.persist !== false) {
|
||||
void updateDesktopSettings({ directoryShowHidden: value });
|
||||
}
|
||||
};
|
||||
|
||||
export const useDirectoryShowHidden = (): boolean => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
|
||||
const SHOW_GITIGNORED_STORAGE_KEY = 'filesViewShowGitignored';
|
||||
const SHOW_GITIGNORED_EVENT = 'files-view-show-gitignored-change';
|
||||
@@ -24,7 +25,10 @@ export const notifyFilesViewShowGitignoredChanged = () => {
|
||||
window.dispatchEvent(new Event(SHOW_GITIGNORED_EVENT));
|
||||
};
|
||||
|
||||
export const setFilesViewShowGitignored = (value: boolean) => {
|
||||
export const setFilesViewShowGitignored = (
|
||||
value: boolean,
|
||||
options: { persist?: boolean } = {}
|
||||
) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
@@ -34,6 +38,10 @@ export const setFilesViewShowGitignored = (value: boolean) => {
|
||||
} catch {
|
||||
// ignore storage errors
|
||||
}
|
||||
|
||||
if (options.persist !== false) {
|
||||
void updateDesktopSettings({ filesViewShowGitignored: value });
|
||||
}
|
||||
};
|
||||
|
||||
export const useFilesViewShowGitignored = (): boolean => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { getDesktopSettings, updateDesktopSettings as updateDesktopSettingsApi,
|
||||
import type { DesktopSettings } from '@/lib/desktop';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
||||
import { setDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
||||
import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||
import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
|
||||
@@ -52,6 +54,12 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
} else {
|
||||
localStorage.removeItem('gitmojiEnabled');
|
||||
}
|
||||
if (typeof settings.directoryShowHidden === 'boolean') {
|
||||
localStorage.setItem('directoryTreeShowHidden', settings.directoryShowHidden ? 'true' : 'false');
|
||||
}
|
||||
if (typeof settings.filesViewShowGitignored === 'boolean') {
|
||||
localStorage.setItem('filesViewShowGitignored', settings.filesViewShowGitignored ? 'true' : 'false');
|
||||
}
|
||||
};
|
||||
|
||||
type PersistApi = {
|
||||
@@ -199,6 +207,54 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
|
||||
if (typeof settings.queueModeEnabled === 'boolean' && settings.queueModeEnabled !== queueStore.queueModeEnabled) {
|
||||
queueStore.setQueueMode(settings.queueModeEnabled);
|
||||
}
|
||||
|
||||
if (typeof settings.showTextJustificationActivity === 'boolean' && settings.showTextJustificationActivity !== store.showTextJustificationActivity) {
|
||||
store.setShowTextJustificationActivity(settings.showTextJustificationActivity);
|
||||
}
|
||||
if (typeof settings.nativeNotificationsEnabled === 'boolean' && settings.nativeNotificationsEnabled !== store.nativeNotificationsEnabled) {
|
||||
store.setNativeNotificationsEnabled(settings.nativeNotificationsEnabled);
|
||||
}
|
||||
if (typeof settings.notificationMode === 'string' && (settings.notificationMode === 'always' || settings.notificationMode === 'hidden-only')) {
|
||||
if (settings.notificationMode !== store.notificationMode) {
|
||||
store.setNotificationMode(settings.notificationMode);
|
||||
}
|
||||
}
|
||||
if (typeof settings.toolCallExpansion === 'string'
|
||||
&& (settings.toolCallExpansion === 'collapsed' || settings.toolCallExpansion === 'activity' || settings.toolCallExpansion === 'detailed')) {
|
||||
if (settings.toolCallExpansion !== store.toolCallExpansion) {
|
||||
store.setToolCallExpansion(settings.toolCallExpansion);
|
||||
}
|
||||
}
|
||||
if (typeof settings.fontSize === 'number' && Number.isFinite(settings.fontSize) && settings.fontSize !== store.fontSize) {
|
||||
store.setFontSize(settings.fontSize);
|
||||
}
|
||||
if (typeof settings.padding === 'number' && Number.isFinite(settings.padding) && settings.padding !== store.padding) {
|
||||
store.setPadding(settings.padding);
|
||||
}
|
||||
if (typeof settings.cornerRadius === 'number' && Number.isFinite(settings.cornerRadius) && settings.cornerRadius !== store.cornerRadius) {
|
||||
store.setCornerRadius(settings.cornerRadius);
|
||||
}
|
||||
if (typeof settings.inputBarOffset === 'number' && Number.isFinite(settings.inputBarOffset) && settings.inputBarOffset !== store.inputBarOffset) {
|
||||
store.setInputBarOffset(settings.inputBarOffset);
|
||||
}
|
||||
if (typeof settings.diffLayoutPreference === 'string'
|
||||
&& (settings.diffLayoutPreference === 'dynamic' || settings.diffLayoutPreference === 'inline' || settings.diffLayoutPreference === 'side-by-side')) {
|
||||
if (settings.diffLayoutPreference !== store.diffLayoutPreference) {
|
||||
store.setDiffLayoutPreference(settings.diffLayoutPreference);
|
||||
}
|
||||
}
|
||||
if (typeof settings.diffViewMode === 'string'
|
||||
&& (settings.diffViewMode === 'single' || settings.diffViewMode === 'stacked')) {
|
||||
if (settings.diffViewMode !== store.diffViewMode) {
|
||||
store.setDiffViewMode(settings.diffViewMode);
|
||||
}
|
||||
}
|
||||
if (typeof settings.directoryShowHidden === 'boolean') {
|
||||
setDirectoryShowHidden(settings.directoryShowHidden, { persist: false });
|
||||
}
|
||||
if (typeof settings.filesViewShowGitignored === 'boolean') {
|
||||
setFilesViewShowGitignored(settings.filesViewShowGitignored, { persist: false });
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
@@ -283,6 +339,55 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
if (typeof candidate.queueModeEnabled === 'boolean') {
|
||||
result.queueModeEnabled = candidate.queueModeEnabled;
|
||||
}
|
||||
if (typeof candidate.showTextJustificationActivity === 'boolean') {
|
||||
result.showTextJustificationActivity = candidate.showTextJustificationActivity;
|
||||
}
|
||||
if (typeof candidate.nativeNotificationsEnabled === 'boolean') {
|
||||
result.nativeNotificationsEnabled = candidate.nativeNotificationsEnabled;
|
||||
}
|
||||
if (typeof candidate.notificationMode === 'string' && (candidate.notificationMode === 'always' || candidate.notificationMode === 'hidden-only')) {
|
||||
result.notificationMode = candidate.notificationMode;
|
||||
}
|
||||
if (
|
||||
typeof candidate.toolCallExpansion === 'string'
|
||||
&& (candidate.toolCallExpansion === 'collapsed'
|
||||
|| candidate.toolCallExpansion === 'activity'
|
||||
|| candidate.toolCallExpansion === 'detailed')
|
||||
) {
|
||||
result.toolCallExpansion = candidate.toolCallExpansion;
|
||||
}
|
||||
if (typeof candidate.fontSize === 'number' && Number.isFinite(candidate.fontSize)) {
|
||||
result.fontSize = candidate.fontSize;
|
||||
}
|
||||
if (typeof candidate.padding === 'number' && Number.isFinite(candidate.padding)) {
|
||||
result.padding = candidate.padding;
|
||||
}
|
||||
if (typeof candidate.cornerRadius === 'number' && Number.isFinite(candidate.cornerRadius)) {
|
||||
result.cornerRadius = candidate.cornerRadius;
|
||||
}
|
||||
if (typeof candidate.inputBarOffset === 'number' && Number.isFinite(candidate.inputBarOffset)) {
|
||||
result.inputBarOffset = candidate.inputBarOffset;
|
||||
}
|
||||
if (
|
||||
typeof candidate.diffLayoutPreference === 'string'
|
||||
&& (candidate.diffLayoutPreference === 'dynamic'
|
||||
|| candidate.diffLayoutPreference === 'inline'
|
||||
|| candidate.diffLayoutPreference === 'side-by-side')
|
||||
) {
|
||||
result.diffLayoutPreference = candidate.diffLayoutPreference;
|
||||
}
|
||||
if (
|
||||
typeof candidate.diffViewMode === 'string'
|
||||
&& (candidate.diffViewMode === 'single' || candidate.diffViewMode === 'stacked')
|
||||
) {
|
||||
result.diffViewMode = candidate.diffViewMode;
|
||||
}
|
||||
if (typeof candidate.directoryShowHidden === 'boolean') {
|
||||
result.directoryShowHidden = candidate.directoryShowHidden;
|
||||
}
|
||||
if (typeof candidate.filesViewShowGitignored === 'boolean') {
|
||||
result.filesViewShowGitignored = candidate.filesViewShowGitignored;
|
||||
}
|
||||
|
||||
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {
|
||||
result.memoryLimitHistorical = candidate.memoryLimitHistorical;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const useMessageQueueStore = create<MessageQueueStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
queuedMessages: {},
|
||||
queueModeEnabled: false,
|
||||
queueModeEnabled: true,
|
||||
|
||||
addToQueue: (sessionId, message) => {
|
||||
const id = `queued-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
@@ -145,7 +145,7 @@ export const useUIStore = create<UIStore>()(
|
||||
sidebarSection: 'sessions',
|
||||
eventStreamStatus: 'idle',
|
||||
eventStreamHint: null,
|
||||
showReasoningTraces: false,
|
||||
showReasoningTraces: true,
|
||||
showTextJustificationActivity: false,
|
||||
autoDeleteEnabled: false,
|
||||
autoDeleteAfterDays: 30,
|
||||
@@ -160,10 +160,10 @@ export const useUIStore = create<UIStore>()(
|
||||
inputBarOffset: 0,
|
||||
favoriteModels: [],
|
||||
recentModels: [],
|
||||
diffLayoutPreference: 'dynamic',
|
||||
diffLayoutPreference: 'inline',
|
||||
diffFileLayout: {},
|
||||
diffWrapLines: false,
|
||||
diffViewMode: 'single',
|
||||
diffViewMode: 'stacked',
|
||||
isTimelineDialogOpen: false,
|
||||
nativeNotificationsEnabled: false,
|
||||
notificationMode: 'hidden-only',
|
||||
|
||||
@@ -736,6 +736,18 @@ const sanitizeSettingsUpdate = (payload) => {
|
||||
if (typeof candidate.showReasoningTraces === 'boolean') {
|
||||
result.showReasoningTraces = candidate.showReasoningTraces;
|
||||
}
|
||||
if (typeof candidate.showTextJustificationActivity === 'boolean') {
|
||||
result.showTextJustificationActivity = candidate.showTextJustificationActivity;
|
||||
}
|
||||
if (typeof candidate.nativeNotificationsEnabled === 'boolean') {
|
||||
result.nativeNotificationsEnabled = candidate.nativeNotificationsEnabled;
|
||||
}
|
||||
if (typeof candidate.notificationMode === 'string') {
|
||||
const mode = candidate.notificationMode.trim();
|
||||
if (mode === 'always' || mode === 'hidden-only') {
|
||||
result.notificationMode = mode;
|
||||
}
|
||||
}
|
||||
if (typeof candidate.autoDeleteEnabled === 'boolean') {
|
||||
result.autoDeleteEnabled = candidate.autoDeleteEnabled;
|
||||
}
|
||||
@@ -774,6 +786,42 @@ const sanitizeSettingsUpdate = (payload) => {
|
||||
if (typeof candidate.gitmojiEnabled === 'boolean') {
|
||||
result.gitmojiEnabled = candidate.gitmojiEnabled;
|
||||
}
|
||||
if (typeof candidate.toolCallExpansion === 'string') {
|
||||
const mode = candidate.toolCallExpansion.trim();
|
||||
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed') {
|
||||
result.toolCallExpansion = mode;
|
||||
}
|
||||
}
|
||||
if (typeof candidate.fontSize === 'number' && Number.isFinite(candidate.fontSize)) {
|
||||
result.fontSize = Math.max(50, Math.min(200, Math.round(candidate.fontSize)));
|
||||
}
|
||||
if (typeof candidate.padding === 'number' && Number.isFinite(candidate.padding)) {
|
||||
result.padding = Math.max(50, Math.min(200, Math.round(candidate.padding)));
|
||||
}
|
||||
if (typeof candidate.cornerRadius === 'number' && Number.isFinite(candidate.cornerRadius)) {
|
||||
result.cornerRadius = Math.max(0, Math.min(32, Math.round(candidate.cornerRadius)));
|
||||
}
|
||||
if (typeof candidate.inputBarOffset === 'number' && Number.isFinite(candidate.inputBarOffset)) {
|
||||
result.inputBarOffset = Math.max(0, Math.min(100, Math.round(candidate.inputBarOffset)));
|
||||
}
|
||||
if (typeof candidate.diffLayoutPreference === 'string') {
|
||||
const mode = candidate.diffLayoutPreference.trim();
|
||||
if (mode === 'dynamic' || mode === 'inline' || mode === 'side-by-side') {
|
||||
result.diffLayoutPreference = mode;
|
||||
}
|
||||
}
|
||||
if (typeof candidate.diffViewMode === 'string') {
|
||||
const mode = candidate.diffViewMode.trim();
|
||||
if (mode === 'single' || mode === 'stacked') {
|
||||
result.diffViewMode = mode;
|
||||
}
|
||||
}
|
||||
if (typeof candidate.directoryShowHidden === 'boolean') {
|
||||
result.directoryShowHidden = candidate.directoryShowHidden;
|
||||
}
|
||||
if (typeof candidate.filesViewShowGitignored === 'boolean') {
|
||||
result.filesViewShowGitignored = candidate.filesViewShowGitignored;
|
||||
}
|
||||
|
||||
// Memory limits for message viewport management
|
||||
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {
|
||||
|
||||
Reference in New Issue
Block a user