or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-workflows.mdcancellation-handling.mdindex.mdinteractive-prompts.mdlogging-system.mdprogress-indicators.mdselection-prompts.mdsession-management.mdsettings-configuration.md
tile.json

cancellation-handling.mddocs/

Cancellation Handling

Utilities for graceful handling of user cancellation across all prompt types with consistent behavior.

Capabilities

Cancellation Detection

Core utility for detecting when a user cancels a prompt operation.

/**
 * Guards against cancellation by detecting the cancellation symbol
 * @param value - The value returned from any prompt function
 * @returns true if the value represents cancellation, false otherwise
 */
function isCancel(value: any): boolean;

Usage Examples:

import { text, isCancel, cancel } from "@clack/prompts";

// Basic cancellation handling
const name = await text({
  message: "What's your name?",
});

if (isCancel(name)) {
  cancel("Operation cancelled by user");
  process.exit(0);
}

// name is now guaranteed to be a string
console.log(`Hello, ${name}!`);

// Multiple prompt cancellation handling
const email = await text({
  message: "What's your email?",
});

if (isCancel(email)) {
  cancel("Email input cancelled");
  process.exit(0);
}

const confirm = await confirm({
  message: "Subscribe to newsletter?",
});

if (isCancel(confirm)) {
  cancel("Subscription prompt cancelled");
  process.exit(0);
}

Cancellation Patterns

Standard Pattern

The most common pattern for handling cancellation:

import { text, isCancel, cancel } from "@clack/prompts";

async function getUserInput() {
  const result = await text({
    message: "Enter a value:",
  });
  
  if (isCancel(result)) {
    cancel("Input cancelled");
    process.exit(0);
  }
  
  return result; // TypeScript knows this is a string
}

Reusable Cancellation Handler

import { isCancel, cancel } from "@clack/prompts";

function handleCancel<T>(value: T | symbol, message: string = "Operation cancelled"): T {
  if (isCancel(value)) {
    cancel(message);
    process.exit(0);
  }
  return value as T;
}

// Usage
const name = handleCancel(
  await text({ message: "Your name:" }),
  "Name input cancelled"
);

const framework = handleCancel(
  await select({
    message: "Choose framework:",
    options: [
      { value: "react", label: "React" },
      { value: "vue", label: "Vue" },
    ],
  }),
  "Framework selection cancelled"
);

Graceful Cleanup

import { text, isCancel, cancel, spinner } from "@clack/prompts";

async function setupProject() {
  const s = spinner();
  
  try {
    const projectName = await text({
      message: "Project name:",
    });
    
    if (isCancel(projectName)) {
      cancel("Project setup cancelled");
      return; // Exit gracefully without process.exit
    }
    
    s.start("Creating project");
    
    // Setup logic here
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    s.stop("Project created successfully");
    
  } catch (error) {
    if (s) s.stop("Setup failed", 1);
    throw error;
  }
}

// Call without process.exit in the cancellation handler
try {
  await setupProject();
} catch (error) {
  console.error("Setup failed:", error.message);
  process.exit(1);
}

Multiple Prompt Sequences

import * as p from "@clack/prompts";

async function collectUserData() {
  const prompts = [
    () => p.text({ message: "Name:" }),
    () => p.text({ message: "Email:" }),
    () => p.select({
      message: "Role:",
      options: [
        { value: "admin", label: "Administrator" },
        { value: "user", label: "User" },
      ],
    }),
  ];
  
  const results: any[] = [];
  
  for (const [index, promptFn] of prompts.entries()) {
    const result = await promptFn();
    
    if (p.isCancel(result)) {
      p.cancel(`Data collection cancelled at step ${index + 1}`);
      process.exit(0);
    }
    
    results.push(result);
  }
  
  return {
    name: results[0],
    email: results[1],
    role: results[2],
  };
}

Advanced Cancellation Handling

With Error Recovery

import { text, isCancel, cancel, confirm } from "@clack/prompts";

async function robustInput(message: string, maxRetries: number = 3): Promise<string> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const result = await text({
      message: attempt === 1 ? message : `${message} (attempt ${attempt}/${maxRetries})`,
      validate: (value) => {
        if (!value.trim()) return "Value cannot be empty";
      }
    });
    
    if (isCancel(result)) {
      if (attempt < maxRetries) {
        const retry = await confirm({
          message: "Do you want to try again?",
          initialValue: true,
        });
        
        if (isCancel(retry) || !retry) {
          cancel("Input cancelled");
          process.exit(0);
        }
        continue;
      } else {
        cancel("Maximum attempts exceeded");
        process.exit(0);
      }
    }
    
    return result;
  }
  
  throw new Error("Should not reach here");
}

