skill_web/pages/
run.rs

1//! Run page - Simplified execution workflow
2//!
3//! Clean, single-page interface with:
4//! - Simple dropdown selection for Skill and Tool
5//! - Dynamic parameter form
6//! - Immediate execution feedback
7
8use std::collections::HashMap;
9use std::rc::Rc;
10use wasm_bindgen_futures::spawn_local;
11use yew::prelude::*;
12use yewdux::prelude::*;
13
14use crate::api::{Api, ExecutionResponse, SkillDetail};
15use crate::components::run::{InlineParameterEditor, TerminalOutput};
16use crate::components::notifications::use_notifications;
17use crate::store::skills::{SkillsAction, SkillsStore};
18use crate::components::SearchableSelect;
19
20/// Run page props (for deep linking)
21#[derive(Properties, PartialEq)]
22pub struct RunPageProps {
23    #[prop_or_default]
24    pub selected_skill: Option<String>,
25    #[prop_or_default]
26    pub selected_tool: Option<String>,
27}
28
29/// Run page component
30#[function_component(RunPage)]
31pub fn run_page(props: &RunPageProps) -> Html {
32    // Store for skills list
33    let skills_store = use_store_value::<SkillsStore>();
34    let skills_dispatch = use_dispatch::<SkillsStore>();
35
36    // Form state
37    let selected_skill = use_state(|| props.selected_skill.clone());
38    let selected_tool = use_state(|| props.selected_tool.clone());
39    let selected_instance = use_state(|| None::<String>);
40    let parameters = use_state(HashMap::<String, serde_json::Value>::new);
41    let validation_errors = use_state(HashMap::<String, String>::new);
42
43    // All skill details (for tool lookup)
44    let all_skill_details = use_state(|| Vec::<SkillDetail>::new());
45    let current_skill_detail = use_state(|| None::<SkillDetail>);
46    let skills_loading = use_state(|| true);
47
48    // Execution state
49    let execution_result = use_state(|| None::<ExecutionResponse>);
50    let is_executing = use_state(|| false);
51    let terminal_visible = use_state(|| false);
52    let terminal_minimized = use_state(|| false);
53    
54    // UI Refs
55    let result_ref = use_node_ref();
56
57    // API client
58    let api = use_memo((), |_| Rc::new(Api::new()));
59
60    // Notifications
61    let notifications = use_notifications();
62
63    // Sync state with props (Deep Linking)
64    {
65        let selected_skill = selected_skill.clone();
66        let selected_tool = selected_tool.clone();
67        use_effect_with((props.selected_skill.clone(), props.selected_tool.clone()), move |(p_skill, p_tool)| {
68            if let Some(s) = p_skill {
69                selected_skill.set(Some(s.clone()));
70            }
71            if let Some(t) = p_tool {
72                selected_tool.set(Some(t.clone()));
73            }
74            || ()
75        });
76    }
77
78    // Load all skill details on mount
79    {
80        let api = api.clone();
81        let skills_dispatch = skills_dispatch.clone();
82        let all_skill_details = all_skill_details.clone();
83        let skills_loading = skills_loading.clone();
84
85        use_effect_with((), move |_| {
86            skills_dispatch.apply(SkillsAction::SetLoading(true));
87            skills_loading.set(true);
88
89            let api = api.clone();
90            let skills_dispatch = skills_dispatch.clone();
91            let all_skill_details = all_skill_details.clone();
92            let skills_loading = skills_loading.clone();
93
94            spawn_local(async move {
95                match api.skills.list_all().await {
96                    Ok(skills) => {
97                        let store_skills: Vec<crate::store::skills::SkillSummary> = skills
98                            .iter()
99                            .map(|s| crate::store::skills::SkillSummary {
100                                name: s.name.clone(),
101                                version: s.version.clone(),
102                                description: s.description.clone(),
103                                source: s.source.clone(),
104                                runtime: match s.runtime.as_str() {
105                                    "docker" => crate::store::skills::SkillRuntime::Docker,
106                                    "native" => crate::store::skills::SkillRuntime::Native,
107                                    _ => crate::store::skills::SkillRuntime::Wasm,
108                                },
109                                tools_count: s.tools_count,
110                                instances_count: s.instances_count,
111                                status: crate::store::skills::SkillStatus::Configured,
112                                last_used: s.last_used.clone(),
113                                execution_count: s.execution_count,
114                            })
115                            .collect();
116
117                        skills_dispatch.apply(SkillsAction::SetSkills(store_skills));
118
119                        let mut details = Vec::new();
120                        for skill in skills.iter() {
121                            match api.skills.get(&skill.name).await {
122                                Ok(detail) => details.push(detail),
123                                Err(e) => {
124                                    web_sys::console::error_1(&format!("Failed to load skill {}: {}", skill.name, e).into());
125                                }
126                            }
127                        }
128                        all_skill_details.set(details);
129                        skills_loading.set(false);
130                    }
131                    Err(e) => {
132                        web_sys::console::error_1(&format!("Failed to load skills: {}", e).into());
133                    }
134                }
135                skills_dispatch.apply(SkillsAction::SetLoading(false));
136            });
137            || ()
138        });
139    }
140
141    // Auto-scroll to result when it arrives
142    {
143        let execution_result = execution_result.clone();
144        let result_ref = result_ref.clone();
145        use_effect_with(execution_result, move |result| {
146            if result.is_some() {
147                 if let Some(element) = result_ref.cast::<web_sys::Element>() {
148                    element.scroll_into_view_with_scroll_into_view_options(
149                        web_sys::ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth)
150                    );
151                }
152            }
153            || ()
154        });
155    }
156
157    // Update current skill detail when selection changes
158    {
159        let selected_skill = selected_skill.clone();
160        let all_skill_details = all_skill_details.clone();
161        let current_skill_detail = current_skill_detail.clone();
162        let selected_tool = selected_tool.clone();
163        let parameters = parameters.clone();
164
165        use_effect_with((*selected_skill).clone(), move |skill_name| {
166            if let Some(name) = skill_name {
167                let detail = (*all_skill_details).iter()
168                    .find(|d| d.summary.name == *name)
169                    .cloned();
170                current_skill_detail.set(detail);
171                // Reset tool and params when skill changes
172                selected_tool.set(None);
173                parameters.set(HashMap::new());
174            } else {
175                current_skill_detail.set(None);
176                selected_tool.set(None);
177            }
178            || ()
179        });
180    }
181
182    // Handle parameter changes
183    let on_parameter_change = {
184        let parameters = parameters.clone();
185        Callback::from(move |(name, value): (String, serde_json::Value)| {
186            let mut params = (*parameters).clone();
187            params.insert(name, value);
188            parameters.set(params);
189        })
190    };
191
192    // Execute command
193    let on_execute = {
194        let api = api.clone();
195        let selected_skill = selected_skill.clone();
196        let selected_tool = selected_tool.clone();
197        let selected_instance = selected_instance.clone();
198        let parameters = parameters.clone();
199        let is_executing = is_executing.clone();
200        let execution_result = execution_result.clone();
201        let notifications = notifications.clone();
202
203        Callback::from(move |e: MouseEvent| {
204            e.prevent_default();
205            let skill = (*selected_skill).clone();
206            let tool = (*selected_tool).clone();
207
208            if let (Some(skill_name), Some(tool_name)) = (skill, tool) {
209                is_executing.set(true);
210
211                let api = api.clone();
212                let parameters = (*parameters).clone();
213                let instance = (*selected_instance).clone();
214                let is_executing = is_executing.clone();
215                let execution_result = execution_result.clone();
216                let notifications = notifications.clone();
217
218                spawn_local(async move {
219                    let request = crate::api::ExecutionRequest {
220                        skill: skill_name.clone(),
221                        tool: tool_name.clone(),
222                        instance,
223                        args: parameters,
224                        stream: false,
225                        timeout_secs: None,
226                    };
227
228                    match api.executions.execute(&request).await {
229                        Ok(result) => {
230                            let duration = result.duration_ms;
231                            execution_result.set(Some(result));
232
233                            // Show success toast
234                            notifications.success(
235                                "Execution completed",
236                                format!("{}/{} executed successfully in {}ms", skill_name, tool_name, duration)
237                            );
238                        }
239                        Err(e) => {
240                            let error_msg = format!("Failed to execute {}/{}: {}", skill_name, tool_name, e);
241                            web_sys::console::error_1(&error_msg.clone().into());
242                            notifications.error("Execution failed", error_msg);
243                        }
244                    }
245                    is_executing.set(false);
246                });
247            }
248        })
249    };
250
251    // Close terminal
252    let on_terminal_close = {
253        let terminal_visible = terminal_visible.clone();
254        Callback::from(move |_| {
255            terminal_visible.set(false);
256        })
257    };
258
259    // Toggle terminal minimize
260    let on_terminal_toggle_minimize = {
261        let terminal_minimized = terminal_minimized.clone();
262        Callback::from(move |_| {
263            terminal_minimized.set(!*terminal_minimized);
264        })
265    };
266
267    // Re-run command
268    let on_rerun = {
269        let api = api.clone();
270        let selected_skill = selected_skill.clone();
271        let selected_tool = selected_tool.clone();
272        let selected_instance = selected_instance.clone();
273        let parameters = parameters.clone();
274        let is_executing = is_executing.clone();
275        let execution_result = execution_result.clone();
276        let notifications = notifications.clone();
277
278        Some(Callback::from(move |_: ()| {
279            let skill = (*selected_skill).clone();
280            let tool = (*selected_tool).clone();
281
282            if let (Some(skill_name), Some(tool_name)) = (skill, tool) {
283                is_executing.set(true);
284
285                let api = api.clone();
286                let parameters = (*parameters).clone();
287                let instance = (*selected_instance).clone();
288                let is_executing = is_executing.clone();
289                let execution_result = execution_result.clone();
290                let notifications = notifications.clone();
291
292                spawn_local(async move {
293                    let request = crate::api::ExecutionRequest {
294                        skill: skill_name.clone(),
295                        tool: tool_name.clone(),
296                        instance,
297                        args: parameters,
298                        stream: false,
299                        timeout_secs: None,
300                    };
301
302                    match api.executions.execute(&request).await {
303                        Ok(result) => {
304                            let duration = result.duration_ms;
305                            execution_result.set(Some(result));
306
307                            notifications.success(
308                                "Execution completed",
309                                format!("{}/{} executed successfully in {}ms", skill_name, tool_name, duration)
310                            );
311                        }
312                        Err(e) => {
313                            let error_msg = format!("Failed to execute {}/{}: {}", skill_name, tool_name, e);
314                            web_sys::console::error_1(&error_msg.clone().into());
315                            notifications.error("Execution failed", error_msg);
316                        }
317                    }
318                    is_executing.set(false);
319                });
320            }
321        }))
322    };
323
324    // Get current tool parameters
325    let current_tool_params = current_skill_detail.as_ref()
326        .and_then(|detail| {
327            detail.tools.iter()
328                .find(|t| Some(&t.name) == selected_tool.as_ref())
329        })
330        .map(|tool| tool.parameters.clone())
331        .unwrap_or_default();
332
333    // Get available tools for selected skill
334    let available_tools = current_skill_detail.as_ref()
335        .map(|detail| detail.tools.clone())
336        .unwrap_or_default();
337
338    // Check if form is complete
339    let can_execute = selected_skill.is_some()
340        && selected_tool.is_some()
341        && !*is_executing;
342
343    // Helper to handle select changes
344    let on_skill_change = {
345        let selected_skill = selected_skill.clone();
346        Callback::from(move |value: String| {
347            if value.is_empty() {
348                selected_skill.set(None);
349            } else {
350                selected_skill.set(Some(value));
351            }
352        })
353    };
354
355    let on_tool_change = {
356        let selected_tool = selected_tool.clone();
357        let parameters = parameters.clone();
358        Callback::from(move |value: String| {
359            if value.is_empty() {
360                selected_tool.set(None);
361            } else {
362                selected_tool.set(Some(value));
363                parameters.set(HashMap::new()); // Reset params when tool changes
364            }
365        })
366    };
367
368    html! {
369        <div class="h-full flex flex-col bg-gray-50 dark:bg-gray-900 overflow-hidden">
370            // Header
371            <div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4 shrink-0">
372                <div class="max-w-5xl mx-auto flex items-center justify-between">
373                    <div>
374                        <h1 class="text-xl font-bold text-gray-900 dark:text-white">
375                            { "Run Skill" }
376                        </h1>
377                        <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
378                            { "Execute skills and tools with a simple dynamic form" }
379                        </p>
380                    </div>
381                </div>
382            </div>
383
384            // Main Content
385            <div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
386                <div class="max-w-5xl mx-auto space-y-6">
387
388                    // Selection Card
389                    <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
390                        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
391                            // Skill Select
392                            <div class="space-y-2">
393                                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
394                                    { "Select Skill" }
395                                </label>
396                                <div class="relative">
397                                    <SearchableSelect
398                                        options={skills_store.skills.iter().map(|s| s.name.clone()).collect::<Vec<_>>()}
399                                        selected={selected_skill.as_deref().map(|s| s.to_string())}
400                                        on_select={on_skill_change}
401                                        placeholder="Choose a skill..."
402                                        loading={*skills_loading}
403                                    />
404                                </div>
405                                if let Some(_skill_name) = selected_skill.as_ref() {
406                                    if let Some(detail) = current_skill_detail.as_ref() {
407                                         <p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
408                                            { &detail.summary.description }
409                                        </p>
410                                    }
411                                }
412                            </div>
413
414                            // Tool Select
415                            <div class="space-y-2">
416                                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
417                                    { "Select Tool" }
418                                </label>
419                                <SearchableSelect
420                                    options={available_tools.iter().map(|t| t.name.clone()).collect::<Vec<_>>()}
421                                    selected={selected_tool.as_deref().map(|s| s.to_string())}
422                                    on_select={on_tool_change}
423                                    placeholder="Choose a tool..."
424                                    disabled={selected_skill.is_none()}
425                                />
426                                if let Some(tool_name) = selected_tool.as_ref() {
427                                    if let Some(tool_detail) = available_tools.iter().find(|t| &t.name == tool_name) {
428                                        <p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
429                                            { &tool_detail.description }
430                                        </p>
431                                    }
432                                }
433                            </div>
434                        </div>
435                    </div>
436
437                    // Parameters & Execution
438                    <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
439                         <div class="p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50 flex items-center gap-2">
440                            <svg class="w-4 h-4 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
441                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
442                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
443                            </svg>
444                            <h3 class="font-medium text-gray-900 dark:text-white">
445                                { "Configuration" }
446                            </h3>
447                        </div>
448
449                        <div class="p-6">
450                            if selected_skill.is_some() && selected_tool.is_some() {
451                                if !current_tool_params.is_empty() {
452                                    <InlineParameterEditor
453                                        parameters={current_tool_params}
454                                        values={(*parameters).clone()}
455                                        on_change={on_parameter_change}
456                                        errors={(*validation_errors).clone()}
457                                    />
458                                } else {
459                                    <div class="text-center py-8 text-gray-500 dark:text-gray-400 text-sm">
460                                        { "No parameters required for this tool." }
461                                    </div>
462                                }
463
464                                <div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 flex justify-end">
465                                    <button
466                                        class={classes!(
467                                            "btn",
468                                            "btn-primary",
469                                            "px-6",
470                                            "py-2.5",
471                                            "rounded-lg",
472                                            "shadow-sm",
473                                            "flex",
474                                            "items-center",
475                                            "gap-2",
476                                            (!can_execute).then(|| "opacity-50 cursor-not-allowed")
477                                        )}
478                                        onclick={on_execute}
479                                        disabled={!can_execute}
480                                    >
481                                        if *is_executing {
482                                            <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
483                                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
484                                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
485                                            </svg>
486                                            { "Executing..." }
487                                        } else {
488                                            <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
489                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
490                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
491                                            </svg>
492                                            { "Run Command" }
493                                        }
494                                    </button>
495                                </div>
496                            } else {
497                                <div class="text-center py-12 text-gray-400 dark:text-gray-500">
498                                    <svg class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
499                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
500                                    </svg>
501                                    <p class="text-sm">
502                                        { "Select a skill and tool to configure parameters" }
503                                    </p>
504                                </div>
505                            }
506                        </div>
507                    </div>
508
509                    // Execution Result (Always visible placeholder)
510                    <div ref={result_ref} class="card bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
511                         <div class="p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50 flex items-center justify-between">
512                            <div class="flex items-center gap-2">
513                                <svg class="w-4 h-4 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
514                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
515                                </svg>
516                                <h3 class="font-medium text-gray-900 dark:text-white">
517                                    { "Execution Result" }
518                                </h3>
519                            </div>
520                            if let Some(result) = &*execution_result {
521                                <div class="text-xs font-mono text-gray-400 dark:text-gray-500">
522                                    { format!("ID: {}", &result.id) }
523                                </div>
524                            }
525                        </div>
526
527                        <div class="p-6">
528                            if let Some(result) = &*execution_result {
529                                <div class="border-l-4 border-l-primary-500 pl-4 py-2">
530                                     // Status header
531                                    <div class="flex items-center justify-between mb-4">
532                                        <div class="flex items-center gap-3">
533                                            if result.status == crate::api::types::ExecutionStatus::Success {
534                                                <div class="p-1.5 rounded-full bg-success-100 dark:bg-success-900/30 text-success-600 dark:text-success-400">
535                                                    <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
536                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
537                                                    </svg>
538                                                </div>
539                                                <div>
540                                                    <h4 class="font-semibold text-gray-900 dark:text-white">
541                                                        { "Execution Successful" }
542                                                    </h4>
543                                                    <p class="text-xs text-gray-500 dark:text-gray-400">
544                                                        { format!("Completed in {}ms", result.duration_ms) }
545                                                    </p>
546                                                </div>
547                                            } else {
548                                                <div class="p-1.5 rounded-full bg-error-100 dark:bg-error-900/30 text-error-600 dark:text-error-400">
549                                                    <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
550                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
551                                                    </svg>
552                                                </div>
553                                                <div>
554                                                    <h4 class="font-semibold text-gray-900 dark:text-white">
555                                                        { "Execution Failed" }
556                                                    </h4>
557                                                    <div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
558                                                        <span class="font-mono text-error-600 dark:text-error-400 bg-error-50 dark:bg-error-900/20 px-1.5 py-0.5 rounded">
559                                                            { format!("{:?}", result.status) }
560                                                        </span>
561                                                        <span>{ format!("({}ms)", result.duration_ms) }</span>
562                                                    </div>
563                                                </div>
564                                            }
565                                        </div>
566                                        <div class="flex gap-2">
567                                             <button
568                                                class="btn btn-secondary text-sm"
569                                                onclick={{
570                                                    let terminal_visible = terminal_visible.clone();
571                                                    Callback::from(move |_| {
572                                                        terminal_visible.set(true);
573                                                    })
574                                                }}
575                                            >
576                                                { "Show Terminal" }
577                                            </button>
578                                        </div>
579                                    </div>
580
581                                    // Output preview
582                                    <div class="relative group mt-6">
583                                        <div class="absolute -top-3 left-2 bg-white dark:bg-gray-800 px-2 text-xs font-semibold text-gray-500 dark:text-gray-400 tracking-wider uppercase">
584                                            { "Output" }
585                                        </div>
586                                        <pre class="text-sm bg-gray-900 text-gray-50 p-5 rounded-lg overflow-x-auto max-h-96 font-mono shadow-inner border border-gray-800 leading-relaxed">
587                                            { &result.output }
588                                        </pre>
589                                        <button
590                                            class="absolute top-3 right-3 p-1.5 rounded bg-gray-800/80 text-gray-400 hover:text-white opacity-0 group-hover:opacity-100 transition-opacity backdrop-blur-sm"
591                                            title="Copy output"
592                                            onclick={{
593                                                let output = result.output.clone();
594                                                Callback::from(move |_| {
595                                                    if let Some(window) = web_sys::window() {
596                                                        let clipboard = window.navigator().clipboard();
597                                                        let _ = clipboard.write_text(&output);
598                                                    }
599                                                })
600                                            }}
601                                        >
602                                            <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
603                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
604                                            </svg>
605                                        </button>
606                                    </div>
607
608                                    // Error message (if any)
609                                    if let Some(error) = &result.error {
610                                        <div class="mt-6">
611                                            <div class="bg-error-50 dark:bg-error-900/10 border border-error-100 dark:border-error-900/30 rounded-lg overflow-hidden">
612                                                 <div class="bg-error-100/50 dark:bg-error-900/20 px-4 py-2 border-b border-error-100 dark:border-error-900/30 flex items-center gap-2">
613                                                    <svg class="w-4 h-4 text-error-600 dark:text-error-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
614                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
615                                                    </svg>
616                                                    <span class="text-xs font-bold text-error-700 dark:text-error-400 uppercase tracking-wide">
617                                                        { "Error Details" }
618                                                    </span>
619                                                </div>
620                                                <div class="p-4">
621                                                    <pre class="text-error-600 dark:text-error-300 whitespace-pre-wrap font-mono text-sm leading-relaxed settings-scroll">
622                                                        { error }
623                                                    </pre>
624                                                </div>
625                                            </div>
626                                        </div>
627                                    }
628                                </div>
629                            } else {
630                                <div class="text-center py-12 text-gray-400 dark:text-gray-500">
631                                    <p class="text-sm">
632                                        { "Execution results will appear here" }
633                                    </p>
634                                </div>
635                            }
636                        </div>
637                    </div>
638
639                    // Bottom padding
640                    <div class="h-8"></div>
641                </div>
642            </div>
643
644            // Terminal Output (slide up from bottom)
645            <TerminalOutput
646                visible={*terminal_visible}
647                execution={(*execution_result).clone()}
648                on_close={on_terminal_close}
649                on_rerun={on_rerun}
650                minimized={*terminal_minimized}
651                on_toggle_minimize={on_terminal_toggle_minimize}
652            />
653        </div>
654    }
655}