@@ -2,11 +2,62 @@ import React, { useEffect, useState, useRef, useCallback, useMemo } from "react"
22import { useInView } from "react-intersection-observer" ;
33import { useNavigate } from "react-router-dom" ;
44
5- import { Trash2 , Check , X , Pencil , MessageCircle , FolderInput , MoreHorizontal , PanelsTopLeft , Loader2 } from "lucide-react" ;
5+ import { Trash2 , Check , X , Pencil , MessageCircle , FolderInput , MoreHorizontal , PanelsTopLeft , Sparkles , Loader2 } from "lucide-react" ;
66
77import { api } from "@/lib/api" ;
8- import { useChatContext , useConfigContext } from "@/lib/hooks" ;
8+ import { useChatContext , useConfigContext , useTitleGeneration , useTitleAnimation } from "@/lib/hooks" ;
99import type { Project , Session } from "@/lib/types" ;
10+
11+ interface SessionNameProps {
12+ session : Session ;
13+ isCurrentSession : boolean ;
14+ isResponding : boolean ;
15+ }
16+
17+ const SessionName : React . FC < SessionNameProps > = ( { session, isCurrentSession, isResponding } ) => {
18+ const { autoTitleGenerationEnabled } = useConfigContext ( ) ;
19+
20+ const displayName = useMemo ( ( ) => {
21+ if ( session . name && session . name . trim ( ) ) {
22+ return session . name ;
23+ }
24+ // Fallback to "New Chat" if no name
25+ return "New Chat" ;
26+ } , [ session . name ] ) ;
27+
28+ // Pass session ID to useTitleAnimation so it can listen for title generation events
29+ const { text : animatedName , isAnimating, isGenerating } = useTitleAnimation ( displayName , session . id ) ;
30+
31+ const isWaitingForTitle = useMemo ( ( ) => {
32+ // Always show pulse when isGenerating (manual "Rename with AI")
33+ if ( isGenerating ) {
34+ return true ;
35+ }
36+
37+ if ( ! autoTitleGenerationEnabled ) {
38+ return false ; // No pulse when auto title generation is disabled
39+ }
40+ const isNewChat = ! session . name || session . name === "New Chat" ;
41+ return isCurrentSession && isNewChat && isResponding ;
42+ } , [ session . name , isCurrentSession , isResponding , isGenerating , autoTitleGenerationEnabled ] ) ;
43+
44+ // Show slow pulse while waiting for title, faster pulse during transition animation
45+ const animationClass = useMemo ( ( ) => {
46+ if ( isGenerating || isAnimating ) {
47+ if ( isWaitingForTitle ) {
48+ return "animate-pulse-slow" ;
49+ }
50+ return "animate-pulse opacity-50" ;
51+ }
52+ // For automatic title generation waiting state
53+ if ( isWaitingForTitle ) {
54+ return "animate-pulse-slow" ;
55+ }
56+ return "opacity-100" ;
57+ } , [ isWaitingForTitle , isAnimating , isGenerating ] ) ;
58+
59+ return < span className = { `truncate font-semibold transition-opacity duration-300 ${ animationClass } ` } > { animatedName } </ span > ;
60+ } ;
1061import { formatTimestamp , getErrorMessage } from "@/lib/utils" ;
1162import { MoveSessionDialog , ProjectBadge , SessionSearch } from "@/lib/components/chat" ;
1263import {
@@ -46,8 +97,9 @@ interface SessionListProps {
4697
4798export const SessionList : React . FC < SessionListProps > = ( { projects = [ ] } ) => {
4899 const navigate = useNavigate ( ) ;
49- const { sessionId, handleSwitchSession, updateSessionName, openSessionDeleteModal, addNotification, displayError } = useChatContext ( ) ;
100+ const { sessionId, handleSwitchSession, updateSessionName, openSessionDeleteModal, addNotification, displayError, isResponding } = useChatContext ( ) ;
50101 const { persistenceEnabled } = useConfigContext ( ) ;
102+ const { generateTitle } = useTitleGeneration ( ) ;
51103 const inputRef = useRef < HTMLInputElement > ( null ) ;
52104
53105 const [ sessions , setSessions ] = useState < Session [ ] > ( [ ] ) ;
@@ -59,6 +111,7 @@ export const SessionList: React.FC<SessionListProps> = ({ projects = [] }) => {
59111 const [ selectedProject , setSelectedProject ] = useState < string > ( "all" ) ;
60112 const [ isMoveDialogOpen , setIsMoveDialogOpen ] = useState ( false ) ;
61113 const [ sessionToMove , setSessionToMove ] = useState < Session | null > ( null ) ;
114+ const [ regeneratingTitleForSession , setRegeneratingTitleForSession ] = useState < string | null > ( null ) ;
62115
63116 const { ref : loadMoreRef , inView } = useInView ( {
64117 threshold : 0 ,
@@ -103,16 +156,38 @@ export const SessionList: React.FC<SessionListProps> = ({ projects = [] }) => {
103156 return prevSessions ;
104157 } ) ;
105158 } ;
159+ const handleTitleUpdated = async ( event : Event ) => {
160+ const customEvent = event as CustomEvent ;
161+ const { sessionId : updatedSessionId } = customEvent . detail ;
162+
163+ // Fetch the updated session from backend to get the new title
164+ try {
165+ const sessionData = await api . webui . get ( `/api/v1/sessions/${ updatedSessionId } ` ) ;
166+ const updatedSession = sessionData ?. data ;
167+
168+ if ( updatedSession ) {
169+ setSessions ( prevSessions => {
170+ return prevSessions . map ( s => ( s . id === updatedSessionId ? { ...s , name : updatedSession . name } : s ) ) ;
171+ } ) ;
172+ }
173+ } catch ( error ) {
174+ console . error ( "[SessionList] Error fetching updated session:" , error ) ;
175+ // Fallback: just refresh the entire list
176+ fetchSessions ( 1 , false ) ;
177+ }
178+ } ;
106179 const handleBackgroundTaskCompleted = ( ) => {
107180 // Refresh session list when background task completes to update indicators
108181 fetchSessions ( 1 , false ) ;
109182 } ;
110183 window . addEventListener ( "new-chat-session" , handleNewSession ) ;
111184 window . addEventListener ( "session-updated" , handleSessionUpdated as EventListener ) ;
185+ window . addEventListener ( "session-title-updated" , handleTitleUpdated ) ;
112186 window . addEventListener ( "background-task-completed" , handleBackgroundTaskCompleted ) ;
113187 return ( ) => {
114188 window . removeEventListener ( "new-chat-session" , handleNewSession ) ;
115189 window . removeEventListener ( "session-updated" , handleSessionUpdated as EventListener ) ;
190+ window . removeEventListener ( "session-title-updated" , handleTitleUpdated ) ;
116191 window . removeEventListener ( "background-task-completed" , handleBackgroundTaskCompleted ) ;
117192 } ;
118193 } , [ fetchSessions ] ) ;
@@ -186,6 +261,70 @@ export const SessionList: React.FC<SessionListProps> = ({ projects = [] }) => {
186261 navigate ( `/projects/${ session . projectId } ` ) ;
187262 } ;
188263
264+ const handleRenameWithAI = useCallback (
265+ async ( session : Session ) => {
266+ if ( regeneratingTitleForSession ) {
267+ addNotification ?.( "AI rename already in progress" , "info" ) ;
268+ return ;
269+ }
270+
271+ setRegeneratingTitleForSession ( session . id ) ;
272+ addNotification ?.( "Generating AI name..." , "info" ) ;
273+
274+ try {
275+ // Fetch all tasks/messages for this session
276+ const data = await api . webui . get ( `/api/v1/sessions/${ session . id } /chat-tasks` ) ;
277+ const tasks = data . tasks || [ ] ;
278+
279+ if ( tasks . length === 0 ) {
280+ addNotification ?.( "No messages found in this session" , "warning" ) ;
281+ setRegeneratingTitleForSession ( null ) ;
282+ return ;
283+ }
284+
285+ // Parse and extract all messages from all tasks
286+ const allMessages : string [ ] = [ ] ;
287+
288+ for ( const task of tasks ) {
289+ const messageBubbles = JSON . parse ( task . messageBubbles ) ;
290+ for ( const bubble of messageBubbles ) {
291+ const text = bubble . text || "" ;
292+ if ( text . trim ( ) ) {
293+ allMessages . push ( text . trim ( ) ) ;
294+ }
295+ }
296+ }
297+
298+ if ( allMessages . length === 0 ) {
299+ addNotification ?.( "No text content found in session" , "warning" ) ;
300+ setRegeneratingTitleForSession ( null ) ;
301+ return ;
302+ }
303+
304+ // Create a summary of the conversation for better context
305+ // Use LAST 3 messages of each type to capture recent conversation
306+ const userMessages = allMessages . filter ( ( _ , idx ) => idx % 2 === 0 ) ; // Assuming alternating user/agent
307+ const agentMessages = allMessages . filter ( ( _ , idx ) => idx % 2 === 1 ) ;
308+
309+ const userSummary = userMessages . slice ( - 3 ) . join ( " | " ) ;
310+ const agentSummary = agentMessages . slice ( - 3 ) . join ( " | " ) ;
311+
312+ // Call the title generation service with the full context
313+ // Pass current title so polling can detect the change
314+ // Pass force=true to bypass the "already has title" check
315+ await generateTitle ( session . id , userSummary , agentSummary , session . name || "New Chat" , true ) ;
316+
317+ addNotification ?.( "Title regenerated successfully" , "success" ) ;
318+ } catch ( error ) {
319+ console . error ( "Error regenerating title:" , error ) ;
320+ addNotification ?.( `Failed to regenerate title: ${ error instanceof Error ? error . message : "Unknown error" } ` , "warning" ) ;
321+ } finally {
322+ setRegeneratingTitleForSession ( null ) ;
323+ }
324+ } ,
325+ [ generateTitle , addNotification , regeneratingTitleForSession ]
326+ ) ;
327+
189328 const handleMoveConfirm = async ( targetProjectId : string | null ) => {
190329 if ( ! sessionToMove ) return ;
191330
@@ -229,22 +368,6 @@ export const SessionList: React.FC<SessionListProps> = ({ projects = [] }) => {
229368 return formatTimestamp ( dateString ) ;
230369 } ;
231370
232- const getSessionDisplayName = ( session : Session ) => {
233- if ( session . name && session . name . trim ( ) ) {
234- return session . name ;
235- }
236- // Generate a short, readable identifier from the session ID
237- const sessionId = session . id ;
238- if ( sessionId . startsWith ( "web-session-" ) ) {
239- // Extract the UUID part and create a short identifier
240- const uuid = sessionId . replace ( "web-session-" , "" ) ;
241- const shortId = uuid . substring ( 0 , 8 ) ;
242- return `Chat ${ shortId } ` ;
243- }
244- // Fallback for other ID formats
245- return `Session ${ sessionId . substring ( 0 , 8 ) } ` ;
246- } ;
247-
248371 // Get unique project names from sessions, sorted alphabetically
249372 const projectNames = useMemo ( ( ) => {
250373 const uniqueProjectNames = new Set < string > ( ) ;
@@ -339,7 +462,7 @@ export const SessionList: React.FC<SessionListProps> = ({ projects = [] }) => {
339462 < div className = "flex items-center gap-2" >
340463 < div className = "flex min-w-0 flex-1 flex-col gap-1" >
341464 < div className = "flex items-center gap-2" >
342- < span className = "truncate font-semibold" > { getSessionDisplayName ( session ) } </ span >
465+ < SessionName session = { session } isCurrentSession = { session . id === sessionId } isResponding = { isResponding } / >
343466 { session . hasRunningBackgroundTask && (
344467 < Tooltip >
345468 < TooltipTrigger asChild >
@@ -396,6 +519,16 @@ export const SessionList: React.FC<SessionListProps> = ({ projects = [] }) => {
396519 < Pencil size = { 16 } className = "mr-2" />
397520 Rename
398521 </ DropdownMenuItem >
522+ < DropdownMenuItem
523+ onClick = { e => {
524+ e . stopPropagation ( ) ;
525+ handleRenameWithAI ( session ) ;
526+ } }
527+ disabled = { regeneratingTitleForSession === session . id }
528+ >
529+ < Sparkles size = { 16 } className = { `mr-2 ${ regeneratingTitleForSession === session . id ? "animate-pulse" : "" } ` } />
530+ Rename with AI
531+ </ DropdownMenuItem >
399532 < DropdownMenuItem
400533 onClick = { e => {
401534 e . stopPropagation ( ) ;
0 commit comments