With Partial Results

import * as p from "@clack/prompts";

interface UserProfile {
  name?: string;
  email?: string;
  age?: number;
  preferences?: string[];
}

async function collectProfile(): Promise<UserProfile> {
  const profile: UserProfile = {};
  
  // Name (required)
  const name = await p.text({ message: "Name (required):" });
  if (p.isCancel(name)) {
    p.cancel("Profile creation cancelled");
    process.exit(0);
  }
  profile.name = name;
  
  // Email (optional)
  const email = await p.text({ message: "Email (optional):" });
  if (p.isCancel(email)) {
    p.note("Profile creation cancelled, but name was saved", "Partial Progress");
    return profile; // Return partial results
  }
  profile.email = email;
  
  // Age (optional)
  const age = await p.text({
    message: "Age (optional):",
    validate: (value) => {
      if (value && isNaN(Number(value))) return "Please enter a valid number";
    }
  });
  if (p.isCancel(age)) {
    p.note("Profile creation cancelled, but name and email were saved", "Partial Progress");
    return profile;
  }
  if (age) profile.age = Number(age);
  
  return profile;
}

Integration with Group Prompts

import * as p from "@clack/prompts";

const results = await p.group(
  {
    name: () => p.text({ message: "Name:" }),
    email: () => p.text({ message: "Email:" }),
    framework: () => p.select({
      message: "Framework:",
      options: [
        { value: "react", label: "React" },
        { value: "vue", label: "Vue" },
      ],
    }),
  },
  {
    // Centralized cancellation handling for the entire group
    onCancel: ({ results }) => {
      p.cancel("Setup wizard cancelled");
      
      // Log partial results for debugging
      if (Object.keys(results).length > 0) {
        console.log("Partial results collected:", results);
      }
      
      process.exit(0);
    },
  }
);

// If we reach here, all prompts completed successfully
console.log("Setup completed:", results);

Cancellation in Different Contexts

CLI Applications

import * as p from "@clack/prompts";

// Standard CLI cancellation
process.on('SIGINT', () => {
  p.cancel("\nOperation interrupted by user");
  process.exit(0);
});

async function main() {
  p.intro("My CLI Tool");
  
  try {
    const input = await p.text({ message: "Enter command:" });
    
    if (p.isCancel(input)) {
      p.cancel("Command cancelled");
      process.exit(0);
    }
    
    // Process command
    p.outro("Command executed successfully");
    
  } catch (error) {
    p.cancel(`Error: ${error.message}`);
    process.exit(1);
  }
}

Interactive Wizards

import * as p from "@clack/prompts";

async function setupWizard() {
  p.intro("Setup Wizard");
  
  const steps = [
    "Basic Information",
    "Configuration",
    "Confirmation"
  ];
  
  let currentStep = 0;
  
  const cleanup = () => {
    p.cancel(`Setup cancelled at: ${steps[currentStep]}`);
    p.note("You can resume setup later by running this command again", "Info");
  };
  
  try {
    // Step 1
    currentStep = 0;
    const basicInfo = await p.group({
      name: () => p.text({ message: "Project name:" }),
      type: () => p.select({
        message: "Project type:",
        options: [
          { value: "web", label: "Web App" },
          { value: "api", label: "API" },
        ],
      }),
    }, { onCancel: cleanup });
    
    // Step 2
    currentStep = 1;
    const config = await p.multiselect({
      message: "Select features:",
      options: [
        { value: "auth", label: "Authentication" },
        { value: "db", label: "Database" },
      ],
    });
    
    if (p.isCancel(config)) {
      cleanup();
      return;
    }
    
    // Step 3
    currentStep = 2;
    const confirmed = await p.confirm({
      message: "Proceed with setup?",
    });
    
    if (p.isCancel(confirmed)) {
      cleanup();
      return;
    }
    
    if (!confirmed) {
      p.cancel("Setup cancelled by user choice");
      return;
    }
    
    p.outro("Setup completed successfully!");
    
  } catch (error) {
    p.cancel(`Setup failed: ${error.message}`);
    process.exit(1);
  }
}

Best Practices

  1. Always check for cancellation after every prompt call
  2. Provide meaningful messages when cancelling operations
  3. Use consistent exit codes (0 for user cancellation, 1 for errors)
  4. Clean up resources before exiting (close files, stop spinners, etc.)
  5. Consider partial results in multi-step workflows
  6. Handle interrupts gracefully with proper signal handlers
  7. Test cancellation paths to ensure they work as expected