diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx index d9ee3611..3cd6dfe0 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -136,6 +136,7 @@ const VisualSectionContent: React.FC = () => { 'fileEditorKeymap', 'spacing', 'inputBarOffset', + 'expandedEditorToolbar', ...(!isVSCode ? ['terminalQuickKeys' as const] : []), 'reportUsage', ]} />; diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 8a558db9..7bf41847 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -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 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 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 ? 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 )} + {shouldShow('expandedEditorToolbar') && ( +
handleExpandedEditorToolbarChange(!expandedEditorToolbar)} + onKeyDown={(event) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + handleExpandedEditorToolbarChange(!expandedEditorToolbar); + } + }} + > + + {t('settings.openchamber.visual.field.expandedEditorToolbar')} +
+ )} {shouldShow('terminalQuickKeys') && !isMobile && (
= ({ 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 = ({ 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) => ( @@ -3022,7 +3031,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { ); return ( -
+
{canEdit && textViewMode === 'edit' && ( <> {isSaving ? ( @@ -3560,10 +3569,27 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
) : null} + {/* Row 2: Docked editor toolbar (expanded). Desktop-only opt-in. */} + {settingsExpandedEditorToolbar && !isMobile && selectedFile ? ( +
+ {displaySelectedPath ? ( + + {displaySelectedPath} + + ) : null} +
+ {renderFloatingFileControls({ layout: 'docked' })} +
+
+ ) : null} +
- {selectedFile && !isSearchOpen && ( + {selectedFile && !isSearchOpen && !(settingsExpandedEditorToolbar && !isMobile) && (
{ if (typeof settings.stickyUserHeader === 'boolean' && settings.stickyUserHeader !== store.stickyUserHeader) { store.setStickyUserHeader(settings.stickyUserHeader); } + if (typeof settings.expandedEditorToolbar === 'boolean' && settings.expandedEditorToolbar !== store.expandedEditorToolbar) { + store.setExpandedEditorToolbar(settings.expandedEditorToolbar); + } if (typeof settings.wideChatLayoutEnabled === 'boolean' && settings.wideChatLayoutEnabled !== store.wideChatLayoutEnabled) { store.setWideChatLayoutEnabled(settings.wideChatLayoutEnabled); } diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index 6ad1812e..c6ce968b 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -115,6 +115,12 @@ export const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ descriptionKey: 'settings.openchamber.visual.field.inputBarOffsetTooltip', keywords: ['input', 'home bar', 'offset'], }, + { + id: 'appearance.expanded-editor-toolbar', + page: 'appearance', + titleKey: 'settings.openchamber.visual.field.expandedEditorToolbar', + keywords: ['editor', 'toolbar', 'tabs', 'docked', 'files'], + }, { id: 'appearance.file-editor-keymap', page: 'appearance', diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 28201da1..aa6570e5 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -620,6 +620,7 @@ interface UIStore { userMessageRenderingMode: UserMessageRenderingMode; collapsibleUserMessages: boolean; stickyUserHeader: boolean; + expandedEditorToolbar: boolean; showSplitAssistantMessageActions: boolean; isMobileSessionStatusBarCollapsed: boolean; mobileSessionPanelOpen: boolean; @@ -763,6 +764,7 @@ interface UIStore { setUserMessageRenderingMode: (value: UserMessageRenderingMode) => void; setCollapsibleUserMessages: (value: boolean) => void; setStickyUserHeader: (value: boolean) => void; + setExpandedEditorToolbar: (value: boolean) => void; setShowSplitAssistantMessageActions: (value: boolean) => void; setIsMobileSessionStatusBarCollapsed: (value: boolean) => void; setMobileSessionPanelOpen: (value: boolean) => void; @@ -899,6 +901,7 @@ export const useUIStore = create()( userMessageRenderingMode: 'markdown', collapsibleUserMessages: true, stickyUserHeader: false, + expandedEditorToolbar: false, showSplitAssistantMessageActions: false, isMobileSessionStatusBarCollapsed: false, mobileSessionPanelOpen: false, @@ -2010,6 +2013,9 @@ export const useUIStore = create()( setStickyUserHeader: (value) => { set({ stickyUserHeader: value }); }, + setExpandedEditorToolbar: (value: boolean) => { + set({ expandedEditorToolbar: value }); + }, setShowSplitAssistantMessageActions: (value) => { set({ showSplitAssistantMessageActions: value }); }, @@ -2240,6 +2246,7 @@ export const useUIStore = create()( userMessageRenderingMode: state.userMessageRenderingMode, collapsibleUserMessages: state.collapsibleUserMessages, stickyUserHeader: state.stickyUserHeader, + expandedEditorToolbar: state.expandedEditorToolbar, showSplitAssistantMessageActions: state.showSplitAssistantMessageActions, isMobileSessionStatusBarCollapsed: state.isMobileSessionStatusBarCollapsed, mobileSessionFilterProjectId: state.mobileSessionFilterProjectId, diff --git a/packages/web/server/lib/opencode/settings-helpers.js b/packages/web/server/lib/opencode/settings-helpers.js index c58ddf43..504e0f4e 100644 --- a/packages/web/server/lib/opencode/settings-helpers.js +++ b/packages/web/server/lib/opencode/settings-helpers.js @@ -438,6 +438,9 @@ export const createSettingsHelpers = (dependencies) => { if (typeof candidate.stickyUserHeader === 'boolean') { result.stickyUserHeader = candidate.stickyUserHeader; } + if (typeof candidate.expandedEditorToolbar === 'boolean') { + result.expandedEditorToolbar = candidate.expandedEditorToolbar; + } if (typeof candidate.showSplitAssistantMessageActions === 'boolean') { result.showSplitAssistantMessageActions = candidate.showSplitAssistantMessageActions; }