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") {
|
if let Some(Value::Bool(b)) = obj.get("showReasoningTraces") {
|
||||||
result_obj.insert("showReasoningTraces".to_string(), json!(b));
|
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") {
|
if let Some(Value::Bool(b)) = obj.get("autoDeleteEnabled") {
|
||||||
result_obj.insert("autoDeleteEnabled".to_string(), json!(b));
|
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") {
|
if let Some(Value::Bool(b)) = obj.get("autoCreateWorktree") {
|
||||||
result_obj.insert("autoCreateWorktree".to_string(), json!(b));
|
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
|
// Number fields
|
||||||
if let Some(Value::Number(n)) = obj.get("autoDeleteAfterDays") {
|
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
|
// Memory limit fields
|
||||||
if let Some(Value::Number(n)) = obj.get("memoryLimitHistorical") {
|
if let Some(Value::Number(n)) = obj.get("memoryLimitHistorical") {
|
||||||
let parsed = n
|
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
|
// Array fields
|
||||||
if let Some(arr) = obj.get("approvedDirectories") {
|
if let Some(arr) = obj.get("approvedDirectories") {
|
||||||
result_obj.insert(
|
result_obj.insert(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toast } from '@/components/ui';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||||
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
|
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import { useIsTextTruncated } from '@/hooks/useIsTextTruncated';
|
||||||
import type { ToolPopupContent } from './message/types';
|
import type { ToolPopupContent } from './message/types';
|
||||||
|
|
||||||
export const FileAttachmentButton = memo(() => {
|
export const FileAttachmentButton = memo(() => {
|
||||||
@@ -125,6 +126,21 @@ interface FileChipProps {
|
|||||||
onRemove: () => void;
|
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 FileChip = memo(({ file, onRemove }: FileChipProps) => {
|
||||||
const getFileIcon = () => {
|
const getFileIcon = () => {
|
||||||
if (file.mimeType.startsWith('image/')) {
|
if (file.mimeType.startsWith('image/')) {
|
||||||
@@ -169,9 +185,7 @@ const FileChip = memo(({ file, onRemove }: FileChipProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{getFileIcon()}
|
{getFileIcon()}
|
||||||
<div className="overflow-hidden max-w-[200px]">
|
<div className="overflow-hidden max-w-[200px]">
|
||||||
<span className="marquee-text" title={file.serverPath || displayName}>
|
<TruncatedMarquee text={displayName} title={file.serverPath || displayName} />
|
||||||
{displayName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground flex-shrink-0">
|
<span className="text-muted-foreground flex-shrink-0">
|
||||||
({formatFileSize(file.size)})
|
({formatFileSize(file.size)})
|
||||||
@@ -292,9 +306,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup }: MessageFilesDis
|
|||||||
>
|
>
|
||||||
{getFileIcon(file.mime)}
|
{getFileIcon(file.mime)}
|
||||||
<div className="overflow-hidden max-w-[200px]">
|
<div className="overflow-hidden max-w-[200px]">
|
||||||
<span className="marquee-text">
|
<TruncatedMarquee text={extractFilename(file.filename)} />
|
||||||
{extractFilename(file.filename)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { useConfigStore } from '@/stores/useConfigStore';
|
|||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useModelLists } from '@/hooks/useModelLists';
|
import { useModelLists } from '@/hooks/useModelLists';
|
||||||
|
import { useIsTextTruncated } from '@/hooks/useIsTextTruncated';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type IconComponent = ComponentType<any>;
|
type IconComponent = ComponentType<any>;
|
||||||
@@ -1083,6 +1084,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
|||||||
return getModelDisplayName(currentModel);
|
return getModelDisplayName(currentModel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentModelDisplayName = getCurrentModelDisplayName();
|
||||||
|
const modelLabelRef = React.useRef<HTMLSpanElement>(null);
|
||||||
|
const isModelLabelTruncated = useIsTextTruncated(modelLabelRef, [currentModelDisplayName, isCompact]);
|
||||||
|
|
||||||
const getAgentDisplayName = () => {
|
const getAgentDisplayName = () => {
|
||||||
if (!uiAgentName) {
|
if (!uiAgentName) {
|
||||||
const primaryAgents = agents.filter(agent => isPrimaryMode(agent.mode));
|
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')} />
|
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
key={`${currentProviderId}-${currentModelId}`}
|
ref={modelLabelRef}
|
||||||
className={cn(
|
key={`${currentProviderId}-${currentModelId}`}
|
||||||
'model-controls__model-label overflow-hidden',
|
className={cn(
|
||||||
controlTextSize,
|
'model-controls__model-label overflow-hidden',
|
||||||
'font-medium whitespace-nowrap text-foreground min-w-0',
|
controlTextSize,
|
||||||
'max-w-[260px]'
|
'font-medium whitespace-nowrap text-foreground min-w-0',
|
||||||
)}
|
'max-w-[260px]'
|
||||||
>
|
)}
|
||||||
<span className="marquee-text">
|
>
|
||||||
{getCurrentModelDisplayName()}
|
<span className={cn('marquee-text', isModelLabelTruncated && 'marquee-text--active')}>
|
||||||
|
{currentModelDisplayName}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -2246,13 +2252,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
|||||||
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
|
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
|
ref={modelLabelRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__model-label typography-micro font-medium overflow-hidden min-w-0',
|
'model-controls__model-label typography-micro font-medium overflow-hidden min-w-0',
|
||||||
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="marquee-text">
|
<span className={cn('marquee-text', isModelLabelTruncated && 'marquee-text--active')}>
|
||||||
{getCurrentModelDisplayName()}
|
{currentModelDisplayName}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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(() => {
|
const resetWriteState = React.useCallback(() => {
|
||||||
pendingWriteRef.current = '';
|
pendingWriteRef.current = '';
|
||||||
if (writeScheduledRef.current !== null && typeof window !== 'undefined') {
|
if (writeScheduledRef.current !== null && typeof window !== 'undefined') {
|
||||||
@@ -782,6 +831,12 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
terminalRef.current?.focus();
|
terminalRef.current?.focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onMouseUp={() => {
|
||||||
|
void copySelectionToClipboard();
|
||||||
|
}}
|
||||||
|
onTouchEnd={() => {
|
||||||
|
void copySelectionToClipboard();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{enableTouchScroll ? (
|
{enableTouchScroll ? (
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type GitViewSnapshot = {
|
|||||||
directory?: string;
|
directory?: string;
|
||||||
selectedPaths: string[];
|
selectedPaths: string[];
|
||||||
commitMessage: string;
|
commitMessage: string;
|
||||||
|
generatedHighlights: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GitmojiEntry = {
|
type GitmojiEntry = {
|
||||||
@@ -179,7 +180,7 @@ const matchGitmojiFromSubject = (subject: string, gitmojis: GitmojiEntry[]): Git
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
let gitViewSnapshot: GitViewSnapshot | null = null;
|
const gitViewSnapshots = new Map<string, GitViewSnapshot>();
|
||||||
|
|
||||||
const useEffectiveDirectory = () => {
|
const useEffectiveDirectory = () => {
|
||||||
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore();
|
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore();
|
||||||
@@ -226,9 +227,8 @@ export const GitView: React.FC = () => {
|
|||||||
} = useGitStore();
|
} = useGitStore();
|
||||||
|
|
||||||
const initialSnapshot = React.useMemo(() => {
|
const initialSnapshot = React.useMemo(() => {
|
||||||
if (!gitViewSnapshot) return null;
|
if (!currentDirectory) return null;
|
||||||
if (gitViewSnapshot.directory !== currentDirectory) return null;
|
return gitViewSnapshots.get(currentDirectory) ?? null;
|
||||||
return gitViewSnapshot;
|
|
||||||
}, [currentDirectory]);
|
}, [currentDirectory]);
|
||||||
|
|
||||||
const settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled);
|
const settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled);
|
||||||
@@ -273,7 +273,9 @@ export const GitView: React.FC = () => {
|
|||||||
const [hasUserAdjustedSelection, setHasUserAdjustedSelection] = React.useState(false);
|
const [hasUserAdjustedSelection, setHasUserAdjustedSelection] = React.useState(false);
|
||||||
const [revertingPaths, setRevertingPaths] = React.useState<Set<string>>(new Set());
|
const [revertingPaths, setRevertingPaths] = React.useState<Set<string>>(new Set());
|
||||||
const [isGeneratingMessage, setIsGeneratingMessage] = React.useState(false);
|
const [isGeneratingMessage, setIsGeneratingMessage] = React.useState(false);
|
||||||
const [generatedHighlights, setGeneratedHighlights] = React.useState<string[]>([]);
|
const [generatedHighlights, setGeneratedHighlights] = React.useState<string[]>(
|
||||||
|
initialSnapshot?.generatedHighlights ?? []
|
||||||
|
);
|
||||||
const clearGeneratedHighlights = React.useCallback(() => {
|
const clearGeneratedHighlights = React.useCallback(() => {
|
||||||
setGeneratedHighlights([]);
|
setGeneratedHighlights([]);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -346,19 +348,14 @@ export const GitView: React.FC = () => {
|
|||||||
}, [expandedCommitHashes, currentDirectory, git, commitFilesMap, loadingCommitHashes]);
|
}, [expandedCommitHashes, currentDirectory, git, commitFilesMap, loadingCommitHashes]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
if (!currentDirectory) return;
|
||||||
if (!currentDirectory) {
|
gitViewSnapshots.set(currentDirectory, {
|
||||||
gitViewSnapshot = null;
|
directory: currentDirectory,
|
||||||
return;
|
selectedPaths: Array.from(selectedPaths),
|
||||||
}
|
commitMessage,
|
||||||
|
generatedHighlights,
|
||||||
gitViewSnapshot = {
|
});
|
||||||
directory: currentDirectory,
|
}, [commitMessage, currentDirectory, selectedPaths, generatedHighlights]);
|
||||||
selectedPaths: Array.from(selectedPaths),
|
|
||||||
commitMessage,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}, [commitMessage, currentDirectory, selectedPaths]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
loadProfiles();
|
loadProfiles();
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const MODIFIER_ARROW_SUFFIX: Record<Modifier, string> = {
|
|||||||
cmd: '3',
|
cmd: '3',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const STREAM_OPTIONS = {
|
const STREAM_OPTIONS = {
|
||||||
retry: {
|
retry: {
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
@@ -429,6 +430,7 @@ export const TerminalView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [clearBuffer, effectiveDirectory, setConnectionError, terminal]);
|
}, [clearBuffer, effectiveDirectory, setConnectionError, terminal]);
|
||||||
|
|
||||||
|
|
||||||
const handleViewportInput = React.useCallback(
|
const handleViewportInput = React.useCallback(
|
||||||
(data: string) => {
|
(data: string) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ const branchToTitle = (branch: string): string => {
|
|||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
.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) => {
|
const openExternal = async (url: string) => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
const desktop = (window as typeof window & { opencodeDesktop?: { openExternal?: (url: string) => Promise<unknown> } }).opencodeDesktop;
|
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(true);
|
||||||
}, [setSettingsDialogOpen, setSidebarSection]);
|
}, [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 [isLoading, setIsLoading] = React.useState(false);
|
||||||
const [status, setStatus] = React.useState<GitHubPullRequestStatus | null>(null);
|
const [status, setStatus] = React.useState<GitHubPullRequestStatus | null>(null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const [title, setTitle] = React.useState(() => branchToTitle(branch));
|
const [title, setTitle] = React.useState(() => initialSnapshot?.title ?? branchToTitle(branch));
|
||||||
const [body, setBody] = React.useState('');
|
const [body, setBody] = React.useState(() => initialSnapshot?.body ?? '');
|
||||||
const [draft, setDraft] = React.useState(false);
|
const [draft, setDraft] = React.useState(() => initialSnapshot?.draft ?? false);
|
||||||
const [mergeMethod, setMergeMethod] = React.useState<MergeMethod>('squash');
|
const [mergeMethod, setMergeMethod] = React.useState<MergeMethod>('squash');
|
||||||
|
|
||||||
const [isGenerating, setIsGenerating] = React.useState(false);
|
const [isGenerating, setIsGenerating] = React.useState(false);
|
||||||
@@ -359,11 +374,25 @@ export const PullRequestSection: React.FC<{
|
|||||||
}, [branch, canShow, directory, github]);
|
}, [branch, canShow, directory, github]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setTitle(branchToTitle(branch));
|
const snapshot = pullRequestDraftSnapshots.get(snapshotKey) ?? null;
|
||||||
setBody('');
|
setTitle(snapshot?.title ?? branchToTitle(branch));
|
||||||
setDraft(false);
|
setBody(snapshot?.body ?? '');
|
||||||
|
setDraft(snapshot?.draft ?? false);
|
||||||
|
setIsOpen(snapshot?.isOpen ?? true);
|
||||||
void refresh();
|
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 () => {
|
const generateDescription = React.useCallback(async () => {
|
||||||
if (isGenerating) return;
|
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;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group:hover .marquee-text,
|
.group:hover .marquee-text--active,
|
||||||
.marquee-text:hover {
|
.marquee-text--active:hover {
|
||||||
animation: marquee-scroll 5s linear infinite alternate;
|
animation: marquee-scroll 5s linear infinite alternate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,10 +395,22 @@ export interface SettingsPayload {
|
|||||||
securityScopedBookmarks?: string[];
|
securityScopedBookmarks?: string[];
|
||||||
pinnedDirectories?: string[];
|
pinnedDirectories?: string[];
|
||||||
showReasoningTraces?: boolean;
|
showReasoningTraces?: boolean;
|
||||||
|
showTextJustificationActivity?: boolean;
|
||||||
|
nativeNotificationsEnabled?: boolean;
|
||||||
|
notificationMode?: 'always' | 'hidden-only';
|
||||||
autoDeleteEnabled?: boolean;
|
autoDeleteEnabled?: boolean;
|
||||||
autoDeleteAfterDays?: number;
|
autoDeleteAfterDays?: number;
|
||||||
queueModeEnabled?: boolean;
|
queueModeEnabled?: boolean;
|
||||||
gitmojiEnabled?: 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;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,18 @@ import type { DesktopSettings } from '@/lib/desktop';
|
|||||||
|
|
||||||
type AppearanceSlice = {
|
type AppearanceSlice = {
|
||||||
showReasoningTraces: boolean;
|
showReasoningTraces: boolean;
|
||||||
|
showTextJustificationActivity: boolean;
|
||||||
|
nativeNotificationsEnabled: boolean;
|
||||||
|
notificationMode: 'always' | 'hidden-only';
|
||||||
autoDeleteEnabled: boolean;
|
autoDeleteEnabled: boolean;
|
||||||
autoDeleteAfterDays: number;
|
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;
|
let initialized = false;
|
||||||
@@ -19,8 +29,18 @@ export const startAppearanceAutoSave = (): void => {
|
|||||||
|
|
||||||
let previous: AppearanceSlice = {
|
let previous: AppearanceSlice = {
|
||||||
showReasoningTraces: useUIStore.getState().showReasoningTraces,
|
showReasoningTraces: useUIStore.getState().showReasoningTraces,
|
||||||
|
showTextJustificationActivity: useUIStore.getState().showTextJustificationActivity,
|
||||||
|
nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled,
|
||||||
|
notificationMode: useUIStore.getState().notificationMode,
|
||||||
autoDeleteEnabled: useUIStore.getState().autoDeleteEnabled,
|
autoDeleteEnabled: useUIStore.getState().autoDeleteEnabled,
|
||||||
autoDeleteAfterDays: useUIStore.getState().autoDeleteAfterDays,
|
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;
|
let pending: Partial<DesktopSettings> | null = null;
|
||||||
@@ -46,8 +66,18 @@ export const startAppearanceAutoSave = (): void => {
|
|||||||
useUIStore.subscribe((state) => {
|
useUIStore.subscribe((state) => {
|
||||||
const current: AppearanceSlice = {
|
const current: AppearanceSlice = {
|
||||||
showReasoningTraces: state.showReasoningTraces,
|
showReasoningTraces: state.showReasoningTraces,
|
||||||
|
showTextJustificationActivity: state.showTextJustificationActivity,
|
||||||
|
nativeNotificationsEnabled: state.nativeNotificationsEnabled,
|
||||||
|
notificationMode: state.notificationMode,
|
||||||
autoDeleteEnabled: state.autoDeleteEnabled,
|
autoDeleteEnabled: state.autoDeleteEnabled,
|
||||||
autoDeleteAfterDays: state.autoDeleteAfterDays,
|
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> = {};
|
const diff: Partial<DesktopSettings> = {};
|
||||||
@@ -55,12 +85,42 @@ export const startAppearanceAutoSave = (): void => {
|
|||||||
if (current.showReasoningTraces !== previous.showReasoningTraces) {
|
if (current.showReasoningTraces !== previous.showReasoningTraces) {
|
||||||
diff.showReasoningTraces = current.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) {
|
if (current.autoDeleteEnabled !== previous.autoDeleteEnabled) {
|
||||||
diff.autoDeleteEnabled = current.autoDeleteEnabled;
|
diff.autoDeleteEnabled = current.autoDeleteEnabled;
|
||||||
}
|
}
|
||||||
if (current.autoDeleteAfterDays !== previous.autoDeleteAfterDays) {
|
if (current.autoDeleteAfterDays !== previous.autoDeleteAfterDays) {
|
||||||
diff.autoDeleteAfterDays = current.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;
|
previous = current;
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export type DesktopSettings = {
|
|||||||
pinnedDirectories?: string[];
|
pinnedDirectories?: string[];
|
||||||
showReasoningTraces?: boolean;
|
showReasoningTraces?: boolean;
|
||||||
showTextJustificationActivity?: boolean;
|
showTextJustificationActivity?: boolean;
|
||||||
|
nativeNotificationsEnabled?: boolean;
|
||||||
|
notificationMode?: 'always' | 'hidden-only';
|
||||||
autoDeleteEnabled?: boolean;
|
autoDeleteEnabled?: boolean;
|
||||||
autoDeleteAfterDays?: number;
|
autoDeleteAfterDays?: number;
|
||||||
defaultModel?: string; // format: "provider/model"
|
defaultModel?: string; // format: "provider/model"
|
||||||
@@ -61,6 +63,15 @@ export type DesktopSettings = {
|
|||||||
autoCreateWorktree?: boolean;
|
autoCreateWorktree?: boolean;
|
||||||
queueModeEnabled?: boolean;
|
queueModeEnabled?: boolean;
|
||||||
gitmojiEnabled?: 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
|
// Memory limits for message viewport management
|
||||||
memoryLimitHistorical?: number; // Default fetch limit when loading/syncing (default: 90)
|
memoryLimitHistorical?: number; // Default fetch limit when loading/syncing (default: 90)
|
||||||
@@ -320,4 +331,3 @@ export const restartToApplyUpdate = async (): Promise<boolean> => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
||||||
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
|
|
||||||
const SHOW_HIDDEN_STORAGE_KEY = 'directoryTreeShowHidden';
|
const SHOW_HIDDEN_STORAGE_KEY = 'directoryTreeShowHidden';
|
||||||
const SHOW_HIDDEN_EVENT = 'directory-show-hidden-change';
|
const SHOW_HIDDEN_EVENT = 'directory-show-hidden-change';
|
||||||
|
|
||||||
const readStoredShowHidden = (): boolean => {
|
const readStoredShowHidden = (): boolean => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const stored = getSafeStorage().getItem(SHOW_HIDDEN_STORAGE_KEY);
|
const stored = getSafeStorage().getItem(SHOW_HIDDEN_STORAGE_KEY);
|
||||||
|
if (stored === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return stored === 'true';
|
return stored === 'true';
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,7 +27,10 @@ export const notifyDirectoryShowHiddenChanged = () => {
|
|||||||
window.dispatchEvent(new Event(SHOW_HIDDEN_EVENT));
|
window.dispatchEvent(new Event(SHOW_HIDDEN_EVENT));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setDirectoryShowHidden = (value: boolean) => {
|
export const setDirectoryShowHidden = (
|
||||||
|
value: boolean,
|
||||||
|
options: { persist?: boolean } = {}
|
||||||
|
) => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,6 +40,10 @@ export const setDirectoryShowHidden = (value: boolean) => {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore storage errors
|
// ignore storage errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.persist !== false) {
|
||||||
|
void updateDesktopSettings({ directoryShowHidden: value });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDirectoryShowHidden = (): boolean => {
|
export const useDirectoryShowHidden = (): boolean => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
||||||
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
|
|
||||||
const SHOW_GITIGNORED_STORAGE_KEY = 'filesViewShowGitignored';
|
const SHOW_GITIGNORED_STORAGE_KEY = 'filesViewShowGitignored';
|
||||||
const SHOW_GITIGNORED_EVENT = 'files-view-show-gitignored-change';
|
const SHOW_GITIGNORED_EVENT = 'files-view-show-gitignored-change';
|
||||||
@@ -24,7 +25,10 @@ export const notifyFilesViewShowGitignoredChanged = () => {
|
|||||||
window.dispatchEvent(new Event(SHOW_GITIGNORED_EVENT));
|
window.dispatchEvent(new Event(SHOW_GITIGNORED_EVENT));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setFilesViewShowGitignored = (value: boolean) => {
|
export const setFilesViewShowGitignored = (
|
||||||
|
value: boolean,
|
||||||
|
options: { persist?: boolean } = {}
|
||||||
|
) => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -34,6 +38,10 @@ export const setFilesViewShowGitignored = (value: boolean) => {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore storage errors
|
// ignore storage errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.persist !== false) {
|
||||||
|
void updateDesktopSettings({ filesViewShowGitignored: value });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFilesViewShowGitignored = (): boolean => {
|
export const useFilesViewShowGitignored = (): boolean => {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { getDesktopSettings, updateDesktopSettings as updateDesktopSettingsApi,
|
|||||||
import type { DesktopSettings } from '@/lib/desktop';
|
import type { DesktopSettings } from '@/lib/desktop';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
||||||
|
import { setDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
||||||
|
import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||||
import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence';
|
import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence';
|
||||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
|
|
||||||
@@ -52,6 +54,12 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
|||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('gitmojiEnabled');
|
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 = {
|
type PersistApi = {
|
||||||
@@ -199,6 +207,54 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
|
|||||||
if (typeof settings.queueModeEnabled === 'boolean' && settings.queueModeEnabled !== queueStore.queueModeEnabled) {
|
if (typeof settings.queueModeEnabled === 'boolean' && settings.queueModeEnabled !== queueStore.queueModeEnabled) {
|
||||||
queueStore.setQueueMode(settings.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 => {
|
const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||||
@@ -283,6 +339,55 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
|||||||
if (typeof candidate.queueModeEnabled === 'boolean') {
|
if (typeof candidate.queueModeEnabled === 'boolean') {
|
||||||
result.queueModeEnabled = candidate.queueModeEnabled;
|
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)) {
|
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {
|
||||||
result.memoryLimitHistorical = candidate.memoryLimitHistorical;
|
result.memoryLimitHistorical = candidate.memoryLimitHistorical;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const useMessageQueueStore = create<MessageQueueStore>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
queuedMessages: {},
|
queuedMessages: {},
|
||||||
queueModeEnabled: false,
|
queueModeEnabled: true,
|
||||||
|
|
||||||
addToQueue: (sessionId, message) => {
|
addToQueue: (sessionId, message) => {
|
||||||
const id = `queued-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
const id = `queued-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
sidebarSection: 'sessions',
|
sidebarSection: 'sessions',
|
||||||
eventStreamStatus: 'idle',
|
eventStreamStatus: 'idle',
|
||||||
eventStreamHint: null,
|
eventStreamHint: null,
|
||||||
showReasoningTraces: false,
|
showReasoningTraces: true,
|
||||||
showTextJustificationActivity: false,
|
showTextJustificationActivity: false,
|
||||||
autoDeleteEnabled: false,
|
autoDeleteEnabled: false,
|
||||||
autoDeleteAfterDays: 30,
|
autoDeleteAfterDays: 30,
|
||||||
@@ -160,10 +160,10 @@ export const useUIStore = create<UIStore>()(
|
|||||||
inputBarOffset: 0,
|
inputBarOffset: 0,
|
||||||
favoriteModels: [],
|
favoriteModels: [],
|
||||||
recentModels: [],
|
recentModels: [],
|
||||||
diffLayoutPreference: 'dynamic',
|
diffLayoutPreference: 'inline',
|
||||||
diffFileLayout: {},
|
diffFileLayout: {},
|
||||||
diffWrapLines: false,
|
diffWrapLines: false,
|
||||||
diffViewMode: 'single',
|
diffViewMode: 'stacked',
|
||||||
isTimelineDialogOpen: false,
|
isTimelineDialogOpen: false,
|
||||||
nativeNotificationsEnabled: false,
|
nativeNotificationsEnabled: false,
|
||||||
notificationMode: 'hidden-only',
|
notificationMode: 'hidden-only',
|
||||||
|
|||||||
@@ -736,6 +736,18 @@ const sanitizeSettingsUpdate = (payload) => {
|
|||||||
if (typeof candidate.showReasoningTraces === 'boolean') {
|
if (typeof candidate.showReasoningTraces === 'boolean') {
|
||||||
result.showReasoningTraces = candidate.showReasoningTraces;
|
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') {
|
if (typeof candidate.autoDeleteEnabled === 'boolean') {
|
||||||
result.autoDeleteEnabled = candidate.autoDeleteEnabled;
|
result.autoDeleteEnabled = candidate.autoDeleteEnabled;
|
||||||
}
|
}
|
||||||
@@ -774,6 +786,42 @@ const sanitizeSettingsUpdate = (payload) => {
|
|||||||
if (typeof candidate.gitmojiEnabled === 'boolean') {
|
if (typeof candidate.gitmojiEnabled === 'boolean') {
|
||||||
result.gitmojiEnabled = candidate.gitmojiEnabled;
|
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
|
// Memory limits for message viewport management
|
||||||
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {
|
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user