Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion extensions/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
### Features

- replace the wide Continue logo with a compact custom ASCII mark sized to fit standard terminal widths; on terminals narrower than the supported logo width `cn` falls back to the compact `CN` mark ([extensions/cli/src/asciiArt.ts](extensions/cli/src/asciiArt.ts))
- replace the Anthropic-only onboarding prompt with an interactive provider picker (arrow/`j`/`k`, PageUp/PageDown, Enter) covering OpenAI, Anthropic, Gemini, xAI, Mistral, DeepSeek, OpenRouter, Perplexity, LiteLLM, OpenCode Zen, Azure, Bedrock, NVIDIA, Hugging Face and a custom OpenAI-compatible endpoint; the selected API key is stored as a `${{ secrets.NAME }}` reference in `~/.continue/config.yaml` ([extensions/cli/src/ui/components/ProviderPicker.tsx](extensions/cli/src/ui/components/ProviderPicker.tsx))
- replace the Anthropic-only onboarding prompt with an interactive provider picker (arrow/`j`/`k`, PageUp/PageDown, Enter) covering OpenAI, Anthropic, Gemini, xAI, Mistral, DeepSeek, OpenRouter, Perplexity, LiteLLM, OpenCode Zen, Azure, Bedrock, NVIDIA, Hugging Face and a custom OpenAI-compatible endpoint; the selected model is written to `~/.continue/config.yaml` with a `${{ secrets.NAME }}` reference and the picker reminds the user to set the matching env var before continuing ([extensions/cli/src/ui/components/ProviderPicker.tsx](extensions/cli/src/ui/components/ProviderPicker.tsx))

### Bug Fixes

Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The TUI applies one column of horizontal padding around the chat and input area

### First-run provider setup

On the first interactive launch, `cn` shows a scrollable provider picker instead of requiring Anthropic specifically. Use the arrow keys or `j`/`k`, PageUp/PageDown for longer lists, and Enter to select a provider. The API key is entered with masked input.
On the first interactive launch, `cn` shows a scrollable provider picker instead of requiring Anthropic specifically. Use the arrow keys or `j`/`k`, PageUp/PageDown for longer lists, and Enter to select a provider. For providers that need a base URL, region, or a non-default model, the picker asks for those next.

The CLI writes the selected model to `~/.continue/config.yaml` using Continue's `${{ secrets.NAME }}` syntax. Define the matching variable in your shell, `~/.continue/.env`, or the project's `.env` file. For example:

Expand Down
43 changes: 26 additions & 17 deletions extensions/cli/spec/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,32 @@ When a user first runs `cn` in interactive mode, they will be taken through "onb

**The onboarding flow runs when the user hasn't completed onboarding before, regardless of whether they have a valid config.yaml file.**

