11import React , { useState , useRef , useEffect , useCallback } from 'react' ;
2- import { Settings , X } from 'lucide-react' ;
2+ import { Settings , X , Trash2 } from 'lucide-react' ;
33import {
44 chat ,
55 loadConfig ,
@@ -35,15 +35,15 @@ import {
3535 isImageGenTool ,
3636 executeImageGenTool ,
3737} from '@/lib/imageGenTools' ;
38+ import {
39+ loadChatHistory ,
40+ loadChatHistorySync ,
41+ saveChatHistory ,
42+ clearChatHistory ,
43+ type DisplayMessage ,
44+ } from '@/lib/chatHistoryStorage' ;
3845import styles from './index.module.scss' ;
3946
40- interface DisplayMessage {
41- id : string ;
42- role : 'user' | 'assistant' | 'tool' ;
43- content : string ;
44- imageUrl ?: string ;
45- }
46-
4747function buildSystemPrompt ( hasImageGen : boolean ) : string {
4848 return `You are a helpful assistant that can interact with apps on the user's device. Respond in English by default. If the user writes in another language, switch to that language.
4949
@@ -67,8 +67,10 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
6767 onClose,
6868 visible = true ,
6969} ) => {
70- const [ messages , setMessages ] = useState < DisplayMessage [ ] > ( [ ] ) ;
71- const [ chatHistory , setChatHistory ] = useState < ChatMessage [ ] > ( [ ] ) ;
70+ // Init display + LLM history from localStorage cache (sync), then override from file
71+ const [ initialCache ] = useState ( ( ) => loadChatHistorySync ( ) ) ;
72+ const [ messages , setMessages ] = useState < DisplayMessage [ ] > ( initialCache ?. messages ?? [ ] ) ;
73+ const [ chatHistory , setChatHistory ] = useState < ChatMessage [ ] > ( initialCache ?. chatHistory ?? [ ] ) ;
7274 const [ input , setInput ] = useState ( '' ) ;
7375 const [ loading , setLoading ] = useState ( false ) ;
7476 const [ showSettings , setShowSettings ] = useState ( false ) ;
@@ -79,7 +81,37 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
7981 ) ;
8082 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
8183
84+ // Refs for latest state — declared before the debounced save effect that uses them
85+ const messagesRef = useRef ( messages ) ;
86+ messagesRef . current = messages ;
87+ const chatHistoryRef = useRef ( chatHistory ) ;
88+ chatHistoryRef . current = chatHistory ;
89+
90+ // Debounced save: persist chat history whenever messages or chatHistory change
91+ const saveTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
92+
93+ useEffect ( ( ) => {
94+ // Skip saving the initial empty state (avoids overwriting persisted data on mount)
95+ if ( messages . length === 0 && chatHistory . length === 0 ) return ;
96+
97+ if ( saveTimerRef . current ) clearTimeout ( saveTimerRef . current ) ;
98+ saveTimerRef . current = setTimeout ( ( ) => {
99+ saveChatHistory ( messagesRef . current , chatHistoryRef . current ) ;
100+ } , 500 ) ;
101+
102+ return ( ) => {
103+ if ( saveTimerRef . current ) clearTimeout ( saveTimerRef . current ) ;
104+ } ;
105+ } , [ messages , chatHistory ] ) ;
106+
82107 useEffect ( ( ) => {
108+ // Load from file (async, overrides localStorage cache if available)
109+ loadChatHistory ( ) . then ( ( data ) => {
110+ if ( data ) {
111+ setMessages ( data . messages ) ;
112+ setChatHistory ( data . chatHistory ) ;
113+ }
114+ } ) ;
83115 loadConfig ( ) . then ( ( fileConfig ) => {
84116 if ( fileConfig ) setConfig ( fileConfig ) ;
85117 } ) ;
@@ -88,6 +120,12 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
88120 } ) ;
89121 } , [ ] ) ;
90122
123+ const handleClearHistory = useCallback ( async ( ) => {
124+ setMessages ( [ ] ) ;
125+ setChatHistory ( [ ] ) ;
126+ await clearChatHistory ( ) ;
127+ } , [ ] ) ;
128+
91129 useEffect ( ( ) => {
92130 messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
93131 } , [ messages , loading ] ) ;
@@ -97,8 +135,6 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
97135 } , [ ] ) ;
98136
99137 // Use refs to keep latest state for user action listener
100- const chatHistoryRef = useRef ( chatHistory ) ;
101- chatHistoryRef . current = chatHistory ;
102138 const configRef = useRef ( config ) ;
103139 configRef . current = config ;
104140 const imageGenConfigRef = useRef ( imageGenConfig ) ;
@@ -452,14 +488,23 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
452488
453489 return (
454490 < >
455- < div className = { styles . panel } >
491+ < div className = { styles . panel } data-testid = "chat-panel" >
456492 < div className = { styles . header } >
457493 < span > Chat</ span >
458494 < div className = { styles . headerActions } >
495+ < button
496+ className = { styles . iconBtn }
497+ onClick = { handleClearHistory }
498+ title = "Clear chat"
499+ data-testid = "clear-chat"
500+ >
501+ < Trash2 size = { 16 } />
502+ </ button >
459503 < button
460504 className = { styles . iconBtn }
461505 onClick = { ( ) => setShowSettings ( true ) }
462506 title = "Settings"
507+ data-testid = "settings-btn"
463508 >
464509 < Settings size = { 16 } />
465510 </ button >
@@ -469,7 +514,7 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
469514 </ div >
470515 </ div >
471516
472- < div className = { styles . messages } >
517+ < div className = { styles . messages } data-testid = "chat-messages" >
473518 { messages . length === 0 && (
474519 < div className = { styles . emptyState } >
475520 { config ?. apiKey ? 'Start a conversation...' : 'Click ⚙ to configure your LLM API key' }
@@ -505,11 +550,13 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
505550 placeholder = "Type a message..."
506551 rows = { 1 }
507552 disabled = { loading }
553+ data-testid = "chat-input"
508554 />
509555 < button
510556 className = { styles . sendBtn }
511557 onClick = { handleSend }
512558 disabled = { loading || ! input . trim ( ) }
559+ data-testid = "send-btn"
513560 >
514561 Send
515562 </ button >
@@ -577,8 +624,8 @@ const SettingsModal: React.FC<{
577624 } ;
578625
579626 return (
580- < div className = { styles . overlay } >
581- < div className = { styles . settingsModal } >
627+ < div className = { styles . overlay } data-testid = "settings-overlay" >
628+ < div className = { styles . settingsModal } data-testid = "settings-modal" >
582629 < div className = { styles . settingsTitle } > LLM Settings</ div >
583630
584631 < div className = { styles . field } >
0 commit comments