← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/viewer-operations.ts

26.5 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import { z } from "zod";

export const SEQUENCE_VIEWER_QUERY_TOOL_NAME = "sequence.query_viewer";
export const SEQUENCE_VIEWER_ANALYSIS_TOOL_NAME = "sequence.run_analysis";
export const SEQUENCE_VIEWER_ALIGN_TOOL_NAME = "sequence.align";
export const SEQUENCE_VIEWER_EDIT_TOOL_NAME = "sequence.edit_copy";
export const SEQUENCE_VIEWER_ANNOTATIONS_TOOL_NAME =
  "sequence.manage_annotations";
export const SEQUENCE_VIEWER_LOAD_TRACK_TOOL_NAME = "sequence.load_track";
export const SEQUENCE_VIEWER_EXPORT_TOOL_NAME = "sequence.export_artifact";
export const SEQUENCE_VIEWER_SAVE_SESSION_TOOL_NAME = "sequence.save_session";
export const SEQUENCE_VIEWER_RESTORE_SESSION_TOOL_NAME =
  "sequence.restore_session";
export const SEQUENCE_VIEWER_CANCEL_JOB_TOOL_NAME = "sequence.cancel_job";

export function isSafeWorkbenchFileName(value: string): boolean {
  return (
    value !== "." &&
    value !== ".." &&
    !value.includes("/") &&
    !value.includes("\\") &&
    !/[\0\r\n]/u.test(value)
  );
}

export function isSafeWorkspaceExportRelativePath(value: string): boolean {
  const normalized = value.trim();
  if (
    normalized !== value ||
    normalized.length === 0 ||
    normalized.length > 4_096 ||
    /[\0\r\n\\:]/u.test(normalized) ||
    normalized.startsWith("/") ||
    /^[a-z]:/iu.test(normalized) ||
    /^[a-z][a-z0-9+.-]*:/iu.test(normalized) ||
    normalized.endsWith("/")
  ) {
    return false;
  }
  const segments = normalized.split("/");
  return segments.every((segment) => {
    if (segment === "..") return true;
    const windowsBase = segment.split(".")[0]?.toUpperCase() ?? "";
    return (
      segment.length > 0 &&
      segment !== "." &&
      !segment.endsWith(".") &&
      !segment.endsWith(" ") &&
      !/^(?:AUX|CON|NUL|PRN|COM[1-9]|LPT[1-9])$/u.test(windowsBase)
    );
  });
}

export function isSafeWorkspaceProvenancePath(value: string): boolean {
  return (
    value === "." ||
    (isSafeWorkspaceExportRelativePath(value) &&
      !value.split("/").includes(".."))
  );
}

export const sequenceWorkspaceExportDestinationSchema = z
  .object({
    base: z.literal("opened-source"),
    kind: z.literal("workspace"),
    relativePath: z
      .string()
      .trim()
      .min(1)
      .max(4_096)
      .refine(
        isSafeWorkspaceExportRelativePath,
        "relativePath must be a safe path relative to the opened source directory.",
      )
      .describe(
        "Destination relative to the opened source directory. Parent segments are allowed only when the server confirms containment in the same workspace root.",
      ),
  })
  .strict();

export const sequenceWorkspacePersistenceDestinationSchema =
  sequenceWorkspaceExportDestinationSchema
    .extend({
      collisionPolicy: z.enum(["exact", "next-version"]).optional(),
    })
    .strict();

export const sequenceExportDestinationSchema = z.discriminatedUnion("kind", [
  z.object({ kind: z.literal("private") }).strict(),
  sequenceWorkspaceExportDestinationSchema,
]);

export const sequencePersistenceDestinationSchema = z.discriminatedUnion(
  "kind",
  [
    z.object({ kind: z.literal("private") }).strict(),
    sequenceWorkspacePersistenceDestinationSchema,
  ],
);

const sessionIdSchema = z
  .string()
  .uuid()
  .describe("Active viewer session ID from the live viewer context.");
const pageSchema = z
  .object({
    cursor: z.string().trim().min(1).max(1_024).optional(),
    limit: z.number().int().min(1).max(500).default(100),
  })
  .strict();

