196 lines · 7.0 KB · TypeScript
RawDownload
import {  ApprovalRequestId,  type OrchestrationThreadActivity,  ProviderApprovalOption,  ProviderRequestKind,  UserInputQuestion,} from "@t3tools/contracts";import * as Option from "effect/Option";import * as Predicate from "effect/Predicate";import * as Schema from "effect/Schema"; export interface PendingApproval {  readonly requestId: ApprovalRequestId;  readonly requestKind: ProviderRequestKind;  readonly createdAt: string;  readonly detail?: string;  readonly appName?: string;  readonly options?: ReadonlyArray<ProviderApprovalOption>;} export interface PendingUserInput {  readonly requestId: ApprovalRequestId;  readonly createdAt: string;  readonly questions: ReadonlyArray<UserInputQuestion>;  /** Async questions can be dismissed without a reply; native callbacks cannot. */  readonly dismissible: boolean;} const isRequestId = Schema.is(ApprovalRequestId);const isProviderRequestKind = Schema.is(ProviderRequestKind);const isProviderApprovalOption = Schema.is(ProviderApprovalOption);const QuestionOption = Schema.Struct({  ...UserInputQuestion.fields.options.value.fields,  label: Schema.String,});const isQuestionOption = Schema.is(QuestionOption);// Native question IDs and option labels can be answer keys. Do not trim them.const decodeQuestion = Schema.decodeUnknownOption(  Schema.Struct({    ...UserInputQuestion.fields,    id: Schema.String,    header: Schema.String,    question: Schema.String,    options: Schema.Array(QuestionOption),  }),); /** Older activities use native request types instead of a request kind. */export function requestKindFromRequestType(requestType: unknown): ProviderRequestKind | null {  switch (requestType) {    case "command_execution_approval":    case "exec_command_approval":    case "dynamic_tool_call":      return "command";    case "file_read_approval":      return "file-read";    case "file_change_approval":    case "apply_patch_approval":      return "file-change";    case "mcp_elicitation_approval":      return "mcp-elicitation";    default:      return null;  }} function parseQuestions(value: unknown): UserInputQuestion[] {  if (!Array.isArray(value)) return [];  return value.flatMap((question) => {    if (!Predicate.isObject(question) || !Array.isArray(question.options)) return [];    const options = question.options.filter(isQuestionOption);    if (options.length === 0 && question.allowCustomAnswer === false) return [];    const parsed = decodeQuestion({      id: question.id,      header: question.header,      question: question.question,      options,      multiSelect: question.multiSelect === true,      ...(typeof question.allowCustomAnswer === "boolean"        ? { allowCustomAnswer: question.allowCustomAnswer }        : {}),    });    return Option.isSome(parsed) ? [parsed.value] : [];  });} const requestActivityKinds = new Set([  "approval.requested",  "approval.resolved",  "provider.approval.respond.failed",  "user-input.requested",  "user-input.resolved",  "provider.user-input.respond.failed",]); // The server reports a stale or unknown request through the failure text.// A failed reply with any other text stays open so the user can retry.const staleRequestFailureDetails = {  "provider.approval.respond.failed": [    "stale pending approval request",    "unknown pending approval request",    "unknown pending permission request",    "unknown pending codex approval request",  ],  "provider.user-input.respond.failed": [    "stale pending user-input request",    "unknown pending user-input request",    "unknown pending user input request",    "unknown pending codex user input request",  ],} as const; function isStaleRequestFailure(  kind: keyof typeof staleRequestFailureDetails,  payload: Record<string, unknown>,): boolean {  const detail = typeof payload.detail === "string" ? payload.detail.toLowerCase() : "";  return staleRequestFailureDetails[kind].some((fragment) => detail.includes(fragment));} /** Reduces request state once for web, desktop, and mobile. Layout stays with each client. */export function derivePendingRequests(activities: ReadonlyArray<OrchestrationThreadActivity>) {  const approvals = new Map<ApprovalRequestId, PendingApproval>();  const userInputs = new Map<ApprovalRequestId, PendingUserInput>();  const closedApprovals = new Set<ApprovalRequestId>();  const closedUserInputs = new Set<ApprovalRequestId>();   // Request IDs are unique. A terminal event stays final even when provider  // sequences and server-generated activities arrive in a different order.  for (const activity of activities) {    if (!requestActivityKinds.has(activity.kind)) continue;    const payload = Predicate.isObject(activity.payload) ? activity.payload : undefined;    if (!payload || !isRequestId(payload.requestId)) continue;    const requestId = payload.requestId;     if (activity.kind === "approval.requested") {      if (        closedApprovals.has(requestId) ||        payload.requestType === "tool_user_input" ||        payload.requestType === "auth_tokens_refresh"      ) {        continue;      }      const requestKind = isProviderRequestKind(payload.requestKind)        ? payload.requestKind        : requestKindFromRequestType(payload.requestType);      const options = Array.isArray(payload.options)        ? payload.options.filter(isProviderApprovalOption)        : [];      approvals.set(requestId, {        requestId,        // Older OpenCode approvals do not always include a recognized kind.        requestKind: requestKind ?? "command",        createdAt: activity.createdAt,        ...(typeof payload.detail === "string" && payload.detail ? { detail: payload.detail } : {}),        ...(typeof payload.appName === "string" && payload.appName          ? { appName: payload.appName }          : {}),        ...(options.length > 0 ? { options } : {}),      });    } else if (activity.kind === "user-input.requested") {      if (closedUserInputs.has(requestId)) continue;      const questions = parseQuestions(payload.questions);      if (questions.length === 0) continue;      userInputs.set(requestId, {        requestId,        createdAt: activity.createdAt,        questions,        dismissible: payload.responseMode === "message",      });    } else if (      activity.kind === "approval.resolved" ||      (activity.kind === "provider.approval.respond.failed" &&        isStaleRequestFailure(activity.kind, payload))    ) {      closedApprovals.add(requestId);      approvals.delete(requestId);    } else if (      activity.kind === "user-input.resolved" ||      (activity.kind === "provider.user-input.respond.failed" &&        isStaleRequestFailure(activity.kind, payload))    ) {      closedUserInputs.add(requestId);      userInputs.delete(requestId);    }  }   const byCreatedAt = (    left: { readonly createdAt: string },    right: { readonly createdAt: string },  ) => left.createdAt.localeCompare(right.createdAt);  return {    approvals: [...approvals.values()].sort(byCreatedAt),    userInputs: [...userInputs.values()].sort(byCreatedAt),  };}