Skip to content

Commit 76f00e9

Browse files
[codecane][fix] feedback feature cleanup (#375)
1 parent dd3fe3a commit 76f00e9

14 files changed

+1002
-409
lines changed

cli/src/chat.tsx

Lines changed: 72 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { useShallow } from 'zustand/react/shallow'
55
import { routeUserPrompt } from './commands/router'
66
import { AgentModeToggle } from './components/agent-mode-toggle'
77
import { 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'
910
import {
1011
MultilineInput,
1112
type MultilineInputHandle,
@@ -17,7 +18,6 @@ import { SLASH_COMMANDS } from './data/slash-commands'
1718
import { useAgentValidation } from './hooks/use-agent-validation'
1819
import { useChatInput } from './hooks/use-chat-input'
1920
import { useClipboard } from './hooks/use-clipboard'
20-
import { showClipboardMessage } from './utils/clipboard'
2121
import { useConnectionStatus } from './hooks/use-connection-status'
2222
import { useElapsedTime } from './hooks/use-elapsed-time'
2323
import { useExitHandler } from './hooks/use-exit-handler'
@@ -44,7 +44,7 @@ import { computeInputLayoutMetrics } from './utils/text-layout'
4444
import { createMarkdownPalette } from './utils/theme-system'
4545
import { BORDER_CHARS } from './utils/ui-constants'
4646

47-
import type { ContentBlock } from './types/chat'
47+
import type { ChatMessage, ContentBlock } from './types/chat'
4848
import type { SendMessageFn } from './types/contracts/send-message'
4949
import type { User } from './utils/auth'
5050
import 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

Comments
 (0)