export const sequenceWorkbenchPanelGroupSchema = z.enum([
  "alignment-tools",
  "sequence-display",
  "sequence-tools",
]);

export const sequenceViewerQueryRequestSchema = z.discriminatedUnion("target", [
  z
    .object({
      group: sequenceWorkbenchPanelGroupSchema.optional(),
      target: z.literal("workbench-panels"),
    })
    .strict(),
  pageSchema.extend({
    limit: z.number().int().min(1).max(100).default(100),
    mode: z.enum(["alignment", "sequence"]).optional(),
    target: z.literal("workbench-disclosures"),
  }),
  pageSchema.extend({
    limit: z.number().int().min(1).max(100).default(100),
    mode: z.enum(["alignment", "sequence"]).optional(),
    target: z.literal("workbench-feedback"),
  }),
  z.object({ target: z.literal("sequence-ui-state") }).strict(),
  z.object({ target: z.literal("read-pileup-state") }).strict(),
  z.object({ target: z.literal("quality-report") }).strict(),
  pageSchema
    .extend({
      end: z.number().int().positive(),
      record: z.string().trim().min(1).max(500).optional(),
      start: z.number().int().positive(),
      target: z.literal("chromatogram"),
    })
    .refine(
      ({ start, end }) => end >= start && end - start < 100,
      "Chromatogram queries require a forward window of at most 100 source bases.",
    ),
  pageSchema.extend({
    end: z.number().int().positive().optional(),
    sourceReadIndex: z
      .number()
      .int()
      .nonnegative()
      .describe(
        "Zero-based index returned by a reads query for the currently materialized loaded track; not a source-file ordinal, line number, or byte offset.",
      ),
    start: z.number().int().positive().optional(),
    target: z.literal("read-detail"),
    trackId: z.string().trim().min(1).max(500),
  }),
  pageSchema.extend({ target: z.literal("records") }),
  pageSchema.extend({
    query: z.string().trim().min(1).max(500).optional(),
    record: z.string().trim().min(1).max(500).optional(),
    target: z.literal("features"),
  }),
  z
    .object({
      end: z.number().int().positive(),
      record: z.string().trim().min(1).max(500).optional(),
      start: z.number().int().positive(),
      target: z.literal("sequence-range"),
    })
    .strict(),
  z
    .object({
      end: z.number().int().positive(),
      record: z.string().trim().min(1).max(500).optional(),
      start: z.number().int().positive(),
      target: z.literal("quality"),
    })
    .strict(),
  pageSchema.extend({ target: z.literal("rows") }),
  z
    .object({
      end: z.number().int().positive(),
      row: z.string().trim().min(1).max(500).optional(),
      start: z.number().int().positive(),
      target: z.literal("columns"),
    })
    .strict(),
  pageSchema.extend({ target: z.literal("metrics") }),
  pageSchema.extend({ target: z.literal("search-hits") }),
  pageSchema.extend({ target: z.literal("annotations") }),
  pageSchema.extend({ target: z.literal("tracks") }),
  pageSchema.extend({ target: z.literal("tree-nodes") }),
  pageSchema.extend({ target: z.literal("jobs") }),
  pageSchema.extend({ target: z.literal("artifacts") }),
  pageSchema.extend({
    end: z.number().int().positive(),
    reference: z.string().trim().min(1).max(500),
    start: z.number().int().positive(),
    target: z.literal("variants"),
  }),
  pageSchema.extend({
    end: z.number().int().positive(),
    reference: z.string().trim().min(1).max(500),
    start: z.number().int().positive(),
    target: z.literal("coverage"),
  }),
  pageSchema.extend({
    end: z.number().int().positive(),
    reference: z.string().trim().min(1).max(500),
    start: z.number().int().positive(),
    target: z.literal("reads"),
  }),
]);

