From 719a788bc7cd1b39687d5a6b6c23942b05ccee79 Mon Sep 17 00:00:00 2001 From: Fauzaan Date: Tue, 6 Oct 2026 07:56:26 +0000 Subject: [PATCH] fix(instances): fix selection loop and require Tailscale for connect The instance picker used fixed index math that assumed a "Main:" section was always present. When an application only had preview-labelled pods, every item was treated as a section header and the prompt looped forever. Map each item to its pod (or None for headers) instead, and default the cursor to the first selectable pod. The Livebook URL is only reachable over Tailscale, so confirm the user is connected before provisioning, and mention Tailscale in the connectivity test and failure messages. Co-Authored-By: Claude Opus 5.5 --- .../commands/application/instances/connect.rs | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/cli/src/commands/application/instances/connect.rs b/cli/src/commands/application/instances/connect.rs index 6d90a08f..5f3b9539 100644 --- a/cli/src/commands/application/instances/connect.rs +++ b/cli/src/commands/application/instances/connect.rs @@ -1,4 +1,4 @@ -use dialoguer::{theme::ColorfulTheme, Select}; +use dialoguer::{theme::ColorfulTheme, Confirm, Select}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use log::debug; use owo_colors::OwoColorize; @@ -143,44 +143,52 @@ pub async fn handle_connect( version.bright_green() )); - let mut items = Vec::new(); - if !main_pods.is_empty() { - items.push("Main:".to_string()); - items.extend(main_pods.iter().map(|pod| format!(" {}", pod.name))); - } - if !preview_pods.is_empty() { - items.push("Preview:".to_string()); - items.extend(preview_pods.iter().map(|pod| format!(" {}", pod.name))); + // Each item is paired with the pod it represents; section headers map to `None`. + let mut items: Vec = Vec::new(); + let mut item_pods: Vec> = Vec::new(); + for (header, pods) in [("Main:", &main_pods), ("Preview:", &preview_pods)] { + if pods.is_empty() { + continue; + } + items.push(header.to_string()); + item_pods.push(None); + for pod in pods.iter() { + items.push(format!(" {}", pod.name)); + item_pods.push(Some(*pod)); + } } - let instance_name_idx = loop { + // Default to the first selectable pod, not the section header. + let default_idx = item_pods.iter().position(Option::is_some).unwrap_or(0); + + let instance_object = loop { let instance_name_idx = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Please choose the instance you want to connect") - .default(0) + .default(default_idx) .items(&items) .interact()?; - if instance_name_idx == 0 || instance_name_idx == main_pods.len() + 1 { - // If "Main pods" or "Preview pods" is selected, continue the loop - continue; - } else { - // Otherwise, break the loop and return the selected index - break instance_name_idx; + // Selecting a section header re-prompts; selecting a pod returns it. + if let Some(pod) = item_pods[instance_name_idx] { + break pod; } }; - let instance_name = if instance_name_idx < main_pods.len() + 1 { - main_pods[instance_name_idx - 1].name.clone() - } else { - preview_pods[instance_name_idx - main_pods.len() - 2] - .name - .clone() - }; - - let instance_object = k8s_pods - .iter() - .find(|pod| pod.name == instance_name) - .unwrap(); + let instance_name = instance_object.name.clone(); + + // The Livebook URL is only reachable over Tailscale. Confirm before provisioning + // so the user doesn't wait on a connectivity test that can never succeed. + if !Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "The Livebook instance is only reachable over {}. Are you connected to Tailscale?", + "Tailscale".bold() + )) + .default(true) + .interact()? + { + eprintln!("Please connect to Tailscale first, then run this command again."); + return Ok(false); + } let preparing_loader = new_spinner(); preparing_loader.set_style(spinner_style.clone()); @@ -259,7 +267,8 @@ pub async fn handle_connect( let connection_test_loader = new_spinner(); connection_test_loader.set_style(spinner_style.clone()); connection_test_loader.set_prefix("[2/2]"); - connection_test_loader.set_message("Testing connectivity to your livebook instance..."); + connection_test_loader + .set_message("Testing connectivity to your livebook instance (requires Tailscale)..."); let url = new_instance.url.unwrap_or_default(); @@ -293,6 +302,7 @@ pub async fn handle_connect( .destroy_livebook(&application, &namespace, &version) .await?; destroy_loader.finish_and_clear(); + eprintln!("Unable to reach your livebook instance. Make sure you're connected to Tailscale, then try again."); eprintln!("The session has been terminated."); return Ok(false); }