feat: add collapsible user message setting
This commit is contained in:
@@ -8,6 +8,7 @@ import { useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { getDirectoryForFilePath } from '@/lib/path-utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import {
|
||||
buildAgentHref,
|
||||
buildAgentMentionUrl,
|
||||
@@ -57,9 +58,11 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const [isTruncated, setIsTruncated] = React.useState(false);
|
||||
const userMessageRenderingMode = useUIStore((state) => state.userMessageRenderingMode);
|
||||
const collapsibleUserMessages = useUIStore((state) => state.collapsibleUserMessages);
|
||||
const skills = useSkillsStore((state) => state.skills);
|
||||
const openContextFile = useUIStore((state) => state.openContextFile);
|
||||
const effectiveDirectory = useEffectiveDirectory();
|
||||
const { t } = useI18n();
|
||||
const normalizedRenderingMode = normalizeUserMessageRenderingMode(userMessageRenderingMode);
|
||||
const textRef = React.useRef<HTMLDivElement>(null);
|
||||
const skillByName = React.useMemo(() => new Map(skills.map((skill) => [skill.name, skill])), [skills]);
|
||||
@@ -89,7 +92,7 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
if (!el) return;
|
||||
|
||||
const checkTruncation = () => {
|
||||
if (!isExpanded) {
|
||||
if (collapsibleUserMessages && !isExpanded) {
|
||||
setIsTruncated(el.scrollHeight > el.clientHeight);
|
||||
}
|
||||
};
|
||||
@@ -100,7 +103,14 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
resizeObserver.observe(el);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [textContent, isExpanded]);
|
||||
}, [collapsibleUserMessages, textContent, isExpanded]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!collapsibleUserMessages) {
|
||||
setIsExpanded(false);
|
||||
setIsTruncated(false);
|
||||
}
|
||||
}, [collapsibleUserMessages]);
|
||||
|
||||
const handleClick = React.useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
@@ -123,10 +133,10 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isExpanded && isTruncated) {
|
||||
if (collapsibleUserMessages && !isExpanded && isTruncated) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [hasActiveSelectionInElement, isExpanded, isTruncated, openSkill]);
|
||||
}, [collapsibleUserMessages, hasActiveSelectionInElement, isExpanded, isTruncated, openSkill]);
|
||||
|
||||
const handleCollapse = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
@@ -222,12 +232,12 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
|
||||
return (
|
||||
<div className="relative" key={part.id || `${messageId}-user-text`}>
|
||||
{isExpanded && (
|
||||
{collapsibleUserMessages && isExpanded && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCollapse}
|
||||
className="absolute top-0 right-0 z-10 flex items-center justify-center rounded-sm bg-[var(--surface-elevated)] p-0.5 text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)] hover:bg-[var(--interactive-hover)] transition-colors"
|
||||
aria-label="Collapse"
|
||||
aria-label={t('chat.message.userText.collapseAria')}
|
||||
>
|
||||
<Icon name="arrow-up-s" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -237,8 +247,8 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
"break-words font-sans typography-markdown-body",
|
||||
isExpanded && "pb-3",
|
||||
normalizedRenderingMode === 'plain' && 'whitespace-pre-wrap',
|
||||
!isExpanded && "line-clamp-2",
|
||||
isTruncated && !isExpanded && "cursor-pointer"
|
||||
collapsibleUserMessages && !isExpanded && "line-clamp-2",
|
||||
collapsibleUserMessages && isTruncated && !isExpanded && "cursor-pointer"
|
||||
)}
|
||||
ref={textRef}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -143,7 +143,7 @@ const VisualSectionContent: React.FC = () => {
|
||||
|
||||
// Chat section: User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Queue mode, Persist draft
|
||||
const ChatSectionContent: React.FC = () => {
|
||||
return <OpenChamberVisualSettings visibleSettings={['chatRenderMode', 'messageTransport', 'activityRenderMode', 'userMessageRendering', 'mermaidRendering', 'reasoning', 'showToolFileIcons', 'showTurnChangedFiles', 'expandedTools', 'stickyUserHeader', 'wideChatLayout', 'splitAssistantMessageActions', 'diffLayout', 'dotfiles', 'fileViewerPreview', 'queueMode', 'persistDraft', 'inputSpellcheck']} />;
|
||||
return <OpenChamberVisualSettings visibleSettings={['chatRenderMode', 'messageTransport', 'activityRenderMode', 'userMessageRendering', 'mermaidRendering', 'reasoning', 'showToolFileIcons', 'showTurnChangedFiles', 'expandedTools', 'collapsibleUserMessages', 'stickyUserHeader', 'wideChatLayout', 'splitAssistantMessageActions', 'diffLayout', 'dotfiles', 'fileViewerPreview', 'queueMode', 'persistDraft', 'inputSpellcheck']} />;
|
||||
};
|
||||
|
||||
// Sessions section: Default model & agent, Session retention
|
||||
|
||||
@@ -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' | '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';
|
||||
|
||||
interface OpenChamberVisualSettingsProps {
|
||||
/** Which settings to show. If undefined, shows all. */
|
||||
@@ -269,6 +269,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const setMermaidRenderingMode = useUIStore(state => state.setMermaidRenderingMode);
|
||||
const userMessageRenderingMode = useUIStore(state => state.userMessageRenderingMode);
|
||||
const setUserMessageRenderingMode = useUIStore(state => state.setUserMessageRenderingMode);
|
||||
const collapsibleUserMessages = useUIStore(state => state.collapsibleUserMessages);
|
||||
const setCollapsibleUserMessages = useUIStore(state => state.setCollapsibleUserMessages);
|
||||
const stickyUserHeader = useUIStore(state => state.stickyUserHeader);
|
||||
const setStickyUserHeader = useUIStore(state => state.setStickyUserHeader);
|
||||
const wideChatLayoutEnabled = useUIStore(state => state.wideChatLayoutEnabled);
|
||||
@@ -413,6 +415,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
void updateDesktopSettings({ stickyUserHeader: enabled });
|
||||
}, [setStickyUserHeader]);
|
||||
|
||||
const handleCollapsibleUserMessagesChange = React.useCallback((enabled: boolean) => {
|
||||
setCollapsibleUserMessages(enabled);
|
||||
void updateDesktopSettings({ collapsibleUserMessages: enabled });
|
||||
}, [setCollapsibleUserMessages]);
|
||||
|
||||
const handleWideChatLayoutChange = React.useCallback((enabled: boolean) => {
|
||||
setWideChatLayoutEnabled(enabled);
|
||||
void updateDesktopSettings({ wideChatLayoutEnabled: enabled });
|
||||
@@ -532,6 +539,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|| shouldShow('chatRenderMode')
|
||||
|| shouldShow('messageTransport')
|
||||
|| (shouldShow('activityRenderMode') && chatRenderMode === 'sorted')
|
||||
|| shouldShow('collapsibleUserMessages')
|
||||
|| shouldShow('stickyUserHeader')
|
||||
|| shouldShow('wideChatLayout')
|
||||
|| shouldShow('splitAssistantMessageActions')
|
||||
@@ -1696,7 +1704,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
|
||||
{(shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
|
||||
<section className="p-2 space-y-0.5">
|
||||
{shouldShow('reasoning') && (
|
||||
<div
|
||||
@@ -1745,6 +1753,30 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('collapsibleUserMessages') && (
|
||||
<div
|
||||
data-settings-item="chat.collapsible-user-messages"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={collapsibleUserMessages}
|
||||
onClick={() => handleCollapsibleUserMessagesChange(!collapsibleUserMessages)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleCollapsibleUserMessagesChange(!collapsibleUserMessages);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={collapsibleUserMessages}
|
||||
onChange={handleCollapsibleUserMessagesChange}
|
||||
ariaLabel={t('settings.openchamber.visual.field.collapsibleUserMessagesAria')}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.collapsibleUserMessages')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('stickyUserHeader') && (
|
||||
<div
|
||||
data-settings-item="chat.sticky-user-header"
|
||||
|
||||
Reference in New Issue
Block a user