fix: resolve 17 P2 defects and 5 P3 defects from pre-launch audit
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Batch fix covering multiple modules:
- P2-01: HandRegistry Semaphore-based max_concurrent enforcement
- P2-03: Populate toolCount/metricCount from Hand trait methods
- P2-06: heartbeat_update_config minimum interval validation
- P2-07: ReflectionResult used_fallback marker for rule-based fallback
- P2-08/09: identity_propose_change parameter naming consistency
- P2-10: ClassroomMetadata is_placeholder flag for LLM failure
- P2-11: classroomStore userDidCloseDuringGeneration intent tracking
- P2-12: workflowStore pipeline_create sends actionType
- P2-13/14: PipelineInfo step_count + PipelineStepInfo for proper step mapping
- P2-15: Pipe transform support in context.resolve (8 transforms)
- P2-16: Mustache {{...}} → \${...} auto-normalization
- P2-17: SaaSLogin password placeholder 6→8
- P2-19: serialize_skill_md + update_skill preserve tools field
- P2-22: ToolOutputGuard sensitive patterns from warn→block
- P2-23: Mutex::unwrap() → unwrap_or_else in relay/service.rs
- P3-01/03/07/08/09: Various P3 fixes
- DEFECT_LIST.md: comprehensive status sync (43/51 fixed, 8 remaining)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,7 @@ pub async fn classroom_generate(
|
||||
source_document: kernel_request.document.map(|_| "user_document".to_string()),
|
||||
model: None,
|
||||
version: "2.0.0".to_string(),
|
||||
is_placeholder: false, // P2-10: Tauri layer always has a driver
|
||||
custom: serde_json::Map::new(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -715,6 +715,17 @@ pub async fn heartbeat_init(
|
||||
config: Option<HeartbeatConfig>,
|
||||
state: tauri::State<'_, HeartbeatEngineState>,
|
||||
) -> Result<(), String> {
|
||||
// P2-06: Validate minimum interval (prevent busy-loop)
|
||||
const MIN_INTERVAL_MINUTES: u64 = 1;
|
||||
if let Some(ref cfg) = config {
|
||||
if cfg.interval_minutes < MIN_INTERVAL_MINUTES {
|
||||
return Err(format!(
|
||||
"interval_minutes must be >= {} (got {})",
|
||||
MIN_INTERVAL_MINUTES, cfg.interval_minutes
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let engine = HeartbeatEngine::new(agent_id.clone(), config);
|
||||
|
||||
// Restore last interaction time from VikingStorage metadata
|
||||
@@ -822,6 +833,14 @@ pub async fn heartbeat_update_config(
|
||||
config: HeartbeatConfig,
|
||||
state: tauri::State<'_, HeartbeatEngineState>,
|
||||
) -> Result<(), String> {
|
||||
// P2-06: Validate minimum interval (same as heartbeat_init)
|
||||
const MIN_INTERVAL_MINUTES: u64 = 1;
|
||||
if config.interval_minutes < MIN_INTERVAL_MINUTES {
|
||||
return Err(format!(
|
||||
"interval_minutes must be >= {} (got {})",
|
||||
MIN_INTERVAL_MINUTES, config.interval_minutes
|
||||
));
|
||||
}
|
||||
let engines = state.lock().await;
|
||||
let engine = engines
|
||||
.get(&agent_id)
|
||||
|
||||
@@ -618,21 +618,20 @@ pub async fn identity_append_user_profile(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Propose a change
|
||||
// @connected
|
||||
/// Propose an identity change for// @connected
|
||||
#[tauri::command]
|
||||
pub async fn identity_propose_change(
|
||||
agent_id: String,
|
||||
file: String,
|
||||
target: String,
|
||||
suggested_content: String,
|
||||
reason: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<IdentityChangeProposal, String> {
|
||||
let mut manager = state.lock().await;
|
||||
let file_type = match file.as_str() {
|
||||
let file_type = match target.as_str() {
|
||||
"soul" => IdentityFile::Soul,
|
||||
"instructions" => IdentityFile::Instructions,
|
||||
_ => return Err(format!("Unknown file: {}", file)),
|
||||
_ => return Err(format!("Invalid file type: '{}'. Expected 'soul' or 'instructions'", target)),
|
||||
};
|
||||
Ok(manager.propose_change(&agent_id, file_type, &suggested_content, &reason))
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ pub struct ImprovementSuggestion {
|
||||
pub priority: Priority,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Priority {
|
||||
High,
|
||||
@@ -113,6 +113,9 @@ pub struct ReflectionResult {
|
||||
pub identity_proposals: Vec<IdentityChangeProposal>,
|
||||
pub new_memories: usize,
|
||||
pub timestamp: String,
|
||||
/// P2-07: Whether rules-based fallback was used instead of LLM
|
||||
#[serde(default)]
|
||||
pub used_fallback: bool,
|
||||
}
|
||||
|
||||
/// Reflection state
|
||||
@@ -197,6 +200,8 @@ impl ReflectionEngine {
|
||||
memories: &[MemoryEntryForAnalysis],
|
||||
driver: Option<Arc<dyn LlmDriver>>,
|
||||
) -> ReflectionResult {
|
||||
// P2-07: Track whether rules-based fallback was used
|
||||
let mut used_fallback = !self.config.use_llm;
|
||||
// 1. Analyze memory patterns (LLM if configured, rules fallback)
|
||||
let patterns = if self.config.use_llm {
|
||||
if let Some(ref llm) = driver {
|
||||
@@ -204,6 +209,7 @@ impl ReflectionEngine {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::warn!("[reflection] LLM analysis failed, falling back to rules: {}", e);
|
||||
used_fallback = true;
|
||||
if self.config.llm_fallback_to_rules {
|
||||
self.analyze_patterns(memories)
|
||||
} else {
|
||||
@@ -213,6 +219,7 @@ impl ReflectionEngine {
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("[reflection] use_llm=true but no driver available, using rules");
|
||||
used_fallback = true;
|
||||
self.analyze_patterns(memories)
|
||||
}
|
||||
} else {
|
||||
@@ -229,13 +236,15 @@ impl ReflectionEngine {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// 4. Count new memories that would be saved
|
||||
// 4. Count new memories (would be saved)
|
||||
// Include LLM-generated patterns and high-priority improvements
|
||||
let new_memories = patterns.iter()
|
||||
.filter(|p| p.frequency >= 3)
|
||||
.filter(|p| p.frequency >= 1 || p.frequency >= 2)
|
||||
.count()
|
||||
+ improvements.iter()
|
||||
.filter(|i| matches!(i.priority, Priority::High))
|
||||
.count();
|
||||
// Include all LLM-proposed improvements
|
||||
|
||||
// 5. Build result
|
||||
let result = ReflectionResult {
|
||||
@@ -244,6 +253,7 @@ impl ReflectionEngine {
|
||||
identity_proposals,
|
||||
new_memories,
|
||||
timestamp: Utc::now().to_rfc3339(),
|
||||
used_fallback, // P2-07: expose fallback status to callers
|
||||
};
|
||||
|
||||
// 6. Update state
|
||||
|
||||
@@ -79,7 +79,7 @@ impl From<zclaw_hands::HandConfig> for HandInfoResponse {
|
||||
enabled: config.enabled,
|
||||
category,
|
||||
icon,
|
||||
tool_count: 0,
|
||||
tool_count: 0, // P2-03: TODO — populated from hand execution metadata
|
||||
metric_count: 0,
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,18 @@ pub async fn hand_list(
|
||||
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
|
||||
|
||||
let hands = kernel.list_hands().await;
|
||||
Ok(hands.into_iter().map(HandInfoResponse::from).collect())
|
||||
let registry = kernel.hands();
|
||||
|
||||
// P2-03: Populate tool_count/metric_count from actual Hand instances
|
||||
let mut results = Vec::new();
|
||||
for config in hands {
|
||||
let (tool_count, metric_count) = registry.get_counts(&config.id).await;
|
||||
let mut info = HandInfoResponse::from(config);
|
||||
info.tool_count = tool_count;
|
||||
info.metric_count = metric_count;
|
||||
results.push(info);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Execute a hand
|
||||
|
||||
@@ -172,6 +172,7 @@ pub async fn skill_create(
|
||||
tags: vec![],
|
||||
category: None,
|
||||
triggers: request.triggers,
|
||||
tools: vec![], // P2-19: Include tools field
|
||||
enabled: request.enabled.unwrap_or(true),
|
||||
};
|
||||
|
||||
@@ -217,6 +218,7 @@ pub async fn skill_update(
|
||||
tags: existing.tags.clone(),
|
||||
category: existing.category.clone(),
|
||||
triggers: request.triggers.unwrap_or(existing.triggers),
|
||||
tools: existing.tools.clone(), // P2-19: Preserve tools on update
|
||||
enabled: request.enabled.unwrap_or(existing.enabled),
|
||||
};
|
||||
|
||||
|
||||
@@ -40,10 +40,18 @@ pub struct UpdatePipelineRequest {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkflowStepInput {
|
||||
/// Action type discriminator (P2-12: enables non-Hand action types)
|
||||
pub action_type: Option<String>,
|
||||
pub hand_name: String,
|
||||
pub name: Option<String>,
|
||||
pub params: Option<HashMap<String, Value>>,
|
||||
pub condition: Option<String>,
|
||||
/// LLM generation template (for action_type = "llm_generate")
|
||||
pub template: Option<String>,
|
||||
/// Parallel collection path (for action_type = "parallel")
|
||||
pub each: Option<String>,
|
||||
/// Condition branches (for action_type = "condition")
|
||||
pub branches: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
/// Create a new pipeline as a YAML file
|
||||
@@ -74,18 +82,57 @@ pub async fn pipeline_create(
|
||||
return Err(format!("Pipeline file already exists: {}", file_path.display()));
|
||||
}
|
||||
|
||||
// Build Pipeline struct
|
||||
// P2-12: Build PipelineSteps with proper action type from WorkflowStepInput
|
||||
let steps: Vec<PipelineStep> = request.steps.into_iter().enumerate().map(|(i, s)| {
|
||||
let step_id = s.name.clone().unwrap_or_else(|| format!("step-{}", i + 1));
|
||||
PipelineStep {
|
||||
id: step_id,
|
||||
action: Action::Hand {
|
||||
let params_map: HashMap<String, String> = s.params
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.to_string()))
|
||||
.collect();
|
||||
|
||||
let action = match s.action_type.as_deref().unwrap_or("hand") {
|
||||
"llm_generate" => Action::LlmGenerate {
|
||||
template: s.template.unwrap_or_default(),
|
||||
input: params_map,
|
||||
model: None,
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
json_mode: false,
|
||||
},
|
||||
"parallel" => Action::Parallel {
|
||||
each: s.each.unwrap_or_else(|| "item".to_string()),
|
||||
step: Box::new(PipelineStep {
|
||||
id: format!("{}-body", step_id),
|
||||
action: Action::Hand {
|
||||
hand_id: s.hand_name.clone(),
|
||||
hand_action: "execute".to_string(),
|
||||
params: params_map,
|
||||
},
|
||||
description: None,
|
||||
when: None,
|
||||
retry: None,
|
||||
timeout_secs: None,
|
||||
}),
|
||||
max_workers: None,
|
||||
},
|
||||
"condition" => Action::Condition {
|
||||
condition: s.condition.unwrap_or_default(),
|
||||
branches: vec![],
|
||||
default: None,
|
||||
},
|
||||
_ => Action::Hand {
|
||||
hand_id: s.hand_name.clone(),
|
||||
hand_action: "execute".to_string(),
|
||||
params: s.params.unwrap_or_default().into_iter().map(|(k, v)| (k, v.to_string())).collect(),
|
||||
params: params_map,
|
||||
},
|
||||
};
|
||||
|
||||
PipelineStep {
|
||||
id: step_id,
|
||||
action,
|
||||
description: s.name,
|
||||
when: s.condition,
|
||||
when: None,
|
||||
retry: None,
|
||||
timeout_secs: None,
|
||||
}
|
||||
@@ -156,18 +203,58 @@ pub async fn pipeline_update(
|
||||
..existing.metadata.clone()
|
||||
};
|
||||
|
||||
// P2-12: Build PipelineSteps with proper action type (mirrors pipeline_create logic)
|
||||
let updated_steps = match request.steps {
|
||||
Some(steps) => steps.into_iter().enumerate().map(|(i, s)| {
|
||||
let step_id = s.name.clone().unwrap_or_else(|| format!("step-{}", i + 1));
|
||||
PipelineStep {
|
||||
id: step_id,
|
||||
action: Action::Hand {
|
||||
let params_map: HashMap<String, String> = s.params
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.to_string()))
|
||||
.collect();
|
||||
|
||||
let action = match s.action_type.as_deref().unwrap_or("hand") {
|
||||
"llm_generate" => Action::LlmGenerate {
|
||||
template: s.template.unwrap_or_default(),
|
||||
input: params_map,
|
||||
model: None,
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
json_mode: false,
|
||||
},
|
||||
"parallel" => Action::Parallel {
|
||||
each: s.each.unwrap_or_else(|| "item".to_string()),
|
||||
step: Box::new(PipelineStep {
|
||||
id: format!("{}-body", step_id),
|
||||
action: Action::Hand {
|
||||
hand_id: s.hand_name.clone(),
|
||||
hand_action: "execute".to_string(),
|
||||
params: params_map,
|
||||
},
|
||||
description: None,
|
||||
when: None,
|
||||
retry: None,
|
||||
timeout_secs: None,
|
||||
}),
|
||||
max_workers: None,
|
||||
},
|
||||
"condition" => Action::Condition {
|
||||
condition: s.condition.unwrap_or_default(),
|
||||
branches: vec![],
|
||||
default: None,
|
||||
},
|
||||
_ => Action::Hand {
|
||||
hand_id: s.hand_name.clone(),
|
||||
hand_action: "execute".to_string(),
|
||||
params: s.params.unwrap_or_default().into_iter().map(|(k, v)| (k, v.to_string())).collect(),
|
||||
params: params_map,
|
||||
},
|
||||
};
|
||||
|
||||
PipelineStep {
|
||||
id: step_id,
|
||||
action,
|
||||
description: s.name,
|
||||
when: s.condition,
|
||||
when: None,
|
||||
retry: None,
|
||||
timeout_secs: None,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use tauri::{AppHandle, Emitter, State};
|
||||
use zclaw_pipeline::{
|
||||
RunStatus,
|
||||
parse_pipeline_yaml,
|
||||
parse_pipeline_v2_yaml,
|
||||
PipelineExecutor,
|
||||
ActionRegistry,
|
||||
LlmActionDriver,
|
||||
@@ -16,7 +15,7 @@ use zclaw_pipeline::{
|
||||
|
||||
use super::{PipelineState, PipelineInfo, PipelineRunResponse, RunPipelineResponse, RunPipelineRequest};
|
||||
use super::adapters::{RuntimeLlmAdapter, PipelineSkillDriver, PipelineHandDriver};
|
||||
use super::helpers::{get_pipelines_directory, scan_pipelines_with_paths, scan_pipelines_full_sync, pipeline_to_info, pipeline_v2_to_info};
|
||||
use super::helpers::{get_pipelines_directory, scan_pipelines_with_paths, scan_pipelines_full_sync, pipeline_to_info};
|
||||
|
||||
use crate::kernel_commands::KernelState;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use zclaw_pipeline::{
|
||||
PipelineV2,
|
||||
};
|
||||
|
||||
use super::types::{PipelineInfo, PipelineInputInfo};
|
||||
use super::types::{PipelineInfo, PipelineInputInfo, PipelineStepInfo};
|
||||
|
||||
pub(crate) fn get_pipelines_directory() -> Result<PathBuf, String> {
|
||||
// Try to find pipelines directory
|
||||
@@ -169,6 +169,32 @@ pub(crate) fn pipeline_to_info(pipeline: &Pipeline) -> PipelineInfo {
|
||||
icon: pipeline.metadata.icon.clone().unwrap_or_else(|| "📦".to_string()),
|
||||
version: pipeline.metadata.version.clone(),
|
||||
author: pipeline.metadata.author.clone().unwrap_or_default(),
|
||||
// P2-13: Expose step count from actual pipeline spec
|
||||
step_count: pipeline.spec.steps.len(),
|
||||
// P2-14: Expose actual pipeline steps
|
||||
steps: pipeline.spec.steps.iter().map(|step| {
|
||||
use zclaw_pipeline::Action;
|
||||
let (action_type, hand_name) = match &step.action {
|
||||
Action::LlmGenerate { .. } => ("llm_generate".to_string(), None),
|
||||
Action::Parallel { .. } => ("parallel".to_string(), None),
|
||||
Action::Condition { .. } => ("condition".to_string(), None),
|
||||
Action::Hand { hand_id, .. } => ("hand".to_string(), Some(hand_id.clone())),
|
||||
Action::Skill { skill_id, .. } => ("skill".to_string(), Some(skill_id.clone())),
|
||||
Action::ClassroomRender { .. } => ("classroom_render".to_string(), None),
|
||||
Action::Sequential { .. } => ("sequential".to_string(), None),
|
||||
Action::FileExport { .. } => ("file_export".to_string(), None),
|
||||
Action::HttpRequest { .. } => ("http_request".to_string(), None),
|
||||
Action::SetVar { .. } => ("set_var".to_string(), None),
|
||||
Action::Delay { .. } => ("delay".to_string(), None),
|
||||
Action::SkillOrchestration { .. } => ("skill_orchestration".to_string(), None),
|
||||
};
|
||||
PipelineStepInfo {
|
||||
name: step.id.clone(),
|
||||
action_type,
|
||||
hand_name,
|
||||
condition: step.when.clone(),
|
||||
}
|
||||
}).collect(),
|
||||
inputs: pipeline.spec.inputs.iter().map(|input| {
|
||||
PipelineInputInfo {
|
||||
name: input.name.clone(),
|
||||
@@ -225,5 +251,8 @@ pub(crate) fn pipeline_v2_to_info(v2: &PipelineV2) -> PipelineInfo {
|
||||
options: param.options.clone(),
|
||||
}
|
||||
}).collect(),
|
||||
// V2 pipelines don't have steps in the same format
|
||||
step_count: 0,
|
||||
steps: vec![],
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,22 @@ pub struct PipelineInfo {
|
||||
pub author: String,
|
||||
/// Input parameters
|
||||
pub inputs: Vec<PipelineInputInfo>,
|
||||
/// P2-13: Step count (was missing, causing frontend to show 0)
|
||||
#[serde(default)]
|
||||
pub step_count: usize,
|
||||
/// P2-14: Actual pipeline steps (populated in pipeline_get detail view)
|
||||
#[serde(default)]
|
||||
pub steps: Vec<PipelineStepInfo>,
|
||||
}
|
||||
|
||||
/// P2-14: Pipeline step info for detail view
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipelineStepInfo {
|
||||
pub name: String,
|
||||
pub action_type: String,
|
||||
pub hand_name: Option<String>,
|
||||
pub condition: Option<String>,
|
||||
}
|
||||
|
||||
/// Pipeline input parameter info
|
||||
|
||||
@@ -55,8 +55,8 @@ export function SaaSLogin({ onLogin, onLoginWithTotp, onRegister, initialUrl, is
|
||||
setLocalError('邮箱格式不正确');
|
||||
return;
|
||||
}
|
||||
if (password.length < 6) {
|
||||
setLocalError('密码长度至少 6 个字符');
|
||||
if (password.length < 8) {
|
||||
setLocalError('密码长度至少 8 个字符');
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
@@ -322,7 +322,7 @@ export function SaaSLogin({ onLogin, onLoginWithTotp, onRegister, initialUrl, is
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={isRegister ? '至少 6 个字符' : 'Enter password'}
|
||||
placeholder={isRegister ? '至少 8 个字符' : 'Enter password'}
|
||||
autoComplete={isRegister ? 'new-password' : 'current-password'}
|
||||
className="w-full px-3 pr-10 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 bg-white text-gray-900"
|
||||
disabled={isLoggingIn}
|
||||
|
||||
@@ -59,6 +59,8 @@ export interface ClassroomState {
|
||||
activeClassroom: Classroom | null;
|
||||
/** Whether the ClassroomPlayer overlay is open */
|
||||
classroomOpen: boolean;
|
||||
/** P2-11: Tracks if user explicitly closed player during generation */
|
||||
userDidCloseDuringGeneration: boolean;
|
||||
/** Chat messages for the active classroom */
|
||||
chatMessages: ClassroomChatMessage[];
|
||||
/** Whether chat is loading */
|
||||
@@ -93,6 +95,7 @@ export const useClassroomStore = create<ClassroomStore>()((set, get) => ({
|
||||
generatingTopic: null,
|
||||
activeClassroom: null,
|
||||
classroomOpen: false,
|
||||
userDidCloseDuringGeneration: false,
|
||||
chatMessages: [],
|
||||
chatLoading: false,
|
||||
error: null,
|
||||
@@ -105,6 +108,7 @@ export const useClassroomStore = create<ClassroomStore>()((set, get) => ({
|
||||
progressActivity: 'Starting generation...',
|
||||
generatingTopic: request.topic,
|
||||
error: null,
|
||||
userDidCloseDuringGeneration: false,
|
||||
});
|
||||
|
||||
// Listen for progress events from Rust
|
||||
@@ -121,7 +125,10 @@ export const useClassroomStore = create<ClassroomStore>()((set, get) => ({
|
||||
const result = await invoke<GenerationResult>('classroom_generate', { request });
|
||||
set({ generating: false });
|
||||
await get().loadClassroom(result.classroomId);
|
||||
set({ classroomOpen: true });
|
||||
// P2-11: Only auto-open if user hasn't explicitly closed during generation
|
||||
if (!get().userDidCloseDuringGeneration) {
|
||||
set({ classroomOpen: true });
|
||||
}
|
||||
return result.classroomId;
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
@@ -161,7 +168,11 @@ export const useClassroomStore = create<ClassroomStore>()((set, get) => ({
|
||||
},
|
||||
|
||||
closeClassroom: () => {
|
||||
set({ classroomOpen: false });
|
||||
set({
|
||||
classroomOpen: false,
|
||||
// P2-11: Track explicit user close during generation
|
||||
userDidCloseDuringGeneration: get().generating,
|
||||
});
|
||||
},
|
||||
|
||||
sendChatMessage: async (message, sceneContext) => {
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface WorkflowStep {
|
||||
name?: string;
|
||||
params?: Record<string, unknown>;
|
||||
condition?: string;
|
||||
/** P2-12: Action type for pipeline step (hand, llm_generate, parallel, condition) */
|
||||
actionType?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowDetail {
|
||||
@@ -341,6 +343,8 @@ interface PipelineInfo {
|
||||
icon: string;
|
||||
version: string;
|
||||
author: string;
|
||||
/** P2-13: Step count from backend */
|
||||
stepCount?: number;
|
||||
inputs: Array<{
|
||||
name: string;
|
||||
inputType: string;
|
||||
@@ -386,7 +390,7 @@ function createWorkflowClientFromKernel(_client: KernelClient): WorkflowClient {
|
||||
workflows: pipelines.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.displayName || p.id,
|
||||
steps: p.inputs.length,
|
||||
steps: p.stepCount ?? p.inputs?.length ?? 0, // P2-13: Use stepCount from backend
|
||||
description: p.description,
|
||||
createdAt: undefined,
|
||||
})),
|
||||
@@ -424,6 +428,7 @@ function createWorkflowClientFromKernel(_client: KernelClient): WorkflowClient {
|
||||
name: s.name || `Step ${i + 1}`,
|
||||
params: s.params,
|
||||
condition: s.condition,
|
||||
actionType: s.actionType, // P2-12: Send actionType to backend
|
||||
})),
|
||||
},
|
||||
});
|
||||
@@ -444,6 +449,7 @@ function createWorkflowClientFromKernel(_client: KernelClient): WorkflowClient {
|
||||
name: s.name || `Step ${i + 1}`,
|
||||
params: s.params,
|
||||
condition: s.condition,
|
||||
actionType: s.actionType, // P2-12: Send actionType to backend
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user