| 1 | import type { OrchestrationV2TurnItem, SecretRequestAnswerInput } from "@t3tools/contracts"; |
| 2 | |
| 3 | export type SecretRequestItem = Extract< |
| 4 | OrchestrationV2TurnItem, |
| 5 | { readonly type: "secret_request" } |
| 6 | >; |
| 7 | |
| 8 | /** Shown under the field: the one promise the card makes about the value. */ |
| 9 | export const SECRET_REQUEST_PRIVACY_NOTE = "Stored securely, never shown to the agent"; |
| 10 | export const SECRET_REQUEST_DEFAULT_PLACEHOLDER = "Paste the secret"; |
| 11 | |
| 12 | /** What a secret request card shows: the form while pending, otherwise a one-line outcome. */ |
| 13 | export type SecretRequestDisplay = |
| 14 | | { readonly kind: "pending" } |
| 15 | | { readonly kind: "pending-elsewhere"; readonly label: string } |
| 16 | | { |
| 17 | readonly kind: "answered"; |
| 18 | readonly outcome: "saved" | "declined" | "ended"; |
| 19 | readonly label: string; |
| 20 | }; |
| 21 | |
| 22 | const SAVED_DISPLAY: SecretRequestDisplay = { |
| 23 | kind: "answered", |
| 24 | outcome: "saved", |
| 25 | label: "Saved securely and kept private", |
| 26 | }; |
| 27 | const DECLINED_DISPLAY: SecretRequestDisplay = { |
| 28 | kind: "answered", |
| 29 | outcome: "declined", |
| 30 | label: "Declined", |
| 31 | }; |
| 32 | const ENDED_DISPLAY: SecretRequestDisplay = { |
| 33 | kind: "answered", |
| 34 | outcome: "ended", |
| 35 | label: "Request ended", |
| 36 | }; |
| 37 | const PENDING_DISPLAY: SecretRequestDisplay = { kind: "pending" }; |
| 38 | const PENDING_ELSEWHERE_DISPLAY: SecretRequestDisplay = { |
| 39 | kind: "pending-elsewhere", |
| 40 | label: "Waiting for an answer in the original thread", |
| 41 | }; |
| 42 | |
| 43 | /** |
| 44 | * `visibility` is the projected row's: a request inherited from another |
| 45 | * thread (a fork) can only be answered where it was asked. |
| 46 | */ |
| 47 | export function secretRequestDisplay( |
| 48 | item: Pick<SecretRequestItem, "secretStatus">, |
| 49 | visibility: "local" | "inherited" | "synthetic", |
| 50 | ): SecretRequestDisplay { |
| 51 | switch (item.secretStatus) { |
| 52 | case "pending": |
| 53 | return visibility === "local" ? PENDING_DISPLAY : PENDING_ELSEWHERE_DISPLAY; |
| 54 | case "saved": |
| 55 | return SAVED_DISPLAY; |
| 56 | case "declined": |
| 57 | return DECLINED_DISPLAY; |
| 58 | case "cancelled": |
| 59 | return ENDED_DISPLAY; |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Builds the RPC payload for an answer. A save with a blank value returns null, |
| 65 | * since the server rejects it; callers keep Save disabled instead. |
| 66 | */ |
| 67 | export function secretRequestAnswerInput( |
| 68 | item: Pick<SecretRequestItem, "id" | "threadId">, |
| 69 | answer: { readonly type: "save"; readonly secret: string } | { readonly type: "decline" }, |
| 70 | ): SecretRequestAnswerInput | null { |
| 71 | if (answer.type === "decline") { |
| 72 | return { threadId: item.threadId, turnItemId: item.id, answer: { type: "decline" } }; |
| 73 | } |
| 74 | const secret = answer.secret.trim(); |
| 75 | if (secret.length === 0) return null; |
| 76 | return { threadId: item.threadId, turnItemId: item.id, answer: { type: "save", secret } }; |
| 77 | } |
| 78 | |
| 79 | /** Failures whose message is written for the user and never echoes the request payload. */ |
| 80 | const USER_FACING_FAILURE_TAGS = new Set(["SecretRequestError", "EnvironmentAuthorizationError"]); |
| 81 | |
| 82 | /** |
| 83 | * Inline error copy for a failed answer. Only known server errors pass their |
| 84 | * message through: anything else (transport or encoding failures) gets the |
| 85 | * generic copy, so the typed value can never surface in the UI. |
| 86 | */ |
| 87 | export function secretRequestFailureMessage(failure: unknown): string { |
| 88 | if ( |
| 89 | typeof failure === "object" && |
| 90 | failure !== null && |
| 91 | "_tag" in failure && |
| 92 | typeof failure._tag === "string" && |
| 93 | USER_FACING_FAILURE_TAGS.has(failure._tag) && |
| 94 | "message" in failure && |
| 95 | typeof failure.message === "string" && |
| 96 | failure.message.trim().length > 0 |
| 97 | ) { |
| 98 | return failure.message; |
| 99 | } |
| 100 | return "Could not answer the request. Try again."; |
| 101 | } |