11import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
2- import { useKeyboard } from '@opentui/react'
32import { useShallow } from 'zustand/react/shallow'
43
54import { routeUserPrompt } from './commands/router'
65import { AgentModeToggle } from './components/agent-mode-toggle'
76import { MessageWithAgents } from './components/message-with-agents'
87import { FeedbackInputMode } from './components/feedback-input-mode'
8+ import { FeedbackUiProvider } from './contexts/feedback-ui-context'
99import {
1010 MultilineInput ,
1111 type MultilineInputHandle ,
@@ -44,7 +44,7 @@ import { computeInputLayoutMetrics } from './utils/text-layout'
4444import { createMarkdownPalette } from './utils/theme-system'
4545import { BORDER_CHARS } from './utils/ui-constants'
4646
47- import type { ContentBlock } from './types/chat'
47+ import type { ChatMessage , ContentBlock } from './types/chat'
4848import type { SendMessageFn } from './types/contracts/send-message'
4949import type { User } from './utils/auth'
5050import type { FileTreeNode } from '@codebuff/common/util/file'
@@ -458,6 +458,15 @@ export const Chat = ({
458458 const [ messagesWithFeedback , setMessagesWithFeedback ] = useState < Set < string > > ( new Set ( ) )
459459 const [ messageFeedbackCategories , setMessageFeedbackCategories ] = useState < Map < string , string > > ( new Map ( ) )
460460
461+ const inputValueRef = useRef ( inputValue )
462+ const cursorPositionRef = useRef ( cursorPosition )
463+ useEffect ( ( ) => {
464+ inputValueRef . current = inputValue
465+ } , [ inputValue ] )
466+ useEffect ( ( ) => {
467+ cursorPositionRef . current = cursorPosition
468+ } , [ cursorPosition ] )
469+
461470 const resetFeedbackForm = useCallback ( ( ) => {
462471 setFeedbackText ( '' )
463472 setFeedbackCursor ( 0 )
@@ -466,38 +475,33 @@ export const Chat = ({
466475
467476 const openFeedbackForMessage = useCallback ( ( id : string ) => {
468477 // Save current input state
469- setSavedInputValue ( inputValue )
470- setSavedCursorPosition ( cursorPosition )
478+ setSavedInputValue ( inputValueRef . current )
479+ setSavedCursorPosition ( cursorPositionRef . current )
471480
472481 // Enter feedback mode
473482 setFeedbackMessageId ( id )
474483 setFeedbackMode ( true )
475484 resetFeedbackForm ( )
476- } , [ inputValue , cursorPosition , resetFeedbackForm ] )
477-
478- const openFeedbackForLatestMessage = useCallback ( ( ) => {
479- const latest = [ ...messages ]
480- . reverse ( )
481- . find ( ( m ) => m . variant === 'ai' && m . isComplete )
482- if ( ! latest ) {
483- return false
484- }
485- openFeedbackForMessage ( latest . id )
486- return true
487- } , [ messages , openFeedbackForMessage ] )
485+ } , [ resetFeedbackForm ] )
488486
489487 const handleFeedbackSubmit = useCallback ( ( ) => {
490488 const text = feedbackText . trim ( )
491489 if ( text . length === 0 ) return
492490
493- const target = feedbackMessageId ? messages . find ( ( m ) => m . id === feedbackMessageId ) : null
494- const recent = messages . slice ( Math . max ( 0 , messages . length - 5 ) ) . map ( ( m ) => ( {
491+ const target = feedbackMessageId
492+ ? messages . find ( ( m ) => m . id === feedbackMessageId )
493+ : null
494+ const summarizeMessage = ( m : ChatMessage ) => ( {
495495 id : m . id ,
496496 variant : m . variant ,
497497 timestamp : m . timestamp ,
498498 hasBlocks : ! ! m . blocks ,
499499 contentPreview : ( m . content || '' ) . slice ( 0 , 400 ) ,
500- } ) )
500+ } )
501+ const recent =
502+ feedbackMessageId && target
503+ ? [ summarizeMessage ( target ) ]
504+ : messages . slice ( Math . max ( 0 , messages . length - 5 ) ) . map ( summarizeMessage )
501505
502506 logger . info (
503507 {
@@ -582,7 +586,7 @@ export const Chat = ({
582586 } )
583587
584588 // Handle /feedback command
585- if ( result && 'openFeedbackMode' in result && result . openFeedbackMode ) {
589+ if ( result ? .openFeedbackMode ) {
586590 setSavedInputValue ( '' )
587591 setSavedCursorPosition ( 0 )
588592 setFeedbackMessageId ( null ) // General feedback, not tied to a message
@@ -734,24 +738,6 @@ export const Chat = ({
734738 const shouldShowStatusLine =
735739 ! feedbackMode && ( hasStatusIndicatorContent || shouldShowQueuePreview || ! isAtBottom )
736740
737- // Ctrl+F to open feedback for latest completed AI message
738- useKeyboard (
739- useCallback (
740- ( key ) => {
741- // Don't handle if already in feedback mode
742- if ( feedbackMode ) return
743-
744- if ( key ?. ctrl && key . name === 'f' ) {
745- if ( 'preventDefault' in key && typeof key . preventDefault === 'function' ) {
746- key . preventDefault ( )
747- }
748- openFeedbackForLatestMessage ( )
749- }
750- } ,
751- [ openFeedbackForLatestMessage , feedbackMode ] ,
752- ) ,
753- )
754-
755741 const validationBanner = useValidationBanner ( {
756742 liveValidationErrors : validationErrors ,
757743 loadedAgentsData,
@@ -802,40 +788,43 @@ export const Chat = ({
802788 >
803789 { headerContent }
804790 { virtualizationNotice }
805- { topLevelMessages . map ( ( message , idx ) => {
806- const isLast = idx === topLevelMessages . length - 1
807- return (
808- < MessageWithAgents
809- key = { message . id }
810- message = { message }
811- depth = { 0 }
812- isLastMessage = { isLast }
813- theme = { theme }
814- markdownPalette = { markdownPalette }
815- collapsedAgents = { collapsedAgents }
816- autoCollapsedAgents = { autoCollapsedAgents }
817- streamingAgents = { streamingAgents }
818- messageTree = { messageTree }
819- messages = { messages }
820- availableWidth = { separatorWidth }
821- setCollapsedAgents = { setCollapsedAgents }
822- addAutoCollapsedAgent = { addAutoCollapsedAgent }
823- setUserOpenedAgents = { setUserOpenedAgents }
824- setFocusedAgentId = { setFocusedAgentId }
825- isWaitingForResponse = { isWaitingForResponse }
826- timerStartTime = { timerStartTime }
827- onToggleCollapsed = { handleCollapseToggle }
828- onBuildFast = { handleBuildFast }
829- onBuildMax = { handleBuildMax }
830- onFeedback = { openFeedbackForMessage }
831- feedbackOpenMessageId = { feedbackMessageId }
832- feedbackMode = { feedbackMode }
833- onCloseFeedback = { handleFeedbackCancel }
834- messagesWithFeedback = { messagesWithFeedback }
835- messageFeedbackCategories = { messageFeedbackCategories }
836- />
837- )
838- } ) }
791+ < FeedbackUiProvider
792+ onFeedback = { openFeedbackForMessage }
793+ onClose = { handleFeedbackCancel }
794+ isFeedbackMode = { feedbackMode }
795+ openMessageId = { feedbackMessageId }
796+ submittedMessageIds = { messagesWithFeedback }
797+ categorySelections = { messageFeedbackCategories }
798+ >
799+ { topLevelMessages . map ( ( message , idx ) => {
800+ const isLast = idx === topLevelMessages . length - 1
801+ return (
802+ < MessageWithAgents
803+ key = { message . id }
804+ message = { message }
805+ depth = { 0 }
806+ isLastMessage = { isLast }
807+ theme = { theme }
808+ markdownPalette = { markdownPalette }
809+ collapsedAgents = { collapsedAgents }
810+ autoCollapsedAgents = { autoCollapsedAgents }
811+ streamingAgents = { streamingAgents }
812+ messageTree = { messageTree }
813+ messages = { messages }
814+ availableWidth = { separatorWidth }
815+ setCollapsedAgents = { setCollapsedAgents }
816+ addAutoCollapsedAgent = { addAutoCollapsedAgent }
817+ setUserOpenedAgents = { setUserOpenedAgents }
818+ setFocusedAgentId = { setFocusedAgentId }
819+ isWaitingForResponse = { isWaitingForResponse }
820+ timerStartTime = { timerStartTime }
821+ onToggleCollapsed = { handleCollapseToggle }
822+ onBuildFast = { handleBuildFast }
823+ onBuildMax = { handleBuildMax }
824+ />
825+ )
826+ } ) }
827+ </ FeedbackUiProvider >
839828 </ scrollbox >
840829
841830 < box
@@ -987,4 +976,4 @@ export const Chat = ({
987976 { validationBanner }
988977 </ box >
989978 )
990- }
979+ }
0 commit comments