@@ -127,6 +127,7 @@ interface ParsedToolCallContent {
127127}
128128
129129const REASONING_CALL_NAME = "__reasoning__" ;
130+ const MESSAGE_DELETE_ANIMATION_MS = 180 ;
130131
131132interface StreamingToolCall {
132133 callId : string ;
@@ -186,6 +187,7 @@ export function ChatScreen() {
186187 const [ editingId , setEditingId ] = useState < string | null > ( null ) ;
187188 const [ editingValue , setEditingValue ] = useState ( "" ) ;
188189 const [ streamText , setStreamText ] = useState ( "" ) ;
190+ const [ streamChunks , setStreamChunks ] = useState < Array < { id : number ; text : string } > > ( [ ] ) ;
189191 const [ streaming , setStreaming ] = useState ( false ) ;
190192 const [ streamingCharacterName , setStreamingCharacterName ] = useState < string | null > ( null ) ;
191193 const [ streamingToolCalls , setStreamingToolCalls ] = useState < StreamingToolCall [ ] > ( [ ] ) ;
@@ -288,12 +290,14 @@ export function ChatScreen() {
288290 } ) ;
289291 const [ toolPanelsExpanded , setToolPanelsExpanded ] = useState < Record < string , boolean > > ( { } ) ;
290292 const [ reasoningPanelsExpanded , setReasoningPanelsExpanded ] = useState < Record < string , boolean > > ( { } ) ;
293+ const [ deletingMessageIds , setDeletingMessageIds ] = useState < Record < string , boolean > > ( { } ) ;
291294
292295 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
293296 const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
294297 const chatSearchInputRef = useRef < HTMLInputElement > ( null ) ;
295298 const modelSelectorRef = useRef < HTMLDivElement > ( null ) ;
296299 const modelSelectorTriggerRef = useRef < HTMLButtonElement > ( null ) ;
300+ const streamChunkIdRef = useRef ( 0 ) ;
297301
298302 const orderedBlocks = useMemo (
299303 ( ) => normalizePromptStack ( promptStack ) ,
@@ -413,6 +417,23 @@ export function ChatScreen() {
413417 return ( ) => window . removeEventListener ( "keydown" , onKeyDown ) ;
414418 } , [ zenMode ] ) ;
415419
420+ useEffect ( ( ) => {
421+ setDeletingMessageIds ( ( prev ) => {
422+ const liveIds = new Set ( messages . map ( ( msg ) => msg . id ) ) ;
423+ let changed = false ;
424+ const next : Record < string , boolean > = { } ;
425+ for ( const [ id , value ] of Object . entries ( prev ) ) {
426+ if ( ! value ) continue ;
427+ if ( liveIds . has ( id ) ) {
428+ next [ id ] = true ;
429+ } else {
430+ changed = true ;
431+ }
432+ }
433+ return changed ? next : prev ;
434+ } ) ;
435+ } , [ messages ] ) ;
436+
416437 useEffect ( ( ) => {
417438 if ( ! simpleModeActive ) return ;
418439 const onKeyDown = ( event : KeyboardEvent ) => {
@@ -714,6 +735,8 @@ export function ChatScreen() {
714735
715736 function startStreamingUi ( characterName : string | null ) {
716737 setStreamText ( "" ) ;
738+ setStreamChunks ( [ ] ) ;
739+ streamChunkIdRef . current = 0 ;
717740 setStreaming ( true ) ;
718741 setStreamingCharacterName ( characterName ) ;
719742 setStreamingToolCalls ( [ ] ) ;
@@ -725,6 +748,8 @@ export function ChatScreen() {
725748 function stopStreamingUi ( ) {
726749 setStreaming ( false ) ;
727750 setStreamText ( "" ) ;
751+ setStreamChunks ( [ ] ) ;
752+ streamChunkIdRef . current = 0 ;
728753 setStreamingCharacterName ( null ) ;
729754 setStreamingToolCalls ( [ ] ) ;
730755 setStreamingReasoningCalls ( [ ] ) ;
@@ -770,6 +795,12 @@ export function ChatScreen() {
770795 } ) ;
771796 }
772797
798+ const appendStreamDelta = useCallback ( ( delta : string ) => {
799+ if ( ! delta ) return ;
800+ setStreamText ( ( prev ) => prev + delta ) ;
801+ setStreamChunks ( ( prev ) => [ ...prev , { id : ++ streamChunkIdRef . current , text : delta } ] ) ;
802+ } , [ ] ) ;
803+
773804 const savePromptStack = useCallback (
774805 ( newBlocks : PromptBlock [ ] ) => {
775806 const normalized = normalizePromptStack ( newBlocks ) ;
@@ -886,7 +917,7 @@ export function ChatScreen() {
886917 startStreamingUi ( null ) ;
887918
888919 const updated = await api . chatSend ( chatId , outgoing , branchId || undefined , {
889- onDelta : ( delta ) => setStreamText ( ( prev ) => prev + delta ) ,
920+ onDelta : appendStreamDelta ,
890921 onToolEvent : handleStreamingToolEvent ,
891922 onDone : ( ) => { stopStreamingUi ( ) ; }
892923 } , activePersonaPayload , currentAttachments ) ;
@@ -921,7 +952,7 @@ export function ChatScreen() {
921952 return prev . filter ( ( msg ) => msg . id !== lastAssistant . id && msg . parentId !== lastAssistant . id ) ;
922953 } ) ;
923954 const updated = await api . chatRegenerate ( activeChat . id , activeBranchId || undefined , {
924- onDelta : ( delta ) => setStreamText ( ( prev ) => prev + delta ) ,
955+ onDelta : appendStreamDelta ,
925956 onToolEvent : handleStreamingToolEvent ,
926957 onDone : ( ) => { stopStreamingUi ( ) ; }
927958 } ) ;
@@ -1051,8 +1082,22 @@ export function ChatScreen() {
10511082 }
10521083
10531084 async function handleDelete ( messageId : string ) {
1054- const result = await api . chatDeleteMessage ( messageId ) ;
1055- setMessages ( result . timeline ) ;
1085+ if ( deletingMessageIds [ messageId ] ) return ;
1086+ setDeletingMessageIds ( ( prev ) => ( { ...prev , [ messageId ] : true } ) ) ;
1087+ try {
1088+ await new Promise ( ( resolve ) => setTimeout ( resolve , MESSAGE_DELETE_ANIMATION_MS ) ) ;
1089+ const result = await api . chatDeleteMessage ( messageId ) ;
1090+ setMessages ( result . timeline ) ;
1091+ } catch ( error ) {
1092+ setErrorText ( String ( error ) ) ;
1093+ } finally {
1094+ setDeletingMessageIds ( ( prev ) => {
1095+ if ( ! prev [ messageId ] ) return prev ;
1096+ const next = { ...prev } ;
1097+ delete next [ messageId ] ;
1098+ return next ;
1099+ } ) ;
1100+ }
10561101 }
10571102
10581103 async function saveEdit ( messageId : string ) {
@@ -1220,7 +1265,7 @@ export function ChatScreen() {
12201265 startStreamingUi ( characterName ) ;
12211266 try {
12221267 const updated = await api . chatNextTurn ( activeChat . id , characterName , activeBranchId || undefined , {
1223- onDelta : ( delta ) => setStreamText ( ( prev ) => prev + delta ) ,
1268+ onDelta : appendStreamDelta ,
12241269 onToolEvent : handleStreamingToolEvent ,
12251270 onDone : ( ) => { stopStreamingUi ( ) ; }
12261271 } , false , activePersonaPayload ) ;
@@ -1264,7 +1309,7 @@ export function ChatScreen() {
12641309
12651310 try {
12661311 const updated = await api . chatNextTurn ( activeChat . id , charName , activeBranchId || undefined , {
1267- onDelta : ( delta ) => setStreamText ( ( prev ) => prev + delta ) ,
1312+ onDelta : appendStreamDelta ,
12681313 onToolEvent : handleStreamingToolEvent ,
12691314 onDone : ( ) => { stopStreamingUi ( ) ; }
12701315 } , true , activePersonaPayload ) ; // isAutoConvo = true
@@ -1843,20 +1888,21 @@ export function ChatScreen() {
18431888 ) }
18441889
18451890 { ! simpleSidebarCollapsed && (
1846- < div className = "flex-1 space-y-1 overflow-y-auto" >
1891+ < div className = "chat-sidebar-list flex-1 space-y-1 overflow-y-auto" >
18471892 { chats . length === 0 ? (
18481893 < EmptyState title = { t ( "chat.noChatYet" ) } description = { t ( "chat.noChatDesc" ) } />
18491894 ) : filteredChats . length === 0 ? (
18501895 < EmptyState title = { t ( "chat.noSearchResults" ) } description = { t ( "chat.noSearchResultsDesc" ) } />
18511896 ) : (
1852- filteredChats . map ( ( chat ) => {
1897+ filteredChats . map ( ( chat , index ) => {
18531898 const primaryChatCharacterId = chat . characterId || chat . characterIds ?. [ 0 ] || null ;
18541899 const chatChar = primaryChatCharacterId ? characters . find ( ( c ) => c . id === primaryChatCharacterId ) : null ;
18551900 const multiCount = chat . characterIds ?. length || 0 ;
18561901 const isRenaming = renamingChatId === chat . id ;
18571902 return (
18581903 < div key = { chat . id }
1859- className = { `group relative flex items-start gap-2 rounded-lg ${ simpleModeActive ? "px-2 py-2" : "px-3 py-2" } transition-colors ${
1904+ style = { { animationDelay : `${ Math . min ( index , 20 ) * 20 } ms` } }
1905+ className = { `chat-sidebar-item group relative flex items-start gap-2 rounded-lg ${ simpleModeActive ? "px-2 py-2" : "px-3 py-2" } transition-colors ${
18601906 activeChat ?. id === chat . id ? "bg-accent-subtle text-text-primary" : "text-text-secondary hover:bg-bg-hover"
18611907 } `} >
18621908 { isRenaming ? (
@@ -2355,7 +2401,7 @@ export function ChatScreen() {
23552401 const renderCharName = msgChar ?. name || activeChatCharacter ?. name ;
23562402 return (
23572403 < article key = { msg . id }
2358- className = { `chat-message group max-w-[88%] rounded-xl px-3 py-2 text-sm leading-relaxed ${
2404+ className = { `chat-message group max-w-[88%] rounded-xl px-3 py-2 text-sm leading-relaxed ${ deletingMessageIds [ msg . id ] ? "is-deleting" : "" } ${
23592405 msg . role === "user"
23602406 ? "chat-message-user ml-auto bg-accent-subtle text-text-primary"
23612407 : "chat-message-assistant mr-auto border border-border-subtle bg-bg-secondary text-text-primary"
@@ -2546,7 +2592,8 @@ export function ChatScreen() {
25462592 </ button >
25472593 ) }
25482594 < button onClick = { ( ) => handleDelete ( msg . id ) }
2549- className = "rounded-md px-2 py-0.5 text-[11px] text-danger/60 hover:bg-danger-subtle hover:text-danger" > { t ( "chat.delete" ) } </ button >
2595+ disabled = { deletingMessageIds [ msg . id ] }
2596+ className = "rounded-md px-2 py-0.5 text-[11px] text-danger/60 hover:bg-danger-subtle hover:text-danger disabled:opacity-40" > { t ( "chat.delete" ) } </ button >
25502597 </ div >
25512598 ) }
25522599 </ article >
@@ -2596,9 +2643,13 @@ export function ChatScreen() {
25962643 ) }
25972644 </ div >
25982645 ) }
2599- < div className = "prose-chat" dangerouslySetInnerHTML = { {
2600- __html : streamText ? renderContent ( streamText , streamChar ?. name , activePersona ?. name || t ( "chat.user" ) ) : "..."
2601- } } />
2646+ < div className = "chat-stream-content chat-stream-live" >
2647+ { streamChunks . length > 0
2648+ ? streamChunks . map ( ( chunk ) => (
2649+ < span key = { chunk . id } className = "chat-stream-chunk" > { chunk . text } </ span >
2650+ ) )
2651+ : ( streamText ? streamText : "..." ) }
2652+ </ div >
26022653 { ! zenMode && streamingToolCalls . length > 0 && (
26032654 < div className = "mt-2 rounded-md border border-warning-border bg-warning-subtle" >
26042655 < button
0 commit comments