openpencil/crates/op-editor-core/src/codegen.rs
Kayshen-X 041cfc4de6 feat(editor-core): wire CodegenState onto EditorState
Add pending_generate + pending_regenerate flags to CodegenState
(mirror of chat.pending_send). Wire codegen: CodegenState as a
field on EditorState immediately after chat, with TDD coverage in
codegen_state_defaults_to_idle_react.
2026-06-07 15:16:40 +08:00

180 lines
5.5 KiB
Rust

//! UI-facing code-generation state, read by the Code panel painter and
//! shared by both hosts. Mirrors the TS `CodeGenProgress` shapes. Plain
//! data — keeps op-editor-core wasm-clean. Pipeline LOGIC lives in
//! op-codegen::ai; these are the types that crate returns (it depends on
//! op-editor-core, so the edge is acyclic).
/// Target framework for code generation. Wire tokens match TS `Framework`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Framework {
React,
Vue,
Svelte,
Html,
Flutter,
SwiftUi,
Compose,
ReactNative,
}
impl Framework {
pub const ALL: [Framework; 8] = [
Framework::React,
Framework::Vue,
Framework::Svelte,
Framework::Html,
Framework::Flutter,
Framework::SwiftUi,
Framework::Compose,
Framework::ReactNative,
];
pub fn as_wire(self) -> &'static str {
match self {
Framework::React => "react",
Framework::Vue => "vue",
Framework::Svelte => "svelte",
Framework::Html => "html",
Framework::Flutter => "flutter",
Framework::SwiftUi => "swiftui",
Framework::Compose => "compose",
Framework::ReactNative => "react-native",
}
}
pub fn from_wire(s: &str) -> Option<Framework> {
Framework::ALL.into_iter().find(|f| f.as_wire() == s)
}
/// The framework-specific knowledge skill name (e.g. "codegen-react").
pub fn skill_name(self) -> &'static str {
match self {
Framework::React => "codegen-react",
Framework::Vue => "codegen-vue",
Framework::Svelte => "codegen-svelte",
Framework::Html => "codegen-html",
Framework::Flutter => "codegen-flutter",
Framework::SwiftUi => "codegen-swiftui",
Framework::Compose => "codegen-compose",
Framework::ReactNative => "codegen-react-native",
}
}
}
/// Per-chunk status. Parity with TS `ChunkStatus`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChunkStatus {
Pending,
Running,
Done,
Degraded,
Failed,
Skipped,
}
/// Top-level phase the panel renders. `Idle` = empty state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodegenPhase {
Idle,
Generating,
Complete,
Error,
}
/// One chunk's progress row for the panel (id + display name + status).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChunkProgress {
pub chunk_id: String,
pub name: String,
pub status: ChunkStatus,
}
/// Progress snapshot the pipeline produces and the panel paints. Parity
/// with the union TS `CodeGenProgress`, flattened into one struct so the
/// painter can render all three phase groups from a single value.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CodeGenProgress {
/// planning: None = not started, Some(false) = running, Some(true) = done.
pub planning_done: Option<bool>,
pub chunks: Vec<ChunkProgress>,
/// assembly: None = not started, Some(false) = running, Some(true) = done.
pub assembly_done: Option<bool>,
}
/// Lightweight asset descriptor for the "includes assets" notice. The
/// raw bytes live in op-codegen's `AssetFile`; the host maps them here.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssetMeta {
pub relative_path: String,
pub byte_len: usize,
}
/// The Code panel's full state. Mirror of `ChatState`'s role for chat.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodegenState {
pub framework: Framework,
pub phase: CodegenPhase,
pub progress: CodeGenProgress,
pub code: String,
pub degraded: bool,
pub assets: Vec<AssetMeta>,
/// Node ids the last generation ran against — to detect selection drift.
pub selection_snapshot: Vec<String>,
pub error: Option<String>,
/// Frame at which "Copied" was shown, to time the transient label.
pub copied_at: Option<u64>,
/// Set by the Code panel's Generate action; drained by the host codegen
/// session (P3). Mirror of `chat.pending_send`.
pub pending_generate: bool,
/// Set by Regenerate; drained by the host codegen session (P3).
pub pending_regenerate: bool,
}
impl Default for CodegenState {
fn default() -> Self {
Self {
framework: Framework::React,
phase: CodegenPhase::Idle,
progress: CodeGenProgress::default(),
code: String::new(),
degraded: false,
assets: Vec::new(),
selection_snapshot: Vec::new(),
error: None,
copied_at: None,
pending_generate: false,
pending_regenerate: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn framework_round_trips_its_wire_token() {
for fw in Framework::ALL {
assert_eq!(Framework::from_wire(fw.as_wire()), Some(fw));
}
assert_eq!(Framework::from_wire("react"), Some(Framework::React));
assert_eq!(
Framework::from_wire("react-native"),
Some(Framework::ReactNative)
);
assert_eq!(Framework::from_wire("nope"), None);
}
#[test]
fn codegen_state_defaults_to_idle_react() {
let s = CodegenState::default();
assert_eq!(s.framework, Framework::React);
assert_eq!(s.phase, CodegenPhase::Idle);
assert!(s.code.is_empty());
assert!(!s.degraded);
assert!(s.error.is_none());
assert!(!s.pending_generate);
assert!(!s.pending_regenerate);
}
}