feat: implement Undo/Redo/Timeline and Fork Features and fixed opencode.json reading issue on broken json (#99)
* feat: add /undo, /redo, /timeline slash commands and fork button
- Add /undo command to revert to previous user message
- Add /redo command to redo previously undone messages
- Add /timeline command to show conversation history
- Add fork button on user messages (hover) to create new session
- Add TimelineDialog component for navigating conversation history
- Show undo/redo/timeline in autocomplete when session exists
- Fix fork to use SDK session.fork API
- Silent no-op behavior for undo/redo when no messages (matches OpenCode CLI)
* fix: correct fork session endpoint and session switching
- Add hybrid SDK approach using v1 for existing methods and v2 for fork only
- v2 SDK has correct endpoint /session/{sessionID}/fork instead of broken /session/{id}/fork
- Fix forkFromMessage to use setCurrentSession instead of direct set
- Ensures both useSessionStore and useSessionManagementStore are consistent
- Fixes issue where forked sessions appeared empty until sending a new message
* fix: use direct fetch for fork to avoid v2 SDK build errors
- Remove v2 SDK imports and hybrid SDK approach
- Use direct fetch() call to /api/session/{sessionID}/fork endpoint
- Avoids type conflicts between v1 and v2 SDK
- Bypasses broken v1 SDK fork endpoint (wrong path parameter)
- All codebase now uses v1 SDK consistently
* fix: use jsonc-parser for config file parsing
- Replace strip-json-comments + JSON.parse with jsonc-parser
- Handles comments, trailing commas, and unquoted keys
- Matches OpenCode CLI config parsing behavior
- Fixes SyntaxError when reading global opencode.json config
* feat: improve timeline dialog with loading state and search
- Add loading spinner on fork button during operation (~30s on big sessions)
- Refresh session list after fork so new session appears in sidebar immediately
- Add search bar to filter messages by prompt text content
- Show 'No messages found' when search has no results
- Disable fork button while forking to prevent duplicate operations
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { RiCommandLine, RiFileLine, RiFlashlightLine, RiRefreshLine, RiScissorsLine, RiTerminalBoxLine } from '@remixicon/react';
|
||||
import { RiCommandLine, RiFileLine, RiFlashlightLine, RiRefreshLine, RiScissorsLine, RiTerminalBoxLine, RiArrowGoBackLine, RiArrowGoForwardLine, RiTimeLine } from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
@@ -29,15 +29,17 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
onCommandSelect,
|
||||
onClose
|
||||
}, ref) => {
|
||||
const { hasMessagesInCurrentSession } = useSessionStore(
|
||||
const { hasMessagesInCurrentSession, currentSessionId } = useSessionStore(
|
||||
useShallow((state) => {
|
||||
const sessionId = state.currentSessionId;
|
||||
const messageCount = sessionId ? (state.messages.get(sessionId)?.length ?? 0) : 0;
|
||||
return {
|
||||
hasMessagesInCurrentSession: messageCount > 0,
|
||||
currentSessionId: sessionId,
|
||||
};
|
||||
})
|
||||
);
|
||||
const hasSession = Boolean(currentSessionId);
|
||||
|
||||
const [commands, setCommands] = React.useState<CommandInfo[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
@@ -79,9 +81,18 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
}));
|
||||
|
||||
const builtInCommands: CommandInfo[] = [
|
||||
...(hasMessagesInCurrentSession
|
||||
? []
|
||||
: [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]),
|
||||
...(hasSession && !hasMessagesInCurrentSession
|
||||
? [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]
|
||||
: []
|
||||
),
|
||||
...(hasSession // Show when session exists, not when hasMessages
|
||||
? [
|
||||
{ name: 'undo', description: 'Undo the last message', isBuiltIn: true },
|
||||
{ name: 'redo', description: 'Redo previously undone messages', isBuiltIn: true },
|
||||
{ name: 'timeline', description: 'Jump to a specific message', isBuiltIn: true },
|
||||
]
|
||||
: []
|
||||
),
|
||||
{ name: 'summarize', description: 'Generate a summary of the current session', isBuiltIn: true },
|
||||
];
|
||||
|
||||
@@ -114,9 +125,18 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
|
||||
const allowInitCommand = !hasMessagesInCurrentSession;
|
||||
const builtInCommands: CommandInfo[] = [
|
||||
...(hasMessagesInCurrentSession
|
||||
? []
|
||||
: [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]),
|
||||
...(hasSession && !hasMessagesInCurrentSession
|
||||
? [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]
|
||||
: []
|
||||
),
|
||||
...(hasSession // Show when session exists, not when hasMessages
|
||||
? [
|
||||
{ name: 'undo', description: 'Undo the last message', isBuiltIn: true },
|
||||
{ name: 'redo', description: 'Redo previously undone messages', isBuiltIn: true },
|
||||
{ name: 'timeline', description: 'Jump to a specific message', isBuiltIn: true },
|
||||
]
|
||||
: []
|
||||
),
|
||||
{ name: 'summarize', description: 'Generate a summary of the current session', isBuiltIn: true },
|
||||
];
|
||||
|
||||
@@ -134,7 +154,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
};
|
||||
|
||||
loadCommands();
|
||||
}, [searchQuery, hasMessagesInCurrentSession]);
|
||||
}, [searchQuery, hasMessagesInCurrentSession, hasSession]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
@@ -184,6 +204,12 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
switch (command.name) {
|
||||
case 'init':
|
||||
return <RiFileLine className="h-3.5 w-3.5 text-green-500" />;
|
||||
case 'undo':
|
||||
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 text-blue-500" />;
|
||||
case 'summarize':
|
||||
return <RiScissorsLine className="h-3.5 w-3.5 text-purple-500" />;
|
||||
case 'test':
|
||||
|
||||
Reference in New Issue
Block a user