Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
264baaf
feat: :sparkles: Remove TTY requirement for the cli in headless mode
chezsmithy Nov 11, 2025
12e9de5
fix: :art: Fix typescript errors
chezsmithy Nov 12, 2025
0e2dcd8
Update .continue/prompts/sub-agent-foreground.md
chezsmithy Nov 12, 2025
253c2e1
Update extensions/cli/src/commands/chat.ts
chezsmithy Nov 12, 2025
895dfaa
Update extensions/cli/spec/tty-less-support.md
chezsmithy Nov 12, 2025
c5f0a2d
fix: :fire: Additional fixes based on commnets
chezsmithy Nov 12, 2025
ba34ec3
Merge branch 'feat-ttyless-cli' of github.com:chezsmithy/continue int…
chezsmithy Nov 12, 2025
91e82db
fix: :zap: More fixes
chezsmithy Nov 12, 2025
606e12c
fix: :bug: More fixes
chezsmithy Nov 12, 2025
19b60ed
feat: :sparkles: Remove TTY requirement for the cli in headless mode
chezsmithy Nov 11, 2025
50156bd
fix: :art: Fix typescript errors
chezsmithy Nov 12, 2025
c6e8ac7
Update .continue/prompts/sub-agent-foreground.md
chezsmithy Nov 12, 2025
58ead92
fix: :fire: Additional fixes based on commnets
chezsmithy Nov 12, 2025
9047bba
Update extensions/cli/src/commands/chat.ts
chezsmithy Nov 12, 2025
20c8620
Update extensions/cli/spec/tty-less-support.md
chezsmithy Nov 12, 2025
7a2acdd
fix: :zap: More fixes
chezsmithy Nov 12, 2025
2514ea8
fix: :bug: More fixes
chezsmithy Nov 12, 2025
33aceb0
Merge branches 'feat-ttyless-cli' and 'feat-ttyless-cli' of github.co…
chezsmithy Nov 12, 2025
32a5093
fix: :bug: More fixes
chezsmithy Nov 12, 2025
45cace7
fix: :bug: More fixes to tests
chezsmithy Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .continue/prompts/sub-agent-background.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: Sub Agent Background Prompt
description: Start a subagent using the continue cli in the background
invokable: true
---

# Continue Sub Agent Background Prompt

Take the prompt provided by the user and using the terminal tool run the following command in the background:

cn -p "{{prompt}}"
11 changes: 11 additions & 0 deletions .continue/prompts/sub-agent-foreground.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: Sub Agent Foreground Prompt
description: Start a subagent using the continue cli in the foreground
invokable: true
---

# Continue Sub Agent Foreground Prompt

Take the prompt provided by the user and using the terminal tool run the following command in the foreground:

cn -p "{{prompt}}"
44 changes: 44 additions & 0 deletions extensions/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,30 @@ cn

### Headless Mode

Headless mode (`-p` flag) runs without an interactive terminal UI, making it perfect for:

- Scripts and automation
- CI/CD pipelines
- Docker containers
- VSCode/IntelliJ extension integration
- Environments without a TTY

```bash
# Basic usage
cn -p "Generate a conventional commit name for the current git changes."

# With piped input
echo "Review this code" | cn -p

# JSON output for scripting
cn -p "Analyze the code" --format json

# Silent mode (strips thinking tags)
cn -p "Write a README" --silent
```

**TTY-less Environments**: Headless mode is designed to work in environments without a terminal (TTY), such as when called from VSCode/IntelliJ extensions using terminal commands. The CLI will not attempt to read stdin or initialize the interactive UI when running in headless mode with a supplied prompt.

### Session Management

The CLI automatically saves your chat history for each terminal session. You can resume where you left off:
Expand All @@ -47,6 +67,7 @@ cn ls --json
## Environment Variables

- `CONTINUE_CLI_DISABLE_COMMIT_SIGNATURE`: Disable adding the Continue commit signature to generated commit messages
- `FORCE_NO_TTY`: Force TTY-less mode, prevents stdin reading (useful for testing and automation)

## Commands

Expand All @@ -62,3 +83,26 @@ cn ls --json
Shows recent sessions, limited by screen height to ensure it fits on your terminal.

- `--json`: Output in JSON format for scripting (always shows 10 sessions)

