@@ -28,7 +28,10 @@ import {
2828 User ,
2929 Clock ,
3030 MapPin ,
31+ History ,
32+ ChevronLeft ,
3133} from "lucide-react" ;
34+ import type { AISession } from "../hooks/useStatus" ;
3235
3336// ---------------------------------------------------------------------------
3437// Types
@@ -92,6 +95,16 @@ export type AIChatProps = {
9295 className ?: string ;
9396 /** Whether the underlying WebSocket connection is ready */
9497 isConnected : boolean ;
98+ /** List of past sessions (fetched on demand) */
99+ sessions ?: AISession [ ] ;
100+ /** The currently active session ID */
101+ currentSessionId ?: string ;
102+ /** Called to load a previous session */
103+ onLoadSession ?: ( sessionId : string ) => void ;
104+ /** Called to trigger a sessions fetch */
105+ onFetchSessions ?: ( ) => void ;
106+ /** Called to rename a session */
107+ onSetSessionName ?: ( sessionId : string , name : string ) => void ;
95108 /**
96109 * @internal – seed messages for Storybook / testing only.
97110 * Not part of the public API.
@@ -129,6 +142,11 @@ export const AIChat = forwardRef<AIChatHandle, AIChatProps>(function AIChat(
129142 suggestions = DEFAULT_SUGGESTIONS ,
130143 className,
131144 isConnected,
145+ sessions,
146+ currentSessionId,
147+ onLoadSession,
148+ onFetchSessions,
149+ onSetSessionName,
132150 initialMessages,
133151 } ,
134152 ref ,
@@ -142,6 +160,11 @@ export const AIChat = forwardRef<AIChatHandle, AIChatProps>(function AIChat(
142160 const [ inputValue , setInputValue ] = useState ( "" ) ;
143161 const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
144162 const bottomRef = useRef < HTMLDivElement > ( null ) ;
163+ const [ showSessions , setShowSessions ] = useState ( false ) ;
164+ const [ renamingSessionId , setRenamingSessionId ] = useState < string | null > (
165+ null ,
166+ ) ;
167+ const [ renameValue , setRenameValue ] = useState ( "" ) ;
145168
146169 // Derive combined list for rendering
147170 const messages : ChatMessage [ ] = currentMessage
@@ -391,23 +414,155 @@ export const AIChat = forwardRef<AIChatHandle, AIChatProps>(function AIChat(
391414 return (
392415 < div
393416 className = { cn (
394- "flex flex-col h-full w-full bg-bg-primary text-fg-primary" ,
417+ "flex flex-col h-full w-full bg-bg-primary text-fg-primary relative overflow-hidden " ,
395418 className ,
396419 ) }
397420 >
398- { /* Header with New Chat button */ }
399- { ! isEmpty && onNewSession && (
400- < div className = "shrink-0 flex justify-end p-2 border-b border-border-primary" >
401- < Button
402- variant = "ghost"
403- size = "sm"
404- onClick = { onNewSession }
405- disabled = { isStreaming }
406- className = "text-xs gap-1"
407- >
408- < Plus className = "h-3 w-3" />
409- New chat
410- </ Button >
421+ { /* Header with New Chat + History buttons */ }
422+ { ( ! isEmpty || onFetchSessions ) && (
423+ < div className = "shrink-0 flex justify-between items-center p-2 border-b border-border-primary" >
424+ < div >
425+ { onFetchSessions && (
426+ < Button
427+ variant = "ghost"
428+ size = "sm"
429+ onClick = { ( ) => {
430+ onFetchSessions ( ) ;
431+ setShowSessions ( true ) ;
432+ } }
433+ className = "text-xs gap-1"
434+ >
435+ < History className = "h-3 w-3" />
436+ History
437+ </ Button >
438+ ) }
439+ </ div >
440+ < div >
441+ { ! isEmpty && onNewSession && (
442+ < Button
443+ variant = "ghost"
444+ size = "sm"
445+ onClick = { onNewSession }
446+ disabled = { isStreaming }
447+ className = "text-xs gap-1"
448+ >
449+ < Plus className = "h-3 w-3" />
450+ New chat
451+ </ Button >
452+ ) }
453+ </ div >
454+ </ div >
455+ ) }
456+
457+ { /* Sessions panel overlay */ }
458+ { showSessions && (
459+ < div className = "absolute inset-0 z-overlay flex flex-col bg-bg-primary" >
460+ < div className = "shrink-0 flex items-center gap-2 p-2 border-b border-border-primary" >
461+ < Button
462+ variant = "ghost"
463+ size = "icon-sm"
464+ onClick = { ( ) => {
465+ setShowSessions ( false ) ;
466+ setRenamingSessionId ( null ) ;
467+ } }
468+ aria-label = "Back to chat"
469+ >
470+ < ChevronLeft className = "h-4 w-4" />
471+ </ Button >
472+ < span className = "text-sm font-medium flex-1" > Chat history</ span >
473+ { onNewSession && (
474+ < Button
475+ variant = "ghost"
476+ size = "sm"
477+ onClick = { ( ) => {
478+ onNewSession ( ) ;
479+ setShowSessions ( false ) ;
480+ } }
481+ className = "text-xs gap-1"
482+ >
483+ < Plus className = "h-3 w-3" />
484+ New chat
485+ </ Button >
486+ ) }
487+ </ div >
488+ < ScrollArea className = "flex-1 min-h-0" >
489+ { ! sessions || sessions . length === 0 ? (
490+ < div className = "p-6 text-center text-sm text-fg-secondary" >
491+ No previous sessions
492+ </ div >
493+ ) : (
494+ < div className = "flex flex-col divide-y divide-border-primary" >
495+ { sessions . map ( ( session ) => {
496+ const isActive = session . id === currentSessionId ;
497+ const isRenaming = renamingSessionId === session . id ;
498+ const displayName =
499+ session . name ??
500+ `Chat, ${ new Date ( session . updatedAt ) . toLocaleDateString ( undefined , { month : "short" , day : "numeric" } ) } ` ;
501+ return (
502+ < div
503+ key = { session . id }
504+ className = { cn (
505+ "flex items-center gap-2 px-3 py-2.5 group" ,
506+ isActive && "bg-bg-secondary" ,
507+ ) }
508+ >
509+ { isRenaming ? (
510+ < input
511+ autoFocus
512+ className = "flex-1 text-sm bg-bg-primary border border-border-primary rounded px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-ring"
513+ value = { renameValue }
514+ onChange = { ( e ) => setRenameValue ( e . target . value ) }
515+ onKeyDown = { ( e ) => {
516+ if ( e . key === "Enter" ) {
517+ const trimmed = renameValue . trim ( ) ;
518+ if ( trimmed && onSetSessionName ) {
519+ onSetSessionName ( session . id , trimmed ) ;
520+ }
521+ setRenamingSessionId ( null ) ;
522+ } else if ( e . key === "Escape" ) {
523+ setRenamingSessionId ( null ) ;
524+ }
525+ } }
526+ onBlur = { ( ) => {
527+ const trimmed = renameValue . trim ( ) ;
528+ if ( trimmed && onSetSessionName ) {
529+ onSetSessionName ( session . id , trimmed ) ;
530+ }
531+ setRenamingSessionId ( null ) ;
532+ } }
533+ />
534+ ) : (
535+ < button
536+ className = "flex-1 text-left text-sm truncate"
537+ onClick = { ( ) => {
538+ onLoadSession ?.( session . id ) ;
539+ setShowSessions ( false ) ;
540+ } }
541+ >
542+ { displayName }
543+ </ button >
544+ ) }
545+ { ! isRenaming && (
546+ < Button
547+ variant = "ghost"
548+ size = "icon-sm"
549+ className = "opacity-0 group-hover:opacity-100 shrink-0"
550+ onClick = { ( e ) => {
551+ e . stopPropagation ( ) ;
552+ setRenameValue ( session . name ?? "" ) ;
553+ setRenamingSessionId ( session . id ) ;
554+ } }
555+ aria-label = "Rename session"
556+ >
557+ < Pencil className = "h-3 w-3" />
558+ </ Button >
559+ ) }
560+ </ div >
561+ ) ;
562+ } ) }
563+ </ div >
564+ ) }
565+ </ ScrollArea >
411566 </ div >
412567 ) }
413568
@@ -648,6 +803,10 @@ const TOOL_DISPLAY: Record<string, { label: string; icon: React.ReactNode }> = {
648803 label : "Getting current location" ,
649804 icon : < MapPin className = "h-3 w-3" /> ,
650805 } ,
806+ set_session_name : {
807+ label : "Naming session" ,
808+ icon : < Pencil className = "h-3 w-3" /> ,
809+ } ,
651810} ;
652811
653812function ToolActivitiesIndicator ( {
0 commit comments