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:
Bohdan Triapitsyn
2026-01-26 00:24:40 +02:00
committed by GitHub
parent f34027df10
commit efe9ad27b7
18 changed files with 537 additions and 57 deletions
+12
View File
@@ -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;
}
+60
View File
@@ -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;
+11 -1
View File
@@ -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;
}
};
+14 -3
View File
@@ -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 => {
+105
View File
@@ -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;