## TTY-less Support

The CLI fully supports running in environments without a TTY (terminal):

```bash
# From Docker without TTY allocation
docker run --rm my-image cn -p "Generate docs"

# From CI/CD pipeline
cn -p "Review changes" --format json

# From VSCode/IntelliJ extension terminal tool
cn -p "Analyze code" --silent
```

The CLI automatically detects TTY-less environments and adjusts its behavior:

- Skips stdin reading when a prompt is supplied
- Disables interactive UI components
- Ensures clean stdout/stderr output

For more details, see [`spec/tty-less-support.md`](./spec/tty-less-support.md).
239 changes: 239 additions & 0 deletions extensions/cli/spec/tty-less-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# TTY-less Environment Support

## Overview

The Continue CLI supports running in TTY-less environments (environments without a terminal/TTY), which is essential for:

- VSCode and IntelliJ extensions using the `run_terminal_command` tool
- Docker containers without TTY allocation
- CI/CD pipelines
- Automated scripts and tools
- Background processes

## Architecture

### Mode Separation

The CLI has two distinct execution modes with complete separation:

1. **Interactive Mode (TUI)**: Requires a TTY, uses Ink for rendering
2. **Headless Mode**: Works in TTY-less environments, outputs to stdout/stderr

```
┌─────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (src/index.ts) │
└────────────────────────┬────────────────────────────────────┘
┌────────────┴────────────┐
│ │
┌───────▼────────┐ ┌───────▼─────────┐
│ Interactive │ │ Headless │
│ Mode (TUI) │ │ Mode (-p) │
│ │ │ │
│ • Requires TTY │ │ • No TTY needed │
│ • Uses Ink │ │ • Stdin/stdout │
│ • Keyboard UI │ │ • One-shot exec │
└────────────────┘ └─────────────────┘
```

### Safeguards Implemented

#### 1. **TTY Detection Utilities** (`src/util/cli.ts`)

```typescript
// Check if running in TTY-less environment
export function isTTYless(): boolean;

// Check if environment supports interactive features
export function supportsInteractive(): boolean;

// Check if prompt was supplied via CLI arguments
export function hasSuppliedPrompt(): boolean;
```

#### 2. **Stdin Reading Protection** (`src/util/stdin.ts`)

Prevents stdin reading when:

- In headless mode with supplied prompt
- `FORCE_NO_TTY` environment variable is set
- In test environments

This avoids blocking/hanging in TTY-less environments where stdin is not available or not readable.

#### 3. **TUI Initialization Guards** (`src/ui/index.ts`)

The `startTUIChat()` function now includes multiple safeguards:

- **Headless mode check**: Throws error if called in headless mode
- **TTY-less check**: Throws error if no TTY is available
- **Raw mode test**: Validates stdin supports raw mode (required by Ink)
- **Explicit stdin/stdout**: Passes streams explicitly to Ink

```typescript
// Critical safeguard: Prevent TUI in headless mode
if (isHeadlessMode()) {
throw new Error("Cannot start TUI in headless mode");
}

// Critical safeguard: Prevent TUI in TTY-less environment
if (isTTYless() && !customStdin) {
throw new Error("Cannot start TUI in TTY-less environment");
}
```

#### 4. **Headless Mode Validation** (`src/commands/chat.ts`)

Ensures headless mode has all required inputs:

```typescript
if (!prompt) {
throw new Error("Headless mode requires a prompt");
}
```

#### 5. **Logger Configuration** (`src/util/logger.ts`)

Configures output handling for TTY-less environments:

- Sets UTF-8 encoding
- Leaves stdout/stderr buffering unchanged in headless mode.
- Disables progress indicators

## Usage Examples

### From VSCode/IntelliJ Extension

```typescript
// Using the run_terminal_command tool
const command = 'cn -p "Analyze the current git diff"';
const result = await runTerminalCommand(command);
```

### From Docker Container

```bash
# Without TTY allocation (-t flag)
docker run --rm my-image cn -p "Generate a README"
```

### From CI/CD Pipeline

```yaml
- name: Run Continue CLI
run: |
cn -p "Review code changes" --format json
```

### From Automated Script

```bash
#!/bin/bash
# Non-interactive script
cn -p "Generate commit message for current changes" --silent
```

