@@ -52,7 +52,7 @@ import {
5252} from "@/browser/utils/ui/keybinds" ;
5353import { ModelSelector , type ModelSelectorRef } from "../ModelSelector" ;
5454import { useModelLRU } from "@/browser/hooks/useModelLRU" ;
55- import { SendHorizontal } from "lucide-react" ;
55+ import { SendHorizontal , X } from "lucide-react" ;
5656import { VimTextArea } from "../VimTextArea" ;
5757import { ImageAttachments , type ImageAttachment } from "../ImageAttachments" ;
5858import {
@@ -75,7 +75,8 @@ import { useTutorial } from "@/browser/contexts/TutorialContext";
7575import { useVoiceInput } from "@/browser/hooks/useVoiceInput" ;
7676import { VoiceInputButton } from "./VoiceInputButton" ;
7777import { RecordingOverlay } from "./RecordingOverlay" ;
78- import { ReviewBlock , hasReviewBlocks } from "../shared/ReviewBlock" ;
78+ import { ReviewBlock } from "../shared/ReviewBlock" ;
79+ import { formatReviewNoteForChat } from "@/common/types/review" ;
7980
8081type TokenCountReader = ( ) => number ;
8182
@@ -146,6 +147,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
146147 const [ providerNames , setProviderNames ] = useState < string [ ] > ( [ ] ) ;
147148 const [ toast , setToast ] = useState < Toast | null > ( null ) ;
148149 const [ imageAttachments , setImageAttachments ] = useState < ImageAttachment [ ] > ( [ ] ) ;
150+ const [ attachedReviewIds , setAttachedReviewIds ] = useState < string [ ] > ( [ ] ) ;
149151 const handleToastDismiss = useCallback ( ( ) => {
150152 setToast ( null ) ;
151153 } , [ ] ) ;
@@ -341,6 +343,19 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
341343 setImageAttachments ( attachments ) ;
342344 } , [ ] ) ;
343345
346+ // Attach a review by ID (for pending reviews feature)
347+ const attachReview = useCallback ( ( reviewId : string ) => {
348+ setAttachedReviewIds ( ( prev ) => ( prev . includes ( reviewId ) ? prev : [ ...prev , reviewId ] ) ) ;
349+ } , [ ] ) ;
350+
351+ // Detach a review by ID
352+ const detachReview = useCallback ( ( reviewId : string ) => {
353+ setAttachedReviewIds ( ( prev ) => prev . filter ( ( id ) => id !== reviewId ) ) ;
354+ } , [ ] ) ;
355+
356+ // Get currently attached reviews
357+ const getAttachedReviews = useCallback ( ( ) => attachedReviewIds , [ attachedReviewIds ] ) ;
358+
344359 // Provide API to parent via callback
345360 useEffect ( ( ) => {
346361 if ( props . onReady ) {
@@ -350,6 +365,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
350365 appendText,
351366 prependText,
352367 restoreImages,
368+ attachReview,
369+ detachReview,
370+ getAttachedReviews,
353371 } ) ;
354372 }
355373 } , [
@@ -359,6 +377,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
359377 appendText ,
360378 prependText ,
361379 restoreImages ,
380+ attachReview ,
381+ detachReview ,
382+ getAttachedReviews ,
362383 props ,
363384 ] ) ;
364385
@@ -1015,6 +1036,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10151036 return ; // Skip normal send
10161037 }
10171038
1039+ // Store attached review IDs before try block so catch block can access them
1040+ const sentReviewIds = [ ...attachedReviewIds ] ;
1041+
10181042 try {
10191043 // Prepare image parts if any
10201044 const imageParts = imageAttachments . map ( ( img , index ) => {
@@ -1073,18 +1097,30 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10731097 }
10741098 }
10751099
1076- // Clear input and images immediately for responsive UI
1100+ // Prepend attached reviews to message
1101+ const attachedReviews = attachedReviewIds
1102+ . map ( ( id ) => props . getReview ?.( id ) )
1103+ . filter ( ( r ) : r is NonNullable < typeof r > => r !== undefined ) ;
1104+ const reviewsText = attachedReviews
1105+ . map ( ( r ) => formatReviewNoteForChat ( r . data ) )
1106+ . join ( "\n\n" ) ;
1107+ const finalMessageText = reviewsText
1108+ ? reviewsText + ( actualMessageText ? "\n\n" + actualMessageText : "" )
1109+ : actualMessageText ;
1110+
1111+ // Clear input, images, and attached reviews immediately for responsive UI
10771112 // These will be restored if the send operation fails
10781113 setInput ( "" ) ;
10791114 setImageAttachments ( [ ] ) ;
1115+ setAttachedReviewIds ( [ ] ) ;
10801116 // Clear inline height style - VimTextArea's useLayoutEffect will handle sizing
10811117 if ( inputRef . current ) {
10821118 inputRef . current . style . height = "" ;
10831119 }
10841120
10851121 const result = await api . workspace . sendMessage ( {
10861122 workspaceId : props . workspaceId ,
1087- message : actualMessageText ,
1123+ message : finalMessageText ,
10881124 options : {
10891125 ...sendMessageOptions ,
10901126 ...compactionOptions ,
@@ -1099,20 +1135,26 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10991135 console . error ( "Failed to send message:" , result . error ) ;
11001136 // Show error using enhanced toast
11011137 setToast ( createErrorToast ( result . error ) ) ;
1102- // Restore input and images on error so user can try again
1138+ // Restore input, images, and attached reviews on error so user can try again
11031139 setInput ( messageText ) ;
11041140 setImageAttachments ( previousImageAttachments ) ;
1141+ setAttachedReviewIds ( sentReviewIds ) ;
11051142 } else {
11061143 // Track telemetry for successful message send
11071144 telemetry . messageSent (
11081145 props . workspaceId ,
11091146 sendMessageOptions . model ,
11101147 mode ,
1111- actualMessageText . length ,
1148+ finalMessageText . length ,
11121149 runtimeType ,
11131150 sendMessageOptions . thinkingLevel ?? "off"
11141151 ) ;
11151152
1153+ // Mark attached reviews as completed
1154+ if ( sentReviewIds . length > 0 ) {
1155+ props . onReviewsSent ?.( sentReviewIds ) ;
1156+ }
1157+
11161158 // Exit editing mode if we were editing
11171159 if ( editingMessage && props . onCancelEdit ) {
11181160 props . onCancelEdit ( ) ;
@@ -1130,6 +1172,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
11301172 ) ;
11311173 setInput ( messageText ) ;
11321174 setImageAttachments ( previousImageAttachments ) ;
1175+ setAttachedReviewIds ( sentReviewIds ) ;
11331176 } finally {
11341177 setIsSending ( false ) ;
11351178 }
@@ -1309,12 +1352,26 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
13091352 < ChatInputToast toast = { toast } onDismiss = { handleToastDismiss } />
13101353 ) }
13111354
1312- { /* Review preview - show styled review blocks above input */ }
1313- { variant === "workspace" && hasReviewBlocks ( input ) && (
1314- < div className = "border-border max-h-40 overflow-y-auto border-b px-2 pb-2" >
1315- { input . match ( / < r e v i e w > ( [ \s \S ] * ?) < \/ r e v i e w > / g) ?. map ( ( match , idx ) => (
1316- < ReviewBlock key = { idx } content = { match . slice ( 8 , - 9 ) } />
1317- ) ) }
1355+ { /* Attached reviews preview - show styled blocks with remove buttons */ }
1356+ { variant === "workspace" && attachedReviewIds . length > 0 && props . getReview && (
1357+ < div className = "border-border max-h-40 space-y-1 overflow-y-auto border-b px-2 py-1.5" >
1358+ { attachedReviewIds . map ( ( reviewId ) => {
1359+ const review = props . getReview ! ( reviewId ) ;
1360+ if ( ! review ) return null ;
1361+ return (
1362+ < div key = { reviewId } className = "group relative" >
1363+ < ReviewBlock content = { formatReviewNoteForChat ( review . data ) . slice ( 8 , - 9 ) } />
1364+ < button
1365+ type = "button"
1366+ onClick = { ( ) => detachReview ( reviewId ) }
1367+ className = "bg-dark/80 text-muted hover:text-error absolute top-3 right-3 rounded-full p-0.5 opacity-0 transition-opacity group-hover:opacity-100"
1368+ title = "Remove from message"
1369+ >
1370+ < X className = "size-3.5" />
1371+ </ button >
1372+ </ div >
1373+ ) ;
1374+ } ) }
13181375 </ div >
13191376 ) }
13201377
0 commit comments