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 { ScrollShadow } from '@/components/ui/ScrollShadow';
import { useChatScrollManager, type AnimationHandlers, type ContentChangeReason } from '@/hooks/useChatScrollManager'; import { useChatScrollManager, type AnimationHandlers, type ContentChangeReason } from '@/hooks/useChatScrollManager';
import { useChatTimelineController } from './hooks/useChatTimelineController'; import { useChatTimelineController } from './hooks/useChatTimelineController';
import { TimelineDialog } from './TimelineDialog';
import { useChatTurnNavigation } from './hooks/useChatTurnNavigation'; import { useChatTurnNavigation } from './hooks/useChatTurnNavigation';
import { useDeviceInfo } from '@/lib/device'; import { useDeviceInfo } from '@/lib/device';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -342,6 +343,8 @@ export const ChatContainer: React.FC = () => {
const isExpandedInput = useUIStore((state) => state.isExpandedInput); const isExpandedInput = useUIStore((state) => state.isExpandedInput);
const stickyUserHeader = useUIStore((state) => state.stickyUserHeader); const stickyUserHeader = useUIStore((state) => state.stickyUserHeader);
const chatRenderMode = useUIStore((state) => state.chatRenderMode); const chatRenderMode = useUIStore((state) => state.chatRenderMode);
const isTimelineDialogOpen = useUIStore((s) => s.isTimelineDialogOpen);
const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen);
// Streaming state // Streaming state
const streamingMessageId = useStreamingStore( const streamingMessageId = useStreamingStore(
@@ -958,19 +961,27 @@ export const ChatContainer: React.FC = () => {
<div <div
className={cn( className={cn(
'relative z-10', 'relative z-10',
isDesktopExpandedInput isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background' ? 'flex-1 min-h-0 bg-background'
: 'bg-background' : 'bg-background'
)} )}
> >
{!isDesktopExpandedInput && sessionMessages.length > 0 && ( {!isDesktopExpandedInput && sessionMessages.length > 0 && (
<ScrollToBottomButton <ScrollToBottomButton
visible={timelineController.showScrollToBottom} visible={timelineController.showScrollToBottom}
onClick={navigation.resumeToLatest} onClick={navigation.resumeToLatest}
/> />
)} )}
<ChatInput scrollToBottom={resumeToLatestInstant} /> <ChatInput scrollToBottom={resumeToLatestInstant} />
</div> </div>
<TimelineDialog
open={isTimelineDialogOpen}
onOpenChange={setTimelineDialogOpen}
onScrollToMessage={timelineController.scrollToMessage}
onScrollByTurnOffset={navigation.scrollByTurnOffset}
onResumeToLatest={resumeToLatestInstant}
/>
</div> </div>
); );
}; };
@@ -799,6 +799,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled); const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
const isExpandedInput = useUIStore((state) => state.isExpandedInput); const isExpandedInput = useUIStore((state) => state.isExpandedInput);
const setExpandedInput = useUIStore((state) => state.setExpandedInput); const setExpandedInput = useUIStore((state) => state.setExpandedInput);
const setTimelineDialogOpen = useUIStore((state) => state.setTimelineDialogOpen);
const { git: runtimeGit } = useRuntimeAPIs(); const { git: runtimeGit } = useRuntimeAPIs();
const { currentTheme } = useThemeSystem(); const { currentTheme } = useThemeSystem();
const chatSearchDirectory = useChatSearchDirectory(); const chatSearchDirectory = useChatSearchDirectory();
@@ -1494,6 +1495,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
scrollToBottom?.({ instant: true, force: true }); scrollToBottom?.({ instant: true, force: true });
return; return;
} }
else if (commandName === 'timeline' && currentSessionId) {
setTimelineDialogOpen(true);
return;
}
else if (commandName === 'compact' && currentSessionId) { else if (commandName === 'compact' && currentSessionId) {
try { try {
await sessionActions.waitForConnectionOrThrow(); await sessionActions.waitForConnectionOrThrow();
@@ -1,5 +1,5 @@
import React from 'react'; 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 { cn, fuzzyMatch } from '@/lib/utils';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionMessages } from '@/sync/sync-context'; 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: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: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: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: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" />; return <RiArrowGoBackLine className="h-3.5 w-3.5 text-orange-500" />;
case 'redo': case 'redo':
return <RiArrowGoForwardLine className="h-3.5 w-3.5 text-orange-500" />; 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': case 'compact':
return <RiScissorsLine className="h-3.5 w-3.5 text-purple-500" />; return <RiScissorsLine className="h-3.5 w-3.5 text-purple-500" />;
case 'review': case 'review':
@@ -41,6 +41,8 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
const [forkingMessageId, setForkingMessageId] = React.useState<string | null>(null); const [forkingMessageId, setForkingMessageId] = React.useState<string | null>(null);
const [searchQuery, setSearchQuery] = React.useState(''); 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 formatRelativeTime = React.useCallback((timestamp: number): string => {
const now = Date.now(); const now = Date.now();
@@ -57,23 +59,79 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
return new Date(timestamp).toLocaleDateString(); return new Date(timestamp).toLocaleDateString();
}, [t]); }, [t]);
// Filter user messages (reversed for newest first) // Timeline actions are only valid for user messages.
const userMessages = React.useMemo(() => { const userMessages = React.useMemo(() => {
const filtered = messages.filter(m => m.info.role === 'user'); return messages
return filtered.reverse(); .filter((message) => message.info.role === 'user')
.map((message, index) => ({
message,
messageNumber: index + 1,
}))
.reverse();
}, [messages]); }, [messages]);
// Filter by search query // Filter by search query using all text parts in each user message.
const filteredMessages = React.useMemo(() => { const filteredMessages = React.useMemo(() => {
if (!searchQuery.trim()) return userMessages; const trimmedQuery = searchQuery.trim();
if (!trimmedQuery) return userMessages;
const query = searchQuery.toLowerCase(); const query = trimmedQuery.toLowerCase();
return userMessages.filter((message) => { return userMessages.filter(({ message }) => {
const preview = getMessagePreview(message.parts).toLowerCase(); const fullText = getFullText(message.parts).toLowerCase();
return preview.includes(query); return fullText.includes(query);
}); });
}, [userMessages, searchQuery]); }, [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 // Handle fork with loading state and session refresh
const handleFork = async (messageId: string) => { const handleFork = async (messageId: string) => {
if (!currentSessionId) return; if (!currentSessionId) return;
@@ -104,9 +162,11 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
<div className="relative mt-2"> <div className="relative mt-2">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
autoFocus
placeholder={t('chat.timeline.searchPlaceholder')} placeholder={t('chat.timeline.searchPlaceholder')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="pl-9 w-full" className="pl-9 w-full"
/> />
</div> </div>
@@ -117,34 +177,49 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
{searchQuery ? t('chat.timeline.empty.search') : t('chat.timeline.empty.session')} {searchQuery ? t('chat.timeline.empty.search') : t('chat.timeline.empty.session')}
</div> </div>
) : ( ) : (
filteredMessages.map((message) => { filteredMessages.map(({ message, messageNumber }, index) => {
const preview = getMessagePreview(message.parts); const preview = getMessagePreview(message.parts);
const timestamp = message.info.time.created; const timestamp = message.info.time.created;
const relativeTime = formatRelativeTime(timestamp); 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 ( return (
<div <div
key={message.info.id} key={message.info.id}
className="group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer" ref={(element) => {
onClick={async () => { itemRefs.current[index] = element;
const didNavigate = await onScrollToMessage?.(message.info.id);
if (didNavigate === false) {
return;
}
onOpenChange(false);
}} }}
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}. {messageNumber}.
</span> </span>
<p className="flex-1 min-w-0 typography-small text-foreground truncate ml-0.5"> <p className={cn(
{preview || t('chat.timeline.noTextContent')} "flex-1 min-w-0 typography-small truncate ml-0.5",
{preview && preview.length >= 80 && '…'} isSelected ? "text-interactive-selection-foreground" : "text-foreground"
)}>
{snippet ?? (preview || t('chat.timeline.noTextContent'))}
{!snippet && preview && preview.length >= 80 && '…'}
</p> </p>
<div className="flex-shrink-0 h-5 flex items-center mr-2"> <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} {relativeTime}
</span> </span>
@@ -238,8 +313,26 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
); );
}; };
function getMessagePreview(parts: Part[]): string { function getFullText(parts: Part[]): string {
const textPart = parts.find(p => p.type === 'text'); return parts
if (!textPart || typeof textPart.text !== 'string') return ''; .filter((p): p is Part & { type: 'text'; text: string } => p.type === 'text' && typeof p.text === 'string')
return textPart.text.replace(/\n/g, ' ').slice(0, 80); .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 setActiveMainTab = useUIStore((s) => s.setActiveMainTab);
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen); const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
const setModelSelectorOpen = useUIStore((s) => s.setModelSelectorOpen); const setModelSelectorOpen = useUIStore((s) => s.setModelSelectorOpen);
const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen);
const toggleExpandedInput = useUIStore((s) => s.toggleExpandedInput); const toggleExpandedInput = useUIStore((s) => s.toggleExpandedInput);
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides); const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
const { themeMode, setThemeMode } = useThemeSystem(); const { themeMode, setThemeMode } = useThemeSystem();
@@ -61,6 +62,12 @@ export const useKeyboardShortcuts = () => {
return; return;
} }
if (eventMatchesShortcut(e, combo('open_timeline_dialog'))) {
e.preventDefault();
setTimelineDialogOpen(true);
return;
}
if (eventMatchesShortcut(e, combo('open_status'))) { if (eventMatchesShortcut(e, combo('open_status'))) {
e.preventDefault(); e.preventDefault();
void showOpenCodeStatus(); void showOpenCodeStatus();
@@ -430,6 +437,7 @@ export const useKeyboardShortcuts = () => {
setActiveMainTab, setActiveMainTab,
setSettingsDialogOpen, setSettingsDialogOpen,
setModelSelectorOpen, setModelSelectorOpen,
setTimelineDialogOpen,
toggleExpandedInput, toggleExpandedInput,
setThemeMode, setThemeMode,
working, 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_forward.label': 'Cycle favorite model forward',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': 'Cycle favorite model backward', '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.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.total': 'Total {count}',
'settings.projects.sidebar.actions.addProject': 'Add project', 'settings.projects.sidebar.actions.addProject': 'Add project',
'settings.projects.page.empty.noProjects': 'No projects available.', '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.initDescription': 'Create/update AGENTS.md file',
'chat.commandAutocomplete.command.undoDescription': 'Undo the last message', 'chat.commandAutocomplete.command.undoDescription': 'Undo the last message',
'chat.commandAutocomplete.command.redoDescription': 'Redo previously undone messages', '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.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.summaryDescription': 'Non-destructive session summary. Optional topic hint after the command.',
'chat.commandAutocomplete.command.workspaceReviewDescription': 'Review current workspace changes for high-signal issues only.', '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_forward.label": "Siguiente modelo favorito",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label": "Modelo favorito anterior", "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.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.total": "Total {count}",
"settings.projects.sidebar.actions.addProject": "Añadir proyecto", "settings.projects.sidebar.actions.addProject": "Añadir proyecto",
"settings.projects.page.empty.noProjects": "No hay proyectos disponibles.", "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.initDescription": "Crear/actualizar el archivo AGENTS.md",
"chat.commandAutocomplete.command.undoDescription": "Deshacer el último mensaje", "chat.commandAutocomplete.command.undoDescription": "Deshacer el último mensaje",
"chat.commandAutocomplete.command.redoDescription": "Rehacer mensajes previamente deshechos", "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.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.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.", "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_forward.label': '즐겨찾기 모델 앞으로 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': '즐겨찾기 모델 뒤로 순환', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': '즐겨찾기 모델 뒤로 순환',
'settings.openchamber.keyboardShortcuts.action.expand_input.label': '입력 확장', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': '입력 확장',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '대화 타임라인 열기',
'settings.projects.sidebar.total': '총 {count}개', 'settings.projects.sidebar.total': '총 {count}개',
'settings.projects.sidebar.actions.addProject': '프로젝트 추가', 'settings.projects.sidebar.actions.addProject': '프로젝트 추가',
'settings.projects.page.empty.noProjects': '사용 가능한 프로젝트가 없습니다.', '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.initDescription': 'AGENTS.md 파일 생성/업데이트',
'chat.commandAutocomplete.command.undoDescription': '마지막 메시지 실행 취소', 'chat.commandAutocomplete.command.undoDescription': '마지막 메시지 실행 취소',
'chat.commandAutocomplete.command.redoDescription': '이전에 실행 취소한 메시지 다시 실행', 'chat.commandAutocomplete.command.redoDescription': '이전에 실행 취소한 메시지 다시 실행',
'chat.commandAutocomplete.command.timelineDescription': '대화 타임라인 열기',
'chat.commandAutocomplete.command.compactDescription': 'AI로 세션 기록을 압축해 컨텍스트 크기를 줄입니다', 'chat.commandAutocomplete.command.compactDescription': 'AI로 세션 기록을 압축해 컨텍스트 크기를 줄입니다',
'chat.commandAutocomplete.command.summaryDescription': '세션 기록을 안전하게 요약합니다. 명령 뒤에 선택적으로 주제 힌트를 넣을 수 있습니다.', 'chat.commandAutocomplete.command.summaryDescription': '세션 기록을 안전하게 요약합니다. 명령 뒤에 선택적으로 주제 힌트를 넣을 수 있습니다.',
'chat.commandAutocomplete.command.workspaceReviewDescription': '현재 워크스페이스 변경 사항에서 중요한 이슈만 리뷰합니다.', '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_forward.label": "Próximo modelo favorito",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label": "Modelo favorito anterior", "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.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.total": "Total {count}",
"settings.projects.sidebar.actions.addProject": "Adicionar projeto", "settings.projects.sidebar.actions.addProject": "Adicionar projeto",
"settings.projects.page.empty.noProjects": "Não há projetos disponíveis.", "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.initDescription": "Criar/atualizar o arquivo AGENTS.md",
"chat.commandAutocomplete.command.undoDescription": "Desfazer a última mensagem", "chat.commandAutocomplete.command.undoDescription": "Desfazer a última mensagem",
"chat.commandAutocomplete.command.redoDescription": "Refazer mensagens desfeitas anteriormente", "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.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.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.", "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_forward.label": "Перемкнути улюблену модель вперед",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label": "Перемкнути улюблену модель назад", "settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label": "Перемкнути улюблену модель назад",
"settings.openchamber.keyboardShortcuts.action.expand_input.label": "Розгорнути введення", "settings.openchamber.keyboardShortcuts.action.expand_input.label": "Розгорнути введення",
"settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Відкрити хронологію розмови",
"settings.projects.sidebar.total": "Усього {count}", "settings.projects.sidebar.total": "Усього {count}",
"settings.projects.sidebar.actions.addProject": "Додати проєкт", "settings.projects.sidebar.actions.addProject": "Додати проєкт",
"settings.projects.page.empty.noProjects": "Немає доступних проєктів.", "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.initDescription": "Створити або оновити файл AGENTS.md",
"chat.commandAutocomplete.command.undoDescription": "Скасувати останнє повідомлення", "chat.commandAutocomplete.command.undoDescription": "Скасувати останнє повідомлення",
"chat.commandAutocomplete.command.redoDescription": "Повторити раніше скасовані повідомлення", "chat.commandAutocomplete.command.redoDescription": "Повторити раніше скасовані повідомлення",
"chat.commandAutocomplete.command.timelineDescription": "Відкрити хронологію розмови",
"chat.commandAutocomplete.command.compactDescription": "Стиснути історію сесії за допомогою ШІ, щоб зменшити розмір контексту", "chat.commandAutocomplete.command.compactDescription": "Стиснути історію сесії за допомогою ШІ, щоб зменшити розмір контексту",
"chat.commandAutocomplete.command.summaryDescription": "Неруйнівний підсумок сесії. Після команди можна додати тему.", "chat.commandAutocomplete.command.summaryDescription": "Неруйнівний підсумок сесії. Після команди можна додати тему.",
"chat.commandAutocomplete.command.workspaceReviewDescription": "Перегляньте поточні зміни в робочому середовищі лише для проблем із сильним сигналом.", "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_forward.label': '向前轮换收藏模型',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': '向后轮换收藏模型', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': '向后轮换收藏模型',
'settings.openchamber.keyboardShortcuts.action.expand_input.label': '展开输入框', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': '展开输入框',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '打开对话时间线',
'settings.projects.sidebar.total': '总计 {count}', 'settings.projects.sidebar.total': '总计 {count}',
'settings.projects.sidebar.actions.addProject': '添加项目', 'settings.projects.sidebar.actions.addProject': '添加项目',
'settings.projects.page.empty.noProjects': '暂无项目。', 'settings.projects.page.empty.noProjects': '暂无项目。',
@@ -1297,6 +1297,7 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.initDescription': '创建/更新 AGENTS.md 文件', 'chat.commandAutocomplete.command.initDescription': '创建/更新 AGENTS.md 文件',
'chat.commandAutocomplete.command.undoDescription': '撤销上一条消息', 'chat.commandAutocomplete.command.undoDescription': '撤销上一条消息',
'chat.commandAutocomplete.command.redoDescription': '重做之前撤销的消息', 'chat.commandAutocomplete.command.redoDescription': '重做之前撤销的消息',
'chat.commandAutocomplete.command.timelineDescription': '打开对话时间线',
'chat.commandAutocomplete.command.compactDescription': '使用 AI 压缩会话历史以减少上下文大小', 'chat.commandAutocomplete.command.compactDescription': '使用 AI 压缩会话历史以减少上下文大小',
'chat.commandAutocomplete.command.summaryDescription': '非破坏性会话总结。命令后可选填主题提示。', 'chat.commandAutocomplete.command.summaryDescription': '非破坏性会话总结。命令后可选填主题提示。',
'chat.commandAutocomplete.command.workspaceReviewDescription': '仅审查当前工作区中高价值的问题。', 'chat.commandAutocomplete.command.workspaceReviewDescription': '仅审查当前工作区中高价值的问题。',
+7
View File
@@ -165,6 +165,13 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
description: 'Toggle the session sidebar', description: 'Toggle the session sidebar',
customizable: true, 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', id: 'toggle_right_sidebar',
defaultCombo: 'mod+b', defaultCombo: 'mod+b',