feat(chat): desktop prompt navigator rail (#2054)
* feat(chat): add desktop prompt navigator rail Add a ChatGPT-style right-center prompt marker rail for web/desktop chat with hover/keyboard preview panel, load-more for partial history (panel only), Chat setting, and mod+alt+p shortcut. Disabled in VS Code across rail, shortcut, settings, help, and search surfaces. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(ui): read promptNavigatorEnabled from getState in shortcut handler Match the file convention used by other shortcut handlers so the toggle does not rely on a hook-level selector closure. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(ui): drop use-no-memo and default prompt navigator off Remove the project-unprecedented React Compiler opt-out, and ship the prompt navigator as opt-in to match other recent chat UI toggles. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Serhii Dziupin
Cursor Agent
parent
502c96630e
commit
1fb448d827
@@ -6,6 +6,7 @@ import { Icon } from "@/components/icon/Icon";
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import {
|
||||
formatShortcutForDisplay,
|
||||
getCustomizableShortcutActions,
|
||||
@@ -54,7 +55,13 @@ export const KeyboardShortcutsSettings: React.FC = () => {
|
||||
const clearShortcutOverride = useUIStore((state) => state.clearShortcutOverride);
|
||||
const resetAllShortcutOverrides = useUIStore((state) => state.resetAllShortcutOverrides);
|
||||
|
||||
const actions = React.useMemo(() => getCustomizableShortcutActions(), []);
|
||||
const actions = React.useMemo(() => {
|
||||
const all = getCustomizableShortcutActions();
|
||||
if (!isVSCodeRuntime()) {
|
||||
return all;
|
||||
}
|
||||
return all.filter((action) => action.id !== 'toggle_prompt_navigator');
|
||||
}, []);
|
||||
const actionLabel = React.useCallback((id: string, fallbackLabel: string): string => {
|
||||
const key = `settings.openchamber.keyboardShortcuts.action.${id}.label`;
|
||||
const translated = tUnsafe(key);
|
||||
|
||||
@@ -145,7 +145,37 @@ const VisualSectionContent: React.FC = () => {
|
||||
|
||||
// Chat section: User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Follow-up behavior, Persist draft
|
||||
const ChatSectionContent: React.FC = () => {
|
||||
return <OpenChamberVisualSettings visibleSettings={['sessionGoal', 'sessionAssist', 'chatRenderMode', 'messageTransport', 'activityRenderMode', 'userMessageRendering', 'mermaidRendering', 'reasoning', 'showToolFileIcons', 'showTurnChangedFiles', 'expandedTools', 'collapsibleUserMessages', 'stickyUserHeader', 'wideChatLayout', 'codeBlockLineWrap', 'splitAssistantMessageActions', 'subagentReadOnlyBanner', 'diffLayout', 'dotfiles', 'fileViewerPreview', 'followUpBehavior', 'persistDraft', 'inputSpellcheck']} />;
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
return (
|
||||
<OpenChamberVisualSettings
|
||||
visibleSettings={[
|
||||
'sessionGoal',
|
||||
'sessionAssist',
|
||||
'chatRenderMode',
|
||||
'messageTransport',
|
||||
'activityRenderMode',
|
||||
'userMessageRendering',
|
||||
'mermaidRendering',
|
||||
'reasoning',
|
||||
'showToolFileIcons',
|
||||
'showTurnChangedFiles',
|
||||
'expandedTools',
|
||||
'collapsibleUserMessages',
|
||||
'stickyUserHeader',
|
||||
...(!isVSCode ? ['promptNavigatorEnabled' as const] : []),
|
||||
'wideChatLayout',
|
||||
'codeBlockLineWrap',
|
||||
'splitAssistantMessageActions',
|
||||
'subagentReadOnlyBanner',
|
||||
'diffLayout',
|
||||
'dotfiles',
|
||||
'fileViewerPreview',
|
||||
'followUpBehavior',
|
||||
'persistDraft',
|
||||
'inputSpellcheck',
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Sessions section: Default model & agent, Session retention
|
||||
|
||||
@@ -245,7 +245,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||
};
|
||||
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
|
||||
interface OpenChamberVisualSettingsProps {
|
||||
/** Which settings to show. If undefined, shows all. */
|
||||
@@ -280,7 +280,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const collapsibleUserMessages = useUIStore(state => state.collapsibleUserMessages);
|
||||
const setCollapsibleUserMessages = useUIStore(state => state.setCollapsibleUserMessages);
|
||||
const stickyUserHeader = useUIStore(state => state.stickyUserHeader);
|
||||
const promptNavigatorEnabled = useUIStore(state => state.promptNavigatorEnabled);
|
||||
const setStickyUserHeader = useUIStore(state => state.setStickyUserHeader);
|
||||
const setPromptNavigatorEnabled = useUIStore(state => state.setPromptNavigatorEnabled);
|
||||
const expandedEditorToolbar = useUIStore(state => state.expandedEditorToolbar);
|
||||
const setExpandedEditorToolbar = useUIStore(state => state.setExpandedEditorToolbar);
|
||||
const wideChatLayoutEnabled = useUIStore(state => state.wideChatLayoutEnabled);
|
||||
@@ -441,6 +443,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
void updateDesktopSettings({ stickyUserHeader: enabled });
|
||||
}, [setStickyUserHeader]);
|
||||
|
||||
const handlePromptNavigatorEnabledChange = React.useCallback((enabled: boolean) => {
|
||||
setPromptNavigatorEnabled(enabled);
|
||||
void updateDesktopSettings({ promptNavigatorEnabled: enabled });
|
||||
}, [setPromptNavigatorEnabled]);
|
||||
|
||||
const handleExpandedEditorToolbarChange = React.useCallback((enabled: boolean) => {
|
||||
setExpandedEditorToolbar(enabled);
|
||||
void updateDesktopSettings({ expandedEditorToolbar: enabled });
|
||||
@@ -572,9 +579,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|| (shouldShow('activityRenderMode') && chatRenderMode === 'sorted')
|
||||
|| shouldShow('collapsibleUserMessages')
|
||||
|| shouldShow('stickyUserHeader')
|
||||
|| (shouldShow('promptNavigatorEnabled') && !isVSCode)
|
||||
|| shouldShow('wideChatLayout')
|
||||
|| shouldShow('codeBlockLineWrap')
|
||||
|| shouldShow('splitAssistantMessageActions')
|
||||
|| shouldShow('subagentReadOnlyBanner')
|
||||
|| shouldShow('diffLayout')
|
||||
|| shouldShow('dotfiles')
|
||||
|| shouldShow('fileViewerPreview')
|
||||
@@ -1901,7 +1910,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{(shouldShow('sessionAssist') || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('codeBlockLineWrap') || shouldShow('splitAssistantMessageActions') || shouldShow('subagentReadOnlyBanner') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
|
||||
{(shouldShow('sessionAssist') || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || (shouldShow('promptNavigatorEnabled') && !isVSCode) || shouldShow('wideChatLayout') || shouldShow('codeBlockLineWrap') || shouldShow('splitAssistantMessageActions') || shouldShow('subagentReadOnlyBanner') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
|
||||
<div className="space-y-6">
|
||||
{(shouldShow('sessionAssist') || shouldShow('subagentReadOnlyBanner')) && (
|
||||
<section className="p-2 space-y-0.5">
|
||||
@@ -2029,7 +2038,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{(shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('codeBlockLineWrap')) && (
|
||||
{(shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || (shouldShow('promptNavigatorEnabled') && !isVSCode) || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('codeBlockLineWrap')) && (
|
||||
<section className="p-2 space-y-0.5">
|
||||
<h3 data-settings-item="chat.message-appearance" className="typography-ui-header font-medium text-foreground py-1.5">{t('settings.openchamber.visual.section.messageAppearance')}</h3>
|
||||
{shouldShow('collapsibleUserMessages') && (
|
||||
@@ -2080,6 +2089,30 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('promptNavigatorEnabled') && !isVSCode && (
|
||||
<div
|
||||
data-settings-item="chat.prompt-navigator"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={promptNavigatorEnabled}
|
||||
onClick={() => handlePromptNavigatorEnabledChange(!promptNavigatorEnabled)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handlePromptNavigatorEnabledChange(!promptNavigatorEnabled);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={promptNavigatorEnabled}
|
||||
onChange={handlePromptNavigatorEnabledChange}
|
||||
ariaLabel={t('settings.openchamber.visual.field.promptNavigatorEnabledAria')}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.promptNavigatorEnabled')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('wideChatLayout') && (
|
||||
<div
|
||||
data-settings-item="chat.wide-layout"
|
||||
|
||||
Reference in New Issue
Block a user