Skip to main content

vibe_workspace/ui/
workflows.rs

1use anyhow::Result;
2use console::style;
3
4use crate::ui::prompts::{prompt_app_selection, prompt_yes_no};
5use crate::ui::state::VibeState;
6use crate::workspace::WorkspaceManager;
7use crate::{
8    display_println,
9    git::{CloneCommand, GitConfig},
10};
11
12/// Represents the next action in a workflow
13pub enum NextAction {
14    /// Workflow is complete
15    Complete,
16    /// Continue to another workflow
17    Continue(Box<dyn Workflow + Send + Sync>),
18    /// Suggest actions to the user
19    Suggest(Vec<String>),
20}
21
22/// Trait for implementing workflows with continuity
23pub trait Workflow: Send + Sync {
24    /// Execute the workflow and return the next action
25    fn execute<'a>(
26        &'a self,
27        manager: &'a mut WorkspaceManager,
28    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>>;
29
30    /// Get a description of this workflow
31    fn description(&self) -> String;
32}
33
34/// Clone and open workflow
35pub struct CloneWorkflow {
36    pub url: String,
37    pub app: Option<String>,
38}
39
40impl Workflow for CloneWorkflow {
41    fn execute<'a>(
42        &'a self,
43        manager: &'a mut WorkspaceManager,
44    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
45        Box::pin(async move {
46            display_println!("{} Cloning repository...", style("đŸ“Ĩ").blue());
47
48            // Clone the repository
49            let git_config = GitConfig::default();
50            let cloned_path = CloneCommand::execute(
51                self.url.clone(),
52                None,
53                false, // Don't open yet
54                false, // Don't install yet
55                manager,
56                &git_config,
57            )
58            .await?;
59
60            // Extract repo name
61            let repo_name = cloned_path
62                .file_name()
63                .and_then(|n| n.to_str())
64                .ok_or_else(|| anyhow::anyhow!("Could not determine repository name"))?
65                .to_string();
66
67            display_println!(
68                "{} Repository '{}' cloned successfully!",
69                style("✓").green().bold(),
70                style(&repo_name).cyan()
71            );
72
73            // Continue to app configuration (optional)
74            Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
75                repo_name,
76                suggested_app: self.app.clone(),
77                open_after: true,
78                force_configure: false, // Allow skipping configuration
79            })))
80        })
81    }
82
83    fn description(&self) -> String {
84        format!("Clone repository from {}", self.url)
85    }
86}
87
88/// Configure app for repository workflow
89pub struct ConfigureAppWorkflow {
90    pub repo_name: String,
91    pub suggested_app: Option<String>,
92    pub open_after: bool,
93    pub force_configure: bool, // If false, can skip to smart opening
94}
95
96impl Workflow for ConfigureAppWorkflow {
97    fn execute<'a>(
98        &'a self,
99        manager: &'a mut WorkspaceManager,
100    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
101        Box::pin(async move {
102            // Check if apps are already configured
103            let apps = manager.list_apps_for_repo(&self.repo_name)?;
104
105            if !apps.is_empty() {
106                display_println!(
107                    "{} Repository already has {} app{} configured",
108                    style("â„šī¸").blue(),
109                    apps.len(),
110                    if apps.len() == 1 { "" } else { "s" }
111                );
112
113                if self.open_after {
114                    return Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
115                        repo_name: self.repo_name.clone(),
116                        preferred_app: self.suggested_app.clone(),
117                    })));
118                } else {
119                    return Ok(NextAction::Complete);
120                }
121            }
122
123            // If not forced to configure, try smart opening without configuration
124            if !self.force_configure {
125                if self.open_after {
126                    // Use smart opening which works for any repository
127                    display_println!(
128                        "\n{} Opening '{}' with smart app selection...",
129                        style("🚀").blue(),
130                        style(&self.repo_name).cyan()
131                    );
132
133                    return Ok(NextAction::Continue(Box::new(SmartOpenWorkflow {
134                        repo_name: self.repo_name.clone(),
135                    })));
136                } else {
137                    return Ok(NextAction::Complete);
138                }
139            }
140
141            // Prompt to configure app (forced configuration path)
142            display_println!(
143                "\n{} Configure app for '{}'?",
144                style("🔧").blue(),
145                style(&self.repo_name).cyan()
146            );
147
148            if prompt_yes_no("Configure app", true)? {
149                let app_name = if let Some(app) = &self.suggested_app {
150                    app.clone()
151                } else {
152                    prompt_app_selection()?
153                };
154
155                manager
156                    .configure_app_for_repo(&self.repo_name, &app_name, "default")
157                    .await?;
158
159                display_println!(
160                    "{} Configured {} for repository",
161                    style("✓").green().bold(),
162                    style(&app_name).cyan()
163                );
164
165                if self.open_after {
166                    // Continue to open
167                    Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
168                        repo_name: self.repo_name.clone(),
169                        preferred_app: Some(app_name),
170                    })))
171                } else {
172                    Ok(NextAction::Complete)
173                }
174            } else {
175                // Skip configuration
176                if self.open_after {
177                    display_println!("\n{} Opening with default app...", style("🚀").blue());
178                    Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
179                        repo_name: self.repo_name.clone(),
180                        preferred_app: None,
181                    })))
182                } else {
183                    Ok(NextAction::Complete)
184                }
185            }
186        })
187    }
188
189    fn description(&self) -> String {
190        format!("Configure app for {}", self.repo_name)
191    }
192}
193
194/// Smart open repository workflow - uses smart_open_repository method
195pub struct SmartOpenWorkflow {
196    pub repo_name: String,
197}
198
199impl Workflow for SmartOpenWorkflow {
200    fn execute<'a>(
201        &'a self,
202        manager: &'a mut WorkspaceManager,
203    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
204        Box::pin(async move {
205            // Use smart_open_repository which handles both configured and unconfigured repos
206            manager.smart_open_repository(&self.repo_name).await?;
207
208            display_println!(
209                "{} Repository '{}' opened successfully",
210                style("🚀").green().bold(),
211                style(&self.repo_name).cyan()
212            );
213
214            Ok(NextAction::Complete)
215        })
216    }
217
218    fn description(&self) -> String {
219        format!("Smart open {}", self.repo_name)
220    }
221}
222
223/// Open repository workflow
224pub struct OpenRepositoryWorkflow {
225    pub repo_name: String,
226    pub preferred_app: Option<String>,
227}
228
229impl Workflow for OpenRepositoryWorkflow {
230    fn execute<'a>(
231        &'a self,
232        manager: &'a mut WorkspaceManager,
233    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
234        Box::pin(async move {
235            // Get repo info
236            let repo = manager
237                .get_repository(&self.repo_name)
238                .ok_or_else(|| anyhow::anyhow!("Repository '{}' not found", self.repo_name))?;
239
240            let repo_path = repo.path.clone();
241
242            // Determine app to use
243            let app_to_use = if let Some(app) = &self.preferred_app {
244                app.clone()
245            } else {
246                // Get configured apps
247                let apps = manager.list_apps_for_repo(&self.repo_name)?;
248                if apps.is_empty() {
249                    "vscode".to_string() // Default fallback
250                } else {
251                    apps[0].0.clone()
252                }
253            };
254
255            // Open the repository
256            manager
257                .open_repo_with_app(&self.repo_name, &app_to_use)
258                .await?;
259
260            // Update state
261            let mut state = VibeState::load().unwrap_or_default();
262            state.add_recent_repo(self.repo_name.clone(), repo_path, Some(app_to_use.clone()));
263            state.save()?;
264
265            display_println!(
266                "{} Opened {} with {}!",
267                style("🎉").green().bold(),
268                style(&self.repo_name).cyan(),
269                style(&app_to_use).blue()
270            );
271
272            // Suggest next actions
273            Ok(NextAction::Suggest(vec![
274                "Run 'vibe' to manage more repositories".to_string(),
275                format!("Use 'vibe launch 1' to quickly reopen {}", self.repo_name),
276                "Configure additional apps with 'vibe apps configure'".to_string(),
277            ]))
278        })
279    }
280
281    fn description(&self) -> String {
282        format!("Open {} repository", self.repo_name)
283    }
284}
285
286/// Setup workspace workflow (first-time setup)
287pub struct SetupWorkspaceWorkflow {
288    pub auto_discover: bool,
289}
290
291impl Workflow for SetupWorkspaceWorkflow {
292    fn execute<'a>(
293        &'a self,
294        manager: &'a mut WorkspaceManager,
295    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
296        Box::pin(async move {
297            // Use the new enhanced setup wizard
298            crate::ui::setup_wizard::run_enhanced_setup_wizard(manager).await?;
299
300            // Mark setup as complete and return
301            Ok(NextAction::Complete)
302        })
303    }
304
305    fn description(&self) -> String {
306        "Setup workspace for first-time use".to_string()
307    }
308}
309
310/// Configure default app workflow
311pub struct ConfigureDefaultAppWorkflow {
312    pub repo_count: usize,
313}
314
315impl Workflow for ConfigureDefaultAppWorkflow {
316    fn execute<'a>(
317        &'a self,
318        manager: &'a mut WorkspaceManager,
319    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
320        Box::pin(async move {
321            display_println!(
322                "\n{} Configure a default app for your {} repositories?",
323                style("🔧").blue(),
324                self.repo_count
325            );
326
327            if prompt_yes_no("Configure default app", true)? {
328                let app_name = prompt_app_selection()?;
329
330                // Configure for all repositories
331                let repo_names: Vec<String> = manager
332                    .list_repositories()
333                    .iter()
334                    .map(|r| r.name.clone())
335                    .collect();
336
337                for repo_name in &repo_names {
338                    manager
339                        .configure_app_for_repo(repo_name, &app_name, "default")
340                        .await?;
341                }
342
343                display_println!(
344                    "{} Configured {} as default app for all repositories!",
345                    style("✓").green().bold(),
346                    style(&app_name).cyan()
347                );
348            }
349
350            // Setup complete
351            display_println!("\n{} Workspace setup complete!", style("✨").green().bold());
352
353            Ok(NextAction::Suggest(vec![
354                "Run 'vibe' to start managing repositories".to_string(),
355                "Use 'vibe launch' to quickly open recent repos".to_string(),
356                "Clone new repos with 'vibe go <url>'".to_string(),
357            ]))
358        })
359    }
360
361    fn description(&self) -> String {
362        format!("Configure default app for {} repositories", self.repo_count)
363    }
364}
365
366/// Create repository workflow
367pub struct CreateRepositoryWorkflow {
368    pub suggested_name: Option<String>,
369    pub app: Option<String>,
370    pub skip_configure: bool,
371    pub skip_open: bool,
372}
373
374impl Workflow for CreateRepositoryWorkflow {
375    fn execute<'a>(
376        &'a self,
377        manager: &'a mut WorkspaceManager,
378    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
379        Box::pin(async move {
380            use crate::repository::RepositoryCreator;
381            use inquire::{Select, Text};
382
383            display_println!("{} Creating new repository...", style("🆕").blue());
384
385            // Initialize repository creator
386            let workspace_root = manager.get_workspace_root().clone();
387            let creator = RepositoryCreator::new(workspace_root);
388
389            // Get GitHub user info
390            let user_info = match creator.get_github_user_info().await {
391                Ok(info) => info,
392                Err(e) => {
393                    display_println!(
394                        "{} Warning: Could not get GitHub info: {}",
395                        style("âš ī¸").yellow(),
396                        e
397                    );
398                    display_println!(
399                        "{} Continuing without GitHub integration...",
400                        style("â„šī¸").blue()
401                    );
402                    // Continue without GitHub integration
403                    return self
404                        .create_without_github_integration(manager, &creator)
405                        .await;
406                }
407            };
408
409            // Show available owners (user + organizations)
410            let mut owners = vec![user_info.username.clone()];
411            owners.extend(user_info.organizations.iter().map(|org| org.login.clone()));
412
413            let selected_owner = if owners.len() == 1 {
414                owners[0].clone()
415            } else {
416                display_println!("\n{} Select repository owner:", style("👤").blue());
417
418                let owner_choices: Vec<String> = owners
419                    .iter()
420                    .map(|owner| {
421                        if owner == &user_info.username {
422                            format!("{owner} (personal)")
423                        } else {
424                            format!("{owner} (organization)")
425                        }
426                    })
427                    .collect();
428
429                let selected_display = Select::new("Repository owner:", owner_choices).prompt()?;
430
431                // Extract owner name from display
432                selected_display.split(" (").next().unwrap().to_string()
433            };
434
435            // Get repository name
436            let repo_name = if let Some(name) = &self.suggested_name {
437                name.clone()
438            } else {
439                Text::new("Repository name:").prompt()?
440            };
441
442            // Validate repository name
443            if let Err(e) = creator.validate_repository_name(&repo_name) {
444                display_println!("{} Invalid repository name: {}", style("❌").red(), e);
445                return Ok(NextAction::Complete);
446            }
447
448            // Check if repository already exists on GitHub
449            match creator
450                .check_repository_availability(&selected_owner, &repo_name)
451                .await
452            {
453                Ok(false) => {
454                    display_println!(
455                        "{} Repository {}/{} already exists on GitHub!",
456                        style("âš ī¸").yellow(),
457                        selected_owner,
458                        repo_name
459                    );
460                    display_println!(
461                        "{} You can still create it locally, but you won't be able to push to GitHub with this name.",
462                        style("â„šī¸").blue()
463                    );
464                }
465                Ok(true) => {
466                    display_println!(
467                        "{} Repository name is available on GitHub",
468                        style("✅").green()
469                    );
470                }
471                Err(e) => {
472                    display_println!(
473                        "{} Could not check GitHub availability: {}",
474                        style("âš ī¸").yellow(),
475                        e
476                    );
477                }
478            }
479
480            // Create the local repository
481            match creator
482                .create_local_repository(&selected_owner, &repo_name, manager)
483                .await
484            {
485                Ok(_path) => {
486                    // Check if we should skip configuration and/or opening
487                    if self.skip_configure && self.skip_open {
488                        // Skip both - workflow complete
489                        Ok(NextAction::Complete)
490                    } else if self.skip_configure {
491                        // Skip configuration but still open
492                        Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
493                            repo_name: repo_name.clone(),
494                            preferred_app: self.app.clone(),
495                        })))
496                    } else {
497                        // Continue to app configuration (which will handle opening)
498                        Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
499                            repo_name: repo_name.clone(),
500                            suggested_app: self.app.clone(),
501                            open_after: !self.skip_open,
502                            force_configure: false, // Allow skipping configuration
503                        })))
504                    }
505                }
506                Err(e) => {
507                    display_println!("{} Failed to create repository: {}", style("❌").red(), e);
508                    Ok(NextAction::Complete)
509                }
510            }
511        })
512    }
513
514    fn description(&self) -> String {
515        if let Some(name) = &self.suggested_name {
516            format!("Create repository '{name}'")
517        } else {
518            "Create new repository".to_string()
519        }
520    }
521}
522
523impl CreateRepositoryWorkflow {
524    async fn create_without_github_integration(
525        &self,
526        manager: &mut WorkspaceManager,
527        creator: &crate::repository::RepositoryCreator,
528    ) -> Result<NextAction> {
529        use inquire::Text;
530
531        // Fallback: use current user as owner
532        let current_user = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
533
534        // Get repository name
535        let repo_name = if let Some(name) = &self.suggested_name {
536            name.clone()
537        } else {
538            Text::new("Repository name:").prompt()?
539        };
540
541        // Validate repository name
542        if let Err(e) = creator.validate_repository_name(&repo_name) {
543            display_println!("{} Invalid repository name: {}", style("❌").red(), e);
544            return Ok(NextAction::Complete);
545        }
546
547        // Create the local repository
548        match creator
549            .create_local_repository(&current_user, &repo_name, manager)
550            .await
551        {
552            Ok(_path) => {
553                // Check if we should skip configuration and/or opening
554                if self.skip_configure && self.skip_open {
555                    // Skip both - workflow complete
556                    Ok(NextAction::Complete)
557                } else if self.skip_configure {
558                    // Skip configuration but still open
559                    Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
560                        repo_name: repo_name.clone(),
561                        preferred_app: self.app.clone(),
562                    })))
563                } else {
564                    // Continue to app configuration (which will handle opening)
565                    Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
566                        repo_name: repo_name.clone(),
567                        suggested_app: self.app.clone(),
568                        open_after: !self.skip_open,
569                        force_configure: false, // Allow skipping configuration
570                    })))
571                }
572            }
573            Err(e) => {
574                display_println!("{} Failed to create repository: {}", style("❌").red(), e);
575                Ok(NextAction::Complete)
576            }
577        }
578    }
579}
580
581/// Execute a workflow and handle continuations
582pub async fn execute_workflow(
583    workflow: Box<dyn Workflow + Send + Sync>,
584    manager: &mut WorkspaceManager,
585) -> Result<()> {
586    let mut current_workflow = workflow;
587
588    loop {
589        display_println!(
590            "\n{} {}",
591            style("â–ļ").blue(),
592            style(current_workflow.description()).dim()
593        );
594
595        match current_workflow.execute(manager).await? {
596            NextAction::Complete => {
597                display_println!("\n{} Workflow complete!", style("✓").green().bold());
598                break;
599            }
600            NextAction::Continue(next) => {
601                display_println!("\n{} Continuing workflow...", style("→").cyan());
602                current_workflow = next;
603            }
604            NextAction::Suggest(suggestions) => {
605                display_println!("\n{} Next steps:", style("💡").yellow());
606                for suggestion in suggestions {
607                    display_println!("  {} {}", style("â€ĸ").dim(), suggestion);
608                }
609                break;
610            }
611        }
612    }
613
614    Ok(())
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[test]
622    fn test_workflow_description() {
623        let workflow = CloneWorkflow {
624            url: "https://github.com/user/repo".to_string(),
625            app: Some("vscode".to_string()),
626        };
627
628        assert_eq!(
629            workflow.description(),
630            "Clone repository from https://github.com/user/repo"
631        );
632    }
633}