export const sequenceViewerQueryInputSchema = z
  .object({
    cursor: z.string().trim().min(1).max(1_024).optional(),
    end: z.number().int().positive().optional(),
    group: sequenceWorkbenchPanelGroupSchema.optional(),
    limit: z.number().int().min(1).max(500).optional(),
    mode: z.enum(["alignment", "sequence"]).optional(),
    query: z.string().trim().min(1).max(500).optional(),
    record: z.string().trim().min(1).max(500).optional(),
    reference: z.string().trim().min(1).max(500).optional(),
    row: z.string().trim().min(1).max(500).optional(),
    sessionId: sessionIdSchema,
    sourceReadIndex: z
      .number()
      .int()
      .nonnegative()
      .describe(
        "Zero-based index in the loaded track.reads array, as returned by target reads; not a file position.",
      )
      .optional(),
    start: z.number().int().positive().optional(),
    target: z.enum([
      "annotations",
      "artifacts",
      "columns",
      "chromatogram",
      "coverage",
      "features",
      "jobs",
      "metrics",
      "quality",
      "quality-report",
      "read-detail",
      "read-pileup-state",
      "reads",
      "records",
      "rows",
      "search-hits",
      "sequence-range",
      "sequence-ui-state",
      "tracks",
      "tree-nodes",
      "variants",
      "workbench-panels",
      "workbench-disclosures",
      "workbench-feedback",
    ]),
    trackId: z.string().trim().min(1).max(500).optional(),
  })
  .strict();

const analysisBaseSchema = z.object({
  record: z.string().trim().min(1).max(500).optional(),
});

export const sequenceViewerAnalysisRequestSchema = z.discriminatedUnion(
  "analysis",
  [
    z
      .object({
        adapterSequence: z
          .string()
          .trim()
          .min(8)
          .max(64)
          .regex(
            /^[ACGT]+$/iu,
            "Adapter sequence must contain 8–64 A, C, G, or T bases.",
          )
          .optional(),
        analysis: z.literal("quality-report"),
      })
      .strict(),
    analysisBaseSchema.extend({ analysis: z.literal("statistics") }).strict(),
    analysisBaseSchema
      .extend({
        analysis: z.literal("translate"),
        end: z.number().int().positive().optional(),
        frame: z
          .union([
            z.literal(1),
            z.literal(2),
            z.literal(3),
            z.literal(-1),
            z.literal(-2),
            z.literal(-3),
          ])
          .optional(),
        geneticCodeId: z.number().int().positive().optional(),
        start: z.number().int().positive().optional(),
      })
      .strict(),
    analysisBaseSchema
      .extend({
        analysis: z.literal("find-orfs"),
        geneticCodeId: z.number().int().positive().optional(),
        includePartial: z.boolean().default(false),
        minAminoAcids: z.number().int().min(1).max(100_000).default(30),
        strands: z.enum(["+", "-", "both"]).default("both"),
      })
      .strict(),
    analysisBaseSchema
      .extend({
        analysis: z.literal("restriction-analysis"),
        circular: z.boolean().optional(),
        enzymes: z.array(z.string().trim().min(1).max(100)).max(200).optional(),
      })
      .strict(),
    analysisBaseSchema
      .extend({
        analysis: z.literal("design-primers"),
        maxPairs: z.number().int().min(1).max(50).default(10),
        maxProductLength: z.number().int().min(20).max(100_000).default(1_500),
        minProductLength: z.number().int().min(20).max(100_000).default(80),
        targetEnd: z.number().int().positive().optional(),
        targetStart: z.number().int().positive().optional(),
      })
      .strict(),
    z
      .object({
        algorithm: z
          .enum(["neighbor-joining", "upgma"])
          .default("neighbor-joining"),
        analysis: z.literal("build-tree"),
        rowIds: z
          .array(z.string().trim().min(1).max(500))
          .min(1)
          .max(100)
          .optional(),
      })
      .strict(),
    z
      .object({
        analysis: z.literal("distance-matrix"),
        rowIds: z
          .array(z.string().trim().min(1).max(500))
          .min(1)
          .max(500)
          .optional(),
      })
      .strict(),
  ],
);

