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 interface LocalChatMessage {
48 readonly id: MessageId;
49 readonly role: "user" | "assistant";
50 readonly text: string;
51 readonly turnId: null;
52 readonly streaming: boolean;
53 readonly createdAt: string;
54 readonly updatedAt: string;
55}
56
57export function codexFeedbackMessage(
58 submission: CodexFeedbackSubmission,
59 role: "user" | "assistant" = "user",
60): LocalChatMessage {
61 const text =
62 role === "user"
63 ? submission.command
64 : submission.status === "sent"
65 ? `Feedback sent to OpenAI.\n\nThread ID: \`${submission.feedbackId}\``
66 : submission.status === "failed"
67 ? `Could not send feedback to OpenAI.\n\n${submission.errorMessage}`
68 : "Sending feedback to OpenAI...";
69
70 return {
71 id: role === "user" ? submission.id : MessageId.make(`${submission.id}:feedback`),
72 role,
73 text,
74 turnId: null,
75 streaming: false,
76 createdAt: submission.createdAt,
77 updatedAt: submission.createdAt,
78 };
79}
80
81export async function submitCodexFeedback<E>(input: {
82 readonly submission: CodexFeedbackSubmissionDetails;
83 readonly clearDraft: () => void;
84 readonly onUpdate: (submission: CodexFeedbackSubmission) => void;
85 readonly upload: () => Promise<AtomCommandResult<ProviderUploadFeedbackResult, E>>;
86}): Promise<AtomCommandResult<ProviderUploadFeedbackResult, E>> {
87 input.onUpdate({ ...input.submission, status: "uploading" });
88 input.clearDraft();
89
90 const result = await input.upload();
91 if (result._tag === "Success") {
92 input.onUpdate({
93 ...input.submission,
94 status: "sent",
95 feedbackId: result.value.feedbackId,
96 });
97 } else if (isAtomCommandInterrupted(result)) {
98 input.onUpdate({ ...input.submission, status: "interrupted" });
99 } else {
100 const error = squashAtomCommandFailure(result);
101 input.onUpdate({
102 ...input.submission,
103 status: "failed",
104 errorMessage: error instanceof Error ? error.message : "An error occurred.",
105 });
106 }
107
108 return result;
109}