@@ -16,6 +16,7 @@ import { QuestionCard } from './QuestionCard';
1616import { StatusRowContainer } from './StatusRowContainer' ;
1717import { SessionRecapNote } from '@/components/chat/SessionRecapSpacer' ;
1818import ScrollToBottomButton from './components/ScrollToBottomButton' ;
19+ import { PromptNavigatorRail } from './components/PromptNavigatorRail' ;
1920import { ScrollShadow } from '@/components/ui/ScrollShadow' ;
2021import { useChatAutoFollow , type AnimationHandlers , type ContentChangeReason } from '@/hooks/useChatAutoFollow' ;
2122import { useChatTimelineController } from './hooks/useChatTimelineController' ;
@@ -162,6 +163,13 @@ type ChatViewportProps = {
162163 isProgrammaticFollowActive : boolean ;
163164 showLoadOlderButton : boolean ;
164165 onLoadOlder : ( ) => void ;
166+ turnIds : string [ ] ;
167+ activeTurnId : string | null ;
168+ onSelectTurn : ( turnId : string ) => void ;
169+ showPromptNavigator : boolean ;
170+ canLoadEarlierPrompts : boolean ;
171+ isLoadingOlderPrompts : boolean ;
172+ onLoadEarlierPrompts : ( ) => void ;
165173} ;
166174
167175const ChatViewport = React . memo ( ( {
@@ -188,8 +196,40 @@ const ChatViewport = React.memo(({
188196 isProgrammaticFollowActive,
189197 showLoadOlderButton,
190198 onLoadOlder,
199+ turnIds,
200+ activeTurnId,
201+ onSelectTurn,
202+ showPromptNavigator,
203+ canLoadEarlierPrompts,
204+ isLoadingOlderPrompts,
205+ onLoadEarlierPrompts,
191206} : ChatViewportProps ) => {
192207 const { t } = useI18n ( ) ;
208+ const promptPreviewsByTurnIdRef = React . useRef < Map < string , Part [ ] > > ( new Map ( ) ) ;
209+ const promptPreviewsByTurnId = React . useMemo ( ( ) => {
210+ const next = new Map < string , Part [ ] > ( ) ;
211+ for ( const message of renderedMessages ) {
212+ if ( message . info . role !== 'user' ) {
213+ continue ;
214+ }
215+ next . set ( message . info . id , message . parts ) ;
216+ }
217+ const prev = promptPreviewsByTurnIdRef . current ;
218+ if ( prev . size === next . size ) {
219+ let unchanged = true ;
220+ for ( const [ id , parts ] of next ) {
221+ if ( prev . get ( id ) !== parts ) {
222+ unchanged = false ;
223+ break ;
224+ }
225+ }
226+ if ( unchanged ) {
227+ return prev ;
228+ }
229+ }
230+ promptPreviewsByTurnIdRef . current = next ;
231+ return next ;
232+ } , [ renderedMessages ] ) ;
193233 const focusScrollContainer = React . useCallback ( ( event : React . MouseEvent < HTMLElement > ) => {
194234 if ( event . defaultPrevented || shouldIgnoreChatNavigationTarget ( event . target ) ) {
195235 return ;
@@ -278,6 +318,17 @@ const ChatViewport = React.memo(({
278318 </ div >
279319 </ ScrollShadow >
280320 < OverlayScrollbar containerRef = { scrollRef } suppressVisibility = { isProgrammaticFollowActive } userIntentOnly observeMutations = { false } />
321+ { showPromptNavigator ? (
322+ < PromptNavigatorRail
323+ turnIds = { turnIds }
324+ previewsByTurnId = { promptPreviewsByTurnId }
325+ activeTurnId = { activeTurnId }
326+ onSelectTurn = { onSelectTurn }
327+ canLoadEarlier = { canLoadEarlierPrompts }
328+ isLoadingOlder = { isLoadingOlderPrompts }
329+ onLoadEarlier = { onLoadEarlierPrompts }
330+ />
331+ ) : null }
281332 </ div >
282333 </ div >
283334 ) ;
@@ -304,7 +355,14 @@ const ChatViewport = React.memo(({
304355 && prev . sessionPermissions === next . sessionPermissions
305356 && prev . isProgrammaticFollowActive === next . isProgrammaticFollowActive
306357 && prev . showLoadOlderButton === next . showLoadOlderButton
307- && prev . onLoadOlder === next . onLoadOlder ;
358+ && prev . onLoadOlder === next . onLoadOlder
359+ && prev . turnIds === next . turnIds
360+ && prev . activeTurnId === next . activeTurnId
361+ && prev . onSelectTurn === next . onSelectTurn
362+ && prev . showPromptNavigator === next . showPromptNavigator
363+ && prev . canLoadEarlierPrompts === next . canLoadEarlierPrompts
364+ && prev . isLoadingOlderPrompts === next . isLoadingOlderPrompts
365+ && prev . onLoadEarlierPrompts === next . onLoadEarlierPrompts ;
308366} ) ;
309367
310368ChatViewport . displayName = 'ChatViewport' ;
@@ -405,6 +463,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
405463 // UI store
406464 const isExpandedInput = useUIStore ( ( state ) => state . isExpandedInput ) ;
407465 const stickyUserHeader = useUIStore ( ( state ) => state . stickyUserHeader ) ;
466+ const promptNavigatorEnabled = useUIStore ( ( state ) => state . promptNavigatorEnabled ) ;
408467 const allowPromptingSubagentSessions = useUIStore ( ( state ) => state . allowPromptingSubagentSessions ) ;
409468 const isTimelineDialogOpen = useUIStore ( ( s ) => s . isTimelineDialogOpen ) ;
410469 const setTimelineDialogOpen = useUIStore ( ( s ) => s . setTimelineDialogOpen ) ;
@@ -706,6 +765,21 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
706765 scrollToMessage : timelineController . scrollToMessage ,
707766 resumeToBottom : timelineController . resumeToBottomInstant ,
708767 } ) ;
768+ const handlePromptNavigatorSelect = React . useCallback ( ( turnId : string ) => {
769+ void navigation . scrollToTurnId ( turnId , { behavior : 'smooth' } ) ;
770+ } , [ navigation ] ) ;
771+ const canLoadEarlierPrompts = timelineController . historySignals . canLoadEarlier ;
772+ const showPromptNavigator = ! isMobile
773+ && ! isVSCode
774+ && ! isDesktopExpandedInput
775+ && promptNavigatorEnabled
776+ && timelineController . turnIds . length >= 2 ;
777+
778+ React . useEffect ( ( ) => {
779+ if ( ! showPromptNavigator ) {
780+ useUIStore . getState ( ) . setPromptNavigatorPanelOpen ( false ) ;
781+ }
782+ } , [ showPromptNavigator ] ) ;
709783
710784 React . useEffect ( ( ) => {
711785 if ( typeof window === 'undefined' || ! currentSessionId ) return ;
@@ -1012,6 +1086,13 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
10121086 isProgrammaticFollowActive = { isFollowingProgrammatically }
10131087 showLoadOlderButton = { showLoadOlderButton }
10141088 onLoadOlder = { handleLoadOlderClick }
1089+ turnIds = { timelineController . turnIds }
1090+ activeTurnId = { timelineController . activeTurnId }
1091+ onSelectTurn = { handlePromptNavigatorSelect }
1092+ showPromptNavigator = { showPromptNavigator }
1093+ canLoadEarlierPrompts = { canLoadEarlierPrompts }
1094+ isLoadingOlderPrompts = { timelineController . isLoadingOlder }
1095+ onLoadEarlierPrompts = { handleLoadOlderClick }
10151096 />
10161097
10171098 < div
0 commit comments