export const sequenceViewerAnalysisInputSchema = z
  .object({
    adapterSequence: z
      .string()
      .trim()
      .min(8)
      .max(64)
      .regex(
        /^[ACGT]+$/iu,
        "Adapter sequence must contain 8–64 A, C, G, or T bases.",
      )
      .optional(),
    algorithm: z.enum(["neighbor-joining", "upgma"]).optional(),
    analysis: z.enum([
      "build-tree",
      "design-primers",
      "distance-matrix",
      "find-orfs",
      "quality-report",
      "restriction-analysis",
      "statistics",
      "translate",
    ]),
    circular: z.boolean().optional(),
    end: z.number().int().positive().optional(),
    enzymes: z.array(z.string().trim().min(1).max(100)).max(200).optional(),
    frame: z
      .union([
        z.literal(1),
        z.literal(2),
        z.literal(3),
        z.literal(-1),
        z.literal(-2),
        z.literal(-3),
      ])
      .optional(),
    geneticCodeId: z.number().int().positive().optional(),
    includePartial: z.boolean().optional(),
    maxPairs: z.number().int().min(1).max(50).optional(),
    maxProductLength: z.number().int().min(20).max(100_000).optional(),
    minAminoAcids: z.number().int().min(1).max(100_000).optional(),
    minProductLength: z.number().int().min(20).max(100_000).optional(),
    record: z.string().trim().min(1).max(500).optional(),
    rowIds: z
      .array(z.string().trim().min(1).max(500))
      .min(1)
      .max(500)
      .optional(),
    sessionId: sessionIdSchema,
    start: z.number().int().positive().optional(),
    strands: z.enum(["+", "-", "both"]).optional(),
    targetEnd: z.number().int().positive().optional(),
    targetStart: z.number().int().positive().optional(),
  })
  .strict();

export const sequenceViewerAlignInputSchema = z
  .object({
    algorithm: z.enum(["builtin-center-star", "builtin-pairwise"]).optional(),
    recordIds: z
      .array(z.string().trim().min(1).max(500))
      .min(2)
      .max(100)
      .optional(),
    rowIds: z
      .array(z.string().trim().min(1).max(500))
      .min(2)
      .max(100)
      .optional(),
    sessionId: sessionIdSchema,
  })
  .strict()
  .superRefine(({ algorithm, recordIds, rowIds }, context) => {
    if (recordIds != null && rowIds != null) {
      context.addIssue({
        code: "custom",
        message: "Pass recordIds or rowIds, not both.",
      });
    }
    const selectedCount = recordIds?.length ?? rowIds?.length;
    if (
      algorithm === "builtin-pairwise" &&
      selectedCount != null &&
      selectedCount !== 2
    ) {
      context.addIssue({
        code: "custom",
        message: "builtin-pairwise requires exactly two selected sequences.",
      });
    }
  });

