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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
495b9d56d2
commit
63b4a5b996
@@ -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.',
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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': '사용 가능한 프로젝트가 없습니다.',
|
||||
|
||||
@@ -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": "Немає доступних проєктів.",
|
||||
|
||||
@@ -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': '仅审查当前工作区中高价值的问题。',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user