1import { MessageId, type ProviderUploadFeedbackResult } from "@t3tools/contracts";
2
3import {
4 isAtomCommandInterrupted,
5 squashAtomCommandFailure,
6 type AtomCommandResult,
7} from "./runtime.ts";
8
9type CodexFeedbackSubmissionDetails = {
10 readonly id: MessageId;
11 readonly command: string;
12 readonly createdAt: string;
13};
14
15export type CodexFeedbackSubmission = CodexFeedbackSubmissionDetails &
16 (
17 | { readonly status: "uploading" | "interrupted" }
18 | { readonly status: "sent"; readonly feedbackId: string }
19 | { readonly status: "failed"; readonly errorMessage: string }
20 );
21
22export function parseCodexFeedbackCommand(text: string): { readonly reason?: string } | null {
23 const match = /^\/feedback(?:\s+([\s\S]*))?$/iu.exec(text.trim());
24 if (!match) {
25 return null;
26 }
27 const reason = match[1]?.trim();
28 return reason ? { reason } : {};
29}
30
31export function codexFeedbackNotice(submission: CodexFeedbackSubmission) {
32 switch (submission.status) {
33 case "interrupted":
34 return null;
35 case "uploading":
36 return { title: "Sending feedback to OpenAI...", description: undefined };
37 case "sent":
38 return {
39 title: "Feedback sent to OpenAI",
40 description: `Thread ID: ${submission.feedbackId}`,
41 };
42 case "failed":
43 return { title: "Could not send feedback to OpenAI", description: submission.errorMessage };
44 }
45}
46
47export function beginCodexFeedbackSubmission(
48 submissionsInFlight: Set<string>,
49 threadKey: string,
50): (() => void) | null {
51 if (submissionsInFlight.has(threadKey)) return null;
52 submissionsInFlight.add(threadKey);
53 return () => submissionsInFlight.delete(threadKey);
54}
55
56/** A chat row that exists only on this client, such as a feedback exchange. */
57export interface LocalChatMessage {
58 readonly id: MessageId;
59 readonly role: "user" | "assistant";
60 readonly text: string;
61 readonly turnId: null;
62 readonly streaming: boolean;
63 readonly createdAt: string;
64 readonly updatedAt: string;
65}
66
67export function codexFeedbackMessage(
68 submission: CodexFeedbackSubmission,
69 role: "user" | "assistant" = "user",
70): LocalChatMessage {
71 const text =
72 role === "user"
73 ? submission.command
74 : submission.status === "sent"
75 ? `Feedback sent to OpenAI.\n\nThread ID: \`${submission.feedbackId}\``
76 : submission.status === "failed"
77 ? `Could not send feedback to OpenAI.\n\n${submission.errorMessage}`
78 : "Sending feedback to OpenAI...";
79
80 return {
81 id: role === "user" ? submission.id : MessageId.make(`${submission.id}:feedback`),
82 role,
83 text,
84 turnId: null,
85 streaming: false,
86 createdAt: submission.createdAt,
87 updatedAt: submission.createdAt,
88 };
89}
90
91export async function submitCodexFeedback<E>(input: {
92 readonly submission: CodexFeedbackSubmissionDetails;
93 readonly clearDraft: () => void;
94 readonly onUpdate: (submission: CodexFeedbackSubmission) => void;
95 readonly upload: () => Promise<AtomCommandResult<ProviderUploadFeedbackResult, E>>;
96}): Promise<AtomCommandResult<ProviderUploadFeedbackResult, E>> {
97 input.onUpdate({ ...input.submission, status: "uploading" });
98 input.clearDraft();
99
100 const result = await input.upload();
101 if (result._tag === "Success") {
102 input.onUpdate({
103 ...input.submission,
104 status: "sent",
105 feedbackId: result.value.feedbackId,
106 });
107 } else if (isAtomCommandInterrupted(result)) {
108 input.onUpdate({ ...input.submission, status: "interrupted" });
109 } else {
110 const error = squashAtomCommandFailure(result);
111 input.onUpdate({
112 ...input.submission,
113 status: "failed",
114 errorMessage: error instanceof Error ? error.message : "An error occurred.",
115 });
116 }
117
118 return result;
119}