@@ -5,7 +5,8 @@ import { useShallow } from 'zustand/react/shallow'
55import { routeUserPrompt } from './commands/router'
66import { AgentModeToggle } from './components/agent-mode-toggle'
77import { MessageWithAgents } from './components/message-with-agents'
8- import { FeedbackInputMode } from './components/feedback-input-mode'
8+ import { FeedbackContainer } from './components/feedback-container'
9+ import { useFeedbackStore } from './state/feedback-store'
910import {
1011 MultilineInput ,
1112 type MultilineInputHandle ,
@@ -17,7 +18,6 @@ import { SLASH_COMMANDS } from './data/slash-commands'
1718import { useAgentValidation } from './hooks/use-agent-validation'
1819import { useChatInput } from './hooks/use-chat-input'
1920import { useClipboard } from './hooks/use-clipboard'
20- import { showClipboardMessage } from './utils/clipboard'
2121import { useConnectionStatus } from './hooks/use-connection-status'
2222import { useElapsedTime } from './hooks/use-elapsed-time'
2323import { useExitHandler } from './hooks/use-exit-handler'
@@ -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'
@@ -122,6 +122,7 @@ export const Chat = ({
122122 addSessionCredits,
123123 resetChatStore,
124124 sessionCreditsUsed,
125+ setRunState,
125126 } = useChatStore (
126127 useShallow ( ( store ) => ( {
127128 inputValue : store . inputValue ,
@@ -154,6 +155,7 @@ export const Chat = ({
154155 addSessionCredits : store . addSessionCredits ,
155156 resetChatStore : store . reset ,
156157 sessionCreditsUsed : store . sessionCreditsUsed ,
158+ setRunState : store . setRunState ,
157159 } ) ) ,
158160 )
159161
@@ -490,6 +492,7 @@ export const Chat = ({
490492 lastMessageMode,
491493 setLastMessageMode,
492494 addSessionCredits,
495+ setRunState,
493496 isQueuePausedRef,
494497 resumeQueue,
495498 continueChat,
@@ -508,132 +511,73 @@ export const Chat = ({
508511 sendMessageRef,
509512 } )
510513
511- // Feedback state and handlers
512- const [ feedbackMessageId , setFeedbackMessageId ] = useState < string | null > (
513- null ,
514+ const {
515+ feedbackMode,
516+ feedbackMessageId,
517+ openFeedbackForMessage,
518+ closeFeedback,
519+ saveCurrentInput,
520+ restoreSavedInput,
521+ } = useFeedbackStore (
522+ useShallow ( ( state ) => ( {
523+ feedbackMode : state . feedbackMode ,
524+ feedbackMessageId : state . feedbackMessageId ,
525+ openFeedbackForMessage : state . openFeedbackForMessage ,
526+ closeFeedback : state . closeFeedback ,
527+ saveCurrentInput : state . saveCurrentInput ,
528+ restoreSavedInput : state . restoreSavedInput ,
529+ } ) ) ,
514530 )
515- const [ feedbackMode , setFeedbackMode ] = useState ( false )
516- const [ feedbackText , setFeedbackText ] = useState ( '' )
517- const [ feedbackCursor , setFeedbackCursor ] = useState ( 0 )
518- const [ feedbackCategory , setFeedbackCategory ] = useState < string > ( 'other' )
519- const [ savedInputValue , setSavedInputValue ] = useState ( '' )
520- const [ savedCursorPosition , setSavedCursorPosition ] = useState ( 0 )
521- const [ showFeedbackConfirmation , setShowFeedbackConfirmation ] =
522- useState ( false )
523-
524- const [ messagesWithFeedback , setMessagesWithFeedback ] = useState < Set < string > > (
525- new Set ( ) ,
531+
532+ const inputValueRef = useRef ( inputValue )
533+ const cursorPositionRef = useRef ( cursorPosition )
534+ useEffect ( ( ) => {
535+ inputValueRef . current = inputValue
536+ } , [ inputValue ] )
537+ useEffect ( ( ) => {
538+ cursorPositionRef . current = cursorPosition
539+ } , [ cursorPosition ] )
540+
541+ const handleOpenFeedbackForMessage = useCallback (
542+ ( id : string | null ) => {
543+ saveCurrentInput ( inputValueRef . current , cursorPositionRef . current )
544+ openFeedbackForMessage ( id )
545+ } ,
546+ [ saveCurrentInput , openFeedbackForMessage ] ,
526547 )
527- const [ messageFeedbackCategories , setMessageFeedbackCategories ] = useState <
528- Map < string , string >
529- > ( new Map ( ) )
530-
531- const resetFeedbackForm = useCallback ( ( ) => {
532- setFeedbackText ( '' )
533- setFeedbackCursor ( 0 )
534- setFeedbackCategory ( 'other' )
535- } , [ ] )
536548
537- const openFeedbackForMessage = useCallback (
549+ const handleMessageFeedback = useCallback (
538550 ( id : string ) => {
539- // Save current input state
540- setSavedInputValue ( inputValue )
541- setSavedCursorPosition ( cursorPosition )
542-
543- // Enter feedback mode
544- setFeedbackMessageId ( id )
545- setFeedbackMode ( true )
546- resetFeedbackForm ( )
551+ handleOpenFeedbackForMessage ( id )
547552 } ,
548- [ inputValue , cursorPosition , resetFeedbackForm ] ,
553+ [ handleOpenFeedbackForMessage ] ,
549554 )
550555
551- const openFeedbackForLatestMessage = useCallback ( ( ) => {
556+ const handleExitFeedback = useCallback ( ( ) => {
557+ const { value, cursor } = restoreSavedInput ( )
558+ setInputValue ( {
559+ text : value ,
560+ cursorPosition : cursor ,
561+ lastEditDueToNav : false ,
562+ } )
563+ setInputFocused ( true )
564+ } , [ restoreSavedInput , setInputValue , setInputFocused ] )
565+
566+ const handleCloseFeedback = useCallback ( ( ) => {
567+ closeFeedback ( )
568+ handleExitFeedback ( )
569+ } , [ closeFeedback , handleExitFeedback ] )
570+
571+ const handleOpenFeedbackForLatestMessage = useCallback ( ( ) => {
552572 const latest = [ ...messages ]
553573 . reverse ( )
554574 . find ( ( m ) => m . variant === 'ai' && m . isComplete )
555575 if ( ! latest ) {
556576 return false
557577 }
558- openFeedbackForMessage ( latest . id )
578+ handleOpenFeedbackForMessage ( latest . id )
559579 return true
560- } , [ messages , openFeedbackForMessage ] )
561-
562- const handleFeedbackSubmit = useCallback ( ( ) => {
563- const text = feedbackText . trim ( )
564- if ( text . length === 0 ) return
565-
566- const target = feedbackMessageId
567- ? messages . find ( ( m ) => m . id === feedbackMessageId )
568- : null
569- const recent = messages
570- . slice ( Math . max ( 0 , messages . length - 5 ) )
571- . map ( ( m ) => ( {
572- id : m . id ,
573- variant : m . variant ,
574- timestamp : m . timestamp ,
575- hasBlocks : ! ! m . blocks ,
576- contentPreview : ( m . content || '' ) . slice ( 0 , 400 ) ,
577- } ) )
578-
579- logger . info ( {
580- eventId : AnalyticsEvent . FEEDBACK_SUBMITTED ,
581- source : 'cli' ,
582- messageId : target ?. id || null ,
583- variant : target ?. variant || null ,
584- completionTime : target ?. completionTime || null ,
585- credits : target ?. credits || null ,
586- agentMode,
587- sessionCreditsUsed,
588- recentMessages : recent ,
589- feedback : {
590- text,
591- category : feedbackCategory ,
592- type : feedbackMessageId ? 'message' : 'general' ,
593- } ,
594- } )
595-
596- // Mark this message as having feedback submitted
597- if ( feedbackMessageId ) {
598- setMessagesWithFeedback ( ( prev ) => new Set ( prev ) . add ( feedbackMessageId ) )
599- // Remove the category since feedback is submitted
600- setMessageFeedbackCategories ( ( prev ) => {
601- const next = new Map ( prev )
602- next . delete ( feedbackMessageId )
603- return next
604- } )
605- }
606-
607- // Exit feedback mode first
608- setFeedbackMode ( false )
609- resetFeedbackForm ( )
610-
611- // Show success message in status indicator for 5 seconds
612- showClipboardMessage ( 'Feedback sent ✔' , { durationMs : 5000 } )
613-
614- // Restore input focus
615- setInputFocused ( true )
616- } , [
617- feedbackText ,
618- feedbackCategory ,
619- feedbackMessageId ,
620- messages ,
621- agentMode ,
622- sessionCreditsUsed ,
623- ] )
624-
625- const handleFeedbackCancel = useCallback ( ( ) => {
626- // Restore saved input
627- setInputValue ( ( prev ) => ( {
628- text : savedInputValue ,
629- cursorPosition : savedCursorPosition ,
630- lastEditDueToNav : false ,
631- } ) )
632-
633- // Exit feedback mode
634- setFeedbackMode ( false )
635- resetFeedbackForm ( )
636- } , [ resetFeedbackForm , savedInputValue , savedCursorPosition , setInputValue ] )
580+ } , [ messages , handleOpenFeedbackForMessage ] )
637581
638582 const handleSubmit = useCallback ( async ( ) => {
639583 ensureQueueActiveBeforeSubmit ( )
@@ -663,13 +607,9 @@ export const Chat = ({
663607 stopStreaming,
664608 } )
665609
666- // Handle /feedback command
667- if ( result && 'openFeedbackMode' in result && result . openFeedbackMode ) {
668- setSavedInputValue ( '' )
669- setSavedCursorPosition ( 0 )
670- setFeedbackMessageId ( null ) // General feedback, not tied to a message
671- setFeedbackMode ( true )
672- resetFeedbackForm ( )
610+ if ( result ?. openFeedbackMode ) {
611+ saveCurrentInput ( '' , 0 )
612+ openFeedbackForMessage ( null )
673613 }
674614 } , [
675615 abortControllerRef ,
@@ -695,7 +635,8 @@ export const Chat = ({
695635 setUser ,
696636 stopStreaming ,
697637 ensureQueueActiveBeforeSubmit ,
698- resetFeedbackForm ,
638+ saveCurrentInput ,
639+ openFeedbackForMessage ,
699640 ] )
700641
701642 const totalMentionMatches = agentMatches . length + fileMatches . length
@@ -830,13 +771,12 @@ export const Chat = ({
830771 ) {
831772 key . preventDefault ( )
832773 }
833- openFeedbackForLatestMessage ( )
774+ handleOpenFeedbackForLatestMessage ( )
834775 }
835776 } ,
836- [ openFeedbackForLatestMessage , feedbackMode ] ,
777+ [ handleOpenFeedbackForLatestMessage , feedbackMode ] ,
837778 ) ,
838779 )
839-
840780 const validationBanner = useValidationBanner ( {
841781 liveValidationErrors : validationErrors ,
842782 loadedAgentsData,
@@ -907,12 +847,8 @@ export const Chat = ({
907847 onToggleCollapsed = { handleCollapseToggle }
908848 onBuildFast = { handleBuildFast }
909849 onBuildMax = { handleBuildMax }
910- onFeedback = { openFeedbackForMessage }
911- feedbackOpenMessageId = { feedbackMessageId }
912- feedbackMode = { feedbackMode }
913- onCloseFeedback = { handleFeedbackCancel }
914- messagesWithFeedback = { messagesWithFeedback }
915- messageFeedbackCategories = { messageFeedbackCategories }
850+ onFeedback = { handleMessageFeedback }
851+ onCloseFeedback = { handleCloseFeedback }
916852 />
917853 )
918854 } ) }
@@ -940,49 +876,11 @@ export const Chat = ({
940876 Non-actionable queue context is injected via the border title to keep the content
941877 area stable while still surfacing that information. */ }
942878 { feedbackMode ? (
943- < FeedbackInputMode
944- feedbackText = { feedbackText }
945- feedbackCursor = { feedbackCursor }
946- category = { feedbackCategory }
947- onFeedbackTextChange = { ( text , cursor ) => {
948- setFeedbackText ( text )
949- setFeedbackCursor ( cursor )
950- } }
951- onCategoryChange = { ( category ) => {
952- setFeedbackCategory ( category )
953- // Store category selection for this message so button can show it
954- if ( feedbackMessageId ) {
955- setMessageFeedbackCategories ( ( prev ) =>
956- new Map ( prev ) . set ( feedbackMessageId , category ) ,
957- )
958- }
959- } }
960- onSubmit = { handleFeedbackSubmit }
961- onCancel = { handleFeedbackCancel }
962- width = { terminalWidth - 2 }
879+ < FeedbackContainer
880+ inputRef = { inputRef }
881+ onExitFeedback = { handleExitFeedback }
882+ width = { separatorWidth }
963883 />
964- ) : showFeedbackConfirmation ? (
965- < box
966- border
967- style = { {
968- width : '100%' ,
969- borderStyle : 'single' ,
970- borderColor : theme . success ,
971- customBorderChars : BORDER_CHARS ,
972- paddingLeft : 1 ,
973- paddingRight : 1 ,
974- paddingTop : 1 ,
975- paddingBottom : 1 ,
976- flexDirection : 'row' ,
977- justifyContent : 'center' ,
978- } }
979- >
980- < text >
981- < span fg = { theme . success } >
982- ✓ Feedback sent! Thanks for helping us improve.
983- </ span >
984- </ text >
985- </ box >
986884 ) : (
987885 < box
988886 title = { inputBoxTitle }
0 commit comments