import * as z from "zod/mini"; import { openRequestMetadataSchema } from "./core-view-host-actions.js"; import { generationSchema } from "./v3-generation.js"; // Experimental, opt-in direct-host projection. The existing WebView wire is unchanged. export const NATIVE_FLOW_SESSION_OPT_IN = "experimental.native-flow-session.v2"; export const NATIVE_FLOW_SESSION_PROTOCOL = "native-flow-session.v2"; export const NATIVE_FLOW_RADIO_DECISIONS_PROTOCOL = "native-flow-radio-decisions.v1"; export const NATIVE_FLOW_SESSION_RPC_METHOD = "nativeFlow.interact"; const id = z.string().check(z.minLength(1), z.maxLength(200)); const text = z.string().check(z.maxLength(4000)); const answer = z.union([text, z.array(text).check(z.maxLength(1)), z.null()]); const answers = z .record(id, answer) .check(z.refine((value) => Object.keys(value).length <= 32)); const question = { key: id, label: text, description: z.optional(text), placeholder: z.optional(text), required: z.boolean(), }; export const nativeFlowBlockSchema = z.discriminatedUnion("kind", [ z.strictObject({ kind: z.literal("statement"), key: id, paragraphs: z.array(text).check(z.maxLength(16)), }), z.strictObject({ kind: z.literal("text"), ...question }), z.strictObject({ kind: z.literal("radio"), ...question, options: z .array(z.strictObject({ value: text, label: text })) .check(z.minLength(1), z.maxLength(32)), }), ]); const radioDecisionsSchema = z.strictObject({ fieldKey: id, entries: z .array( z.strictObject({ value: z.union([text, z.null()]), intent: z.enum(["continue", "submit"]), }), ) .check(z.minLength(2), z.maxLength(34)), }); export const nativeFlowProjectionSchema = z .strictObject({ renderer: z.literal("native"), mode: z.enum(["simulated", "persisted", "disabled"]), nodeId: id, blocks: z.array(nativeFlowBlockSchema).check(z.maxLength(32)), answers, errors: z .record(id, text) .check(z.refine((value) => Object.keys(value).length <= 32)), formErrors: z.array(text).check(z.maxLength(16)), status: z.enum(["editing", "ready-to-submit", "succeeded"]), canGoBack: z.boolean(), canClose: z.boolean(), terminal: z.array(text).check(z.maxLength(16)), radioDecisions: z.optional(radioDecisionsSchema), }) .check( z.refine((projection) => { const table = projection.radioDecisions; if (!table) return true; const fields = projection.blocks.filter( (block) => block.kind !== "statement", ); const field = fields[0]; if ( projection.status === "succeeded" || fields.length !== 1 || field?.kind !== "radio" || field.key !== table.fieldKey ) return false; const options = new Set(field.options.map((option) => option.value)); if (options.size !== field.options.length) return false; // Null and the raw clear string are independently evaluated by Core. const domain = new Set([null, "", ...options]); return ( table.entries.length === domain.size && table.entries.every((entry) => domain.delete(entry.value)) ); }), ); export type NativeFlowProjection = z.output; export type NativeFlowBlock = z.output; export const nativeFlowInteractionSchema = z.strictObject({ flowRunId: id, nodeId: id, intent: z.enum(["draft", "continue", "back", "submit"]), draft: answers, }); export type NativeFlowInteraction = z.output< typeof nativeFlowInteractionSchema >; export const actionPrerenderNativeFlowSchema = z.strictObject({ t: z.literal("getuserfeedback:action:prerenderNativeFlow"), gen: generationSchema, instanceId: id, viewId: id, flowRunId: id, openRequest: z.optional(openRequestMetadataSchema), projection: nativeFlowProjectionSchema, }); export type ActionPrerenderNativeFlow = z.output< typeof actionPrerenderNativeFlowSchema >;