export const sequenceViewerEditRequestSchema = z.discriminatedUnion(
  "operation",
  [
    z.object({ operation: z.literal("undo") }).strict(),
    z.object({ operation: z.literal("redo") }).strict(),
    z
      .object({
        coordinate: z.number().int().positive(),
        operation: z.literal("insert-sequence"),
        record: z.string().trim().min(1).max(500).optional(),
        sequence: z.string().min(1).max(1_000_000),
      })
      .strict(),
    z
      .object({
        end: z.number().int().positive(),
        operation: z.literal("delete-sequence-range"),
        record: z.string().trim().min(1).max(500).optional(),
        start: z.number().int().positive(),
      })
      .strict(),
    z
      .object({
        end: z.number().int().positive(),
        operation: z.literal("replace-sequence-range"),
        record: z.string().trim().min(1).max(500).optional(),
        sequence: z.string().min(1).max(1_000_000),
        start: z.number().int().positive(),
      })
      .strict(),
    z
      .object({
        end: z.number().int().positive(),
        operation: z.literal("reverse-complement-range"),
        record: z.string().trim().min(1).max(500).optional(),
        start: z.number().int().positive(),
      })
      .strict(),
    z
      .object({
        newOrigin: z.number().int().positive(),
        operation: z.literal("rotate-sequence"),
        record: z.string().trim().min(1).max(500).optional(),
      })
      .strict(),
    z
      .object({
        column: z.number().int().positive(),
        operation: z.literal("add-alignment-gap"),
        row: z.string().trim().min(1).max(500),
      })
      .strict(),
    z
      .object({
        column: z.number().int().positive(),
        operation: z.literal("delete-alignment-gap"),
        row: z.string().trim().min(1).max(500),
      })
      .strict(),
    z
      .object({
        end: z.number().int().positive(),
        operation: z.literal("remove-alignment-columns"),
        start: z.number().int().positive(),
      })
      .strict(),
    z
      .object({
        minimumGapFraction: z.number().min(0).max(1),
        operation: z.literal("remove-gappy-columns"),
      })
      .strict(),
    z
      .object({
        group: z.string().trim().min(1).max(100).nullable(),
        operation: z.literal("assign-alignment-row-group"),
        rowIds: z.array(z.string().trim().min(1).max(500)).min(1).max(500),
      })
      .strict(),
    z
      .object({
        operation: z.literal("remove-alignment-rows"),
        rowIds: z.array(z.string().trim().min(1).max(500)).min(1).max(500),
      })
      .strict(),
    z
      .object({
        operation: z.literal("reorder-alignment-rows"),
        rowIds: z.array(z.string().trim().min(1).max(500)).min(1).max(500),
      })
      .strict(),
    z
      .object({
        mode: z.enum(["group", "identity-to-reference", "label", "tree"]),
        operation: z.literal("sort-alignment-rows"),
        referenceRowId: z.string().trim().min(1).max(500).optional(),
      })
      .strict(),
  ],
);

export const sequenceViewerEditInputSchema = z
  .object({
    column: z.number().int().positive().optional(),
    coordinate: z.number().int().positive().optional(),
    end: z.number().int().positive().optional(),
    group: z.string().trim().min(1).max(100).nullable().optional(),
    minimumGapFraction: z.number().min(0).max(1).optional(),
    mode: z
      .enum(["group", "identity-to-reference", "label", "tree"])
      .optional(),
    newOrigin: z.number().int().positive().optional(),
    operation: z.enum([
      "add-alignment-gap",
      "assign-alignment-row-group",
      "delete-alignment-gap",
      "delete-sequence-range",
      "insert-sequence",
      "redo",
      "remove-alignment-columns",
      "remove-alignment-rows",
      "remove-gappy-columns",
      "reorder-alignment-rows",
      "replace-sequence-range",
      "reverse-complement-range",
      "rotate-sequence",
      "sort-alignment-rows",
      "undo",
    ]),
    record: z.string().trim().min(1).max(500).optional(),
    referenceRowId: z.string().trim().min(1).max(500).optional(),
    row: z.string().trim().min(1).max(500).optional(),
    rowIds: z
      .array(z.string().trim().min(1).max(500))
      .min(1)
      .max(500)
      .optional(),
    sequence: z.string().min(1).max(1_000_000).optional(),
    sessionId: sessionIdSchema,
    start: z.number().int().positive().optional(),
  })
  .strict();

const featureInputSchema = z
  .object({
    end: z.number().int().positive(),
    id: z.string().trim().min(1).max(500),
    label: z.string().trim().min(1).max(500).optional(),
    qualifiers: z
      .record(z.string(), z.union([z.string(), z.array(z.string())]))
      .default({}),
    start: z.number().int().positive(),
    strand: z.enum(["+", "-", ".", "?"]).default("."),
    type: z.string().trim().min(1).max(200),
  })
  .strict();

export const sequenceViewerAnnotationsInputSchema = z
  .object({
    action: z.enum(["add", "delete", "import", "update"]),
    feature: featureInputSchema.optional(),
    featureId: z.string().trim().min(1).max(500).optional(),
    record: z.string().trim().min(1).max(500).optional(),
    sessionId: sessionIdSchema,
    trackId: z.string().trim().min(1).max(500).optional(),
  })
  .strict();

