feat(ui): enable TimelineDialog with full-text search across all message roles in one session (#1104)

* feat: register open_timeline_dialog shortcut (mod+t)

* feat: enhance TimelineDialog with full-text search across all roles

* feat: add open_timeline_dialog shortcut labels to all locales

* feat: wire TimelineDialog into ChatContainer

* fix: add setTimelineDialogOpen to hook dependency array

* fix: tighten timeline dialog interactions

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
jwcrystal
2026-05-05 11:21:06 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 495b9d56d2
commit 63b4a5b996
18 changed files with 175 additions and 35 deletions
@@ -14,6 +14,7 @@ import ScrollToBottomButton from './components/ScrollToBottomButton';
import { ScrollShadow } from '@/components/ui/ScrollShadow';
import { useChatScrollManager, type AnimationHandlers, type ContentChangeReason } from '@/hooks/useChatScrollManager';
import { useChatTimelineController } from './hooks/useChatTimelineController';
import { TimelineDialog } from './TimelineDialog';
import { useChatTurnNavigation } from './hooks/useChatTurnNavigation';
import { useDeviceInfo } from '@/lib/device';
import { Button } from '@/components/ui/button';
@@ -342,6 +343,8 @@ export const ChatContainer: React.FC = () => {
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
const stickyUserHeader = useUIStore((state) => state.stickyUserHeader);
const chatRenderMode = useUIStore((state) => state.chatRenderMode);
const isTimelineDialogOpen = useUIStore((s) => s.isTimelineDialogOpen);
const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen);
// Streaming state
const streamingMessageId = useStreamingStore(
@@ -958,19 +961,27 @@ export const ChatContainer: React.FC = () => {
<div
className={cn(
'relative z-10',
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
<ScrollToBottomButton
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
<ScrollToBottomButton
visible={timelineController.showScrollToBottom}
onClick={navigation.resumeToLatest}
/>
)}
<ChatInput scrollToBottom={resumeToLatestInstant} />
</div>
<TimelineDialog
open={isTimelineDialogOpen}
onOpenChange={setTimelineDialogOpen}
onScrollToMessage={timelineController.scrollToMessage}
onScrollByTurnOffset={navigation.scrollByTurnOffset}
onResumeToLatest={resumeToLatestInstant}
/>
</div>
);
};
@@ -799,6 +799,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
const setExpandedInput = useUIStore((state) => state.setExpandedInput);
const setTimelineDialogOpen = useUIStore((state) => state.setTimelineDialogOpen);
const { git: runtimeGit } = useRuntimeAPIs();
const { currentTheme } = useThemeSystem();
const chatSearchDirectory = useChatSearchDirectory();
@@ -1494,6 +1495,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
scrollToBottom?.({ instant: true, force: true });
return;
}
else if (commandName === 'timeline' && currentSessionId) {
setTimelineDialogOpen(true);
return;
}
else if (commandName === 'compact' && currentSessionId) {
try {
await sessionActions.waitForConnectionOrThrow();
@@ -1,5 +1,5 @@
import React from 'react';
import { RiCommandLine, RiFileLine, RiFlashlightLine, RiRefreshLine, RiScissorsLine, RiTerminalBoxLine, RiArrowGoBackLine, RiArrowGoForwardLine, RiSearchEyeLine } from '@remixicon/react';
import { RiCommandLine, RiFileLine, RiFlashlightLine, RiRefreshLine, RiScissorsLine, RiTerminalBoxLine, RiArrowGoBackLine, RiArrowGoForwardLine, RiSearchEyeLine, RiTimeLine } from '@remixicon/react';
import { cn, fuzzyMatch } from '@/lib/utils';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionMessages } from '@/sync/sync-context';
@@ -120,6 +120,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
? [
{ id: 'openchamber:undo', name: 'undo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.undoDescription'), isBuiltIn: true },
{ id: 'openchamber:redo', name: 'redo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.redoDescription'), isBuiltIn: true },
{ id: 'openchamber:timeline', name: 'timeline', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.timelineDescription'), isBuiltIn: true },
]
: []
),
@@ -164,6 +165,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
? [
{ id: 'openchamber:undo', name: 'undo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.undoDescription'), isBuiltIn: true },
{ id: 'openchamber:redo', name: 'redo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.redoDescription'), isBuiltIn: true },
{ id: 'openchamber:timeline', name: 'timeline', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.timelineDescription'), isBuiltIn: true },
]
: []
),
@@ -246,6 +248,8 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
return <RiArrowGoBackLine className="h-3.5 w-3.5 text-orange-500" />;
case 'redo':
return <RiArrowGoForwardLine className="h-3.5 w-3.5 text-orange-500" />;
case 'timeline':
return <RiTimeLine className="h-3.5 w-3.5" />;
case 'compact':
return <RiScissorsLine className="h-3.5 w-3.5 text-purple-500" />;
case 'review':
@@ -41,6 +41,8 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
const [forkingMessageId, setForkingMessageId] = React.useState<string | null>(null);
const [searchQuery, setSearchQuery] = React.useState('');
const [selectedIndex, setSelectedIndex] = React.useState(0);
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
const formatRelativeTime = React.useCallback((timestamp: number): string => {
const now = Date.now();
@@ -57,23 +59,79 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
return new Date(timestamp).toLocaleDateString();
}, [t]);
// Filter user messages (reversed for newest first)
// Timeline actions are only valid for user messages.
const userMessages = React.useMemo(() => {
const filtered = messages.filter(m => m.info.role === 'user');
return filtered.reverse();
return messages
.filter((message) => message.info.role === 'user')
.map((message, index) => ({
message,
messageNumber: index + 1,
}))
.reverse();
}, [messages]);
// Filter by search query
// Filter by search query using all text parts in each user message.
const filteredMessages = React.useMemo(() => {
if (!searchQuery.trim()) return userMessages;
const trimmedQuery = searchQuery.trim();
if (!trimmedQuery) return userMessages;
const query = searchQuery.toLowerCase();
return userMessages.filter((message) => {
const preview = getMessagePreview(message.parts).toLowerCase();
return preview.includes(query);
const query = trimmedQuery.toLowerCase();
return userMessages.filter(({ message }) => {
const fullText = getFullText(message.parts).toLowerCase();
return fullText.includes(query);
});
}, [userMessages, searchQuery]);
React.useEffect(() => {
setSelectedIndex(0);
}, [filteredMessages]);
React.useEffect(() => {
itemRefs.current = itemRefs.current.slice(0, filteredMessages.length);
}, [filteredMessages.length]);
React.useEffect(() => {
itemRefs.current[selectedIndex]?.scrollIntoView({
block: 'nearest',
});
}, [selectedIndex]);
const navigateToMessage = React.useCallback(async (messageId: string) => {
const didNavigate = await onScrollToMessage?.(messageId);
if (didNavigate === false) {
return;
}
onOpenChange(false);
}, [onOpenChange, onScrollToMessage]);
const handleSearchKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
const total = filteredMessages.length;
if (total === 0) {
return;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
setSelectedIndex((current) => (current + 1) % total);
return;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
setSelectedIndex((current) => (current - 1 + total) % total);
return;
}
if (event.key === 'Enter') {
event.preventDefault();
const safeIndex = ((selectedIndex % total) + total) % total;
const selected = filteredMessages[safeIndex];
if (selected) {
void navigateToMessage(selected.message.info.id);
}
}
}, [filteredMessages, navigateToMessage, selectedIndex]);
// Handle fork with loading state and session refresh
const handleFork = async (messageId: string) => {
if (!currentSessionId) return;
@@ -104,9 +162,11 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
<div className="relative mt-2">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
autoFocus
placeholder={t('chat.timeline.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="pl-9 w-full"
/>
</div>
@@ -117,34 +177,49 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
{searchQuery ? t('chat.timeline.empty.search') : t('chat.timeline.empty.session')}
</div>
) : (
filteredMessages.map((message) => {
filteredMessages.map(({ message, messageNumber }, index) => {
const preview = getMessagePreview(message.parts);
const timestamp = message.info.time.created;
const relativeTime = formatRelativeTime(timestamp);
const messageNumber = userMessages.length - userMessages.indexOf(message);
const isSelected = index === selectedIndex;
const snippet = searchQuery.trim()
? getSearchSnippet(getFullText(message.parts), searchQuery)
: null;
return (
<div
key={message.info.id}
className="group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer"
onClick={async () => {
const didNavigate = await onScrollToMessage?.(message.info.id);
if (didNavigate === false) {
return;
}
onOpenChange(false);
ref={(element) => {
itemRefs.current[index] = element;
}}
className={cn(
"group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer",
isSelected && "bg-interactive-selection text-interactive-selection-foreground"
)}
onClick={() => void navigateToMessage(message.info.id)}
onMouseEnter={() => setSelectedIndex(index)}
>
<span className="typography-meta text-muted-foreground w-5 text-right flex-shrink-0">
<span className={cn(
"typography-meta w-5 text-right flex-shrink-0",
isSelected ? "text-interactive-selection-foreground/70" : "text-muted-foreground"
)}>
{messageNumber}.
</span>
<p className="flex-1 min-w-0 typography-small text-foreground truncate ml-0.5">
{preview || t('chat.timeline.noTextContent')}
{preview && preview.length >= 80 && '…'}
<p className={cn(
"flex-1 min-w-0 typography-small truncate ml-0.5",
isSelected ? "text-interactive-selection-foreground" : "text-foreground"
)}>
{snippet ?? (preview || t('chat.timeline.noTextContent'))}
{!snippet && preview && preview.length >= 80 && '…'}
</p>
<div className="flex-shrink-0 h-5 flex items-center mr-2">
<span className={cn("typography-meta text-muted-foreground whitespace-nowrap", alwaysShowActions ? "hidden" : "group-hover:hidden")}>
<span className={cn(
"typography-meta whitespace-nowrap",
isSelected ? "text-interactive-selection-foreground/70" : "text-muted-foreground",
alwaysShowActions ? "hidden" : "group-hover:hidden"
)}>
{relativeTime}
</span>
@@ -238,8 +313,26 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
);
};
function getMessagePreview(parts: Part[]): string {
const textPart = parts.find(p => p.type === 'text');
if (!textPart || typeof textPart.text !== 'string') return '';
return textPart.text.replace(/\n/g, ' ').slice(0, 80);
function getFullText(parts: Part[]): string {
return parts
.filter((p): p is Part & { type: 'text'; text: string } => p.type === 'text' && typeof p.text === 'string')
.map((p) => p.text)
.join('\n');
}
function getMessagePreview(parts: Part[]): string {
const full = getFullText(parts);
const singleLine = full.replace(/\n/g, ' ');
return singleLine.length > 80 ? singleLine.slice(0, 80) : singleLine;
}
function getSearchSnippet(text: string, query: string, contextChars: number = 30): string | null {
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const matchIndex = lowerText.indexOf(lowerQuery);
if (matchIndex === -1) return null;
const start = Math.max(0, matchIndex - contextChars);
const end = Math.min(text.length, matchIndex + query.length + contextChars);
return `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\n/g, ' ')}${end < text.length ? '…' : ''}`;
}
@@ -30,6 +30,7 @@ export const useKeyboardShortcuts = () => {
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab);
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
const setModelSelectorOpen = useUIStore((s) => s.setModelSelectorOpen);
const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen);
const toggleExpandedInput = useUIStore((s) => s.toggleExpandedInput);
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
const { themeMode, setThemeMode } = useThemeSystem();
@@ -61,6 +62,12 @@ export const useKeyboardShortcuts = () => {
return;
}
if (eventMatchesShortcut(e, combo('open_timeline_dialog'))) {
e.preventDefault();
setTimelineDialogOpen(true);
return;
}
if (eventMatchesShortcut(e, combo('open_status'))) {
e.preventDefault();
void showOpenCodeStatus();
@@ -430,6 +437,7 @@ export const useKeyboardShortcuts = () => {
setActiveMainTab,
setSettingsDialogOpen,
setModelSelectorOpen,
setTimelineDialogOpen,
toggleExpandedInput,
setThemeMode,
working,
@@ -761,6 +761,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Cycle favorite model forward',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': 'Cycle favorite model backward',
'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Expand input',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Open conversation timeline',
'settings.projects.sidebar.total': 'Total {count}',
'settings.projects.sidebar.actions.addProject': 'Add project',
'settings.projects.page.empty.noProjects': 'No projects available.',
+1
View File
@@ -1331,6 +1331,7 @@ export const dict = {
'chat.commandAutocomplete.command.initDescription': 'Create/update AGENTS.md file',
'chat.commandAutocomplete.command.undoDescription': 'Undo the last message',
'chat.commandAutocomplete.command.redoDescription': 'Redo previously undone messages',
'chat.commandAutocomplete.command.timelineDescription': 'Open the conversation timeline',
'chat.commandAutocomplete.command.compactDescription': 'Compress session history using AI to reduce context size',
'chat.commandAutocomplete.command.summaryDescription': 'Non-destructive session summary. Optional topic hint after the command.',
'chat.commandAutocomplete.command.workspaceReviewDescription': 'Review current workspace changes for high-signal issues only.',
@@ -761,6 +761,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Siguiente modelo favorito",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label": "Modelo favorito anterior",
"settings.openchamber.keyboardShortcuts.action.expand_input.label": "Expandir entrada",
"settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Abrir línea de tiempo de conversación",
"settings.projects.sidebar.total": "Total {count}",
"settings.projects.sidebar.actions.addProject": "Añadir proyecto",
"settings.projects.page.empty.noProjects": "No hay proyectos disponibles.",
+1
View File
@@ -1297,6 +1297,7 @@ export const dict: Record<I18nKey, string> = {
"chat.commandAutocomplete.command.initDescription": "Crear/actualizar el archivo AGENTS.md",
"chat.commandAutocomplete.command.undoDescription": "Deshacer el último mensaje",
"chat.commandAutocomplete.command.redoDescription": "Rehacer mensajes previamente deshechos",
"chat.commandAutocomplete.command.timelineDescription": "Abrir la línea de tiempo de la conversación",
"chat.commandAutocomplete.command.compactDescription": "Comprimir el historial de la sesión usando IA para reducir el tamaño del contexto",
"chat.commandAutocomplete.command.summaryDescription": "Resumen no destructivo de la sesión. Pista opcional del tema después del comando.",
"chat.commandAutocomplete.command.workspaceReviewDescription": "Revisar los cambios actuales del espacio de trabajo solo para problemas de alto impacto.",
@@ -761,6 +761,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '즐겨찾기 모델 앞으로 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': '즐겨찾기 모델 뒤로 순환',
'settings.openchamber.keyboardShortcuts.action.expand_input.label': '입력 확장',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '대화 타임라인 열기',
'settings.projects.sidebar.total': '총 {count}개',
'settings.projects.sidebar.actions.addProject': '프로젝트 추가',
'settings.projects.page.empty.noProjects': '사용 가능한 프로젝트가 없습니다.',
+1
View File
@@ -1333,6 +1333,7 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.initDescription': 'AGENTS.md 파일 생성/업데이트',
'chat.commandAutocomplete.command.undoDescription': '마지막 메시지 실행 취소',
'chat.commandAutocomplete.command.redoDescription': '이전에 실행 취소한 메시지 다시 실행',
'chat.commandAutocomplete.command.timelineDescription': '대화 타임라인 열기',
'chat.commandAutocomplete.command.compactDescription': 'AI로 세션 기록을 압축해 컨텍스트 크기를 줄입니다',
'chat.commandAutocomplete.command.summaryDescription': '세션 기록을 안전하게 요약합니다. 명령 뒤에 선택적으로 주제 힌트를 넣을 수 있습니다.',
'chat.commandAutocomplete.command.workspaceReviewDescription': '현재 워크스페이스 변경 사항에서 중요한 이슈만 리뷰합니다.',
@@ -761,6 +761,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Próximo modelo favorito",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label": "Modelo favorito anterior",
"settings.openchamber.keyboardShortcuts.action.expand_input.label": "Expandir entrada",
"settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Abrir linha do tempo da conversa",
"settings.projects.sidebar.total": "Total {count}",
"settings.projects.sidebar.actions.addProject": "Adicionar projeto",
"settings.projects.page.empty.noProjects": "Não há projetos disponíveis.",
@@ -1297,6 +1297,7 @@ export const dict: Record<I18nKey, string> = {
"chat.commandAutocomplete.command.initDescription": "Criar/atualizar o arquivo AGENTS.md",
"chat.commandAutocomplete.command.undoDescription": "Desfazer a última mensagem",
"chat.commandAutocomplete.command.redoDescription": "Refazer mensagens desfeitas anteriormente",
"chat.commandAutocomplete.command.timelineDescription": "Abrir a linha do tempo da conversa",
"chat.commandAutocomplete.command.compactDescription": "Comprimir o histórico da sessão usando IA para reduzir o tamanho do contexto",
"chat.commandAutocomplete.command.summaryDescription": "Resumo não destrutivo da sessão. Dica opcional do tema após o comando.",
"chat.commandAutocomplete.command.workspaceReviewDescription": "Revisar as alterações atuais do workspace apenas para problemas de alto impacto.",
@@ -761,6 +761,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Перемкнути улюблену модель вперед",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label": "Перемкнути улюблену модель назад",
"settings.openchamber.keyboardShortcuts.action.expand_input.label": "Розгорнути введення",
"settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Відкрити хронологію розмови",
"settings.projects.sidebar.total": "Усього {count}",
"settings.projects.sidebar.actions.addProject": "Додати проєкт",
"settings.projects.page.empty.noProjects": "Немає доступних проєктів.",
+1
View File
@@ -1297,6 +1297,7 @@ export const dict: Record<I18nKey, string> = {
"chat.commandAutocomplete.command.initDescription": "Створити або оновити файл AGENTS.md",
"chat.commandAutocomplete.command.undoDescription": "Скасувати останнє повідомлення",
"chat.commandAutocomplete.command.redoDescription": "Повторити раніше скасовані повідомлення",
"chat.commandAutocomplete.command.timelineDescription": "Відкрити хронологію розмови",
"chat.commandAutocomplete.command.compactDescription": "Стиснути історію сесії за допомогою ШІ, щоб зменшити розмір контексту",
"chat.commandAutocomplete.command.summaryDescription": "Неруйнівний підсумок сесії. Після команди можна додати тему.",
"chat.commandAutocomplete.command.workspaceReviewDescription": "Перегляньте поточні зміни в робочому середовищі лише для проблем із сильним сигналом.",
@@ -761,6 +761,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '向前轮换收藏模型',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': '向后轮换收藏模型',
'settings.openchamber.keyboardShortcuts.action.expand_input.label': '展开输入框',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '打开对话时间线',
'settings.projects.sidebar.total': '总计 {count}',
'settings.projects.sidebar.actions.addProject': '添加项目',
'settings.projects.page.empty.noProjects': '暂无项目。',
@@ -1297,6 +1297,7 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.initDescription': '创建/更新 AGENTS.md 文件',
'chat.commandAutocomplete.command.undoDescription': '撤销上一条消息',
'chat.commandAutocomplete.command.redoDescription': '重做之前撤销的消息',
'chat.commandAutocomplete.command.timelineDescription': '打开对话时间线',
'chat.commandAutocomplete.command.compactDescription': '使用 AI 压缩会话历史以减少上下文大小',
'chat.commandAutocomplete.command.summaryDescription': '非破坏性会话总结。命令后可选填主题提示。',
'chat.commandAutocomplete.command.workspaceReviewDescription': '仅审查当前工作区中高价值的问题。',
+7
View File
@@ -165,6 +165,13 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
description: 'Toggle the session sidebar',
customizable: true,
},
{
id: 'open_timeline_dialog',
defaultCombo: 'mod+t',
label: 'Open conversation timeline',
description: 'Search and navigate within current conversation',
customizable: true,
},
{
id: 'toggle_right_sidebar',
defaultCombo: 'mod+b',