feat(chat): persist more composer input history with global or session scope; recallable with up/down arrow keys (#3035)
* feat(chat): persist input history * feat(settings): configure input history scope * fix(web): keep input history validation packaged * fix(chat): preserve input history across tabs * fix(settings): restore prompt history limit * fix(settings): keep history deletion warning visible * fix(i18n): restore Turkish Git empty state translations --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
29480383cc
commit
1d6b15bc04
@@ -204,6 +204,8 @@ const ChatSectionContent: React.FC = () => {
|
||||
'splitAssistantMessageActions',
|
||||
'subagentReadOnlyBanner',
|
||||
'diffLayout',
|
||||
'inputHistoryScope',
|
||||
'inputHistoryLimit',
|
||||
'dotfiles',
|
||||
'fileViewerPreview',
|
||||
'followUpBehavior',
|
||||
|
||||
@@ -61,9 +61,18 @@ import {
|
||||
import { SettingsInfoHint } from '@/components/sections/shared/SettingsInfoHint';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import type { TerminalShellOption } from '@/lib/api/types';
|
||||
import {
|
||||
MAX_INPUT_HISTORY_LIMIT,
|
||||
MIN_INPUT_HISTORY_LIMIT,
|
||||
isInputHistoryLimit,
|
||||
type InputHistoryScope,
|
||||
} from '@/lib/inputHistoryScope';
|
||||
import { isTerminalShell } from '@/lib/terminalShell';
|
||||
import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
|
||||
import { formatShortcutForDisplay } from '@/lib/shortcuts';
|
||||
import {
|
||||
useInputHistoryStore,
|
||||
} from '@/stores/useInputHistoryStore';
|
||||
|
||||
interface Option<T extends string> {
|
||||
id: T;
|
||||
@@ -279,11 +288,22 @@ const LARGE_TEXT_PASTE_BEHAVIOR_OPTIONS: Option<LargeTextPasteBehavior>[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const INPUT_HISTORY_SCOPE_OPTIONS: Option<InputHistoryScope>[] = [
|
||||
{
|
||||
id: 'global',
|
||||
labelKey: 'settings.openchamber.visual.option.inputHistoryScope.global.label',
|
||||
},
|
||||
{
|
||||
id: 'session',
|
||||
labelKey: 'settings.openchamber.visual.option.inputHistoryScope.session.label',
|
||||
},
|
||||
];
|
||||
|
||||
const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' => {
|
||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||
};
|
||||
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | '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' | 'largeTextPaste' | 'enterToSend' | 'reportUsage' | 'autoSaveEnabled' | 'sessionTabs';
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'inputHistoryScope' | 'inputHistoryLimit' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'largeTextPaste' | 'enterToSend' | 'reportUsage' | 'autoSaveEnabled' | 'sessionTabs';
|
||||
|
||||
const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPosition; labelKey: string }> = [
|
||||
{ id: 'left', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsLeft' },
|
||||
@@ -374,6 +394,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const setFileEditorKeymap = useUIStore(state => state.setFileEditorKeymap);
|
||||
const followUpBehavior = useMessageQueueStore(state => state.followUpBehavior);
|
||||
const setFollowUpBehavior = useMessageQueueStore(state => state.setFollowUpBehavior);
|
||||
const inputHistoryScope = useInputHistoryStore(state => state.scope);
|
||||
const inputHistoryLimit = useInputHistoryStore(state => state.entryLimit);
|
||||
const applyInputHistoryScope = useInputHistoryStore(state => state.applyScope);
|
||||
const applyInputHistoryLimit = useInputHistoryStore(state => state.applyEntryLimit);
|
||||
const persistChatDraft = useUIStore(state => state.persistChatDraft);
|
||||
const setPersistChatDraft = useUIStore(state => state.setPersistChatDraft);
|
||||
const inputSpellcheckEnabled = useUIStore(state => state.inputSpellcheckEnabled);
|
||||
@@ -560,6 +584,20 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
void updateDesktopSettings({ messageStreamTransport: mode });
|
||||
}, [setMessageStreamTransport]);
|
||||
|
||||
const handleInputHistoryScopeChange = React.useCallback((scope: InputHistoryScope) => {
|
||||
applyInputHistoryScope(scope);
|
||||
void updateDesktopSettings({ inputHistoryScope: scope });
|
||||
}, [applyInputHistoryScope]);
|
||||
|
||||
const handleInputHistoryLimitChange = React.useCallback((value: number) => {
|
||||
const nextLimit = Math.round(value);
|
||||
if (!isInputHistoryLimit(nextLimit)) {
|
||||
return;
|
||||
}
|
||||
applyInputHistoryLimit(nextLimit);
|
||||
void updateDesktopSettings({ inputHistoryLimit: nextLimit });
|
||||
}, [applyInputHistoryLimit]);
|
||||
|
||||
const handleActivityRenderModeChange = React.useCallback((mode: 'collapsed' | 'summary') => {
|
||||
setActivityRenderMode(mode);
|
||||
void updateDesktopSettings({ activityRenderMode: mode });
|
||||
@@ -667,6 +705,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|| shouldShow('fileViewerPreview')
|
||||
|| shouldShow('reasoning')
|
||||
|| shouldShow('followUpBehavior')
|
||||
|| shouldShow('inputHistoryScope')
|
||||
|| shouldShow('inputHistoryLimit')
|
||||
|| shouldShow('persistDraft')
|
||||
|| shouldShow('largeTextPaste')
|
||||
|| shouldShow('showToolFileIcons')
|
||||
@@ -679,7 +719,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const showBehaviorMessageOptions = shouldShow('userMessageRendering')
|
||||
|| shouldShow('mermaidRendering')
|
||||
|| (shouldShow('diffLayout') && !isVSCode)
|
||||
|| shouldShow('followUpBehavior');
|
||||
|| shouldShow('followUpBehavior')
|
||||
|| shouldShow('inputHistoryScope')
|
||||
|| shouldShow('inputHistoryLimit');
|
||||
const showBehaviorFeatureCheckboxes = shouldShow('sessionAssist')
|
||||
|| (shouldShow('sessionGoal') && !isVSCode)
|
||||
|| shouldShow('subagentReadOnlyBanner')
|
||||
@@ -1755,6 +1797,51 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</SettingsRadioGroup>
|
||||
</SettingsControlGroup>
|
||||
)}
|
||||
|
||||
{shouldShow('inputHistoryScope') && (
|
||||
<SettingsControlGroup
|
||||
title={t('settings.openchamber.visual.field.inputHistoryScope')}
|
||||
info={t('settings.openchamber.visual.field.inputHistoryScopeDescription')}
|
||||
settingsItem="chat.input-history-scope"
|
||||
>
|
||||
<SettingsRadioGroup aria-label={t('settings.openchamber.visual.section.inputHistoryScopeAria')}>
|
||||
{INPUT_HISTORY_SCOPE_OPTIONS.map((option) => (
|
||||
<SettingsRadioOption
|
||||
key={option.id}
|
||||
selected={inputHistoryScope === option.id}
|
||||
onSelect={() => handleInputHistoryScopeChange(option.id)}
|
||||
label={tUnsafe(option.labelKey)}
|
||||
ariaLabel={tUnsafe(option.labelKey)}
|
||||
/>
|
||||
))}
|
||||
</SettingsRadioGroup>
|
||||
</SettingsControlGroup>
|
||||
)}
|
||||
|
||||
{shouldShow('inputHistoryLimit') && (
|
||||
<SettingsControlGroup
|
||||
title={t('settings.openchamber.visual.field.inputHistoryLimit')}
|
||||
description={t('settings.openchamber.visual.field.inputHistoryLimitDescription')}
|
||||
contentClassName={SETTINGS_CONTROL_CLUSTER_CLASS}
|
||||
settingsItem="chat.input-history-limit"
|
||||
>
|
||||
<div className={SETTINGS_NUMBER_STEPPER_ROW_CLASS}>
|
||||
<NumberInput
|
||||
value={inputHistoryLimit}
|
||||
onValueChange={handleInputHistoryLimitChange}
|
||||
min={MIN_INPUT_HISTORY_LIMIT}
|
||||
max={MAX_INPUT_HISTORY_LIMIT}
|
||||
step={1}
|
||||
className={SETTINGS_NUMBER_INPUT_CLASS}
|
||||
deferExternalValueWhileFocused
|
||||
aria-label={t('settings.openchamber.visual.field.inputHistoryLimitAria')}
|
||||
/>
|
||||
<span className={SETTINGS_NUMBER_UNIT_CLASS}>
|
||||
{t('settings.openchamber.visual.field.inputHistoryLimitUnit')}
|
||||
</span>
|
||||
</div>
|
||||
</SettingsControlGroup>
|
||||
)}
|
||||
</SettingsTwoColumn>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user