11import { useAtomValue } from "@effect/atom-react" ;
2- import { useCallback , useEffect , useMemo } from "react" ;
2+ import { useCallback , useEffect , useMemo , useState } from "react" ;
3+ import { Alert } from "react-native" ;
4+ import * as Cause from "effect/Cause" ;
35
46import {
57 CommandId ,
@@ -11,6 +13,13 @@ import {
1113 type ThreadId ,
1214} from "@t3tools/contracts" ;
1315import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors" ;
16+ import {
17+ codexFeedbackMessage ,
18+ parseCodexFeedbackCommand ,
19+ submitCodexFeedback ,
20+ type CodexFeedbackSubmission ,
21+ } from "@t3tools/client-runtime/state/threads" ;
22+ import { isAtomCommandInterrupted } from "@t3tools/client-runtime/state/runtime" ;
1423import { deriveActiveWorkStartedAt } from "@t3tools/shared/orchestrationTiming" ;
1524
1625import { makeQueuedMessageMetadata } from "../lib/commandMetadata" ;
@@ -21,6 +30,7 @@ import {
2130} from "../lib/composerImages" ;
2231import type { DraftComposerImageAttachment } from "../lib/composerImages" ;
2332import { scopedThreadKey } from "../lib/scopedEntities" ;
33+ import { copyTextWithHaptic } from "../lib/copyTextWithHaptic" ;
2434import { buildThreadFeed } from "../lib/threadActivity" ;
2535import { appAtomRegistry } from "../state/atom-registry" ;
2636import {
@@ -41,6 +51,8 @@ import { useSelectedThreadDetail } from "../state/use-thread-detail";
4151import { useThreadSelection } from "../state/use-thread-selection" ;
4252import { enqueueThreadOutboxMessage } from "./thread-outbox" ;
4353import { useThreadOutboxMessages } from "./use-thread-outbox" ;
54+ import { threadEnvironment } from "./threads" ;
55+ import { useAtomCommand } from "./use-atom-command" ;
4456
4557export function appendReviewCommentToDraft ( input : {
4658 readonly environmentId : EnvironmentId ;
@@ -74,10 +86,16 @@ export function useThreadDraftForThread(input: {
7486}
7587
7688export function useThreadComposerState ( ) {
77- const { selectedThread : selectedThreadShell } = useThreadSelection ( ) ;
89+ const { selectedThread : selectedThreadShell , selectedEnvironmentRuntime } = useThreadSelection ( ) ;
7890 const selectedThreadDetail = useSelectedThreadDetail ( ) ;
7991 const composerDrafts = useAtomValue ( composerDraftsAtom ) ;
8092 const queuedMessagesByThreadKey = useThreadOutboxMessages ( ) ;
93+ const [ feedbackSubmissionsByThreadKey , setFeedbackSubmissionsByThreadKey ] = useState <
94+ Record < string , ReadonlyArray < CodexFeedbackSubmission > >
95+ > ( { } ) ;
96+ const uploadThreadFeedback = useAtomCommand ( threadEnvironment . uploadFeedback , {
97+ reportFailure : false ,
98+ } ) ;
8199
82100 useEffect ( ( ) => {
83101 ensureComposerDraftsLoaded ( ) ;
@@ -90,10 +108,21 @@ export function useThreadComposerState() {
90108 ( ) => ( selectedThreadKey ? ( queuedMessagesByThreadKey [ selectedThreadKey ] ?? [ ] ) : [ ] ) ,
91109 [ queuedMessagesByThreadKey , selectedThreadKey ] ,
92110 ) ;
93- const selectedThreadFeed = useMemo (
94- ( ) => ( selectedThreadDetail ? buildThreadFeed ( selectedThreadDetail ) : [ ] ) ,
95- [ selectedThreadDetail ] ,
96- ) ;
111+ const selectedThreadFeed = useMemo ( ( ) => {
112+ if ( ! selectedThreadDetail ) {
113+ return [ ] ;
114+ }
115+ const submissions = selectedThreadKey
116+ ? ( feedbackSubmissionsByThreadKey [ selectedThreadKey ] ?? [ ] )
117+ : [ ] ;
118+ return buildThreadFeed ( selectedThreadDetail , {
119+ localMessages : submissions . flatMap ( ( submission ) =>
120+ submission . status === "interrupted"
121+ ? [ ]
122+ : [ codexFeedbackMessage ( submission ) , codexFeedbackMessage ( submission , "assistant" ) ] ,
123+ ) ,
124+ } ) ;
125+ } , [ feedbackSubmissionsByThreadKey , selectedThreadDetail , selectedThreadKey ] ) ;
97126
98127 const selectedDraft = selectedThreadKey ? composerDrafts [ selectedThreadKey ] : null ;
99128 const draftMessage = selectedDraft ?. text ?? "" ;
@@ -143,6 +172,70 @@ export function useThreadComposerState() {
143172 return null ;
144173 }
145174
175+ const provider = selectedEnvironmentRuntime ?. serverConfig ?. providers . find (
176+ ( entry ) => entry . instanceId === thread . modelSelection . instanceId ,
177+ ) ;
178+ const feedbackCommand =
179+ attachments . length === 0 &&
180+ ( provider ?. driver === "codex" || thread . session ?. providerName === "codex" )
181+ ? parseCodexFeedbackCommand ( text )
182+ : null ;
183+ if ( feedbackCommand ) {
184+ if ( thread . session === null ) {
185+ Alert . alert ( "Start a Codex thread first" , "Send a message before you submit feedback." ) ;
186+ return null ;
187+ }
188+ const metadata = makeQueuedMessageMetadata ( ) ;
189+ const result = await submitCodexFeedback ( {
190+ submission : {
191+ id : MessageId . make ( metadata . messageId ) ,
192+ command : text ,
193+ createdAt : metadata . createdAt ,
194+ } ,
195+ clearDraft : ( ) => clearComposerDraftContent ( threadKey ) ,
196+ onUpdate : ( submission ) => {
197+ setFeedbackSubmissionsByThreadKey ( ( current ) => {
198+ const existing = current [ threadKey ] ?? [ ] ;
199+ const found = existing . some ( ( entry ) => entry . id === submission . id ) ;
200+ return {
201+ ...current ,
202+ [ threadKey ] : found
203+ ? existing . map ( ( entry ) => ( entry . id === submission . id ? submission : entry ) )
204+ : [ ...existing , submission ] ,
205+ } ;
206+ } ) ;
207+ } ,
208+ upload : ( ) =>
209+ uploadThreadFeedback ( {
210+ environmentId : selectedThreadShell . environmentId ,
211+ input : {
212+ threadId : selectedThreadShell . id ,
213+ ...feedbackCommand ,
214+ } ,
215+ } ) ,
216+ } ) ;
217+ if ( result . _tag === "Failure" ) {
218+ if ( isAtomCommandInterrupted ( result ) ) {
219+ return null ;
220+ }
221+ const error = Cause . squash ( result . cause ) ;
222+ Alert . alert (
223+ "Could not send feedback to OpenAI" ,
224+ error instanceof Error ? error . message : "An error occurred." ,
225+ ) ;
226+ return null ;
227+ }
228+ const feedbackId = result . value . feedbackId ;
229+ Alert . alert ( "Feedback sent to OpenAI" , `Thread ID: ${ feedbackId } ` , [
230+ { text : "OK" , style : "cancel" } ,
231+ {
232+ text : "Copy ID" ,
233+ onPress : ( ) => copyTextWithHaptic ( feedbackId , { target : "Codex feedback thread ID" } ) ,
234+ } ,
235+ ] ) ;
236+ return null ;
237+ }
238+
146239 const metadata = makeQueuedMessageMetadata ( ) ;
147240 const messageId = MessageId . make ( metadata . messageId ) ;
148241 // Enqueue publishes the queued atom synchronously (the durable write
@@ -175,7 +268,12 @@ export function useThreadComposerState() {
175268 ) ;
176269 } ) ;
177270 return messageId ;
178- } , [ selectedThreadDetail , selectedThreadShell ] ) ;
271+ } , [
272+ selectedEnvironmentRuntime ?. serverConfig ?. providers ,
273+ selectedThreadDetail ,
274+ selectedThreadShell ,
275+ uploadThreadFeedback ,
276+ ] ) ;
179277
180278 const onChangeDraftMessage = useCallback (
181279 ( value : string ) => {
0 commit comments