//! 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). use jian_core::text_input::prev_char_boundary; /// 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::ALL.into_iter().find(|f| f.as_wire() == s) } /// Human display name (capitalized), for UI labels. TS parity. pub fn display_name(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", } } /// 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, } /// Byte-offset text selection inside the generated code preview. /// Offsets are clamped by the painter/hit-test against the currently /// visible code text, so stale ranges after regeneration are harmless. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CodeSelection { pub anchor: usize, pub focus: usize, } impl CodeSelection { pub fn ordered(self) -> (usize, usize) { if self.anchor <= self.focus { (self.anchor, self.focus) } else { (self.focus, self.anchor) } } pub fn is_collapsed(self) -> bool { self.anchor == self.focus } } /// Code panel non-framework hover target. Framework chips keep their own /// `framework_hover` because their state carries a selected framework value. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodegenHover { Generate, Regenerate, Cancel, Copy, Download, ExportBundle, ScrollFrameworksLeft, ScrollFrameworksRight, } /// 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, pub chunks: Vec, /// assembly: None = not started, Some(false) = running, Some(true) = done. pub assembly_done: Option, } /// 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. /// `PartialEq` only (not `Eq`) — scroll offsets carry `f32` values. #[derive(Debug, Clone, PartialEq)] pub struct CodegenState { pub framework: Framework, /// Horizontal scroll offset (px, ≥ 0) of the framework tab strip, so the /// single-row selector scrolls to reach off-screen frameworks (TS parity). pub framework_scroll: jian_core::scroll::ScrollState, /// The inactive framework chip the cursor is hovering, for a subtle /// background highlight. `None` when the cursor is off the strip. pub framework_hover: Option, /// Non-framework button / chevron the cursor is hovering. pub action_hover: Option, pub phase: CodegenPhase, pub progress: CodeGenProgress, pub code: String, /// Vertical scroll offset (px, >= 0) inside the generated-code preview. pub code_scroll: jian_core::scroll::ScrollState, /// Text selection inside the generated-code preview. pub code_selection: Option, pub degraded: bool, pub assets: Vec, /// Node ids the last generation ran against — to detect selection drift. pub selection_snapshot: Vec, pub error: Option, /// Frame at which "Copied" was shown, to time the transient label. pub copied_at: Option, /// 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, /// Set by the Code panel's Download action; drained by the desktop /// codegen-export drain (Task 5) which pops a save dialog + writes the /// generated code (single file, or a .zip when there are image assets). pub pending_download: bool, /// Set by Export AI Bundle; drained by the desktop codegen-export drain /// which writes a structure-bundle .zip. pub pending_export_bundle: bool, /// Set by the Code panel's Cancel action; drained by the desktop /// codegen-session cancel drain, which raises the in-flight worker's /// shared abort flag so the run actually stops (TS parity: /// `abortRef.current?.abort()`), not just the painted phase. pub pending_cancel: bool, } impl Default for CodegenState { fn default() -> Self { Self { framework: Framework::React, framework_scroll: Default::default(), framework_hover: None, action_hover: None, phase: CodegenPhase::Idle, progress: CodeGenProgress::default(), code: String::new(), code_scroll: Default::default(), code_selection: None, degraded: false, assets: Vec::new(), selection_snapshot: Vec::new(), error: None, copied_at: None, pending_generate: false, pending_regenerate: false, pending_download: false, pending_export_bundle: false, pending_cancel: false, } } } impl CodegenState { pub fn selected_code_text(&self) -> Option<&str> { let selection = self.code_selection?; if selection.is_collapsed() || self.code.is_empty() { return None; } let (start, end) = selection.ordered(); let start = prev_char_boundary(&self.code, start.min(self.code.len())); let end = prev_char_boundary(&self.code, end.min(self.code.len())); if start >= end { return None; } Some(&self.code[start..end]) } } #[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); // display_name is the capitalized UI label (TS parity). assert_eq!(Framework::React.display_name(), "React"); assert_eq!(Framework::ReactNative.display_name(), "React Native"); assert_eq!(Framework::SwiftUi.display_name(), "SwiftUI"); } #[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_eq!(s.code_scroll.offset, 0.0); assert!(!s.degraded); assert!(s.error.is_none()); assert!(!s.pending_generate); assert!(!s.pending_regenerate); assert!(!s.pending_download); assert!(!s.pending_export_bundle); assert!(!s.pending_cancel); } #[test] fn codegen_scroll_fields_use_scroll_state() { let mut s = CodegenState::default(); s.framework_scroll.offset = 16.0; s.code_scroll.offset = 32.0; assert_eq!(s.framework_scroll.offset, 16.0); assert_eq!(s.code_scroll.offset, 32.0); } #[test] fn selected_code_text_returns_non_collapsed_range() { let s = CodegenState { code: "import React\nconst n = 1".into(), code_selection: Some(CodeSelection { anchor: 0, focus: 6, }), ..CodegenState::default() }; assert_eq!(s.selected_code_text(), Some("import")); } }