export const sequenceViewerLoadTrackInputSchema = z
  .object({
    end: z
      .number()
      .int()
      .positive()
      .describe("Indexed BAM/CRAM regional end coordinate, 1-based inclusive.")
      .optional(),
    format: z.enum(["bam", "bed", "cram", "gff3", "gtf", "sam", "vcf"]),
    indexPath: z
      .string()
      .trim()
      .min(1)
      .max(4_096)
      .describe(
        "BAM BAI/CSI or CRAM CRAI path; defaults to path + .bai for BAM and path + .crai for CRAM.",
      )
      .optional(),
    path: z
      .string()
      .trim()
      .min(1)
      .max(4_096)
      .describe("Workspace evidence-track path."),
    reference: z
      .string()
      .trim()
      .min(1)
      .max(500)
      .describe("Exact or uniquely normalized reference/contig name.")
      .optional(),
    referencePath: z
      .string()
      .trim()
      .min(1)
      .max(4_096)
      .describe("Matching FASTA reference path required by most CRAM files.")
      .optional(),
    sessionId: sessionIdSchema,
    start: z
      .number()
      .int()
      .positive()
      .describe(
        "Indexed BAM/CRAM regional start coordinate, 1-based inclusive.",
      )
      .optional(),
  })
  .strict()
  .superRefine(({ end, format, indexPath, referencePath, start }, context) => {
    if (
      format !== "bam" &&
      format !== "cram" &&
      (indexPath != null || start != null || end != null)
    ) {
      context.addIssue({
        code: "custom",
        message: "indexPath, start, and end are indexed BAM/CRAM-only options.",
      });
    }
    if (format !== "cram" && referencePath != null) {
      context.addIssue({
        code: "custom",
        message: "referencePath is a CRAM-only option.",
      });
    }
    if (end != null && start != null && end < start) {
      context.addIssue({
        code: "custom",
        message: "Indexed evidence end must be at or after start.",
      });
    }
    if (end != null && start != null && end - start + 1 > 100_000) {
      context.addIssue({
        code: "custom",
        message: "BAM/CRAM windows are limited to 100,000 bases.",
      });
    }
  });

export const sequenceViewerExportInputSchema = z
  .object({
    destination: sequenceExportDestinationSchema
      .default({ kind: "private" })
      .describe(
        "Keep the default private artifact, or request create-new publication relative to a trusted opened workspace source.",
      ),
    format: z.enum([
      "a3m",
      "aligned-fasta",
      "bed",
      "clustal",
      "csv",
      "embl",
      "fasta",
      "fastq",
      "genbank",
      "gff3",
      "gtf",
      "json",
      "newick",
      "pdf",
      "stockholm",
      "svg",
      "tsv",
      "vcf",
    ]),
    name: z.string().trim().min(1).max(255).optional(),
    scope: z.enum(["all", "selection", "visible"]).default("all"),
    sessionId: sessionIdSchema,
  })
  .strict();

export function isWorkspaceExportNameForFormat(
  format: z.infer<typeof sequenceViewerExportInputSchema>["format"],
  name: string,
): boolean {
  const extensions: Record<
    z.infer<typeof sequenceViewerExportInputSchema>["format"],
    ReadonlyArray<string>
  > = {
    a3m: [".a3m"],
    "aligned-fasta": [".afa", ".aln", ".aln-fasta", ".fa", ".fas", ".fasta"],
    bed: [".bed"],
    clustal: [".aln", ".clustal", ".clw"],
    csv: [".csv"],
    embl: [".embl"],
    fasta: [".fa", ".faa", ".fas", ".fasta", ".fna"],
    fastq: [".fastq", ".fq"],
    genbank: [".gb", ".gbk", ".genbank"],
    gff3: [".gff", ".gff3"],
    gtf: [".gtf"],
    json: [".json"],
    newick: [".newick", ".nwk", ".tree"],
    pdf: [".pdf"],
    stockholm: [".sto", ".stk", ".stockholm"],
    svg: [".svg"],
    tsv: [".tsv"],
    vcf: [".vcf"],
  };
  const lowerName = name.toLowerCase();
  return extensions[format].some(
    (extension) =>
      lowerName.length > extension.length && lowerName.endsWith(extension),
  );
}