## Environment Variables

- `FORCE_NO_TTY`: Forces TTY-less mode, prevents stdin reading
- `CONTINUE_CLI_TEST`: Marks test environment, prevents stdin reading

## Testing

### TTY-less Test

```typescript
const result = await runCLI(context, {
args: ["-p", "Hello, world!"],
env: {
FORCE_NO_TTY: "true",
},
});
```

### Expected Behavior

- ✅ Should not hang on stdin
- ✅ Should not attempt to initialize Ink
- ✅ Should output results to stdout
- ✅ Should exit cleanly

## Error Messages

### Attempting TUI in TTY-less Environment

```
Error: Cannot start TUI in TTY-less environment. No TTY available for interactive mode.
For non-interactive use, run with -p flag:
cn -p "your prompt here"
```

### Missing Prompt in Headless Mode

```
Error: A prompt is required when using the -p/--print flag, unless --prompt or --agent is provided.

Usage examples:
cn -p "please review my current git diff"
echo "hello" | cn -p
cn -p "analyze the code in src/"
cn -p --agent my-org/my-agent
```

## Troubleshooting

### CLI Hangs in Docker/CI

**Cause**: CLI attempting to read stdin in TTY-less environment

**Solution**: Ensure using `-p` flag with a prompt:

```bash
cn -p "your prompt" --config config.yaml
```

### "Cannot start TUI" Error

**Cause**: Attempting interactive mode in TTY-less environment

**Solution**: Use headless mode:

```bash
cn -p "your prompt"
```

### Raw Mode Error

**Cause**: Terminal doesn't support raw mode (required by Ink)

**Solution**: Use headless mode instead of interactive mode

## Design Principles

1. **Fail Fast**: Detect environment early and fail with clear messages
2. **Explicit Separation**: No code path should allow Ink to load in headless mode
3. **No Blocking**: Never block on stdin in TTY-less environments
4. **Clear Errors**: Provide actionable error messages with examples
5. **Testing**: Comprehensive tests for TTY-less scenarios

## Implementation Checklist

- [x] Add TTY detection utilities
- [x] Protect stdin reading in headless mode
- [x] Guard TUI initialization
- [x] Validate headless mode inputs
- [x] Configure logger for TTY-less output
- [x] Update test helpers
- [x] Add TTY-less tests
- [x] Document TTY-less support

## Related Files

- `src/util/cli.ts` - TTY detection utilities
- `src/util/stdin.ts` - Stdin reading protection
- `src/ui/index.ts` - TUI initialization guards
- `src/commands/chat.ts` - Mode routing and validation
- `src/util/logger.ts` - Output configuration
- `src/test-helpers/cli-helpers.ts` - Test support
- `src/e2e/headless-minimal.test.ts` - TTY-less tests
21 changes: 21 additions & 0 deletions extensions/cli/src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,18 @@ async function runHeadlessMode(
initialPrompt,
);

// Critical validation: Ensure we have actual prompt text in headless mode
// This prevents the CLI from hanging in TTY-less environments when question() is called
// We check AFTER processing all prompts (including agent files) to ensure we have real content
if (!initialUserInput || !initialUserInput.trim()) {
throw new Error(
'Headless mode requires a prompt. Use: cn -p "your prompt"\n' +
'Or pipe input: echo "prompt" | cn -p\n' +
"Or use agent files: cn -p --agent my-org/my-agent\n" +
"Note: Agent files must contain a prompt field.",
);
}

let isFirstMessage = true;
while (true) {
// When in headless mode, don't ask for user input
Expand Down Expand Up @@ -544,6 +556,15 @@ export async function chat(prompt?: string, options: ChatOptions = {}) {
// Start active time tracking
telemetryService.startActiveTime();

// Critical routing: Explicit separation of headless and interactive modes
if (options.headless) {
// Headless path - no Ink, no TUI, works in TTY-less environments
logger.debug("Running in headless mode (TTY-less compatible)");
await runHeadlessMode(prompt, options);
return;
}

// Interactive path - requires TTY for Ink rendering
// If not in headless mode, use unified initialization with TUI
if (!options.headless) {
// Process flags for TUI mode
Expand Down
Loading
Loading