From 846dfd6d2ec5a892915559c85d8d2e8b4e209b08 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sun, 7 Jun 2026 14:55:22 +0800 Subject: [PATCH] feat(codegen): port ai pipeline-internal types --- crates/op-codegen/src/ai/types.rs | 179 +++++++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 1 deletion(-) diff --git a/crates/op-codegen/src/ai/types.rs b/crates/op-codegen/src/ai/types.rs index 2b21d827c..8b22d5f9d 100644 --- a/crates/op-codegen/src/ai/types.rs +++ b/crates/op-codegen/src/ai/types.rs @@ -1 +1,178 @@ -// Implemented in later tasks of this plan. +//! Pipeline-internal types. Ported from packages/pen-types/src/codegen.ts. +//! Shared UI-facing types come from op-editor-core; param enums from op-ai. + +use op_ai::chat_provider::{EffortLevel, ThinkingMode}; +use op_editor_core::codegen::Framework; +use serde::Deserialize; + +/// Stable id correlating a dispatched request with its streamed deltas. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RequestId(pub u64); + +/// Which pipeline phase a request belongs to. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RequestKind { + Planning, + Chunk { chunk_id: String }, + Assembly, +} + +/// Step-1 planner output (no node data). Parity with TS `CodePlanFromAI`. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct CodePlan { + #[serde(default)] + pub chunks: Vec, + #[serde(default, rename = "sharedStyles")] + pub shared_styles: Vec, + #[serde(rename = "rootLayout")] + pub root_layout: RootLayout, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct SharedStyle { + pub name: String, + #[serde(default)] + pub description: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct RootLayout { + #[serde(default)] + pub direction: String, + #[serde(default)] + pub gap: f64, + #[serde(default)] + pub responsive: bool, +} + +/// Parity with TS `PlannedChunk`. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct PlannedChunk { + pub id: String, + #[serde(default)] + pub name: String, + #[serde(default, rename = "nodeIds")] + pub node_ids: Vec, + #[serde(default)] + pub role: String, + #[serde(default, rename = "suggestedComponentName")] + pub suggested_component_name: String, + #[serde(default)] + pub dependencies: Vec, +} + +/// Hydrated chunk with execution order. Parity with TS `ExecutableChunk` +/// (we carry the planned chunk + order; the prompt builder reads node data +/// from `CodegenInput`). +#[derive(Debug, Clone, PartialEq)] +pub struct ExecutableChunk { + pub plan: PlannedChunk, + pub order: usize, +} + +/// Structured metadata each chunk emits. Parity with TS `ChunkContract`. +#[derive(Debug, Clone, Default, PartialEq, Deserialize)] +pub struct ChunkContract { + #[serde(default, rename = "chunkId")] + pub chunk_id: String, + #[serde(default, rename = "componentName")] + pub component_name: String, + #[serde(default, rename = "cssClasses")] + pub css_classes: Vec, + #[serde(default, rename = "cssVariables")] + pub css_variables: Vec, +} + +/// A chunk's generated code + parsed contract. Parity with TS `ChunkResult`. +#[derive(Debug, Clone, PartialEq)] +pub struct ChunkResult { + pub chunk_id: String, + pub code: String, + pub contract: ChunkContract, +} + +/// An extracted embedded image. Bytes stay here; the host maps to +/// `op_editor_core::codegen::AssetMeta` for the UI. Parity with TS +/// `CodegenAssetFile`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AssetFile { + pub id: String, + pub relative_path: String, + pub zip_path: String, + pub mime_type: String, + pub bytes: Vec, + pub source_node_id: String, +} + +/// Inputs to construct a pipeline run. +#[derive(Debug, Clone)] +pub struct CodegenInput { + /// Selected node subtree as canonical JSON (already sanitized of + /// embedded asset data-URLs by `assets::extract_codegen_assets`). + pub nodes_json: String, + pub framework: Framework, + /// Design variables as JSON (None if the doc has none). + pub variables_json: Option, + /// Per-request token cap. + pub max_output_tokens: u32, + pub thinking: ThinkingMode, + pub effort: EffortLevel, +} + +/// A logical request the host must execute against the model. Skill NAMES +/// (not text) + the pipeline-owned user message + params. The transport +/// expands the skills into the final `op_ai::ChatRequest`. +#[derive(Debug, Clone, PartialEq)] +pub struct PendingRequest { + pub id: RequestId, + pub kind: RequestKind, + pub skills: Vec<&'static str>, + pub user_message: String, + pub max_output_tokens: u32, + pub thinking: ThinkingMode, + pub effort: EffortLevel, +} + +/// What the host should do next. Returned by `CodegenPipeline::step`. +#[derive(Debug, Clone, PartialEq)] +pub enum PipelineStep { + Dispatch(Vec), + Waiting, + Done { + code: String, + degraded: bool, + assets: Vec, + }, + Failed { + message: String, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn code_plan_deserializes_ts_shape() { + let json = r#"{ + "chunks": [ + {"id":"a","name":"Nav","nodeIds":["1"],"role":"nav", + "suggestedComponentName":"Navbar","dependencies":[]} + ], + "sharedStyles": [], + "rootLayout": {"direction":"column","gap":16,"responsive":true} + }"#; + let plan: CodePlan = serde_json::from_str(json).unwrap(); + assert_eq!(plan.chunks.len(), 1); + assert_eq!(plan.chunks[0].suggested_component_name, "Navbar"); + assert_eq!(plan.root_layout.direction, "column"); + assert!(plan.root_layout.responsive); + } + + #[test] + fn chunk_contract_defaults_missing_arrays() { + let c: ChunkContract = serde_json::from_str(r#"{"componentName":"Card"}"#).unwrap(); + assert_eq!(c.component_name, "Card"); + assert!(c.css_classes.is_empty()); + } +}