* feat(mobile): refactor drawer system with swipe gestures and improved status bar
- Add DrawerContext for centralized drawer state management
- Implement swipe gesture support for left/right drawers
- Refactor MobileSessionStatusBar with token usage indicator
- Update Header component with drawer toggle props
- Improve RightSidebar touch handling
- Add mobile-specific styling improvements
- Update UI store for drawer state management
* feat(mobile): update empty session hint text to clarify swipe gesture area
* fix(mobile): restore MobileAgentButton tap-to-cycle and long-press behavior
- Revert removal of tap-to-cycle agent switching functionality
- Re-add onCycleAgent prop and long-press detection (500ms)
- Click now cycles through primary agents, long-press opens selector panel
- Fixes regression from commit 569cc411
* feat(mobile): add project switcher bar and fix button interactions
- Add ProjectBar component to MobileSessionStatusBar for quick project switching
- Add useProjectStatus hook to track project-level session indicators
- Integrate project store with directory store for project management
- Add project status indicators (running/unread) to session status bar
- Fix MobileAgentButton pointer event handling with preventDefault
- Support horizontal scroll in project bar with touch gesture isolation
* feat(mobile): add long-press to remove projects and filter sessions by project
* fix: improve mobile agent switching UX
* fix: move drawer swipe hook to dedicated file
* refactor(mobile): simplify session status bar, remove More/Less toggle button
* fix: git diff navigation on mobile in sidebar mode
* feat(mobile): add configurable status bar and refine mobile chat controls
---------
Co-authored-by: Jovines <jovines@qq.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
743 lines
44 KiB
TypeScript
743 lines
44 KiB
TypeScript
import React from 'react';
|
|
import { RiRestartLine, RiInformationLine } from '@remixicon/react';
|
|
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
|
import type { ThemeMode } from '@/types/theme';
|
|
import { useUIStore } from '@/stores/useUIStore';
|
|
import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
|
import { cn, getModifierLabel } from '@/lib/utils';
|
|
import { ButtonSmall } from '@/components/ui/button-small';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { NumberInput } from '@/components/ui/number-input';
|
|
import { Radio } from '@/components/ui/radio';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
|
import { useDeviceInfo } from '@/lib/device';
|
|
import {
|
|
setDirectoryShowHidden,
|
|
useDirectoryShowHidden,
|
|
} from '@/lib/directoryShowHidden';
|
|
|
|
interface Option<T extends string> {
|
|
id: T;
|
|
label: string;
|
|
description?: string;
|
|
}
|
|
|
|
const THEME_MODE_OPTIONS: Array<{ value: ThemeMode; label: string }> = [
|
|
{
|
|
value: 'system',
|
|
label: 'System',
|
|
},
|
|
{
|
|
value: 'light',
|
|
label: 'Light',
|
|
},
|
|
{
|
|
value: 'dark',
|
|
label: 'Dark',
|
|
},
|
|
];
|
|
|
|
const TOOL_EXPANSION_OPTIONS: Array<{ value: 'collapsed' | 'activity' | 'detailed'; label: string; description: string }> = [
|
|
{ value: 'collapsed', label: 'Collapsed', description: 'Activity and tools start collapsed' },
|
|
{ value: 'activity', label: 'Summary', description: 'Activity expanded, tools collapsed' },
|
|
{ value: 'detailed', label: 'Detailed', description: 'Activity expanded, key tools expanded' },
|
|
];
|
|
|
|
const DIFF_LAYOUT_OPTIONS: Option<'dynamic' | 'inline' | 'side-by-side'>[] = [
|
|
{
|
|
id: 'dynamic',
|
|
label: 'Dynamic',
|
|
description: 'New inline, modified side-by-side.',
|
|
},
|
|
{
|
|
id: 'inline',
|
|
label: 'Always inline',
|
|
description: 'Show as a single unified view.',
|
|
},
|
|
{
|
|
id: 'side-by-side',
|
|
label: 'Always side-by-side',
|
|
description: 'Compare original and modified files.',
|
|
},
|
|
];
|
|
|
|
const DIFF_VIEW_MODE_OPTIONS: Option<'single' | 'stacked'>[] = [
|
|
{
|
|
id: 'single',
|
|
label: 'Single file',
|
|
description: 'Show one file at a time.',
|
|
},
|
|
{
|
|
id: 'stacked',
|
|
label: 'All files',
|
|
description: 'Stack all changed files together.',
|
|
},
|
|
];
|
|
|
|
export type VisibleSetting = 'theme' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'cornerRadius' | 'inputBarOffset' | 'toolOutput' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'queueMode' | 'textJustificationActivity' | 'terminalQuickKeys' | 'persistDraft';
|
|
|
|
interface OpenChamberVisualSettingsProps {
|
|
/** Which settings to show. If undefined, shows all. */
|
|
visibleSettings?: VisibleSetting[];
|
|
}
|
|
|
|
export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps> = ({ visibleSettings }) => {
|
|
const { isMobile } = useDeviceInfo();
|
|
const directoryShowHidden = useDirectoryShowHidden();
|
|
const showReasoningTraces = useUIStore(state => state.showReasoningTraces);
|
|
const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces);
|
|
const showTextJustificationActivity = useUIStore(state => state.showTextJustificationActivity);
|
|
const setShowTextJustificationActivity = useUIStore(state => state.setShowTextJustificationActivity);
|
|
const toolCallExpansion = useUIStore(state => state.toolCallExpansion);
|
|
const setToolCallExpansion = useUIStore(state => state.setToolCallExpansion);
|
|
const fontSize = useUIStore(state => state.fontSize);
|
|
const setFontSize = useUIStore(state => state.setFontSize);
|
|
const terminalFontSize = useUIStore(state => state.terminalFontSize);
|
|
const setTerminalFontSize = useUIStore(state => state.setTerminalFontSize);
|
|
const padding = useUIStore(state => state.padding);
|
|
const setPadding = useUIStore(state => state.setPadding);
|
|
const cornerRadius = useUIStore(state => state.cornerRadius);
|
|
const setCornerRadius = useUIStore(state => state.setCornerRadius);
|
|
const inputBarOffset = useUIStore(state => state.inputBarOffset);
|
|
const setInputBarOffset = useUIStore(state => state.setInputBarOffset);
|
|
const diffLayoutPreference = useUIStore(state => state.diffLayoutPreference);
|
|
const setDiffLayoutPreference = useUIStore(state => state.setDiffLayoutPreference);
|
|
const diffViewMode = useUIStore(state => state.diffViewMode);
|
|
const setDiffViewMode = useUIStore(state => state.setDiffViewMode);
|
|
const showTerminalQuickKeysOnDesktop = useUIStore(state => state.showTerminalQuickKeysOnDesktop);
|
|
const setShowTerminalQuickKeysOnDesktop = useUIStore(state => state.setShowTerminalQuickKeysOnDesktop);
|
|
const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled);
|
|
const setQueueMode = useMessageQueueStore(state => state.setQueueMode);
|
|
const persistChatDraft = useUIStore(state => state.persistChatDraft);
|
|
const setPersistChatDraft = useUIStore(state => state.setPersistChatDraft);
|
|
const showMobileSessionStatusBar = useUIStore(state => state.showMobileSessionStatusBar);
|
|
const setShowMobileSessionStatusBar = useUIStore(state => state.setShowMobileSessionStatusBar);
|
|
const {
|
|
themeMode,
|
|
setThemeMode,
|
|
availableThemes,
|
|
customThemesLoading,
|
|
reloadCustomThemes,
|
|
lightThemeId,
|
|
darkThemeId,
|
|
setLightThemePreference,
|
|
setDarkThemePreference,
|
|
} = useThemeSystem();
|
|
|
|
const [themesReloading, setThemesReloading] = React.useState(false);
|
|
|
|
const lightThemes = React.useMemo(
|
|
() => availableThemes
|
|
.filter((theme) => theme.metadata.variant === 'light')
|
|
.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)),
|
|
[availableThemes],
|
|
);
|
|
|
|
const darkThemes = React.useMemo(
|
|
() => availableThemes
|
|
.filter((theme) => theme.metadata.variant === 'dark')
|
|
.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)),
|
|
[availableThemes],
|
|
);
|
|
|
|
const selectedLightTheme = React.useMemo(
|
|
() => lightThemes.find((theme) => theme.metadata.id === lightThemeId) ?? lightThemes[0],
|
|
[lightThemes, lightThemeId],
|
|
);
|
|
|
|
const selectedDarkTheme = React.useMemo(
|
|
() => darkThemes.find((theme) => theme.metadata.id === darkThemeId) ?? darkThemes[0],
|
|
[darkThemes, darkThemeId],
|
|
);
|
|
|
|
const formatThemeLabel = React.useCallback((themeName: string, variant: 'light' | 'dark') => {
|
|
const suffix = variant === 'dark' ? ' Dark' : ' Light';
|
|
return themeName.endsWith(suffix) ? themeName.slice(0, -suffix.length) : themeName;
|
|
}, []);
|
|
|
|
const shouldShow = (setting: VisibleSetting): boolean => {
|
|
if (!visibleSettings) return true;
|
|
return visibleSettings.includes(setting);
|
|
};
|
|
|
|
const hasAppearanceSettings = shouldShow('theme') && !isVSCodeRuntime();
|
|
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('cornerRadius') || shouldShow('inputBarOffset');
|
|
const hasBehaviorSettings = shouldShow('toolOutput')
|
|
|| shouldShow('diffLayout')
|
|
|| shouldShow('mobileStatusBar')
|
|
|| shouldShow('dotfiles')
|
|
|| shouldShow('reasoning')
|
|
|| shouldShow('queueMode')
|
|
|| shouldShow('textJustificationActivity')
|
|
|| shouldShow('persistDraft')
|
|
|| (shouldShow('terminalQuickKeys') && !isMobile);
|
|
return (
|
|
<div className="space-y-8">
|
|
|
|
{/* --- Appearance & Themes --- */}
|
|
{hasAppearanceSettings && (
|
|
<div className="mb-8 space-y-3">
|
|
<section className="px-2 pb-2 pt-0 space-y-0.5">
|
|
|
|
<div className="pb-1.5">
|
|
<div className="flex min-w-0 flex-col gap-1.5">
|
|
<span className="typography-ui-header font-medium text-foreground">Color Mode</span>
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
{THEME_MODE_OPTIONS.map((option) => (
|
|
<ButtonSmall
|
|
key={option.value}
|
|
variant="outline"
|
|
size="xs"
|
|
className={cn(
|
|
'!font-normal',
|
|
themeMode === option.value
|
|
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
|
: 'text-foreground'
|
|
)}
|
|
onClick={() => setThemeMode(option.value)}
|
|
>
|
|
{option.label}
|
|
</ButtonSmall>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-2 grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span className="typography-ui-label text-foreground shrink-0">Light Theme</span>
|
|
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
|
|
<SelectTrigger aria-label="Select light theme" className="w-fit">
|
|
<SelectValue placeholder="Select theme" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{lightThemes.map((theme) => (
|
|
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
|
|
{formatThemeLabel(theme.metadata.name, 'light')}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span className="typography-ui-label text-foreground shrink-0">Dark Theme</span>
|
|
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
|
|
<SelectTrigger aria-label="Select dark theme" className="w-fit">
|
|
<SelectValue placeholder="Select theme" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{darkThemes.map((theme) => (
|
|
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
|
|
{formatThemeLabel(theme.metadata.name, 'dark')}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 py-1.5">
|
|
<ButtonSmall
|
|
type="button"
|
|
variant="outline"
|
|
size="xs"
|
|
disabled={customThemesLoading || themesReloading}
|
|
onClick={async () => {
|
|
setThemesReloading(true);
|
|
try {
|
|
await reloadCustomThemes();
|
|
} finally {
|
|
setThemesReloading(false);
|
|
}
|
|
}}
|
|
className="!font-normal"
|
|
>
|
|
<RiRestartLine className={cn('h-3.5 w-3.5', themesReloading && 'animate-spin')} />
|
|
Reload themes
|
|
</ButtonSmall>
|
|
<Tooltip delayDuration={700}>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex items-center justify-center rounded-md p-1 text-muted-foreground/70 hover:text-foreground"
|
|
aria-label="Theme import info"
|
|
>
|
|
<RiInformationLine className="h-3.5 w-3.5" />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent sideOffset={8}>
|
|
Import custom themes from ~/.config/openchamber/themes/
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- UI Scaling & Layout --- */}
|
|
{hasLayoutSettings && (
|
|
<div className="mb-8 space-y-3">
|
|
<section className="p-2 space-y-0.5">
|
|
|
|
{shouldShow('fontSize') && !isMobile && (
|
|
<div className="flex items-center gap-8 py-1.5">
|
|
<div className="flex min-w-0 flex-col w-56 shrink-0">
|
|
<span className="typography-ui-label text-foreground">Interface Font Size</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 w-fit">
|
|
<NumberInput
|
|
value={fontSize}
|
|
onValueChange={setFontSize}
|
|
min={50}
|
|
max={200}
|
|
step={5}
|
|
aria-label="Font size percentage"
|
|
className="w-16"
|
|
/>
|
|
<ButtonSmall
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => setFontSize(100)}
|
|
disabled={fontSize === 100}
|
|
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
|
|
aria-label="Reset font size"
|
|
title="Reset"
|
|
>
|
|
<RiRestartLine className="h-3.5 w-3.5" />
|
|
</ButtonSmall>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('terminalFontSize') && (
|
|
<div className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
|
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
|
<span className="typography-ui-label text-foreground">Terminal Font Size</span>
|
|
</div>
|
|
<div className={cn("flex items-center gap-2", isMobile ? "w-full" : "w-fit")}>
|
|
<NumberInput
|
|
value={terminalFontSize}
|
|
onValueChange={setTerminalFontSize}
|
|
min={9}
|
|
max={52}
|
|
step={1}
|
|
className="w-16"
|
|
/>
|
|
<ButtonSmall
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => setTerminalFontSize(13)}
|
|
disabled={terminalFontSize === 13}
|
|
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
|
|
aria-label="Reset terminal font size"
|
|
title="Reset"
|
|
>
|
|
<RiRestartLine className="h-3.5 w-3.5" />
|
|
</ButtonSmall>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('spacing') && (
|
|
<div className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
|
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
|
<span className="typography-ui-label text-foreground">Spacing Density</span>
|
|
</div>
|
|
<div className={cn("flex items-center gap-2", isMobile ? "w-full" : "w-fit")}>
|
|
<NumberInput
|
|
value={padding}
|
|
onValueChange={setPadding}
|
|
min={50}
|
|
max={200}
|
|
step={5}
|
|
className="w-16"
|
|
/>
|
|
<ButtonSmall
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => setPadding(100)}
|
|
disabled={padding === 100}
|
|
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
|
|
aria-label="Reset spacing"
|
|
title="Reset"
|
|
>
|
|
<RiRestartLine className="h-3.5 w-3.5" />
|
|
</ButtonSmall>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('cornerRadius') && (
|
|
<div className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
|
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
|
<span className="typography-ui-label text-foreground">Corner Radius</span>
|
|
</div>
|
|
<div className={cn("flex items-center gap-2", isMobile ? "w-full" : "w-fit")}>
|
|
<NumberInput
|
|
value={cornerRadius}
|
|
onValueChange={setCornerRadius}
|
|
min={0}
|
|
max={32}
|
|
step={1}
|
|
className="w-16"
|
|
/>
|
|
<ButtonSmall
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => setCornerRadius(12)}
|
|
disabled={cornerRadius === 12}
|
|
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
|
|
aria-label="Reset corner radius"
|
|
title="Reset"
|
|
>
|
|
<RiRestartLine className="h-3.5 w-3.5" />
|
|
</ButtonSmall>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('inputBarOffset') && (
|
|
<div className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
|
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="typography-ui-label text-foreground">Input Bar Offset</span>
|
|
<Tooltip delayDuration={1000}>
|
|
<TooltipTrigger asChild>
|
|
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent sideOffset={8} className="max-w-xs">
|
|
Raise input bar to avoid OS-level screen obstructions like home bars.
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
<div className={cn("flex items-center gap-2", isMobile ? "w-full" : "w-fit")}>
|
|
<NumberInput
|
|
value={inputBarOffset}
|
|
onValueChange={setInputBarOffset}
|
|
min={0}
|
|
max={100}
|
|
step={5}
|
|
className="w-16"
|
|
/>
|
|
<ButtonSmall
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => setInputBarOffset(0)}
|
|
disabled={inputBarOffset === 0}
|
|
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
|
|
aria-label="Reset input bar offset"
|
|
title="Reset"
|
|
>
|
|
<RiRestartLine className="h-3.5 w-3.5" />
|
|
</ButtonSmall>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</section>
|
|
</div>
|
|
)}
|
|
|
|
{hasBehaviorSettings && (
|
|
<div className="space-y-3">
|
|
|
|
{shouldShow('toolOutput') && (
|
|
<section className="px-2 pb-2 pt-0">
|
|
<h4 className="typography-ui-header font-medium text-foreground">Default Tool Output</h4>
|
|
<div className="mt-1.5 flex flex-wrap items-center gap-1">
|
|
{TOOL_EXPANSION_OPTIONS.map((option) => {
|
|
return (
|
|
<ButtonSmall
|
|
key={option.value}
|
|
variant="outline"
|
|
size="xs"
|
|
className={cn(
|
|
'!font-normal',
|
|
toolCallExpansion === option.value
|
|
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
|
: 'text-foreground'
|
|
)}
|
|
onClick={() => setToolCallExpansion(option.value)}
|
|
>
|
|
{option.label}
|
|
</ButtonSmall>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{shouldShow('diffLayout') && !isVSCodeRuntime() && (
|
|
<section className="p-2">
|
|
<h4 className="typography-ui-header font-medium text-foreground">Diff Layout</h4>
|
|
<div role="radiogroup" aria-label="Diff layout" className="mt-1 space-y-0">
|
|
{DIFF_LAYOUT_OPTIONS.map((option) => {
|
|
const selected = diffLayoutPreference === option.id;
|
|
return (
|
|
<div
|
|
key={option.id}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={selected}
|
|
onClick={() => setDiffLayoutPreference(option.id)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setDiffLayoutPreference(option.id);
|
|
}
|
|
}}
|
|
className="flex w-full items-center gap-2 py-0.5 text-left"
|
|
>
|
|
<Radio
|
|
checked={selected}
|
|
onChange={() => setDiffLayoutPreference(option.id)}
|
|
ariaLabel={`Diff layout: ${option.label}`}
|
|
/>
|
|
<span className={cn('typography-ui-label font-normal', selected ? 'text-foreground' : 'text-foreground/50')}>
|
|
{option.label}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{shouldShow('diffLayout') && !isVSCodeRuntime() && (
|
|
<section className="p-2">
|
|
<h4 className="typography-ui-header font-medium text-foreground">Diff View Mode</h4>
|
|
<div role="radiogroup" aria-label="Diff view mode" className="mt-1 space-y-0">
|
|
{DIFF_VIEW_MODE_OPTIONS.map((option) => {
|
|
const selected = diffViewMode === option.id;
|
|
return (
|
|
<div
|
|
key={option.id}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={selected}
|
|
onClick={() => setDiffViewMode(option.id)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setDiffViewMode(option.id);
|
|
}
|
|
}}
|
|
className="flex w-full items-center gap-2 py-0.5 text-left"
|
|
>
|
|
<Radio
|
|
checked={selected}
|
|
onChange={() => setDiffViewMode(option.id)}
|
|
ariaLabel={`Diff view mode: ${option.label}`}
|
|
/>
|
|
<span className={cn('typography-ui-label font-normal', selected ? 'text-foreground' : 'text-foreground/50')}>
|
|
{option.label}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{(shouldShow('mobileStatusBar') || shouldShow('dotfiles') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('reasoning') || shouldShow('textJustificationActivity')) && (
|
|
<section className="p-2 space-y-0.5">
|
|
{shouldShow('mobileStatusBar') && (
|
|
<div
|
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={showMobileSessionStatusBar}
|
|
onClick={() => setShowMobileSessionStatusBar(!showMobileSessionStatusBar)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setShowMobileSessionStatusBar(!showMobileSessionStatusBar);
|
|
}
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={showMobileSessionStatusBar}
|
|
onChange={setShowMobileSessionStatusBar}
|
|
ariaLabel="Show mobile status bar"
|
|
/>
|
|
<span className="typography-ui-label text-foreground">Show Mobile Status Bar</span>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('dotfiles') && !isVSCodeRuntime() && (
|
|
<div
|
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={directoryShowHidden}
|
|
onClick={() => setDirectoryShowHidden(!directoryShowHidden)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setDirectoryShowHidden(!directoryShowHidden);
|
|
}
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={directoryShowHidden}
|
|
onChange={setDirectoryShowHidden}
|
|
ariaLabel="Show dotfiles"
|
|
/>
|
|
<span className="typography-ui-label text-foreground">Show Dotfiles</span>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('queueMode') && (
|
|
<div
|
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={queueModeEnabled}
|
|
onClick={() => setQueueMode(!queueModeEnabled)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setQueueMode(!queueModeEnabled);
|
|
}
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={queueModeEnabled}
|
|
onChange={setQueueMode}
|
|
ariaLabel="Queue messages by default"
|
|
/>
|
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
<span className="typography-ui-label text-foreground">Queue Messages by Default</span>
|
|
<Tooltip delayDuration={1000}>
|
|
<TooltipTrigger asChild>
|
|
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent sideOffset={8} className="max-w-xs">
|
|
When enabled, Enter queues messages. Use {getModifierLabel()}+Enter to send.
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('persistDraft') && (
|
|
<div
|
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={persistChatDraft}
|
|
onClick={() => setPersistChatDraft(!persistChatDraft)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setPersistChatDraft(!persistChatDraft);
|
|
}
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={persistChatDraft}
|
|
onChange={setPersistChatDraft}
|
|
ariaLabel="Persist draft messages"
|
|
/>
|
|
<span className="typography-ui-label text-foreground">Persist Draft Messages</span>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('reasoning') && (
|
|
<div
|
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={showReasoningTraces}
|
|
onClick={() => setShowReasoningTraces(!showReasoningTraces)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setShowReasoningTraces(!showReasoningTraces);
|
|
}
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={showReasoningTraces}
|
|
onChange={setShowReasoningTraces}
|
|
ariaLabel="Show reasoning traces"
|
|
/>
|
|
<span className="typography-ui-label text-foreground">Show Reasoning Traces</span>
|
|
</div>
|
|
)}
|
|
|
|
{shouldShow('textJustificationActivity') && (
|
|
<div
|
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={showTextJustificationActivity}
|
|
onClick={() => setShowTextJustificationActivity(!showTextJustificationActivity)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setShowTextJustificationActivity(!showTextJustificationActivity);
|
|
}
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={showTextJustificationActivity}
|
|
onChange={setShowTextJustificationActivity}
|
|
ariaLabel="Show justification activity"
|
|
/>
|
|
<span className="typography-ui-label text-foreground">Show Justification Activity</span>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{shouldShow('terminalQuickKeys') && !isMobile && (
|
|
<section className="p-2">
|
|
<div
|
|
className="group flex cursor-pointer items-center gap-2 rounded-md py-1.5 transition-colors hover:bg-[var(--interactive-hover)]/30"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={showTerminalQuickKeysOnDesktop}
|
|
onClick={() => setShowTerminalQuickKeysOnDesktop(!showTerminalQuickKeysOnDesktop)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
setShowTerminalQuickKeysOnDesktop(!showTerminalQuickKeysOnDesktop);
|
|
}
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={showTerminalQuickKeysOnDesktop}
|
|
onChange={setShowTerminalQuickKeysOnDesktop}
|
|
ariaLabel="Terminal quick keys"
|
|
/>
|
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
<span className="typography-ui-label text-foreground">Terminal Quick Keys</span>
|
|
<Tooltip delayDuration={1000}>
|
|
<TooltipTrigger asChild>
|
|
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent sideOffset={8} className="max-w-xs">
|
|
Show Esc, Ctrl, Arrows in terminal view
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
};
|