From 09d38ead4336e20d88f37fe8759acc004ccee79b Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 15 Jun 2026 08:05:16 -0700 Subject: [PATCH 1/2] chore: remove CLI-install banner and Generate Rule feature, use explicit onboarding models - Remove CliInstallBanner component, test, and dismissal localStorage key - Remove "Generate Rule" button, GenerateRuleDialog, command, and protocol message - Convert onboarding and new-assistant template off Hub `uses:` slugs to explicit model definitions - Update deprecation banner export link to https://continue.dev/export Co-authored-by: Cursor --- core/config/createNewAssistantFile.ts | 31 +- core/config/onboarding.ts | 128 +++-- core/protocol/ideWebview.ts | 1 - extensions/vscode/package.json | 9 - extensions/vscode/src/commands.ts | 4 - gui/src/components/CliInstallBanner.test.tsx | 491 ------------------ gui/src/components/CliInstallBanner.tsx | 167 ------ gui/src/components/DeprecationBanner.tsx | 2 +- .../GenerateRuleDialog/GenerationScreen.tsx | 297 ----------- .../GenerateRuleDialog/InputScreen.tsx | 93 ---- .../GenerateRuleDialog/RuleTemplateChip.tsx | 26 - .../components/GenerateRuleDialog/index.tsx | 62 --- .../GenerateRuleDialog/ruleTemplates.ts | 36 -- .../GenerateRuleDialog/useRuleGeneration.ts | 139 ----- gui/src/components/Layout.tsx | 12 +- .../StepContainer/ResponseActions.tsx | 28 +- gui/src/pages/config/index.tsx | 2 - gui/src/pages/gui/Chat.tsx | 10 - gui/src/util/localStorage.ts | 1 - 19 files changed, 116 insertions(+), 1423 deletions(-) delete mode 100644 gui/src/components/CliInstallBanner.test.tsx delete mode 100644 gui/src/components/CliInstallBanner.tsx delete mode 100644 gui/src/components/GenerateRuleDialog/GenerationScreen.tsx delete mode 100644 gui/src/components/GenerateRuleDialog/InputScreen.tsx delete mode 100644 gui/src/components/GenerateRuleDialog/RuleTemplateChip.tsx delete mode 100644 gui/src/components/GenerateRuleDialog/index.tsx delete mode 100644 gui/src/components/GenerateRuleDialog/ruleTemplates.ts delete mode 100644 gui/src/components/GenerateRuleDialog/useRuleGeneration.ts diff --git a/core/config/createNewAssistantFile.ts b/core/config/createNewAssistantFile.ts index dc6ecceff1b..acbc275eca7 100644 --- a/core/config/createNewAssistantFile.ts +++ b/core/config/createNewAssistantFile.ts @@ -15,15 +15,28 @@ models: provider: openai model: gpt-5 apiKey: YOUR_OPENAI_API_KEY_HERE - - uses: ollama/qwen2.5-coder-7b - - uses: anthropic/claude-4-sonnet - with: - ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} - -# MCP Servers that Continue can access -# https://docs.continue.dev/customization/mcp-tools -mcpServers: - - uses: anthropic/memory-mcp + - name: qwen2.5-coder 7b + provider: ollama + model: qwen2.5-coder:7b + roles: + - apply + - autocomplete + - chat + - edit + - name: Claude 4 Sonnet + provider: anthropic + model: claude-sonnet-4-20250514 + apiKey: \${{ secrets.ANTHROPIC_API_KEY }} + roles: + - chat + - edit + - apply + defaultCompletionOptions: + contextLength: 200000 + maxTokens: 64000 + capabilities: + - tool_use + - image_input `; export async function createNewAssistantFile( diff --git a/core/config/onboarding.ts b/core/config/onboarding.ts index ed5f0019828..c4fff708be0 100644 --- a/core/config/onboarding.ts +++ b/core/config/onboarding.ts @@ -8,20 +8,84 @@ export const LOCAL_ONBOARDING_CHAT_TITLE = "Llama 3.1 8B"; export const LOCAL_ONBOARDING_EMBEDDINGS_MODEL = "nomic-embed-text:latest"; export const LOCAL_ONBOARDING_EMBEDDINGS_TITLE = "Nomic Embed"; -const ANTHROPIC_MODEL_CONFIG = { - slugs: ["anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6"], - apiKeyInputName: "ANTHROPIC_API_KEY", -}; -const OPENAI_MODEL_CONFIG = { - slugs: ["openai/gpt-4.1", "openai/o3", "openai/gpt-4.1-mini"], - apiKeyInputName: "OPENAI_API_KEY", -}; +type OnboardingModel = NonNullable[number]; -// TODO: These need updating on the hub -const GEMINI_MODEL_CONFIG = { - slugs: ["google/gemini-3.1-pro-preview", "google/gemini-3-flash-preview"], - apiKeyInputName: "GEMINI_API_KEY", -}; +// These model definitions are inlined copies of the corresponding Continue Hub +// blocks (e.g. anthropic/claude-sonnet-4-6) that onboarding previously resolved +// via `uses:` slugs. Since Hub/slug resolution has been removed, we reproduce +// the exact block contents here, with `apiKey` substituted for the block's +// `${{ inputs.*_API_KEY }}` placeholder. Keep these in sync with the Hub blocks. +const ANTHROPIC_ONBOARDING_MODELS = (apiKey: string): OnboardingModel[] => [ + { + name: "Claude Sonnet 4.6", + provider: "anthropic", + model: "claude-sonnet-4-6", + apiKey, + roles: ["chat", "edit", "apply"], + defaultCompletionOptions: { contextLength: 200000, maxTokens: 64000 }, + capabilities: ["tool_use", "image_input"], + }, + { + name: "Claude Opus 4.6", + provider: "anthropic", + model: "claude-opus-4-6", + apiKey, + roles: ["chat", "edit", "apply"], + defaultCompletionOptions: { contextLength: 200000, maxTokens: 64000 }, + capabilities: ["tool_use", "image_input"], + }, +]; + +const OPENAI_ONBOARDING_MODELS = (apiKey: string): OnboardingModel[] => [ + { + name: "OpenAI GPT-4.1", + provider: "openai", + model: "gpt-4.1-2025-04-14", + apiKey, + roles: ["chat", "edit", "apply"], + defaultCompletionOptions: { contextLength: 1047576, maxTokens: 32768 }, + useLegacyCompletionsEndpoint: false, + }, + { + name: "o3", + provider: "openai", + model: "o3", + apiKey, + roles: ["chat"], + defaultCompletionOptions: { contextLength: 200000, maxTokens: 100000 }, + capabilities: ["image_input"], + }, + { + name: "OpenAI GPT-4.1 mini", + provider: "openai", + model: "gpt-4.1-mini-2025-04-14", + apiKey, + roles: ["chat", "edit", "apply"], + defaultCompletionOptions: { contextLength: 1047576, maxTokens: 32768 }, + useLegacyCompletionsEndpoint: false, + }, +]; + +const GEMINI_ONBOARDING_MODELS = (apiKey: string): OnboardingModel[] => [ + { + name: "Gemini 3 Pro Preview", + provider: "gemini", + model: "gemini-3-pro-preview", + apiKey, + roles: ["chat", "edit", "apply"], + defaultCompletionOptions: { contextLength: 1048576, maxTokens: 65536 }, + capabilities: ["tool_use", "image_input"], + }, + { + name: "Gemini 3 Flash Preview", + provider: "gemini", + model: "gemini-3-flash-preview", + apiKey, + roles: ["chat", "edit", "apply"], + defaultCompletionOptions: { contextLength: 1048576, maxTokens: 65536 }, + capabilities: ["tool_use", "image_input"], + }, +]; /** * We set the "best" chat + autocopmlete models by default @@ -70,32 +134,17 @@ export function setupProviderConfig( provider: string, apiKey: string, ): ConfigYaml { - let newModels; + let newModels: OnboardingModel[]; switch (provider) { case "openai": - newModels = OPENAI_MODEL_CONFIG.slugs.map((slug) => ({ - uses: slug, - with: { - [OPENAI_MODEL_CONFIG.apiKeyInputName]: apiKey, - }, - })); + newModels = OPENAI_ONBOARDING_MODELS(apiKey); break; case "anthropic": - newModels = ANTHROPIC_MODEL_CONFIG.slugs.map((slug) => ({ - uses: slug, - with: { - [ANTHROPIC_MODEL_CONFIG.apiKeyInputName]: apiKey, - }, - })); + newModels = ANTHROPIC_ONBOARDING_MODELS(apiKey); break; case "gemini": - newModels = GEMINI_MODEL_CONFIG.slugs.map((slug) => ({ - uses: slug, - with: { - [GEMINI_MODEL_CONFIG.apiKeyInputName]: apiKey, - }, - })); + newModels = GEMINI_ONBOARDING_MODELS(apiKey); break; default: throw new Error(`Unknown provider: ${provider}`); @@ -103,14 +152,19 @@ export function setupProviderConfig( const existingModels = config.models ?? []; - // Update API key on existing models; add new entries for any missing slugs + const isSameModel = (m: OnboardingModel, n: OnboardingModel) => + "provider" in m && + "provider" in n && + m.provider === n.provider && + m.model === n.model; + + // Update API key on existing models; add new entries for any missing models const updatedModels = existingModels.map((m) => { - if (!("uses" in m)) return m; - const match = newModels.find((n) => n.uses === m.uses); - return match ? { ...m, with: { ...m.with, ...match.with } } : m; + const match = newModels.find((n) => isSameModel(m, n)); + return match ? { ...m, apiKey } : m; }); const modelsToAdd = newModels.filter( - (n) => !existingModels.some((m) => "uses" in m && m.uses === n.uses), + (n) => !existingModels.some((m) => isSameModel(m, n)), ); return { ...config, models: [...updatedModels, ...modelsToAdd] }; diff --git a/core/protocol/ideWebview.ts b/core/protocol/ideWebview.ts index 79e7de10b4f..11db483a6fd 100644 --- a/core/protocol/ideWebview.ts +++ b/core/protocol/ideWebview.ts @@ -78,6 +78,5 @@ export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & { updateApplyState: [ApplyState, void]; exitEditMode: [undefined, void]; focusEdit: [undefined, void]; - generateRule: [undefined, void]; addToChat: [AddToChatPayload, void]; }; diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 5a6dbd4a2c9..c7d93b6435e 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -370,12 +370,6 @@ "category": "Continue", "title": "Continue: Reject Jump Suggestion" }, - { - "command": "continue.generateRule", - "category": "Continue", - "title": "Generate Rule", - "group": "Continue" - }, { "command": "continue.openInNewWindow", "category": "Continue", @@ -524,9 +518,6 @@ { "command": "continue.enterEnterpriseLicenseKey" }, - { - "command": "continue.generateRule" - }, { "command": "continue.openInNewWindow" } diff --git a/extensions/vscode/src/commands.ts b/extensions/vscode/src/commands.ts index effd2044179..f9bf3254807 100644 --- a/extensions/vscode/src/commands.ts +++ b/extensions/vscode/src/commands.ts @@ -323,10 +323,6 @@ const getCommandsMap: ( editDecorationManager.clear(); void sidebar.webviewProtocol?.request("exitEditMode", undefined); }, - "continue.generateRule": async () => { - focusGUI(); - void sidebar.webviewProtocol?.request("generateRule", undefined); - }, "continue.writeCommentsForCode": async () => { streamInlineEdit( "comment", diff --git a/gui/src/components/CliInstallBanner.test.tsx b/gui/src/components/CliInstallBanner.test.tsx deleted file mode 100644 index 134b5aae214..00000000000 --- a/gui/src/components/CliInstallBanner.test.tsx +++ /dev/null @@ -1,491 +0,0 @@ -import { - act, - fireEvent, - render, - screen, - waitFor, -} from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { IdeMessengerContext } from "../context/IdeMessenger"; -import { MockIdeMessenger } from "../context/MockIdeMessenger"; -import * as util from "../util"; -import * as localStorage from "../util/localStorage"; -import { CliInstallBanner } from "./CliInstallBanner"; - -vi.mock("../util", async () => { - const actual = await vi.importActual("../util"); - return { - ...actual, - getPlatform: vi.fn(), - }; -}); - -vi.mock("../util/localStorage", async () => { - const actual = await vi.importActual("../util/localStorage"); - return { - ...actual, - getLocalStorage: vi.fn(), - setLocalStorage: vi.fn(), - }; -}); - -describe("CliInstallBanner", () => { - let mockIdeMessenger: MockIdeMessenger; - - beforeEach(() => { - vi.clearAllMocks(); - mockIdeMessenger = new MockIdeMessenger(); - vi.mocked(util.getPlatform).mockReturnValue("mac"); - vi.mocked(localStorage.getLocalStorage).mockReturnValue(undefined); - }); - - const renderComponent = async (subprocessResponse: [string, string]) => { - // Mock the subprocess call on the IDE - vi.spyOn(mockIdeMessenger.ide, "subprocess").mockResolvedValue( - subprocessResponse, - ); - - return act(async () => - render( - - - , - ), - ); - }; - - describe("CLI detection", () => { - it("does not render when CLI is installed (subprocess returns path)", async () => { - await renderComponent(["/usr/local/bin/cn", ""]); - - await waitFor(() => { - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - }); - - it("renders when CLI is not installed (subprocess returns empty)", async () => { - await renderComponent(["", "command not found"]); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - - it("renders when CLI is not installed (subprocess returns empty stdout)", async () => { - await renderComponent(["", ""]); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - - it("uses 'which cn' command on mac platform", async () => { - vi.mocked(util.getPlatform).mockReturnValue("mac"); - const subprocessSpy = vi - .spyOn(mockIdeMessenger.ide, "subprocess") - .mockResolvedValue(["", ""]); - - await act(async () => - render( - - - , - ), - ); - - await waitFor(() => { - expect(subprocessSpy).toHaveBeenCalledWith("which cn"); - }); - }); - - it("uses 'which cn' command on linux platform", async () => { - vi.mocked(util.getPlatform).mockReturnValue("linux"); - const subprocessSpy = vi - .spyOn(mockIdeMessenger.ide, "subprocess") - .mockResolvedValue(["", ""]); - - await act(async () => - render( - - - , - ), - ); - - await waitFor(() => { - expect(subprocessSpy).toHaveBeenCalledWith("which cn"); - }); - }); - - it("uses 'where cn' command on windows platform", async () => { - vi.mocked(util.getPlatform).mockReturnValue("windows"); - const subprocessSpy = vi - .spyOn(mockIdeMessenger.ide, "subprocess") - .mockResolvedValue(["", ""]); - - await act(async () => - render( - - - , - ), - ); - - await waitFor(() => { - expect(subprocessSpy).toHaveBeenCalledWith("where cn"); - }); - }); - - it("handles subprocess errors gracefully", async () => { - vi.spyOn(mockIdeMessenger.ide, "subprocess").mockRejectedValue( - new Error("Command failed"), - ); - - await act(async () => - render( - - - , - ), - ); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - }); - - describe("Banner content", () => { - beforeEach(async () => { - await renderComponent(["", "not found"]); - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - - it("displays the title", () => { - expect(screen.getByText("Try out the Continue CLI")).toBeInTheDocument(); - }); - - it("displays the description with 'cn' code element", () => { - const description = screen.getByText(/Use/); - expect(description).toBeInTheDocument(); - expect(screen.getByText("cn")).toBeInTheDocument(); - }); - - it("displays the installation command", () => { - expect(screen.getByText("npm i -g @continuedev/cli")).toBeInTheDocument(); - }); - - it("displays the Learn more link", () => { - expect(screen.getByText("Learn more.")).toBeInTheDocument(); - }); - - it("displays the close button", () => { - // Get the styled close button (it doesn't have a text label) - const buttons = screen.getAllByRole("button"); - // There should be multiple buttons (close, copy, run) - expect(buttons.length).toBeGreaterThan(0); - }); - - it("displays the CommandLine icon", () => { - // The icon should be present in the component - const banner = screen - .getByText("Try out the Continue CLI") - .closest("div"); - expect(banner).toBeInTheDocument(); - }); - }); - - describe("User interactions", () => { - beforeEach(async () => { - await renderComponent(["", "not found"]); - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - - it("dismisses banner when close button is clicked", async () => { - const buttons = screen.getAllByRole("button"); - // First button should be the close button (CloseButton component) - const closeButton = buttons[0]; - fireEvent.click(closeButton); - - await waitFor(() => { - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - }); - - it("opens documentation URL when Learn more link is clicked", async () => { - const postSpy = vi.spyOn(mockIdeMessenger, "post"); - const learnMoreLink = screen.getByText("Learn more."); - - fireEvent.click(learnMoreLink); - - expect(postSpy).toHaveBeenCalledWith( - "openUrl", - "https://docs.continue.dev/guides/cli", - ); - }); - - it("displays the installation command with interactive controls", async () => { - // The installation command should be visible - expect(screen.getByText("npm i -g @continuedev/cli")).toBeInTheDocument(); - // The "Run" text should be visible for the run button - expect(screen.getByText(/Run/i)).toBeInTheDocument(); - }); - - it("runs installation command in terminal when run button is clicked", async () => { - const postSpy = vi.spyOn(mockIdeMessenger, "post"); - - // Find the "Run" text or CommandLineIcon - const runButton = screen.getByText(/Run/i).closest("div"); - if (runButton) { - fireEvent.click(runButton); - - expect(postSpy).toHaveBeenCalledWith("runCommand", { - command: `npm i -g @continuedev/cli && cn "Explore this repo and provide a concise summary of it's contents"`, - }); - } - }); - }); - - describe("Banner visibility states", () => { - it("does not render while CLI check is loading", async () => { - vi.spyOn(mockIdeMessenger.ide, "subprocess").mockImplementation( - () => - new Promise((resolve) => setTimeout(() => resolve(["", ""]), 100)), - ); - - await act(async () => - render( - - - , - ), - ); - - // Should not be visible immediately - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - - it("remains hidden after dismissal even on re-render", async () => { - const { rerender } = await renderComponent(["", "not found"]); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - - // Dismiss the banner - const buttons = screen.getAllByRole("button"); - const closeButton = buttons[0]; - fireEvent.click(closeButton); - - await waitFor(() => { - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - - // Re-render the component - rerender( - - - , - ); - - // Should still be hidden - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - }); - - describe("Edge cases", () => { - it("handles whitespace in subprocess output", async () => { - await renderComponent([" \n ", ""]); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - - it("detects CLI when path has trailing newline", async () => { - await renderComponent(["/usr/local/bin/cn\n", ""]); - - await waitFor(() => { - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - }); - - it("renders banner when stderr contains 'not found'", async () => { - await renderComponent(["", "cn: command not found"]); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - }); - - describe("Session threshold logic", () => { - const renderWithSessionCount = async ( - sessionCount?: number, - sessionThreshold?: number, - ) => { - vi.spyOn(mockIdeMessenger.ide, "subprocess").mockResolvedValue([ - "", - "not found", - ]); - - return act(async () => - render( - - - , - ), - ); - }; - - it("shows banner when no threshold is set", async () => { - await renderWithSessionCount(0, undefined); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - - it("does not show banner when session count is below threshold", async () => { - await renderWithSessionCount(2, 3); - - await waitFor(() => { - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - }); - - it("shows banner when session count meets threshold", async () => { - await renderWithSessionCount(3, 3); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - - it("shows banner when session count exceeds threshold", async () => { - await renderWithSessionCount(5, 3); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - }); - }); - - describe("Permanent dismissal with localStorage", () => { - const renderWithPermanentDismissal = async () => { - vi.spyOn(mockIdeMessenger.ide, "subprocess").mockResolvedValue([ - "", - "not found", - ]); - - return act(async () => - render( - - - , - ), - ); - }; - - it("does not show banner when previously dismissed permanently", async () => { - vi.mocked(localStorage.getLocalStorage).mockReturnValue(true); - - await renderWithPermanentDismissal(); - - await waitFor(() => { - expect( - screen.queryByText("Try out the Continue CLI"), - ).not.toBeInTheDocument(); - }); - }); - - it("sets localStorage when dismissed with permanentDismissal=true", async () => { - await renderWithPermanentDismissal(); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - - const buttons = screen.getAllByRole("button"); - const closeButton = buttons[0]; - fireEvent.click(closeButton); - - expect(localStorage.setLocalStorage).toHaveBeenCalledWith( - "hasDismissedCliInstallBanner", - true, - ); - }); - - it("does not set localStorage when dismissed with permanentDismissal=false", async () => { - vi.spyOn(mockIdeMessenger.ide, "subprocess").mockResolvedValue([ - "", - "not found", - ]); - - await act(async () => - render( - - - , - ), - ); - - await waitFor(() => { - expect( - screen.getByText("Try out the Continue CLI"), - ).toBeInTheDocument(); - }); - - const buttons = screen.getAllByRole("button"); - const closeButton = buttons[0]; - fireEvent.click(closeButton); - - expect(localStorage.setLocalStorage).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/gui/src/components/CliInstallBanner.tsx b/gui/src/components/CliInstallBanner.tsx deleted file mode 100644 index 36662309afb..00000000000 --- a/gui/src/components/CliInstallBanner.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { CommandLineIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { useContext, useEffect, useRef, useState } from "react"; -import { CloseButton } from "."; -import { IdeMessengerContext } from "../context/IdeMessenger"; -import useCopy from "../hooks/useCopy"; -import { getPlatform } from "../util"; -import { getLocalStorage, setLocalStorage } from "../util/localStorage"; -import { CopyButton } from "./StyledMarkdownPreview/StepContainerPreToolbar/CopyButton"; -import { RunInTerminalButton } from "./StyledMarkdownPreview/StepContainerPreToolbar/RunInTerminalButton"; -import { Card } from "./ui"; - -interface CliInstallBannerProps { - /** Number of sessions user has had - banner shows only if >= sessionThreshold */ - sessionCount?: number; - /** Minimum sessions before showing banner (default: always show) */ - sessionThreshold?: number; - /** If true, dismissal is permanent via localStorage (default: session only) */ - permanentDismissal?: boolean; -} - -export function CliInstallBanner({ - sessionCount, - sessionThreshold, - permanentDismissal = false, -}: CliInstallBannerProps = {}) { - const ideMessenger = useContext(IdeMessengerContext); - const [cliInstalled, setCliInstalled] = useState(null); - const [dismissed, setDismissed] = useState(false); - const commandTextRef = useRef(null); - const { copyText } = useCopy("npm i -g @continuedev/cli"); - const [showCopiedMessage, setShowCopiedMessage] = useState(false); - - const handleCommandClick = () => { - // Select the text - if (commandTextRef.current) { - const selection = window.getSelection(); - const range = document.createRange(); - range.selectNodeContents(commandTextRef.current); - selection?.removeAllRanges(); - selection?.addRange(range); - } - // Copy to clipboard - copyText(); - - // Show "Copied!" message for 3 seconds - setShowCopiedMessage(true); - setTimeout(() => setShowCopiedMessage(false), 3000); - }; - - useEffect(() => { - // Check if user has permanently dismissed the banner - if (permanentDismissal) { - const hasDismissed = getLocalStorage("hasDismissedCliInstallBanner"); - if (hasDismissed) { - setDismissed(true); - return; - } - } - - const checkCliInstallation = async () => { - try { - const platform = getPlatform(); - // Use 'which' on mac/linux, 'where' on windows - const command = platform === "windows" ? "where cn" : "which cn"; - - const [stdout, stderr] = await ideMessenger.ide.subprocess(command); - - // If stdout has content (path to cn), it's installed - // If empty or stderr has "not found", it's not installed - const isInstalled = - stdout.trim().length > 0 && !stderr.includes("not found"); - setCliInstalled(isInstalled); - } catch (error) { - // If subprocess throws an error, assume CLI is not installed - setCliInstalled(false); - } - }; - - void checkCliInstallation(); - }, [ideMessenger, permanentDismissal]); - - const handleDismiss = () => { - setDismissed(true); - if (permanentDismissal) { - setLocalStorage("hasDismissedCliInstallBanner", true); - } - }; - - // Don't show if: - // - Still loading CLI status - // - CLI is already installed - // - User has dismissed it - // - Session threshold not met (if threshold is set) - if ( - cliInstalled === null || - cliInstalled === true || - dismissed || - (sessionThreshold !== undefined && - (sessionCount === undefined || sessionCount < sessionThreshold)) - ) { - return null; - } - - return ( -
- - - - -
-
-
- - Try out the Continue CLI -
-
- Use{" "} - - cn - {" "} - in your terminal interactively and then deploy Continuous AI - workflows.{" "} - - ideMessenger.post( - "openUrl", - "https://docs.continue.dev/guides/cli", - ) - } - className="cursor-pointer underline hover:brightness-125" - > - Learn more. - -
-
-
-
-
- - npm i -g @continuedev/cli - - {showCopiedMessage && ( - - Copied! - - )} -
-
- - -
-
-
-
-
-
- ); -} diff --git a/gui/src/components/DeprecationBanner.tsx b/gui/src/components/DeprecationBanner.tsx index cbfeff77f08..75f362fed76 100644 --- a/gui/src/components/DeprecationBanner.tsx +++ b/gui/src/components/DeprecationBanner.tsx @@ -6,7 +6,7 @@ import { varWithFallback } from "../styles/theme"; import { getLocalStorage, setLocalStorage } from "../util/localStorage"; const EXPIRATION_DATE = new Date("2026-09-09"); -const EXPORT_URL = "https://continue.dev/settings/export"; +const EXPORT_URL = "https://continue.dev/export"; const REPO_URL = "https://github.com/continuedev/continue/blob/main/README.md"; interface DeprecationBannerProps { diff --git a/gui/src/components/GenerateRuleDialog/GenerationScreen.tsx b/gui/src/components/GenerateRuleDialog/GenerationScreen.tsx deleted file mode 100644 index 3868b5a3b7b..00000000000 --- a/gui/src/components/GenerateRuleDialog/GenerationScreen.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import { - createRuleMarkdown, - getRuleType, - RuleType, - RuleTypeDescriptions, -} from "@continuedev/config-yaml"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; -import { createRuleFilePath } from "core/config/markdown/utils"; -import { CreateRuleBlockArgs } from "core/tools/implementations/createRuleBlock"; -import { useContext, useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { IdeMessengerContext } from "../../context/IdeMessenger"; -import Spinner from "../gui/Spinner"; -import { ToolTip } from "../gui/Tooltip"; -import { Button } from "../ui"; -import { useRuleGeneration } from "./useRuleGeneration"; - -interface GenerationScreenProps { - inputPrompt: string; - onBack: () => void; - onSuccess: () => void; - isManualMode?: boolean; -} - -export function GenerationScreen({ - inputPrompt, - onBack, - onSuccess, - isManualMode = false, -}: GenerationScreenProps) { - const ideMessenger = useContext(IdeMessengerContext); - - const { register, watch, setValue, reset } = useForm({ - defaultValues: { - name: "", - description: "", - globs: "", - alwaysApply: undefined, - rule: "", - }, - }); - - const formData = watch(); - - // Track rule type separately from form data - const [selectedRuleType, setSelectedRuleType] = useState( - RuleType.Always, - ); - const [formError, setFormError] = useState(null); - - // Use the generation hook with the input prompt - const { generateRule, isGenerating, error } = useRuleGeneration( - inputPrompt, - (args) => { - // Streaming causes a lot of jank, so wait until done generating - if (!isGenerating) { - reset(args); - handleRuleTypeChange(getRuleType(args)); - } - }, - ); - - // Start generation once when component mounts (only if not in manual mode) - useEffect(() => { - if (!isManualMode) { - void generateRule(); - } - }, [isManualMode]); - - const handleRuleTypeChange = (newRuleType: RuleType) => { - setSelectedRuleType(newRuleType); - - // Update alwaysApply based on rule type - const alwaysApply = newRuleType === RuleType.Always; - setValue("alwaysApply", alwaysApply); - - // Don't clear optional fields - preserve their state - // Users can manually clear them if needed - }; - - const handleContinue = async () => { - // Clear any previous errors - setFormError(null); - - if (!formData.name) { - setFormError("Rule name is required"); - return; - } - - if (!formData.rule) { - setFormError("Rule content is required"); - return; - } - - try { - const options: any = { - alwaysApply: formData.alwaysApply, - }; - - if (formData.description) { - options.description = formData.description; - } - - if (formData.globs) { - options.globs = formData.globs; - } - - const fileContent = createRuleMarkdown( - formData.name, - formData.rule, - options, - ); - - const workspaceDirs = await ideMessenger.request( - "getWorkspaceDirs", - undefined, - ); - - if (workspaceDirs.status !== "success") { - setFormError("Failed to get workspace directory"); - return; - } - - const localContinueDir = workspaceDirs.content[0]; - const ruleFilePath = createRuleFilePath(localContinueDir, formData.name); - - await ideMessenger.request("writeFile", { - path: ruleFilePath, - contents: fileContent, - }); - ideMessenger.post("openFile", { path: ruleFilePath }); - - onSuccess(); - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : "Unknown error occurred"; - setFormError(`Failed to create rule file: ${errorMessage}`); - } - }; - - const showNameSpinner = isGenerating && !formData.name && !isManualMode; - - return ( -
-
-
-

Your rule

-

- Review and edit your generated rule below -

-
-
-
- {/* Rule metadata form */} -
- {/* Rule Name - Always visible */} -
- -
- - {showNameSpinner && ( -
- -
- )} -
-
- - {/* Rule Type Selector - Always visible */} -
-
- - - - -
-
- -
-
- - {/* Description (for Agent Requested only) */} -
- -