From b9fb36f585f3f11d90f1202ebf7e2ef5171f707e Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 3 Jan 2026 14:45:46 +0200 Subject: [PATCH] feat: refactored timeline dialog visuals and update keyboard shortcuts --- CHANGELOG.md | 6 + .../ui/src/components/chat/TimelineDialog.tsx | 142 ++++++++++-------- .../ui/src/components/ui/CommandPalette.tsx | 25 ++- packages/ui/src/components/ui/HelpDialog.tsx | 14 +- packages/ui/src/hooks/useKeyboardShortcuts.ts | 20 +-- 5 files changed, 113 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 668291e7..93d2ee8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- Added timeline dialog (`/timeline` command or Cmd/Ctrl+T) for navigating, reverting, and forking from any point in the conversation (thanks to @aptdnfapt). +- Added `/undo` and `/redo` commands for reverting and restoring messages in a session (thanks to @aptdnfapt). +- Added fork button on user messages to create a new session from any point (thanks to @aptdnfapt). +- Desktop app: keyboard shortcuts now use Cmd on macOS and Ctrl on web/other platforms (thanks to @sakhnyuk). +- Migrated to OpenCode SDK v2 with improved API types and streaming. + ## [1.4.1] - 2026-01-02 - Added the ability to select the same model multiple times in multi-agent runs for response comparison. diff --git a/packages/ui/src/components/chat/TimelineDialog.tsx b/packages/ui/src/components/chat/TimelineDialog.tsx index 03e2ded6..c00b5671 100644 --- a/packages/ui/src/components/chat/TimelineDialog.tsx +++ b/packages/ui/src/components/chat/TimelineDialog.tsx @@ -11,7 +11,8 @@ import { Input } from '@/components/ui/input'; import { useSessionStore } from '@/stores/useSessionStore'; import { useMessageStore } from '@/stores/messageStore'; import { cn } from '@/lib/utils'; -import { RiLoader4Line, RiSearchLine, RiTimeLine, RiGitBranchLine } from '@remixicon/react'; +import { RiLoader4Line, RiSearchLine, RiTimeLine, RiGitBranchLine, RiArrowGoBackLine } from '@remixicon/react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import type { Part } from '@opencode-ai/sdk/v2'; interface TimelineDialogProps { @@ -93,85 +94,89 @@ export const TimelineDialog: React.FC = ({ open, onOpenChan -
- +
+ setSearchQuery(e.target.value)} - className="flex-1" + className="pl-9 w-full" />
-
+
{filteredMessages.length === 0 ? (
{searchQuery ? 'No messages found' : 'No messages in this session yet'}
) : ( - filteredMessages.map((message) => { + filteredMessages.map((message, index) => { const preview = getMessagePreview(message.parts); const timestamp = message.info.time.created; const relativeTime = formatRelativeTime(timestamp); + const messageNumber = userMessages.length - userMessages.indexOf(message); return (
{ + onScrollToMessage?.(message.info.id); + onOpenChange(false); + }} > -
-
- - Message {userMessages.length - userMessages.indexOf(message)} - - - • - - - {relativeTime} - + + {messageNumber}. + +

+ {preview || '[No text content]'} + {preview && preview.length >= 80 && '…'} +

+ +
+ + {relativeTime} + + +
+ + + + + Revert from here + + + + + + + Fork from here +
-

- {preview || '[No text content]'} -

-
- -
- - - - -
); @@ -180,13 +185,18 @@ export const TimelineDialog: React.FC = ({ open, onOpenChan
-
- -
-

Actions

-

Go here - Scroll to this message in the conversation

-

Revert - Undo to this point (message text will populate input)

-

Fork - Create a new session starting from here

+

Actions

+
+
+ Click on a message to scroll to it in the conversation +
+
+ + Undo to this point (message text will populate input) +
+
+ + Create a new session starting from here
diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index c8349a5c..c88d8fe4 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -14,7 +14,7 @@ import { useSessionStore } from '@/stores/useSessionStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useDeviceInfo } from '@/lib/device'; -import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react'; +import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine, RiTimeLine } from '@remixicon/react'; import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore'; import { getModifierLabel } from '@/lib/utils'; @@ -27,6 +27,7 @@ export const CommandPalette: React.FC = () => { setActiveMainTab, setSettingsDialogOpen, setSessionSwitcherOpen, + setTimelineDialogOpen, toggleSidebar, } = useUIStore(); @@ -107,6 +108,11 @@ export const CommandPalette: React.FC = () => { handleClose(); }; + const handleOpenTimeline = () => { + setTimelineDialogOpen(true); + handleClose(); + }; + const directorySessions = getSessionsByDirectory(currentDirectory ?? ''); const currentSessions = React.useMemo(() => { return directorySessions.slice(0, 5); @@ -142,16 +148,21 @@ export const CommandPalette: React.FC = () => { Open Diff Panel - {getModifierLabel()} + E - - - - Open Git Panel - {getModifierLabel()} + G + {getModifierLabel()} + 2 Open Terminal + {getModifierLabel()} + 3 + + + + Open Git Panel + {getModifierLabel()} + 4 + + + + Open Timeline {getModifierLabel()} + T diff --git a/packages/ui/src/components/ui/HelpDialog.tsx b/packages/ui/src/components/ui/HelpDialog.tsx index 06e9a29e..f05c821b 100644 --- a/packages/ui/src/components/ui/HelpDialog.tsx +++ b/packages/ui/src/components/ui/HelpDialog.tsx @@ -21,6 +21,7 @@ import { RiSettings3Line, RiTerminalBoxLine, RiText, + RiTimeLine, } from "@remixicon/react"; import { getModifierLabel } from "@/lib/utils"; @@ -140,19 +141,24 @@ export const HelpDialog: React.FC = () => { icon: RiPaletteLine, }, { - keys: [`${mod} + E`], + keys: [`${mod} + 2`], description: "Open Diff Panel", icon: RiCodeLine, }, { - keys: [`${mod} + G`], + keys: [`${mod} + 3`], + description: "Open Terminal", + icon: RiTerminalBoxLine, + }, + { + keys: [`${mod} + 4`], description: "Open Git Panel", icon: RiGitBranchLine, }, { keys: [`${mod} + T`], - description: "Open Terminal", - icon: RiTerminalBoxLine, + description: "Open Timeline", + icon: RiTimeLine, }, { keys: [`${mod} + ,`], diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index 58a0bd26..4c071b50 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -99,24 +99,10 @@ export const useKeyboardShortcuts = () => { setThemeMode(modes[nextIndex]); } - if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'g') { + if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 't') { e.preventDefault(); - const { activeMainTab } = useUIStore.getState(); - setActiveMainTab(activeMainTab === 'git' ? 'chat' : 'git'); - return; - } - - if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'e') { - e.preventDefault(); - const { activeMainTab } = useUIStore.getState(); - setActiveMainTab(activeMainTab === 'diff' ? 'chat' : 'diff'); - return; - } - - if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 't') { - e.preventDefault(); - const { activeMainTab } = useUIStore.getState(); - setActiveMainTab(activeMainTab === 'terminal' ? 'chat' : 'terminal'); + const { isTimelineDialogOpen, setTimelineDialogOpen } = useUIStore.getState(); + setTimelineDialogOpen(!isTimelineDialogOpen); return; }