export const sequenceViewerSaveSessionInputSchema = z
  .object({
    name: z
      .string()
      .trim()
      .min(1)
      .max(255)
      .refine(isSafeWorkbenchFileName, "name must be a basename.")
      .optional(),
    sessionId: sessionIdSchema,
  })
  .strict();

export const sequenceViewerRestoreSessionInputSchema = z
  .object({
    savedSessionId: z.string().uuid(),
    sessionId: sessionIdSchema,
  })
  .strict();

export const sequenceViewerCancelJobInputSchema = z
  .object({
    jobId: z.string().uuid(),
    sessionId: sessionIdSchema,
  })
  .strict();

export const sequenceViewerOperationCommandSchema = z.discriminatedUnion(
  "action",
  [
    z
      .object({
        action: z.literal("query_viewer"),
        request: sequenceViewerQueryRequestSchema,
      })
      .strict(),
    z
      .object({
        action: z.literal("run_analysis"),
        jobId: z.string().uuid(),
        request: sequenceViewerAnalysisRequestSchema,
      })
      .strict(),
    z
      .object({
        action: z.literal("align_sequences"),
        algorithm: z
          .enum(["builtin-center-star", "builtin-pairwise"])
          .optional(),
        jobId: z.string().uuid(),
        recordIds: z.array(z.string()).optional(),
        rowIds: z.array(z.string()).optional(),
      })
      .strict(),
    z
      .object({
        action: z.literal("edit_copy"),
        request: sequenceViewerEditRequestSchema,
      })
      .strict(),
    z
      .object({
        action: z.literal("manage_annotations"),
        request: sequenceViewerAnnotationsInputSchema.omit({ sessionId: true }),
      })
      .strict(),
    z
      .object({
        action: z.literal("load_track"),
        content: z.string().max(16 * 1_024 * 1_024),
        displayName: z.string().trim().min(1).max(1_024),
        encoding: z.enum(["base64", "utf8"]),
        format: z.enum(["bam", "bed", "cram", "gff3", "gtf", "sam", "vcf"]),
        reference: z.string().trim().min(1).max(500).optional(),
        sourceContentHash: z
          .string()
          .regex(/^[a-f0-9]{64}$/u)
          .optional(),
        sourceItemCount: z.number().int().nonnegative().optional(),
        sourceTruncated: z.boolean().optional(),
        sourceWorkspacePath: z
          .string()
          .refine(isSafeWorkspaceProvenancePath)
          .optional(),
        trackId: z.string().uuid(),
      })
      .strict(),
    z
      .object({
        action: z.literal("export_artifact"),
        destination: sequenceExportDestinationSchema.default({
          kind: "private",
        }),
        format: sequenceViewerExportInputSchema.shape.format,
        name: z.string().trim().min(1).max(255).optional(),
        scope: sequenceViewerExportInputSchema.shape.scope,
      })
      .strict(),
    z
      .object({
        action: z.literal("save_session"),
        name: z.string().trim().min(1).max(255),
      })
      .strict(),
    z
      .object({
        action: z.literal("restore_session"),
        mode: z.enum(["alignment", "sequence"]).optional(),
        session: z.string().max(512 * 1_024),
      })
      .strict(),
    z
      .object({ action: z.literal("cancel_job"), jobId: z.string().uuid() })
      .strict(),
  ],
);

export type SequenceViewerQueryRequest = z.infer<
  typeof sequenceViewerQueryRequestSchema
>;
export type SequenceViewerAnalysisRequest = z.infer<
  typeof sequenceViewerAnalysisRequestSchema
>;
export type SequenceViewerEditRequest = z.infer<
  typeof sequenceViewerEditRequestSchema
>;
export type SequenceViewerAnnotationsInput = z.infer<
  typeof sequenceViewerAnnotationsInputSchema
>;
export type SequenceViewerAnnotationRequest = Omit<
  SequenceViewerAnnotationsInput,
  "sessionId"
>;
export type SequenceViewerOperationCommand = z.infer<
  typeof sequenceViewerOperationCommandSchema
>;

SHA-256: 3e90bd200e07e3eff29fc4d7d729bfd6e2dc0de6da63dd488a6054ba7a4e5585