1. If the --config flag is provided, load this config
2. If the CONTINUE_USE_BEDROCK environment variable is set to "1", automatically use AWS Bedrock configuration and skip interactive prompts
3. Present the user with available options:
1. If the `--config` flag is provided, load this config (this short-circuits the rest of onboarding).
2. If the `CONTINUE_USE_BEDROCK` environment variable is set to `"1"`, automatically use AWS Bedrock configuration and skip interactive prompts.
3. Otherwise, present the user with a scrollable provider picker that supports arrow keys, `j`/`k`, PageUp/PageDown, and Enter. The picker covers native Continue providers (OpenAI, Anthropic, Google Gemini, xAI, Mistral, DeepSeek, OpenRouter, AWS Bedrock, NVIDIA, Hugging Face) plus OpenAI-compatible entries (Meta/Llama, Perplexity, LiteLLM, OpenCode Zen, Azure, and a custom endpoint). For providers that require a base URL or region, the picker prompts for those values, then prompts for the model identifier (pre-filled with the provider's default if any).
4. The picker does **not** ask the user to type their API key. It writes a reference into `~/.continue/config.yaml` using Continue's `${{ secrets.NAME }}` syntax, where `NAME` matches the provider's environment variable (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`). After confirming the model, the picker shows a short "Set `NAME` in your shell, `~/.continue/.env`, or project `.env`" reminder and waits for any keypress to exit.

- Log in with Continue: log them in, which will automatically create their assistant and then we can load the first assistant from the first org
- Enter your Anthropic API key: let them enter the key, and then either create a ~/.continue/config.yaml with the following contents OR update the existing config.yaml to add the model
Example of the generated config:

```yaml
name: Local Config
name: Main Config
version: 1.0.0
schema: v1

models:
- uses: anthropic/claude-4-sonnet
with:
ANTHROPIC_API_KEY: <THEIR_ANTHROPIC_API_KEY>
- name: OpenAI
provider: openai
model: gpt-5
apiKey: ${{ secrets.OPENAI_API_KEY }}
roles: [chat, edit, apply]
```

When CONTINUE_USE_BEDROCK=1 is detected, it will use AWS Bedrock configuration. The user must have AWS credentials configured through the standard AWS credential chain (AWS CLI, environment variables, IAM roles, etc.).
The matching env var must be defined in the user's shell, `~/.continue/.env`, or the project's `.env` file. For example:

```bash
export OPENAI_API_KEY=your-key
```

When something in the onboarding flow is done automatically, we should tell the user what happened. For example, when CONTINUE_USE_BEDROCK=1 is detected, the CLI displays: "✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)"
When something in the onboarding flow is done automatically, we should tell the user what happened. For example, when `CONTINUE_USE_BEDROCK=1` is detected, the CLI displays: "✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)".

### AWS Bedrock Environment Variable

Expand All @@ -45,12 +50,16 @@ This will:
- Display a confirmation message to the user
- Mark onboarding as completed

### Headless ANTHROPIC_API_KEY fast path

When the CLI is launched in headless mode (`-p`) without a `--config` flag and `ANTHROPIC_API_KEY` is present in the environment, the CLI writes a minimal Anthropic config to `~/.continue/config.yaml` so the run can proceed without interactive prompts. The key is referenced as `${{ secrets.ANTHROPIC_API_KEY }}` — the existing env var is what authenticates the request, not a value stored in `config.yaml`.

## Normal flow

**The normal flow runs when the user has already completed onboarding.**

1. If the --config flag is provided, load this config
2. If the user is logged in, look for the first assistant in the selected org
3. If there are no assistants in that org, then look for a local ~/.continue/config.yaml
4. If there is no config.yaml, look for an ANTHROPIC_API_KEY in the environment and manually construct the config object to include just the claude-4-sonnet model with that API key
5. If none of the above, then bring the user to step 3 of the onboarding flow
1. If the `--config` flag is provided, load this config.
2. If the user is logged in, look for the first assistant in the selected org.
3. If there are no assistants in that org, then look for a local `~/.continue/config.yaml`.
4. If there is no config.yaml, look for an `ANTHROPIC_API_KEY` in the environment and manually construct the config object to include just the Anthropic model with that API key.
5. If none of the above, then bring the user to the provider picker in step 3 of the onboarding flow.
22 changes: 22 additions & 0 deletions extensions/cli/src/onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,26 @@ describe("CONTINUE_USE_BEDROCK environment variable", () => {
process.stdin.isTTY = originalIsTTY;
}
});

test("should skip interactive onboarding when stdout is not a TTY even if stdin is", async () => {
process.env.CONTINUE_USE_BEDROCK = "0";

vi.resetModules();
const { runOnboardingFlow } = await import("./onboarding.js");

const originalStdinTTY = process.stdin.isTTY;
const originalStdoutTTY = process.stdout.isTTY;
process.stdin.isTTY = true;
process.stdout.isTTY = false;

try {
const result = await runOnboardingFlow(undefined);
// No interactive prompt should be shown; the function should bail out
// and the caller (initializeWithOnboarding) treats it as a no-op.
expect(result).toBe(false);
} finally {
process.stdin.isTTY = originalStdinTTY;
process.stdout.isTTY = originalStdoutTTY;
}
});
});
7 changes: 6 additions & 1 deletion extensions/cli/src/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,17 @@ export async function runOnboardingFlow(
return true;
}

// Skip interactive prompts in non-interactive contexts. We rely on
// explicit signals (env vars) plus the absence of a TTY. The TTY check
// also covers `cn < script` invocations, which would otherwise hang on
// `useInput` with no input available.
const isTestEnv =
process.env.NODE_ENV === "test" ||
process.env.CI === "true" ||
process.env.VITEST === "true" ||
process.env.GITHUB_ACTIONS === "true" ||
!process.stdin.isTTY;
!process.stdin.isTTY ||
!process.stdout.isTTY;

if (isTestEnv) {
return false;
Expand Down
131 changes: 131 additions & 0 deletions extensions/cli/src/ui/components/ProviderPicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { render } from "ink-testing-library";
import React from "react";
import { describe, expect, it, vi } from "vitest";

import { ONBOARDING_PROVIDERS } from "../../onboardingProviders.js";

import { ProviderPicker } from "./ProviderPicker.js";

describe("ProviderPicker", () => {
it("renders the provider list with the first provider selected", () => {
const onSelect = vi.fn();
const onCancel = vi.fn();
const { lastFrame, unmount } = render(
<ProviderPicker onSelect={onSelect} onCancel={onCancel} />,
);

try {
const frame = lastFrame() ?? "";
expect(frame).toContain("Choose your model provider");
expect(frame).toContain(ONBOARDING_PROVIDERS[0].label);
} finally {
unmount();
}
});

it("navigates down with the j key", () => {
const onSelect = vi.fn();
const onCancel = vi.fn();
const { lastFrame, stdin, unmount } = render(
<ProviderPicker onSelect={onSelect} onCancel={onCancel} />,
);

try {
stdin.write("j");
const frame = lastFrame() ?? "";
expect(frame).toContain(ONBOARDING_PROVIDERS[1].label);
} finally {
unmount();
}
});

it("cancels when Escape is pressed on the provider step", () => {
const onSelect = vi.fn();
const onCancel = vi.fn();
const { stdin, unmount } = render(
<ProviderPicker onSelect={onSelect} onCancel={onCancel} />,
);

try {
stdin.write("\u001B"); // ESC
expect(onCancel).toHaveBeenCalled();
expect(onSelect).not.toHaveBeenCalled();
} finally {
unmount();
}
});

it("confirms a simple provider, calls onSelect, and shows the env var reminder", () => {
const onSelect = vi.fn();
const onCancel = vi.fn();
const { lastFrame, stdin, unmount, rerender } = render(
<ProviderPicker onSelect={onSelect} onCancel={onCancel} />,
);

try {
// First provider (OpenAI) has a default model and no apiBase/region,
// so pressing Enter on the provider step advances to the model step.
// Pressing Enter again confirms the pre-filled model and fires onSelect.
stdin.write("\r");
rerender(<ProviderPicker onSelect={onSelect} onCancel={onCancel} />);
stdin.write("\r");
rerender(<ProviderPicker onSelect={onSelect} onCancel={onCancel} />);

expect(onSelect).toHaveBeenCalledTimes(1);
const config = onSelect.mock.calls[0][0];
expect(config).toMatchObject({
name: ONBOARDING_PROVIDERS[0].label,
provider: ONBOARDING_PROVIDERS[0].provider,
apiKeySecret: ONBOARDING_PROVIDERS[0].envVar,
});

const frame = lastFrame() ?? "";
expect(frame).toContain("Configured " + ONBOARDING_PROVIDERS[0].label);
expect(frame).toContain(ONBOARDING_PROVIDERS[0].envVar);
// No more "API key →" input prompt.
expect(frame).not.toContain("API key →");
} finally {
unmount();
}
});

it("collects an apiBase for providers that need one and passes it to onSelect", () => {
// The LiteLLM provider requires an apiBase.
const litellmIndex = ONBOARDING_PROVIDERS.findIndex(
(p) => p.label === "LiteLLM (OpenAI-compatible)",
);
expect(litellmIndex).toBeGreaterThanOrEqual(0);

const onSelect = vi.fn();
const onCancel = vi.fn();
const { stdin, unmount, rerender } = render(
<ProviderPicker onSelect={onSelect} onCancel={onCancel} />,
);

try {
// Move down to the LiteLLM row.
for (let i = 0; i < litellmIndex; i++) {
stdin.write("j");
rerender(<ProviderPicker onSelect={onSelect} onCancel={onCancel} />);
}
// Enter → apiBase step.
stdin.write("\r");
rerender(<ProviderPicker onSelect={onSelect} onCancel={onCancel} />);
// Type the base URL.
stdin.write("https://litellm.example.com");
rerender(<ProviderPicker onSelect={onSelect} onCancel={onCancel} />);
// Enter → model step.
stdin.write("\r");
rerender(<ProviderPicker onSelect={onSelect} onCancel={onCancel} />);
// Enter → confirm (using the default model).
stdin.write("\r");
rerender(<ProviderPicker onSelect={onSelect} onCancel={onCancel} />);

expect(onSelect).toHaveBeenCalledTimes(1);
const config = onSelect.mock.calls[0][0];
expect(config.apiBase).toBe("https://litellm.example.com");
} finally {
unmount();
}
});
});
Loading
Loading