feat: add opt-in docked editor toolbar (#1562)

* feat: add opt-in docked editor toolbar

* fix: align docked editor toolbar actions

* fix: move docked toolbar setting to navigation

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Roberto Bertó
2026-06-13 00:01:07 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 1235e4db71
commit 15813b61c3
17 changed files with 102 additions and 7 deletions
@@ -136,6 +136,7 @@ const VisualSectionContent: React.FC = () => {
'fileEditorKeymap',
'spacing',
'inputBarOffset',
'expandedEditorToolbar',
...(!isVSCode ? ['terminalQuickKeys' as const] : []),
'reportUsage',
]} />;
@@ -247,7 +247,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
return mode === 'markdown' ? 'markdown' : 'plain';
};
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage';
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
interface OpenChamberVisualSettingsProps {
/** Which settings to show. If undefined, shows all. */
@@ -273,6 +273,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const setCollapsibleUserMessages = useUIStore(state => state.setCollapsibleUserMessages);
const stickyUserHeader = useUIStore(state => state.stickyUserHeader);
const setStickyUserHeader = useUIStore(state => state.setStickyUserHeader);
const expandedEditorToolbar = useUIStore(state => state.expandedEditorToolbar);
const setExpandedEditorToolbar = useUIStore(state => state.setExpandedEditorToolbar);
const wideChatLayoutEnabled = useUIStore(state => state.wideChatLayoutEnabled);
const setWideChatLayoutEnabled = useUIStore(state => state.setWideChatLayoutEnabled);
const chatRenderMode = useUIStore(state => state.chatRenderMode);
@@ -415,6 +417,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
void updateDesktopSettings({ stickyUserHeader: enabled });
}, [setStickyUserHeader]);
const handleExpandedEditorToolbarChange = React.useCallback((enabled: boolean) => {
setExpandedEditorToolbar(enabled);
void updateDesktopSettings({ expandedEditorToolbar: enabled });
}, [setExpandedEditorToolbar]);
const handleCollapsibleUserMessagesChange = React.useCallback((enabled: boolean) => {
setCollapsibleUserMessages(enabled);
void updateDesktopSettings({ collapsibleUserMessages: enabled });
@@ -533,7 +540,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
? hasLocalizationSettings
: (shouldShow('theme') || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || shouldShow('fileEditorKeymap');
const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || shouldShow('fileEditorKeymap') || shouldShow('expandedEditorToolbar');
const hasBehaviorSettings = shouldShow('mermaidRendering')
|| shouldShow('userMessageRendering')
|| shouldShow('chatRenderMode')
@@ -1320,6 +1327,29 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
</div>
</div>
)}
{shouldShow('expandedEditorToolbar') && (
<div
data-settings-item="appearance.expanded-editor-toolbar"
className="group flex cursor-pointer items-center gap-2 py-1.5"
role="button"
tabIndex={0}
aria-pressed={expandedEditorToolbar}
onClick={() => handleExpandedEditorToolbarChange(!expandedEditorToolbar)}
onKeyDown={(event) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
handleExpandedEditorToolbarChange(!expandedEditorToolbar);
}
}}
>
<Checkbox
checked={expandedEditorToolbar}
onChange={handleExpandedEditorToolbarChange}
ariaLabel={t('settings.openchamber.visual.field.expandedEditorToolbarAria')}
/>
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.expandedEditorToolbar')}</span>
</div>
)}
{shouldShow('terminalQuickKeys') && !isMobile && (
<div
data-settings-item="appearance.terminal-quick-keys"
+31 -5
View File
@@ -945,8 +945,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap);
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
const settingsExpandedEditorToolbar = useUIStore((state) => state.expandedEditorToolbar);
// Global mouseup to end drag selection
React.useEffect(() => {
@@ -3005,11 +3006,19 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
);
}, [currentTheme.metadata.variant, pierreTheme, wrapLines]);
const renderFloatingFileControls = ({ exitFullscreenOnly = false }: { exitFullscreenOnly?: boolean } = {}) => {
const renderFloatingFileControls = ({
exitFullscreenOnly = false,
layout = 'floating',
}: { exitFullscreenOnly?: boolean; layout?: 'floating' | 'docked' } = {}) => {
if (!selectedFile) {
return null;
}
const docked = layout === 'docked';
const wrapperCls = docked
? 'pointer-events-auto flex flex-wrap items-center gap-1'
: 'pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-1 shadow-sm';
const withTooltip = (label: React.ReactNode, trigger: React.ReactElement) => (
<Tooltip>
<TooltipTrigger asChild>
@@ -3022,7 +3031,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
);
return (
<div className="pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-1 shadow-sm">
<div className={wrapperCls}>
{canEdit && textViewMode === 'edit' && (
<>
{isSaving ? (
@@ -3560,10 +3569,27 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
</div>
) : null}
{/* Row 2: Docked editor toolbar (expanded). Desktop-only opt-in. */}
{settingsExpandedEditorToolbar && !isMobile && selectedFile ? (
<div className="flex min-w-0 items-center gap-3 border-t border-border/40 bg-[var(--surface-subtle)] px-3 py-1">
{displaySelectedPath ? (
<span
className="min-w-0 flex-1 truncate typography-meta text-muted-foreground"
title={displaySelectedPath}
>
{displaySelectedPath}
</span>
) : null}
<div className="ml-auto min-w-0 shrink-0 overflow-x-auto">
{renderFloatingFileControls({ layout: 'docked' })}
</div>
</div>
) : null}
</div>
<div className="flex-1 min-h-0 min-w-0 relative">
{selectedFile && !isSearchOpen && (
{selectedFile && !isSearchOpen && !(settingsExpandedEditorToolbar && !isMobile) && (
<div
ref={floatingToolbarRef}
className="absolute right-3 top-3 z-30"