Merge branch 'main' into pe/apply-fixes-pt2
This commit is contained in:
commit
890c9b3e7b
|
@ -0,0 +1,30 @@
|
|||
name: unit-testing-rules
|
||||
version: 0.0.1
|
||||
schema: v1
|
||||
rules:
|
||||
- name: unit-testing-rules
|
||||
rule: >-
|
||||
For unit testing in this project:
|
||||
|
||||
|
||||
1. The project uses Jest as the testing framework
|
||||
|
||||
2. Run tests using `npm test` from within the specific package/module
|
||||
directory
|
||||
|
||||
3. Command structure: `cd [directory] && npm test -- [test file path]`
|
||||
|
||||
4. The test script uses experimental VM modules via NODE_OPTIONS flag
|
||||
|
||||
5. Test files follow the pattern `*.test.ts`
|
||||
|
||||
6. Tests must import Jest with `import { jest } from "@jest/globals";`
|
||||
|
||||
7. Run tests from within the specific package directory (e.g., `cd core`
|
||||
for core module tests)
|
||||
|
||||
8. Write tests as top-level `test()` functions - DO NOT use `describe()`
|
||||
blocks
|
||||
|
||||
9. Include the function name being tested in the test description for
|
||||
clarity
|
|
@ -0,0 +1,29 @@
|
|||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
pull_request_target:
|
||||
types: [ opened, closed, synchronize ]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
CLAAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run this workflow on the main repository (continuedev/continue)
|
||||
if: github.repository == 'continuedev/continue'
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (contains(github.event.comment.body, 'recheck') || contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA')) || github.event_name == 'pull_request_target'
|
||||
uses: contributor-assistant/github-action@v2.6.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
path-to-signatures: "signatures/version1/cla.json"
|
||||
path-to-document: "https://github.com/continuedev/continue/blob/main/docs/docs/CLA.md"
|
||||
branch: cla-signatures
|
||||
allowlist: dependabot[bot]
|
|
@ -63,7 +63,7 @@ jobs:
|
|||
|
||||
- name: Bump version in gradle.properties
|
||||
run: |
|
||||
cd extensions/intelllij
|
||||
cd extensions/intellij
|
||||
awk '/pluginVersion=/{split($0,a,"="); split(a[2],b,"."); b[3]+=1; printf "%s=%s.%s.%s\n",a[1],b[1],b[2],b[3];next}1' gradle.properties > tmp && mv tmp gradle.properties
|
||||
rm -rf tmp
|
||||
NEW_VERSION=$(grep 'pluginVersion=' gradle.properties | cut -d'=' -f2)
|
||||
|
|
|
@ -235,10 +235,11 @@ jobs:
|
|||
cd packages/config-yaml
|
||||
npm run build
|
||||
|
||||
- name: Type check
|
||||
- name: Type check and lint
|
||||
run: |
|
||||
cd gui
|
||||
npx tsc --noEmit
|
||||
npm run lint
|
||||
|
||||
binary-checks:
|
||||
needs: [install-root, install-core, install-config-yaml]
|
||||
|
|
|
@ -162,5 +162,8 @@ extensions/.continue-debug/
|
|||
|
||||
*.vsix
|
||||
|
||||
# intellij module library files
|
||||
*.iml
|
||||
|
||||
.continuerules
|
||||
**/.continue/assistants/
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EslintConfiguration">
|
||||
<option name="fix-on-save" value="true" />
|
||||
</component>
|
||||
</project>
|
|
@ -2,5 +2,6 @@
|
|||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,22 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="config-yaml tests" type="JavaScriptTestRunnerJest">
|
||||
<config-file value="$PROJECT_DIR$/packages/config-yaml/jest.config.mjs" />
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--experimental-vm-modules" />
|
||||
<jest-package value="$PROJECT_DIR$/binary/node_modules/jest" />
|
||||
<working-dir value="$PROJECT_DIR$/packages/config-yaml" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2">
|
||||
<option name="NpmBeforeRunTask" enabled="true">
|
||||
<package-json value="$PROJECT_DIR$/packages/config-yaml/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="build" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
</option>
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="core tests" type="JavaScriptTestRunnerJest">
|
||||
<config-file value="$PROJECT_DIR$/core/jest.config.js" />
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--experimental-vm-modules" />
|
||||
<jest-package value="$PROJECT_DIR$/binary/node_modules/jest" />
|
||||
<working-dir value="$PROJECT_DIR$/core" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,25 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="openai-adapters configuration" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--experimental-vm-modules" />
|
||||
<jest-package value="$PROJECT_DIR$/binary/node_modules/jest" />
|
||||
<working-dir value="$PROJECT_DIR$/packages/openai-adapters" />
|
||||
<envs />
|
||||
<scope-kind value="SUITE" />
|
||||
<test-file value="$PROJECT_DIR$/packages/openai-adapters/src/test/main.test.ts" />
|
||||
<test-names>
|
||||
<test-name value="openai configuration" />
|
||||
</test-names>
|
||||
<method v="2">
|
||||
<option name="NpmBeforeRunTask" enabled="true">
|
||||
<package-json value="$PROJECT_DIR$/packages/openai-adapters/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="build" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
</option>
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
196
CONTRIBUTING.md
196
CONTRIBUTING.md
|
@ -3,44 +3,49 @@
|
|||
## Table of Contents
|
||||
|
||||
- [Contributing to Continue](#contributing-to-continue)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [❤️ Ways to Contribute](#️-ways-to-contribute)
|
||||
- [👋 Continue Contribution Ideas](#-continue-contribution-ideas)
|
||||
- [🐛 Report Bugs](#-report-bugs)
|
||||
- [✨ Suggest Enhancements](#-suggest-enhancements)
|
||||
- [📖 Updating / Improving Documentation](#-updating--improving-documentation)
|
||||
- [Running the Documentation Server Locally](#running-the-documentation-server-locally)
|
||||
- [Method 1: NPM Script](#method-1-npm-script)
|
||||
- [Method 2: VS Code Task](#method-2-vs-code-task)
|
||||
- [🧑💻 Contributing Code](#-contributing-code)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [Pre-requisites](#pre-requisites)
|
||||
- [Fork the Continue Repository](#fork-the-continue-repository)
|
||||
- [VS Code](#vs-code)
|
||||
- [Debugging](#debugging)
|
||||
- [JetBrains](#jetbrains)
|
||||
- [Our Git Workflow](#our-git-workflow)
|
||||
- [Development workflow](#development-workflow)
|
||||
- [Formatting](#formatting)
|
||||
- [Testing](#testing)
|
||||
- [Review Process](#review-process)
|
||||
- [Getting help](#getting-help)
|
||||
- [Contribtuing new LLM Providers/Models](#contribtuing-new-llm-providersmodels)
|
||||
- [Adding an LLM Provider](#adding-an-llm-provider)
|
||||
- [Adding Models](#adding-models)
|
||||
- [📐 Continue Architecture](#-continue-architecture)
|
||||
- [Continue VS Code Extension](#continue-vs-code-extension)
|
||||
- [Continue JetBrains Extension](#continue-jetbrains-extension)
|
||||
- [👋 Continue Contribution Ideas](#-continue-contribution-ideas)
|
||||
- [🐛 Report Bugs](#-report-bugs)
|
||||
- [✨ Suggest Enhancements](#-suggest-enhancements)
|
||||
- [📖 Updating / Improving Documentation](#-updating--improving-documentation)
|
||||
- [Running the Documentation Server Locally](#running-the-documentation-server-locally)
|
||||
- [Method 1: NPM Script](#method-1-npm-script)
|
||||
- [Method 2: VS Code Task](#method-2-vs-code-task)
|
||||
- [🧑💻 Contributing Code](#-contributing-code)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [Pre-requisites](#pre-requisites)
|
||||
- [Fork the Continue Repository](#fork-the-continue-repository)
|
||||
- [VS Code](#vs-code)
|
||||
- [Debugging](#debugging)
|
||||
- [JetBrains](#jetbrains)
|
||||
- [Our Git Workflow](#our-git-workflow)
|
||||
- [Development workflow](#development-workflow)
|
||||
- [Formatting](#formatting)
|
||||
- [Testing](#testing)
|
||||
- [Review Process](#review-process)
|
||||
- [Getting help](#getting-help)
|
||||
- [Contribtuing new LLM Providers/Models](#contribtuing-new-llm-providersmodels)
|
||||
- [Adding an LLM Provider](#adding-an-llm-provider)
|
||||
- [Adding Models](#adding-models)
|
||||
- [📐 Continue Architecture](#-continue-architecture)
|
||||
- [Continue VS Code Extension](#continue-vs-code-extension)
|
||||
- [Continue JetBrains Extension](#continue-jetbrains-extension)
|
||||
- [Contibutor License Agreement](#contributor-license-agreement-cla)
|
||||
|
||||
# ❤️ Ways to Contribute
|
||||
|
||||
## 👋 Continue Contribution Ideas
|
||||
|
||||
[This GitHub project board](https://github.com/orgs/continuedev/projects/2) is a list of ideas for how you can contribute to Continue. These aren't the only ways, but are a great starting point if you are new to the project. You can also browse the list of [good first issues](https://github.com/continuedev/continue/issues?q=is:issue%20state:open%20label:good-first-issue).
|
||||
[This GitHub project board](https://github.com/orgs/continuedev/projects/2) is a list of ideas for how you can
|
||||
contribute to Continue. These aren't the only ways, but are a great starting point if you are new to the project. You
|
||||
can also browse the list
|
||||
of [good first issues](https://github.com/continuedev/continue/issues?q=is:issue%20state:open%20label:good-first-issue).
|
||||
|
||||
## 🐛 Report Bugs
|
||||
|
||||
If you find a bug, please [create an issue](https://github.com/continuedev/continue/issues) to report it! A great bug report includes:
|
||||
If you find a bug, please [create an issue](https://github.com/continuedev/continue/issues) to report it! A great bug
|
||||
report includes:
|
||||
|
||||
- A description of the bug
|
||||
- Steps to reproduce
|
||||
|
@ -50,19 +55,22 @@ If you find a bug, please [create an issue](https://github.com/continuedev/conti
|
|||
|
||||
## ✨ Suggest Enhancements
|
||||
|
||||
Continue is quickly adding features, and we'd love to hear which are the most important to you. The best ways to suggest an enhancement are:
|
||||
Continue is quickly adding features, and we'd love to hear which are the most important to you. The best ways to suggest
|
||||
an enhancement are:
|
||||
|
||||
- Create an issue
|
||||
|
||||
- First, check whether a similar proposal has already been made
|
||||
- If not, [create an issue](https://github.com/continuedev/continue/issues)
|
||||
- Please describe the enhancement in as much detail as you can, and why it would be useful
|
||||
- First, check whether a similar proposal has already been made
|
||||
- If not, [create an issue](https://github.com/continuedev/continue/issues)
|
||||
- Please describe the enhancement in as much detail as you can, and why it would be useful
|
||||
|
||||
- Join the [Continue Discord](https://discord.gg/NWtdYexhMs) and tell us about your idea in the `#feedback` channel
|
||||
|
||||
## 📖 Updating / Improving Documentation
|
||||
|
||||
Continue is continuously improving, but a feature isn't complete until it is reflected in the documentation! If you see something out-of-date or missing, you can help by clicking "Edit this page" at the bottom of any page on [docs.continue.dev](https://docs.continue.dev).
|
||||
Continue is continuously improving, but a feature isn't complete until it is reflected in the documentation! If you see
|
||||
something out-of-date or missing, you can help by clicking "Edit this page" at the bottom of any page
|
||||
on [docs.continue.dev](https://docs.continue.dev).
|
||||
|
||||
### Running the Documentation Server Locally
|
||||
|
||||
|
@ -70,7 +78,8 @@ You can run the documentation server locally using either of the following metho
|
|||
|
||||
#### Method 1: NPM Script
|
||||
|
||||
1. Open your terminal and navigate to the `docs` subdirectory of the project. The `docusaurus.config.js` file you'll see there is a sign you're in the right place.
|
||||
1. Open your terminal and navigate to the `docs` subdirectory of the project. The `docusaurus.config.js` file you'll see
|
||||
there is a sign you're in the right place.
|
||||
|
||||
2. Run the following command to install the necessary dependencies for the documentation server:
|
||||
|
||||
|
@ -92,17 +101,22 @@ You can run the documentation server locally using either of the following metho
|
|||
|
||||
3. Look for the `docs:start` task and select it.
|
||||
|
||||
This will start a local server and you can see the documentation rendered in your default browser, typically accessible at `http://localhost:3000`.
|
||||
This will start a local server and you can see the documentation rendered in your default browser, typically accessible
|
||||
at `http://localhost:3000`.
|
||||
|
||||
## 🧑💻 Contributing Code
|
||||
|
||||
We welcome contributions from developers of all experience levels - from first-time contributors to seasoned open source maintainers. While we aim to maintain high standards for reliability and maintainability, our goal is to keep the process as welcoming and straightforward as possible.
|
||||
We welcome contributions from developers of all experience levels - from first-time contributors to seasoned open source
|
||||
maintainers. While we aim to maintain high standards for reliability and maintainability, our goal is to keep the
|
||||
process as welcoming and straightforward as possible.
|
||||
|
||||
### Environment Setup
|
||||
|
||||
#### Pre-requisites
|
||||
|
||||
You should have Node.js version 20.19.0 (LTS) or higher installed. You can get it on [nodejs.org](https://nodejs.org/en/download) or, if you are using NVM (Node Version Manager), you can set the correct version of Node.js for this project by running the following command in the root of the project:
|
||||
You should have Node.js version 20.19.0 (LTS) or higher installed. You can get it
|
||||
on [nodejs.org](https://nodejs.org/en/download) or, if you are using NVM (Node Version Manager), you can set the correct
|
||||
version of Node.js for this project by running the following command in the root of the project:
|
||||
|
||||
```bash
|
||||
nvm use
|
||||
|
@ -114,32 +128,39 @@ nvm use
|
|||
|
||||
2. Clone your forked repository to your local machine. Use: `git clone https://github.com/YOUR_USERNAME/continue.git`
|
||||
|
||||
3. Navigate to the cloned directory and make sure you are on the main branch. Create your feature/fix branch from there, like so: `git checkout -b 123-my-feature-branch`
|
||||
3. Navigate to the cloned directory and make sure you are on the main branch. Create your feature/fix branch from there,
|
||||
like so: `git checkout -b 123-my-feature-branch`
|
||||
|
||||
4. Send your pull request to the main branch.
|
||||
|
||||
#### VS Code
|
||||
|
||||
1. Open the VS Code command pallet (`cmd/ctrl+shift+p`) and select `Tasks: Run Task` and then select `install-all-dependencies`
|
||||
1. Open the VS Code command pallet (`cmd/ctrl+shift+p`) and select `Tasks: Run Task` and then select
|
||||
`install-all-dependencies`
|
||||
|
||||
2. Start debugging:
|
||||
|
||||
1. Switch to Run and Debug view
|
||||
2. Select `Launch extension` from drop down
|
||||
3. Hit play button
|
||||
4. This will start the extension in debug mode and open a new VS Code window with it installed
|
||||
1. The new VS Code window with the extension is referred to as the _Host VS Code_
|
||||
2. The window you started debugging from is referred to as the _Main VS Code_
|
||||
1. Switch to Run and Debug view
|
||||
2. Select `Launch extension` from drop down
|
||||
3. Hit play button
|
||||
4. This will start the extension in debug mode and open a new VS Code window with it installed
|
||||
1. The new VS Code window with the extension is referred to as the _Host VS Code_
|
||||
2. The window you started debugging from is referred to as the _Main VS Code_
|
||||
|
||||
3. To package the extension, run `npm run package` in the `extensions/vscode` directory, select `Tasks: Run Task` and then select `vscode-extension:package`. This will generate `extensions/vscode/build/continue-{VERSION}.vsix`, which you can install by right-clicking and selecting "Install Extension VSIX".
|
||||
3. To package the extension, run `npm run package` in the `extensions/vscode` directory, select `Tasks: Run Task` and
|
||||
then select `vscode-extension:package`. This will generate `extensions/vscode/build/continue-{VERSION}.vsix`, which
|
||||
you can install by right-clicking and selecting "Install Extension VSIX".
|
||||
|
||||
##### Debugging
|
||||
|
||||
**Breakpoints** can be used in both the `core` and `extensions/vscode` folders while debugging, but are not currently supported inside of `gui` code.
|
||||
**Breakpoints** can be used in both the `core` and `extensions/vscode` folders while debugging, but are not currently
|
||||
supported inside of `gui` code.
|
||||
|
||||
**Hot-reloading** is enabled with Vite, so if you make any changes to the `gui`, they should be automatically reflected without rebuilding. In some cases, you may need to refresh the _Host VS Code_ window to see the changes.
|
||||
**Hot-reloading** is enabled with Vite, so if you make any changes to the `gui`, they should be automatically reflected
|
||||
without rebuilding. In some cases, you may need to refresh the _Host VS Code_ window to see the changes.
|
||||
|
||||
Similarly, any changes to `core` or `extensions/vscode` will be automatically included by just reloading the _Host VS Code_ window with cmd/ctrl+shift+p "Reload Window".
|
||||
Similarly, any changes to `core` or `extensions/vscode` will be automatically included by just reloading the _Host VS
|
||||
Code_ window with cmd/ctrl+shift+p "Reload Window".
|
||||
|
||||
#### JetBrains
|
||||
|
||||
|
@ -147,13 +168,20 @@ See [`intellij/CONTRIBUTING.md`](./extensions/intellij/CONTRIBUTING.md) for the
|
|||
|
||||
### Our Git Workflow
|
||||
|
||||
We keep a single permanent branch: `main`. When we are ready to create a "pre-release" version, we create a tag on the `main` branch titled `v1.1.x-vscode`, which automatically triggers the workflow in [preview.yaml](./.github/workflows/preview.yaml), which builds and releases a version of the VS Code extension. When a release has been sufficiently tested, we will create a new release titled `v1.0x-vscode`, triggering a similar workflow in [main.yaml](./.github/workflows/main.yaml), which will build and release a main release of the VS Code extension. Any hotfixes can be made by creating a feature branch from the tag for the release in question. This workflow is well explained by <http://releaseflow.org>.
|
||||
We keep a single permanent branch: `main`. When we are ready to create a "pre-release" version, we create a tag on the
|
||||
`main` branch titled `v1.1.x-vscode`, which automatically triggers the workflow
|
||||
in [preview.yaml](./.github/workflows/preview.yaml), which builds and releases a version of the VS Code extension. When
|
||||
a release has been sufficiently tested, we will create a new release titled `v1.0x-vscode`, triggering a similar
|
||||
workflow in [main.yaml](./.github/workflows/main.yaml), which will build and release a main release of the VS Code
|
||||
extension. Any hotfixes can be made by creating a feature branch from the tag for the release in question. This workflow
|
||||
is well explained by <http://releaseflow.org>.
|
||||
|
||||
### What makes a good PR?
|
||||
|
||||
To keep the Continue codebase clean and maintainable, we expect the following from our own team and all contributors:
|
||||
|
||||
- Open a new issue or comment on an existing one before writing code. This ensures your proposed changes are aligned with the project direction
|
||||
- Open a new issue or comment on an existing one before writing code. This ensures your proposed changes are aligned
|
||||
with the project direction
|
||||
- Keep changes focused. Multiple unrelated fixes should be opened as separate PRs
|
||||
- Write or update tests for new functionality
|
||||
- Update relevant documentation in the `docs` folder
|
||||
|
@ -161,18 +189,22 @@ To keep the Continue codebase clean and maintainable, we expect the following fr
|
|||
|
||||
### Formatting
|
||||
|
||||
Continue uses [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to format JavaScript/TypeScript. Please install the Prettier extension in VS Code and enable "Format on Save" in your settings.
|
||||
Continue uses [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to format
|
||||
JavaScript/TypeScript. Please install the Prettier extension in VS Code and enable "Format on Save" in your settings.
|
||||
|
||||
### Testing
|
||||
|
||||
We have a mix of unit, functional, and e2e test suites, with a primary focus on functional testing. These tests run on each pull request. If your PR causes one of these tests to fail, we will ask that you to resolve the issue before we merge.
|
||||
We have a mix of unit, functional, and e2e test suites, with a primary focus on functional testing. These tests run on
|
||||
each pull request. If your PR causes one of these tests to fail, we will ask that you to resolve the issue before we
|
||||
merge.
|
||||
|
||||
When contributing, please update or create the appropriate tests to help verify the correctness of your implementaiton.
|
||||
|
||||
### Review Process
|
||||
|
||||
- **Initial Review** - A maintainer will be assigned as primary reviewer
|
||||
- **Feedback Loop** - The reviewer may request changes. We value your work, but also want to ensure the code is maintainable and follows our patterns.
|
||||
- **Feedback Loop** - The reviewer may request changes. We value your work, but also want to ensure the code is
|
||||
maintainable and follows our patterns.
|
||||
- **Approval & Merge** - Once the PR is approved, it will be merged into the `main` branch.
|
||||
|
||||
### Getting help
|
||||
|
@ -183,19 +215,53 @@ Join [#contribute on Discord](https://discord.gg/vapESyrFmJ) to engage with main
|
|||
|
||||
### Adding an LLM Provider
|
||||
|
||||
Continue has support for more than a dozen different LLM "providers", making it easy to use models running on OpenAI, Ollama, Together, LM Studio, Msty, and more. You can find all of the existing providers [here](https://github.com/continuedev/continue/tree/main/core/llm/llms), and if you see one missing, you can add it with the following steps:
|
||||
Continue has support for more than a dozen different LLM "providers", making it easy to use models running on OpenAI,
|
||||
Ollama, Together, LM Studio, Msty, and more. You can find all of the existing
|
||||
providers [here](https://github.com/continuedev/continue/tree/main/core/llm/llms), and if you see one missing, you can
|
||||
add it with the following steps:
|
||||
|
||||
1. Create a new file in the `core/llm/llms` directory. The name of the file should be the name of the provider, and it should export a class that extends `BaseLLM`. This class should contain the following minimal implementation. We recommend viewing pre-existing providers for more details. The [LlamaCpp Provider](./core/llm/llms/LlamaCpp.ts) is a good simple example.
|
||||
1. Create a new file in the `core/llm/llms` directory. The name of the file should be the name of the provider, and it
|
||||
should export a class that extends `BaseLLM`. This class should contain the following minimal implementation. We
|
||||
recommend viewing pre-existing providers for more details. The [LlamaCpp Provider](./core/llm/llms/LlamaCpp.ts) is a
|
||||
good simple example.
|
||||
2. Add your provider to the `LLMs` array in [core/llm/llms/index.ts](./core/llm/llms/index.ts).
|
||||
3. If your provider supports images, add it to the `PROVIDER_SUPPORTS_IMAGES` array in [core/llm/autodetect.ts](./core/llm/autodetect.ts).
|
||||
4. Add a documentation page for your provider in [`docs/docs/customize/model-providers/more`](./docs/docs/customize/model-providers/more). This should show an example of configuring your provider in `config.yaml` and explain what options are available.
|
||||
3. If your provider supports images, add it to the `PROVIDER_SUPPORTS_IMAGES` array
|
||||
in [core/llm/autodetect.ts](./core/llm/autodetect.ts).
|
||||
4. Add a documentation page for your provider in [
|
||||
`docs/docs/customize/model-providers/more`](./docs/docs/customize/model-providers/more). This should show an example
|
||||
of configuring your provider in `config.yaml` and explain what options are available.
|
||||
|
||||
### Adding Models
|
||||
|
||||
While any model that works with a supported provider can be used with Continue, we keep a list of recommended models that can be automatically configured from the UI or `config.json`. The following files should be updated when adding a model:
|
||||
While any model that works with a supported provider can be used with Continue, we keep a list of recommended models
|
||||
that can be automatically configured from the UI or `config.json`. The following files should be updated when adding a
|
||||
model:
|
||||
|
||||
- [AddNewModel page](./gui/src/pages/AddNewModel/configs/) - This directory defines which model options are shown in the side bar model selection UI. To add a new model:
|
||||
1. Add a `ModelPackage` entry for the model into [configs/models.ts](./gui/src/pages/AddNewModel/configs/models.ts), following the lead of the many examples near the top of the file
|
||||
2. Add the model within its provider's array to [configs/providers.ts](./gui/src/pages/AddNewModel/configs/providers.ts) (add provider if needed)
|
||||
- LLM Providers: Since many providers use their own custom strings to identify models, you'll have to add the translation from Continue's model name (the one you added to `index.d.ts`) and the model string for each of these providers: [Ollama](./core/llm/llms/Ollama.ts), [Together](./core/llm/llms/Together.ts), and [Replicate](./core/llm/llms/Replicate.ts). You can find their full model lists here: [Ollama](https://ollama.ai/library), [Together](https://docs.together.ai/docs/inference-models), [Replicate](https://replicate.com/collections/streaming-language-models).
|
||||
- [Prompt Templates](./core/llm/autodetect.ts) - In this file you'll find the `autodetectTemplateType` function. Make sure that for the model name you just added, this function returns the correct template type. This is assuming that the chat template for that model is already built in Continue. If not, you will have to add the template type and corresponding edit and chat templates.
|
||||
- [AddNewModel page](./gui/src/pages/AddNewModel/configs/) - This directory defines which model options are shown in the
|
||||
side bar model selection UI. To add a new model:
|
||||
1. Add a `ModelPackage` entry for the model into [configs/models.ts](./gui/src/pages/AddNewModel/configs/models.ts),
|
||||
following the lead of the many examples near the top of the file
|
||||
2. Add the model within its provider's array
|
||||
to [configs/providers.ts](./gui/src/pages/AddNewModel/configs/providers.ts) (add provider if needed)
|
||||
- LLM Providers: Since many providers use their own custom strings to identify models, you'll have to add the
|
||||
translation from Continue's model name (the one you added to `index.d.ts`) and the model string for each of these
|
||||
providers: [Ollama](./core/llm/llms/Ollama.ts), [Together](./core/llm/llms/Together.ts),
|
||||
and [Replicate](./core/llm/llms/Replicate.ts). You can find their full model lists
|
||||
here: [Ollama](https://ollama.ai/library), [Together](https://docs.together.ai/docs/inference-models), [Replicate](https://replicate.com/collections/streaming-language-models).
|
||||
- [Prompt Templates](./core/llm/autodetect.ts) - In this file you'll find the `autodetectTemplateType` function. Make
|
||||
sure that for the model name you just added, this function returns the correct template type. This is assuming that
|
||||
the chat template for that model is already built in Continue. If not, you will have to add the template type and
|
||||
corresponding edit and chat templates.
|
||||
|
||||
## Contributor License Agreement (CLA)
|
||||
|
||||
We require all contributors to accept the CLA and have made it as easy as commenting on your PR:
|
||||
|
||||
1. Open your pull request.
|
||||
2. Paste the following comment (or reply `recheck` if you’ve signed before):
|
||||
|
||||
```text
|
||||
I have read the CLA Document and I hereby sign the CLA
|
||||
```
|
||||
|
||||
3. The CLA‑Assistant bot records your signature in the repo and marks the status check as passed.
|
|
@ -33,6 +33,7 @@
|
|||
"@types/jest": "^29.5.12",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "0.19.11",
|
||||
"jest": "^29.7.0",
|
||||
"pkg": "^5.8.1",
|
||||
|
@ -3196,6 +3197,25 @@
|
|||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"@types/jest": "^29.5.12",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "0.19.11",
|
||||
"jest": "^29.7.0",
|
||||
"pkg": "^5.8.1",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { jest } from "@jest/globals";
|
|||
|
||||
import * as lineStream from "./lineStream";
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
describe("lineStream", () => {
|
||||
let mockFullStop: jest.Mock;
|
||||
|
||||
|
@ -204,6 +205,33 @@ describe("lineStream", () => {
|
|||
});
|
||||
|
||||
describe("filterCodeBlockLines", () => {
|
||||
it("should handle unfenced code", async () => {
|
||||
const linesGenerator = await getLineGenerator(["const x = 5;"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;"]);
|
||||
});
|
||||
|
||||
it("should handle unfenced code with a code block", async () => {
|
||||
const linesGenerator = await getLineGenerator(["const x = 5;","```bash","ls -al","```"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;","```bash","ls -al","```"]);
|
||||
});
|
||||
|
||||
it("should handle unfenced code with two code blocks", async () => {
|
||||
const linesGenerator = await getLineGenerator(["const x = 5;","```bash","ls -al","```","```bash","ls -al","```"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;","```bash","ls -al","```","```bash","ls -al","```"]);
|
||||
});
|
||||
|
||||
it("should remove lines before the first valid line", async () => {
|
||||
const linesGenerator = await getLineGenerator(["```ts", "const x = 5;"]);
|
||||
|
||||
|
@ -213,7 +241,59 @@ describe("lineStream", () => {
|
|||
expect(filteredLines).toEqual(["const x = 5;"]);
|
||||
});
|
||||
|
||||
it.todo("Need some sample inputs to properly test this");
|
||||
it("should remove outer blocks", async () => {
|
||||
const linesGenerator = await getLineGenerator(["```ts", "const x = 5;","```"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;"]);
|
||||
});
|
||||
|
||||
it("should leave inner blocks intact", async () => {
|
||||
const linesGenerator = await getLineGenerator(["```md", "const x = 5;", "```bash","ls -al","```","```"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;","```bash","ls -al","```"]);
|
||||
});
|
||||
|
||||
it("should handle included inner ticks", async () => {
|
||||
const linesGenerator = await getLineGenerator(["```md", "const x = 5;", "```bash","echo ```test```","```","```"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;","```bash","echo ```test```","```"]);
|
||||
});
|
||||
|
||||
it("should leave single inner blocks intact but not return trailing text", async () => {
|
||||
const linesGenerator = await getLineGenerator(["```md", "const x = 5;", "```bash","ls -al","```","```","trailing text"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;","```bash","ls -al","```"]);
|
||||
});
|
||||
|
||||
it("should leave double inner blocks intact but not return trailing text", async () => {
|
||||
const linesGenerator = await getLineGenerator(["```md", "const x = 5;", "```bash","ls -al","```","const y = 10;","```sh","echo `hello world`","```","```","trailing text"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;","```bash","ls -al","```","const y = 10;","```sh","echo `hello world`","```"]);
|
||||
});
|
||||
|
||||
it("should leave inner blocks intact but not return trailing or leading text", async () => {
|
||||
const linesGenerator = await getLineGenerator(["[CODE]", "const x = 5;", "```bash","ls -al","```","[/CODE]","trailing text"]);
|
||||
|
||||
const result = lineStream.filterCodeBlockLines(linesGenerator);
|
||||
const filteredLines = await getFilteredLines(result);
|
||||
|
||||
expect(filteredLines).toEqual(["const x = 5;","```bash","ls -al","```"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterEnglishLinesAtStart", () => {
|
||||
|
|
|
@ -55,8 +55,8 @@ function shouldChangeLineAndStop(line: string): string | undefined {
|
|||
return line;
|
||||
}
|
||||
|
||||
if (line.includes(CODE_START_BLOCK)) {
|
||||
return line.split(CODE_START_BLOCK)[0].trimEnd();
|
||||
if (line.includes(CODE_STOP_BLOCK)) {
|
||||
return line.split(CODE_STOP_BLOCK)[0].trimEnd();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -71,9 +71,9 @@ function isUselessLine(line: string): boolean {
|
|||
return hasUselessLine || trimmed.startsWith("// end");
|
||||
}
|
||||
|
||||
export const USELESS_LINES = ["", "```"];
|
||||
export const USELESS_LINES = [""];
|
||||
export const CODE_KEYWORDS_ENDING_IN_SEMICOLON = ["def"];
|
||||
export const CODE_START_BLOCK = "[/CODE]";
|
||||
export const CODE_STOP_BLOCK = "[/CODE]";
|
||||
export const BRACKET_ENDING_CHARS = [")", "]", "}", ";"];
|
||||
export const PREFIXES_TO_SKIP = ["<COMPLETION>"];
|
||||
export const LINES_TO_STOP_AT = ["# End of file.", "<STOP EDITING HERE"];
|
||||
|
@ -343,34 +343,48 @@ export async function* removeTrailingWhitespace(
|
|||
* 4. Yields processed lines that are part of the actual code block content.
|
||||
*/
|
||||
export async function* filterCodeBlockLines(rawLines: LineStream): LineStream {
|
||||
let seenValidLine = false;
|
||||
let waitingToSeeIfLineIsLast = undefined;
|
||||
let seenFirstFence = false;
|
||||
// nestCount is set to 1 when the entire code block is wrapped with ``` or START blocks. It's then incremented
|
||||
// when an inner code block is discovered to avoid exiting the function prematurly. The function will exit early
|
||||
// when all blocks are matched. When no outer fence is discovered the function will always continue to the end.
|
||||
let nestCount = 0;
|
||||
|
||||
for await (const line of rawLines) {
|
||||
// Filter out starting ```
|
||||
if (!seenValidLine) {
|
||||
|
||||
|
||||
if (!seenFirstFence) {
|
||||
if (shouldRemoveLineBeforeStart(line)) {
|
||||
continue;
|
||||
// Filter out starting ``` or START block
|
||||
continue
|
||||
}
|
||||
seenValidLine = true;
|
||||
// Regardless of a fence or START block start tracking the nesting level
|
||||
seenFirstFence = true;
|
||||
nestCount = 1;
|
||||
}
|
||||
|
||||
// Filter out ending ```
|
||||
if (typeof waitingToSeeIfLineIsLast !== "undefined") {
|
||||
yield waitingToSeeIfLineIsLast;
|
||||
waitingToSeeIfLineIsLast = undefined;
|
||||
}
|
||||
|
||||
const changedEndLine = shouldChangeLineAndStop(line);
|
||||
if (typeof changedEndLine === "string") {
|
||||
yield changedEndLine;
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith("```")) {
|
||||
waitingToSeeIfLineIsLast = line;
|
||||
} else {
|
||||
yield line;
|
||||
if (nestCount > 0) {
|
||||
// Inside a block including the outer block
|
||||
const changedEndLine = shouldChangeLineAndStop(line);
|
||||
if (typeof changedEndLine === "string") {
|
||||
// Ending a block with just backticks (```) or STOP
|
||||
nestCount--;
|
||||
if (nestCount === 0) {
|
||||
// if we are closing the outer block then exit early
|
||||
// only exit early if the outer block was started with a block
|
||||
// it it was text, we will never exit early
|
||||
return;
|
||||
} else {
|
||||
// otherwise just yield the line
|
||||
yield line;
|
||||
}
|
||||
} else if (line.startsWith("```")) {
|
||||
// Going into a nested codeblock
|
||||
nestCount++;
|
||||
yield line;
|
||||
} else {
|
||||
// otherwise just yield the line
|
||||
yield line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,21 @@ const qwenCoderFimTemplate: AutocompleteTemplate = {
|
|||
},
|
||||
};
|
||||
|
||||
const seedCoderFimTemplate: AutocompleteTemplate = {
|
||||
template: "<[fim-prefix]>{{{prefix}}}<[fim-suffix]>{{{suffix}}}<[fim-middle]>",
|
||||
completionOptions: {
|
||||
stop: [
|
||||
"<[end▁of▁sentence]>",
|
||||
"<[fim-prefix]>",
|
||||
"<[fim-middle]>",
|
||||
"<[fim-suffix]>",
|
||||
"<[PAD▁TOKEN]>",
|
||||
"<[SEP▁TOKEN]>",
|
||||
"<[begin▁of▁sentence]>",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const codestralFimTemplate: AutocompleteTemplate = {
|
||||
template: "[SUFFIX]{{{suffix}}}[PREFIX]{{{prefix}}}",
|
||||
completionOptions: {
|
||||
|
@ -426,6 +441,10 @@ export function getTemplateForModel(model: string): AutocompleteTemplate {
|
|||
return qwenCoderFimTemplate;
|
||||
}
|
||||
|
||||
if (lowerCaseModel.includes("seed") && lowerCaseModel.includes("coder")) {
|
||||
return seedCoderFimTemplate;
|
||||
}
|
||||
|
||||
if (
|
||||
lowerCaseModel.includes("starcoder") ||
|
||||
lowerCaseModel.includes("star-coder") ||
|
||||
|
|
|
@ -255,7 +255,7 @@ export class ConfigHandler {
|
|||
if (currentProfile) {
|
||||
this.globalContext.update("lastSelectedProfileForWorkspace", {
|
||||
...selectedProfiles,
|
||||
[profileKey]: selectedProfiles.id ?? null,
|
||||
[profileKey]: currentProfile.profileDescription.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -458,6 +458,7 @@ export interface ChatHistoryItem {
|
|||
toolCallState?: ToolCallState;
|
||||
isGatheringContext?: boolean;
|
||||
reasoning?: Reasoning;
|
||||
appliedRules?: RuleWithSource[];
|
||||
}
|
||||
|
||||
export interface LLMFullCompletionOptions extends BaseCompletionOptions {
|
||||
|
@ -1220,7 +1221,7 @@ export type ApplyStateStatus =
|
|||
| "done" // All changes have been applied, awaiting user to accept/reject
|
||||
| "closed"; // All changes have been applied. Note that for new files, we immediately set the status to "closed"
|
||||
|
||||
export interface UpdateApplyStatePayload {
|
||||
export interface ApplyState {
|
||||
streamId: string;
|
||||
status?: ApplyStateStatus;
|
||||
numDiffs?: number;
|
||||
|
@ -1518,17 +1519,19 @@ export interface TerminalOptions {
|
|||
waitForCompletion?: boolean;
|
||||
}
|
||||
|
||||
export type RuleSource =
|
||||
| "default-chat"
|
||||
| "default-agent"
|
||||
| "model-chat-options"
|
||||
| "model-agent-options"
|
||||
| "rules-block"
|
||||
| "json-systemMessage"
|
||||
| ".continuerules";
|
||||
|
||||
export interface RuleWithSource {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
source:
|
||||
| "default-chat"
|
||||
| "default-agent"
|
||||
| "model-chat-options"
|
||||
| "model-agent-options"
|
||||
| "rules-block"
|
||||
| "json-systemMessage"
|
||||
| ".continuerules";
|
||||
source: RuleSource;
|
||||
globs?: string | string[];
|
||||
rule: string;
|
||||
description?: string;
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
getQueryForFile,
|
||||
} from "../util/treeSitter";
|
||||
|
||||
import { DatabaseConnection, SqliteDb, tagToString } from "./refreshIndex";
|
||||
import { DatabaseConnection, SqliteDb } from "./refreshIndex";
|
||||
import {
|
||||
IndexResultType,
|
||||
MarkCompleteCallback,
|
||||
|
@ -29,6 +29,7 @@ import {
|
|||
getLastNUriRelativePathParts,
|
||||
getUriPathBasename,
|
||||
} from "../util/uri";
|
||||
import { tagToString } from "./utils";
|
||||
|
||||
type SnippetChunk = ChunkWithoutID & { title: string; signature: string };
|
||||
|
||||
|
|
|
@ -3,13 +3,14 @@ import { RETRIEVAL_PARAMS } from "../util/parameters";
|
|||
import { getUriPathBasename } from "../util/uri";
|
||||
|
||||
import { ChunkCodebaseIndex } from "./chunk/ChunkCodebaseIndex";
|
||||
import { DatabaseConnection, SqliteDb, tagToString } from "./refreshIndex";
|
||||
import { DatabaseConnection, SqliteDb } from "./refreshIndex";
|
||||
import {
|
||||
IndexResultType,
|
||||
MarkCompleteCallback,
|
||||
RefreshIndexResults,
|
||||
type CodebaseIndex,
|
||||
} from "./types";
|
||||
import { tagToString } from "./utils";
|
||||
|
||||
export interface RetrieveConfig {
|
||||
tags: BranchAndDir[];
|
||||
|
@ -97,11 +98,14 @@ export class FullTextSearchCodebaseIndex implements CodebaseIndex {
|
|||
|
||||
// Delete
|
||||
for (const item of results.del) {
|
||||
await db.run(`
|
||||
await db.run(
|
||||
`
|
||||
DELETE FROM fts WHERE rowid IN (
|
||||
SELECT id FROM fts_metadata WHERE path = ? AND cacheKey = ?
|
||||
)
|
||||
`,[item.path, item.cacheKey]);
|
||||
`,
|
||||
[item.path, item.cacheKey],
|
||||
);
|
||||
await db.run("DELETE FROM fts_metadata WHERE path = ? AND cacheKey = ?", [
|
||||
item.path,
|
||||
item.cacheKey,
|
||||
|
|
|
@ -14,7 +14,7 @@ import { getUriPathBasename } from "../util/uri";
|
|||
|
||||
import { basicChunker } from "./chunk/basic.js";
|
||||
import { chunkDocument, shouldChunk } from "./chunk/chunk.js";
|
||||
import { DatabaseConnection, SqliteDb, tagToString } from "./refreshIndex.js";
|
||||
import { DatabaseConnection, SqliteDb } from "./refreshIndex.js";
|
||||
import {
|
||||
CodebaseIndex,
|
||||
IndexResultType,
|
||||
|
@ -24,6 +24,7 @@ import {
|
|||
} from "./types";
|
||||
|
||||
import type * as LanceType from "vectordb";
|
||||
import { tagToString } from "./utils";
|
||||
|
||||
interface LanceDbRow {
|
||||
uuid: string;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { RunResult } from "sqlite3";
|
|||
|
||||
import { IContinueServerClient } from "../../continueServer/interface.js";
|
||||
import { Chunk, IndexTag, IndexingProgressUpdate } from "../../index.js";
|
||||
import { DatabaseConnection, SqliteDb, tagToString } from "../refreshIndex.js";
|
||||
import { DatabaseConnection, SqliteDb } from "../refreshIndex.js";
|
||||
import {
|
||||
IndexResultType,
|
||||
MarkCompleteCallback,
|
||||
|
@ -13,8 +13,9 @@ import {
|
|||
type CodebaseIndex,
|
||||
} from "../types.js";
|
||||
|
||||
import { chunkDocument, shouldChunk } from "./chunk.js";
|
||||
import { getUriPathBasename } from "../../util/uri.js";
|
||||
import { tagToString } from "../utils.js";
|
||||
import { chunkDocument, shouldChunk } from "./chunk.js";
|
||||
|
||||
export class ChunkCodebaseIndex implements CodebaseIndex {
|
||||
relativeExpectedTime: number = 1;
|
||||
|
|
|
@ -18,10 +18,6 @@ import {
|
|||
|
||||
export type DatabaseConnection = Database<sqlite3.Database>;
|
||||
|
||||
export function tagToString(tag: IndexTag): string {
|
||||
return `${tag.directory}::${tag.branch}::${tag.artifactId}`;
|
||||
}
|
||||
|
||||
export class SqliteDb {
|
||||
static db: DatabaseConnection | null = null;
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ import { jest } from "@jest/globals";
|
|||
import { IndexTag } from "../..";
|
||||
import { IContinueServerClient } from "../../continueServer/interface";
|
||||
import { ChunkCodebaseIndex } from "../chunk/ChunkCodebaseIndex";
|
||||
import { tagToString } from "../refreshIndex";
|
||||
import { CodebaseIndex, RefreshIndexResults } from "../types";
|
||||
|
||||
import { testIde } from "../../test/fixtures";
|
||||
import { addToTestDir, TEST_DIR } from "../../test/testDir";
|
||||
import { tagToString } from "../utils";
|
||||
|
||||
export const mockFilename = "test.py";
|
||||
export const mockPathAndCacheKey = {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { IndexTag } from "..";
|
||||
import { tagToString } from "./utils";
|
||||
|
||||
test("tagToString returns full tag string when under length limit", () => {
|
||||
const tag: IndexTag = {
|
||||
directory: "/normal/path/to/repo",
|
||||
branch: "main",
|
||||
artifactId: "12345",
|
||||
};
|
||||
|
||||
expect(tagToString(tag)).toBe("/normal/path/to/repo::main::12345");
|
||||
});
|
||||
|
||||
test("tagToString truncates beginning of directory when path is too long", () => {
|
||||
// Create a very long directory path that exceeds MAX_DIR_LENGTH (200)
|
||||
const longPrefix = "/very/long/path/that/will/be/truncated/";
|
||||
const importantSuffix = "/user/important-project/src/feature";
|
||||
const longPath = longPrefix + "x".repeat(200) + importantSuffix;
|
||||
|
||||
const tag: IndexTag = {
|
||||
directory: longPath,
|
||||
branch: "feature-branch",
|
||||
artifactId: "67890",
|
||||
};
|
||||
|
||||
const result = tagToString(tag);
|
||||
|
||||
// The result should keep the important suffix part
|
||||
expect(result).toContain(importantSuffix);
|
||||
// The result should NOT contain the beginning of the path
|
||||
expect(result).not.toContain(longPrefix);
|
||||
// The result should include the branch and artifactId
|
||||
expect(result).toContain("::feature-branch::67890");
|
||||
// The result should be within the MAX_TABLE_NAME_LENGTH limit (240)
|
||||
expect(result.length).toBeLessThanOrEqual(240);
|
||||
});
|
||||
|
||||
test("tagToString preserves branch and artifactId exactly, even when truncating", () => {
|
||||
const longPath = "/a".repeat(300); // Much longer than MAX_DIR_LENGTH
|
||||
const tag: IndexTag = {
|
||||
directory: longPath,
|
||||
branch: "release-v2.0",
|
||||
artifactId: "build-123",
|
||||
};
|
||||
|
||||
const result = tagToString(tag);
|
||||
|
||||
// Should contain the exact branch and artifactId
|
||||
expect(result).toContain("::release-v2.0::build-123");
|
||||
// Should contain the end of the path
|
||||
expect(result).toContain("/a/a/a");
|
||||
// Should not contain the full original path (it should be truncated)
|
||||
expect(result.length).toBeLessThan(
|
||||
longPath.length + "::release-v2.0::build-123".length,
|
||||
);
|
||||
// The result should be within the MAX_TABLE_NAME_LENGTH limit
|
||||
expect(result.length).toBeLessThanOrEqual(240);
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
import { IndexTag } from "..";
|
||||
|
||||
// Maximum length for table names to stay under OS filename limits
|
||||
const MAX_TABLE_NAME_LENGTH = 240;
|
||||
|
||||
// Leave room for branch and artifactId
|
||||
const MAX_DIR_LENGTH = 200;
|
||||
|
||||
/**
|
||||
* Converts an IndexTag to a string representation, safely handling long paths.
|
||||
*
|
||||
* The string is used as a table name and identifier in various places, so it needs
|
||||
* to stay under OS filename length limits (typically 255 chars). This is especially
|
||||
* important for dev containers where the directory path can be very long due to
|
||||
* containing container configuration.
|
||||
*
|
||||
* The format is: "{directory}::{branch}::{artifactId}"
|
||||
*
|
||||
* To handle long paths:
|
||||
* 1. First tries the full string - most backwards compatible
|
||||
* 2. If too long, truncates directory from the beginning to maintain uniqueness
|
||||
* (since final parts of paths are more unique than prefixes)
|
||||
* 3. Finally ensures entire string stays under MAX_TABLE_NAME_LENGTH for OS compatibility
|
||||
*
|
||||
* @param tag The tag containing directory, branch, and artifactId
|
||||
* @returns A string representation safe for use as a table name
|
||||
*/
|
||||
export function tagToString(tag: IndexTag): string {
|
||||
const result = `${tag.directory}::${tag.branch}::${tag.artifactId}`;
|
||||
|
||||
if (result.length <= MAX_TABLE_NAME_LENGTH) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Truncate from the beginning of directory path to preserve the more unique end parts
|
||||
const dir =
|
||||
tag.directory.length > MAX_DIR_LENGTH
|
||||
? tag.directory.slice(tag.directory.length - MAX_DIR_LENGTH)
|
||||
: tag.directory;
|
||||
|
||||
return `${dir}::${tag.branch}::${tag.artifactId}`.slice(
|
||||
0,
|
||||
MAX_TABLE_NAME_LENGTH,
|
||||
);
|
||||
}
|
|
@ -230,6 +230,7 @@ describe("LLM", () => {
|
|||
testFim: true,
|
||||
skip: false,
|
||||
testToolCall: true,
|
||||
timeout: 60000,
|
||||
},
|
||||
);
|
||||
testLLM(
|
||||
|
|
|
@ -23,6 +23,29 @@ const matchesGlobs = (
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters rules that apply to the given message
|
||||
*/
|
||||
export const getApplicableRules = (
|
||||
userMessage: UserChatMessage | ToolResultChatMessage | undefined,
|
||||
rules: RuleWithSource[],
|
||||
): RuleWithSource[] => {
|
||||
const filePathsFromMessage = userMessage
|
||||
? extractPathsFromCodeBlocks(renderChatMessage(userMessage))
|
||||
: [];
|
||||
|
||||
return rules.filter((rule) => {
|
||||
// A rule is active if it has no globs (applies to all files)
|
||||
// or if at least one file path matches its globs
|
||||
const hasNoGlobs = !rule.globs;
|
||||
const matchesAnyFilePath = filePathsFromMessage.some((path) =>
|
||||
matchesGlobs(path, rule.globs),
|
||||
);
|
||||
|
||||
return hasNoGlobs || matchesAnyFilePath;
|
||||
});
|
||||
};
|
||||
|
||||
export const getSystemMessageWithRules = ({
|
||||
baseSystemMessage,
|
||||
userMessage,
|
||||
|
@ -32,23 +55,11 @@ export const getSystemMessageWithRules = ({
|
|||
userMessage: UserChatMessage | ToolResultChatMessage | undefined;
|
||||
rules: RuleWithSource[];
|
||||
}) => {
|
||||
const filePathsFromMessage = userMessage
|
||||
? extractPathsFromCodeBlocks(renderChatMessage(userMessage))
|
||||
: [];
|
||||
|
||||
const applicableRules = getApplicableRules(userMessage, rules);
|
||||
let systemMessage = baseSystemMessage ?? "";
|
||||
|
||||
for (const rule of rules) {
|
||||
// A rule is active if it has no globs (applies to all files)
|
||||
// or if at least one file path matches its globs
|
||||
const hasNoGlobs = !rule.globs;
|
||||
const matchesAnyFilePath = filePathsFromMessage.some((path) =>
|
||||
matchesGlobs(path, rule.globs),
|
||||
);
|
||||
|
||||
if (hasNoGlobs || matchesAnyFilePath) {
|
||||
systemMessage += `\n\n${rule.rule}`;
|
||||
}
|
||||
for (const rule of applicableRules) {
|
||||
systemMessage += `\n\n${rule.rule}`;
|
||||
}
|
||||
|
||||
return systemMessage;
|
||||
|
|
|
@ -165,4 +165,71 @@ export const PROVIDER_TOOL_SUPPORT: Record<
|
|||
)
|
||||
return true;
|
||||
},
|
||||
openrouter: (model) => {
|
||||
// https://openrouter.ai/models?fmt=cards&supported_parameters=tools
|
||||
if (
|
||||
["vision", "math", "guard", "mistrallite", "mistral-openorca"].some(
|
||||
(part) => model.toLowerCase().includes(part),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const supportedPrefixes = [
|
||||
"openai/gpt-3.5",
|
||||
"openai/gpt-4",
|
||||
"openai/o1",
|
||||
"openai/o3",
|
||||
"openai/o4",
|
||||
"anthropic/claude-3",
|
||||
"microsoft/phi-3",
|
||||
"google/gemini-flash-1.5",
|
||||
"google/gemini-2",
|
||||
"google/gemini-pro",
|
||||
"x-ai/grok",
|
||||
"qwen/qwen3",
|
||||
"qwen/qwen-",
|
||||
"cohere/command-r",
|
||||
"ai21/jamba-1.6",
|
||||
"mistralai/mistral",
|
||||
"mistralai/ministral",
|
||||
"mistralai/codestral",
|
||||
"mistralai/mixtral",
|
||||
"mistral/ministral",
|
||||
"mistralai/pixtral",
|
||||
"meta-llama/llama-3.3",
|
||||
"amazon/nova",
|
||||
"deepseek/deepseek-r1",
|
||||
"deepseek/deepseek-chat",
|
||||
"meta-llama/llama-4",
|
||||
"all-hands/openhands-lm-32b",
|
||||
];
|
||||
for (const prefix of supportedPrefixes) {
|
||||
if (model.toLowerCase().startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const specificModels = [
|
||||
"qwen/qwq-32b",
|
||||
"qwen/qwen-2.5-72b-instruct",
|
||||
"meta-llama/llama-3.2-3b-instruct",
|
||||
"meta-llama/llama-3-8b-instruct",
|
||||
"meta-llama/llama-3-70b-instruct",
|
||||
"arcee-ai/caller-large",
|
||||
"nousresearch/hermes-3-llama-3.1-70b",
|
||||
];
|
||||
for (const model of specificModels) {
|
||||
if (model.toLowerCase() === model) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const supportedContains = ["llama-3.1"];
|
||||
for (const model of supportedContains) {
|
||||
if (model.toLowerCase().includes(model)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -15266,9 +15266,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ToIdeFromWebviewOrCoreProtocol } from "./ide";
|
|||
import { ToWebviewFromIdeOrCoreProtocol } from "./webview";
|
||||
|
||||
import {
|
||||
UpdateApplyStatePayload,
|
||||
ApplyState,
|
||||
SetCodeToEditPayload,
|
||||
HighlightedCodePayload,
|
||||
MessageContent,
|
||||
|
@ -81,7 +81,7 @@ export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & {
|
|||
incrementFtc: [undefined, void];
|
||||
openOnboardingCard: [undefined, void];
|
||||
applyCodeFromChat: [undefined, void];
|
||||
updateApplyState: [UpdateApplyStatePayload, void];
|
||||
updateApplyState: [ApplyState, void];
|
||||
exitEditMode: [undefined, void];
|
||||
focusEdit: [undefined, void];
|
||||
};
|
||||
|
|
|
@ -29,11 +29,12 @@ async function callHttpTool(
|
|||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to call tool: ${url}`);
|
||||
throw new Error(`Failed to call tool at ${url}:\n${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.output;
|
||||
}
|
||||
|
||||
|
@ -84,7 +85,7 @@ async function callToolFromUri(
|
|||
});
|
||||
|
||||
if (response.isError === true) {
|
||||
throw new Error(`Failed to call tool: ${toolName}`);
|
||||
throw new Error(JSON.stringify(response.content));
|
||||
}
|
||||
|
||||
const contextItems: ContextItem[] = [];
|
||||
|
@ -130,6 +131,37 @@ async function callToolFromUri(
|
|||
}
|
||||
}
|
||||
|
||||
async function callBuiltInTool(
|
||||
functionName: string,
|
||||
args: any,
|
||||
extras: ToolExtras,
|
||||
): Promise<ContextItem[]> {
|
||||
switch (functionName) {
|
||||
case BuiltInToolNames.ReadFile:
|
||||
return await readFileImpl(args, extras);
|
||||
case BuiltInToolNames.CreateNewFile:
|
||||
return await createNewFileImpl(args, extras);
|
||||
case BuiltInToolNames.GrepSearch:
|
||||
return await grepSearchImpl(args, extras);
|
||||
case BuiltInToolNames.FileGlobSearch:
|
||||
return await fileGlobSearchImpl(args, extras);
|
||||
case BuiltInToolNames.RunTerminalCommand:
|
||||
return await runTerminalCommandImpl(args, extras);
|
||||
case BuiltInToolNames.SearchWeb:
|
||||
return await searchWebImpl(args, extras);
|
||||
case BuiltInToolNames.ViewDiff:
|
||||
return await viewDiffImpl(args, extras);
|
||||
case BuiltInToolNames.LSTool:
|
||||
return await lsToolImpl(args, extras);
|
||||
case BuiltInToolNames.ReadCurrentlyOpenFile:
|
||||
return await readCurrentlyOpenFileImpl(args, extras);
|
||||
case BuiltInToolNames.CreateRuleBlock:
|
||||
return await createRuleBlockImpl(args, extras);
|
||||
default:
|
||||
throw new Error(`Tool "${functionName}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handles calls for core/non-client tools
|
||||
// Returns an error context item if the tool call fails
|
||||
// Note: Edit tool is handled on client
|
||||
|
@ -141,44 +173,11 @@ export async function callTool(
|
|||
contextItems: ContextItem[];
|
||||
errorMessage: string | undefined;
|
||||
}> {
|
||||
const args = JSON.parse(callArgs || "{}");
|
||||
const uri = tool.uri ?? tool.function.name;
|
||||
try {
|
||||
let contextItems: ContextItem[] = [];
|
||||
switch (uri) {
|
||||
case BuiltInToolNames.ReadFile:
|
||||
contextItems = await readFileImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.CreateNewFile:
|
||||
contextItems = await createNewFileImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.GrepSearch:
|
||||
contextItems = await grepSearchImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.FileGlobSearch:
|
||||
contextItems = await fileGlobSearchImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.RunTerminalCommand:
|
||||
contextItems = await runTerminalCommandImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.SearchWeb:
|
||||
contextItems = await searchWebImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.ViewDiff:
|
||||
contextItems = await viewDiffImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.LSTool:
|
||||
contextItems = await lsToolImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.ReadCurrentlyOpenFile:
|
||||
contextItems = await readCurrentlyOpenFileImpl(args, extras);
|
||||
break;
|
||||
case BuiltInToolNames.CreateRuleBlock:
|
||||
contextItems = await createRuleBlockImpl(args, extras);
|
||||
break;
|
||||
default:
|
||||
contextItems = await callToolFromUri(uri, args, extras);
|
||||
}
|
||||
const args = JSON.parse(callArgs || "{}");
|
||||
const contextItems = tool.uri
|
||||
? await callToolFromUri(tool.uri, args, extras)
|
||||
: await callBuiltInTool(tool.function.name, args, extras);
|
||||
if (tool.faviconUrl) {
|
||||
contextItems.forEach((item) => {
|
||||
item.icon = tool.faviconUrl;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# Individual Contributor License Agreement (v1.0, Continue)
|
||||
|
||||
_Based on the Apache Software Foundation Individual CLA v 2.2._
|
||||
|
||||
By commenting **“I have read the CLA Document and I hereby sign the CLA”**
|
||||
on a Pull Request, **you (“Contributor”) agree to the following terms** for any
|
||||
past and future “Contributions” submitted to **Continue (the “Project”)**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
- **“Contribution”** – any original work of authorship submitted to the Project
|
||||
(code, documentation, designs, etc.).
|
||||
- **“You” / “Your”** – the individual (or legal entity) posting the acceptance
|
||||
comment.
|
||||
|
||||
## 2. Copyright License
|
||||
|
||||
You grant **Continue Dev, Inc.** and all recipients of software distributed by the
|
||||
Project a perpetual, worldwide, non‑exclusive, royalty‑free, irrevocable
|
||||
license to reproduce, prepare derivative works of, publicly display, publicly
|
||||
perform, sublicense, and distribute Your Contributions and derivative works.
|
||||
|
||||
## 3. Patent License
|
||||
|
||||
You grant **Continue Dev, Inc.** and all recipients of the Project a perpetual,
|
||||
worldwide, non‑exclusive, royalty‑free, irrevocable (except as below) patent
|
||||
license to make, have made, use, sell, offer to sell, import, and otherwise
|
||||
transfer Your Contributions alone or in combination with the Project.
|
||||
|
||||
If any entity brings patent litigation alleging that the Project or a
|
||||
Contribution infringes a patent, the patent licenses granted by You to that
|
||||
entity under this CLA terminate.
|
||||
|
||||
## 4. Representations
|
||||
|
||||
1. You are legally entitled to grant the licenses above.
|
||||
2. Each Contribution is either Your original creation or You have authority to
|
||||
submit it under this CLA.
|
||||
3. Your Contributions are provided **“AS IS”** without warranties of any kind.
|
||||
4. You will notify the Project if any statement above becomes inaccurate.
|
||||
|
||||
## 5. Miscellany
|
||||
|
||||
This Agreement is governed by the laws of the **State of California**, USA,
|
||||
excluding its conflict‑of‑laws rules. If any provision is held unenforceable,
|
||||
the remaining provisions remain in force.
|
|
@ -81,6 +81,12 @@ Blocks:
|
|||
You can find many examples of each of these block types on
|
||||
the [Continue Explore Page](https://hub.continue.dev/explore/models)
|
||||
|
||||
:::info
|
||||
Local blocks utilizing mustache notation for secrets (`${{ secrets.SECRET_NAME }}`) can read secret values:
|
||||
- globally, from a `.env` located in the global `.continue` folder (`~/.continue/.env`)
|
||||
- per-workspace, from a `.env` file located at the root of the current workspace.
|
||||
:::
|
||||
|
||||
### Inputs
|
||||
|
||||
Blocks can be passed user inputs, including hub secrets and raw text values. To create a block that has an input, use
|
||||
|
@ -107,7 +113,7 @@ models:
|
|||
TEMP: 0.9
|
||||
```
|
||||
|
||||
Note that hub secrets can be passed as inputs, using the a similar mustache format: `secrets.SECRET_NAME`.
|
||||
Note that hub secrets can be passed as inputs, using a similar mustache format: `secrets.SECRET_NAME`.
|
||||
|
||||
### Overrides
|
||||
|
||||
|
@ -200,7 +206,6 @@ chat, editing, and summarizing.
|
|||
- `topP`: The cumulative probability for nucleus sampling.
|
||||
- `topK`: Maximum number of tokens considered at each step.
|
||||
- `stop`: An array of stop tokens that will terminate the completion.
|
||||
- `n`: Number of completions to generate.
|
||||
- `reasoning`: Boolean to enable thinking/reasoning for Anthropic Claude 3.7+ models.
|
||||
- `reasoningBudgetTokens`: Budget tokens for thinking/reasoning in Anthropic Claude 3.7+ models.
|
||||
- `requestOptions`: HTTP request options specific to the model.
|
||||
|
|
|
@ -7,8 +7,6 @@ keywords: [reload, delete, manually, logs, server, console]
|
|||
import TabItem from "@theme/TabItem";
|
||||
import Tabs from "@theme/Tabs";
|
||||
|
||||
The Continue VS Code extension is currently in beta, and the JetBrains extension is in Alpha. If you are having trouble, please follow the steps below.
|
||||
|
||||
1. [Check the logs](#check-the-logs)
|
||||
2. [Try the latest pre-release](#download-the-latest-pre-release)
|
||||
3. [Download an older version](#download-an-older-version)
|
||||
|
|
|
@ -65,6 +65,10 @@ const config = {
|
|||
],
|
||||
],
|
||||
|
||||
scripts: [
|
||||
'!function(){var e,t,n;e="7aa28ed11570734",t=function(){Reo.init({clientID:"7aa28ed11570734"})},(n=document.createElement("script")).src="https://static.reo.dev/"+e+"/reo.js",n.defer=!0,n.onload=t,document.head.appendChild(n)}();',
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import("@docusaurus/preset-classic").ThemeConfig} */
|
||||
({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Start Core Dev Server (CE)" type="ShConfigurationType">
|
||||
<option name="SCRIPT_TEXT" value="cross-env 'PROJECT_DIR="$PROJECT_DIR$"' node core-dev-server.js" />
|
||||
<option name="SCRIPT_TEXT" value="npx cross-env 'PROJECT_DIR="$PROJECT_DIR$"' node core-dev-server.js" />
|
||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||
<option name="SCRIPT_PATH" value="" />
|
||||
<option name="SCRIPT_OPTIONS" value="" />
|
||||
|
|
|
@ -3,13 +3,13 @@ pluginGroup=com.github.continuedev.continueintellijextension
|
|||
pluginName=continue-intellij-extension
|
||||
pluginRepositoryUrl=https://github.com/continuedev/continue
|
||||
# SemVer format -> https://semver.org
|
||||
pluginVersion=1.0.17
|
||||
pluginVersion=1.0.18
|
||||
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
|
||||
pluginSinceBuild=223
|
||||
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
|
||||
platformType=IC
|
||||
platformVersion=2022.3.3
|
||||
#org.gradle.java.home=/opt/homebrew/opt/openjdk@17
|
||||
# org.gradle.java.home=/opt/homebrew/opt/openjdk@17
|
||||
#platformVersion = LATEST-EAP-SNAPSHOT
|
||||
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
|
||||
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.github.continuedev.continueintellijextension.activities
|
||||
|
||||
import IntelliJIDE
|
||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
|
||||
import com.github.continuedev.continueintellijextension.auth.AuthListener
|
||||
import com.github.continuedev.continueintellijextension.auth.ContinueAuthService
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import com.github.continuedev.continueintellijextension.ApplyState
|
||||
import com.github.continuedev.continueintellijextension.ApplyStateStatus
|
||||
import com.github.continuedev.continueintellijextension.IDE
|
||||
import com.github.continuedev.continueintellijextension.ToastType
|
||||
import com.github.continuedev.continueintellijextension.editor.DiffStreamHandler
|
||||
|
@ -27,26 +29,54 @@ class ApplyToFileHandler(
|
|||
) {
|
||||
|
||||
suspend fun handleApplyToFile() {
|
||||
// Notify webview that we're starting to stream
|
||||
notifyStreamStarted()
|
||||
|
||||
if (editorUtils == null) {
|
||||
ide.showToast(ToastType.ERROR, "No active editor to apply edits to")
|
||||
notifyStreamClosed()
|
||||
return
|
||||
}
|
||||
|
||||
if (editorUtils.isDocumentEmpty()) {
|
||||
editorUtils.insertTextIntoEmptyDocument(params.text)
|
||||
notifyStreamClosed()
|
||||
return
|
||||
}
|
||||
|
||||
// Get the LLM configuration for applying edits
|
||||
val llm = fetchApplyLLMConfig() ?: run {
|
||||
ide.showToast(ToastType.ERROR, "Failed to fetch model configuration")
|
||||
notifyStreamClosed()
|
||||
return
|
||||
}
|
||||
|
||||
setupAndStreamDiffs(editorUtils, llm)
|
||||
}
|
||||
|
||||
private fun notifyStreamStarted() {
|
||||
sendApplyStateUpdate(ApplyStateStatus.STREAMING)
|
||||
}
|
||||
|
||||
private fun notifyStreamClosed(numDiffs: Int? = 0) {
|
||||
sendApplyStateUpdate(ApplyStateStatus.CLOSED, numDiffs)
|
||||
}
|
||||
|
||||
private fun sendApplyStateUpdate(
|
||||
status: ApplyStateStatus,
|
||||
numDiffs: Int? = null
|
||||
) {
|
||||
val payload = ApplyState(
|
||||
streamId = params.streamId,
|
||||
status = status,
|
||||
numDiffs = numDiffs,
|
||||
filepath = params.filepath,
|
||||
fileContent = params.text,
|
||||
toolCallId = params.toolCallId.toString()
|
||||
)
|
||||
|
||||
continuePluginService.sendToWebview("updateApplyState", payload)
|
||||
}
|
||||
|
||||
private suspend fun fetchApplyLLMConfig(): Any? {
|
||||
return try {
|
||||
|
|
|
@ -62,7 +62,7 @@ class DiffManager(private val project: Project) : DumbAware {
|
|||
file.createNewFile()
|
||||
}
|
||||
file.writeText(replacement)
|
||||
openDiffWindow(URI(filepath).toString(), file.toURI().toString(), stepIndex)
|
||||
openDiffWindow(UriUtils.parseUri(filepath).toString(), file.toURI().toString(), stepIndex)
|
||||
}
|
||||
|
||||
private fun cleanUpFile(file2: String) {
|
||||
|
@ -80,7 +80,8 @@ class DiffManager(private val project: Project) : DumbAware {
|
|||
val diffInfo = diffInfoMap[file] ?: return
|
||||
|
||||
// Write contents to original file
|
||||
val virtualFile = LocalFileSystem.getInstance().findFileByPath(URI(diffInfo.originalFilepath).path) ?: return
|
||||
val virtualFile =
|
||||
LocalFileSystem.getInstance().findFileByPath(UriUtils.parseUri(diffInfo.originalFilepath).path) ?: return
|
||||
val document = FileDocumentManager.getInstance().getDocument(virtualFile) ?: return
|
||||
WriteCommandAction.runWriteCommandAction(project) {
|
||||
document.setText(File(file).readText())
|
||||
|
@ -119,8 +120,8 @@ class DiffManager(private val project: Project) : DumbAware {
|
|||
lastFile2 = file2
|
||||
|
||||
// Create a DiffContent for each of the texts you want to compare
|
||||
val content1: DiffContent = DiffContentFactory.getInstance().create(File(URI(file1)).readText())
|
||||
val content2: DiffContent = DiffContentFactory.getInstance().create(File(URI(file2)).readText())
|
||||
val content1: DiffContent = DiffContentFactory.getInstance().create(UriUtils.uriToFile(file1).readText())
|
||||
val content2: DiffContent = DiffContentFactory.getInstance().create(UriUtils.uriToFile(file2).readText())
|
||||
|
||||
// Create a SimpleDiffRequest and populate it with the DiffContents and titles
|
||||
val diffRequest = SimpleDiffRequest("Continue Diff", content1, content2, "Old", "New")
|
||||
|
|
|
@ -7,9 +7,7 @@ import com.intellij.openapi.project.guessProjectDir
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import java.net.URI
|
||||
|
||||
class GitService(
|
||||
private val project: Project,
|
||||
|
@ -52,7 +50,7 @@ class GitService(
|
|||
} else {
|
||||
ProcessBuilder("git", "diff", "--cached")
|
||||
}
|
||||
builder.directory(File(URI(workspaceDir)))
|
||||
builder.directory(UriUtils.uriToFile(workspaceDir))
|
||||
val process = withContext(Dispatchers.IO) {
|
||||
builder.start()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import IntelliJIDE
|
||||
import com.github.continuedev.continueintellijextension.*
|
||||
import com.github.continuedev.continueintellijextension.activities.ContinuePluginDisposable
|
||||
import com.github.continuedev.continueintellijextension.activities.showTutorial
|
||||
|
@ -42,6 +41,7 @@ class IdeProtocolClient(
|
|||
*
|
||||
* See this thread for details: https://github.com/continuedev/continue/issues/4098#issuecomment-2854865310
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val limitedDispatcher = Dispatchers.IO.limitedParallelism(4)
|
||||
|
||||
init {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import com.github.continuedev.continueintellijextension.*
|
||||
import com.github.continuedev.continueintellijextension.constants.getContinueGlobalPath
|
||||
import com.github.continuedev.continueintellijextension.constants.ContinueConstants
|
||||
|
@ -167,7 +169,7 @@ class IntelliJIDE(
|
|||
// Find any .continuerc.json files
|
||||
for (file in contents) {
|
||||
if (file.endsWith(".continuerc.json")) {
|
||||
val fileContent = File(URI(file)).readText()
|
||||
val fileContent = UriUtils.uriToFile(file).readText()
|
||||
configs.add(fileContent)
|
||||
}
|
||||
}
|
||||
|
@ -178,12 +180,12 @@ class IntelliJIDE(
|
|||
}
|
||||
|
||||
override suspend fun fileExists(filepath: String): Boolean {
|
||||
val file = File(URI(filepath))
|
||||
val file = UriUtils.uriToFile(filepath)
|
||||
return file.exists()
|
||||
}
|
||||
|
||||
override suspend fun writeFile(path: String, contents: String) {
|
||||
val file = File(URI(path))
|
||||
val file = UriUtils.uriToFile(path)
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeText(contents)
|
||||
}
|
||||
|
@ -201,7 +203,7 @@ class IntelliJIDE(
|
|||
|
||||
override suspend fun openFile(path: String) {
|
||||
// Convert URI path to absolute file path
|
||||
val filePath = File(URI(path)).absolutePath
|
||||
val filePath = UriUtils.uriToFile(path).absolutePath
|
||||
// Find the file using the absolute path
|
||||
val file = withContext(Dispatchers.IO) {
|
||||
LocalFileSystem.getInstance().refreshAndFindFileByPath(filePath)
|
||||
|
@ -216,7 +218,7 @@ class IntelliJIDE(
|
|||
|
||||
override suspend fun openUrl(url: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Desktop.browse(java.net.URI(url))
|
||||
Desktop.browse(URI(url))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,7 +228,8 @@ class IntelliJIDE(
|
|||
|
||||
override suspend fun saveFile(filepath: String) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
val file = LocalFileSystem.getInstance().findFileByPath(URI(filepath).path) ?: return@invokeLater
|
||||
val file =
|
||||
LocalFileSystem.getInstance().findFileByPath(UriUtils.parseUri(filepath).path) ?: return@invokeLater
|
||||
val fileDocumentManager = FileDocumentManager.getInstance()
|
||||
val document = fileDocumentManager.getDocument(file)
|
||||
|
||||
|
@ -239,7 +242,7 @@ class IntelliJIDE(
|
|||
override suspend fun readFile(filepath: String): String {
|
||||
return try {
|
||||
val content = ApplicationManager.getApplication().runReadAction<String?> {
|
||||
val virtualFile = LocalFileSystem.getInstance().findFileByPath(URI(filepath).path)
|
||||
val virtualFile = LocalFileSystem.getInstance().findFileByPath(UriUtils.parseUri(filepath).path)
|
||||
if (virtualFile != null && FileDocumentManager.getInstance().isFileModified(virtualFile)) {
|
||||
return@runReadAction FileDocumentManager.getInstance().getDocument(virtualFile)?.text
|
||||
}
|
||||
|
@ -249,7 +252,7 @@ class IntelliJIDE(
|
|||
if (content != null) {
|
||||
content
|
||||
} else {
|
||||
val file = File(URI(filepath))
|
||||
val file = UriUtils.uriToFile(filepath)
|
||||
if (!file.exists() || file.isDirectory) return ""
|
||||
withContext(Dispatchers.IO) {
|
||||
FileInputStream(file).use { fis ->
|
||||
|
@ -455,7 +458,7 @@ class IntelliJIDE(
|
|||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val builder = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
builder.directory(File(URI(dir)))
|
||||
builder.directory(UriUtils.uriToFile(dir))
|
||||
val process = builder.start()
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val output = reader.readLine()
|
||||
|
@ -485,7 +488,7 @@ class IntelliJIDE(
|
|||
|
||||
override suspend fun getRepoName(dir: String): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val directory = File(URI(dir))
|
||||
val directory = UriUtils.uriToFile(dir)
|
||||
val targetDir = if (directory.isFile) directory.parentFile else directory
|
||||
val builder = ProcessBuilder("git", "config", "--get", "remote.origin.url")
|
||||
builder.directory(targetDir)
|
||||
|
@ -549,7 +552,7 @@ class IntelliJIDE(
|
|||
override suspend fun getGitRootPath(dir: String): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val builder = ProcessBuilder("git", "rev-parse", "--show-toplevel")
|
||||
builder.directory(File(URI(dir)))
|
||||
builder.directory(UriUtils.uriToFile(dir))
|
||||
val process = builder.start()
|
||||
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
|
@ -560,7 +563,7 @@ class IntelliJIDE(
|
|||
}
|
||||
|
||||
override suspend fun listDir(dir: String): List<List<Any>> {
|
||||
val files = File(URI(dir)).listFiles()?.map {
|
||||
val files = UriUtils.uriToFile(dir).listFiles()?.map {
|
||||
listOf(it.name, if (it.isDirectory) FileType.DIRECTORY.value else FileType.FILE.value)
|
||||
} ?: emptyList()
|
||||
|
||||
|
@ -569,7 +572,7 @@ class IntelliJIDE(
|
|||
|
||||
override suspend fun getFileStats(files: List<String>): Map<String, FileStats> {
|
||||
return files.associateWith { file ->
|
||||
FileStats(File(URI(file)).lastModified(), File(URI(file)).length())
|
||||
FileStats(UriUtils.uriToFile(file).lastModified(), UriUtils.uriToFile(file).length())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -587,7 +590,7 @@ class IntelliJIDE(
|
|||
}
|
||||
|
||||
private fun setFileOpen(filepath: String, open: Boolean = true) {
|
||||
val file = LocalFileSystem.getInstance().findFileByPath(URI(filepath).path)
|
||||
val file = LocalFileSystem.getInstance().findFileByPath(UriUtils.uriToFile(filepath).path)
|
||||
|
||||
file?.let {
|
||||
if (open) {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Utility class for URI operations
|
||||
*/
|
||||
object UriUtils {
|
||||
/**
|
||||
* Parses a URI string into a URI object, handling special cases for Windows file paths
|
||||
*/
|
||||
fun parseUri(uri: String): URI {
|
||||
try {
|
||||
// Remove query parameters if present
|
||||
val uriStr = uri.substringBefore("?")
|
||||
|
||||
// Handle Windows file paths with authority component
|
||||
if (uriStr.startsWith("file://") && !uriStr.startsWith("file:///")) {
|
||||
val path = uriStr.substringAfter("file://")
|
||||
return URI("file:///$path")
|
||||
}
|
||||
|
||||
// Standard URI handling for other cases
|
||||
val uriWithoutQuery = URI(uriStr)
|
||||
return uriWithoutQuery
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing URI: $uri ${e.message}")
|
||||
throw Exception("Invalid URI: $uri ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a URI string to a File object
|
||||
*/
|
||||
fun uriToFile(uri: String): File {
|
||||
return File(parseUri(uri))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package com.github.continuedev.continueintellijextension.services
|
||||
|
||||
import IntelliJIDE
|
||||
import com.github.continuedev.continueintellijextension.`continue`.CoreMessenger
|
||||
import com.github.continuedev.continueintellijextension.`continue`.CoreMessengerManager
|
||||
import com.github.continuedev.continueintellijextension.`continue`.DiffManager
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.github.continuedev.continueintellijextension
|
||||
|
||||
import com.github.continuedev.continueintellijextension.editor.RangeInFileWithContents
|
||||
import com.google.gson.JsonElement
|
||||
|
||||
enum class ToastType(val value: String) {
|
||||
|
@ -57,6 +56,12 @@ data class RangeInFile(
|
|||
val range: Range
|
||||
)
|
||||
|
||||
data class RangeInFileWithContents(
|
||||
val filepath: String,
|
||||
val range: Range,
|
||||
val contents: String
|
||||
)
|
||||
|
||||
data class ControlPlaneSessionInfo(
|
||||
val accessToken: String,
|
||||
val account: Account
|
||||
|
@ -84,6 +89,7 @@ data class ContinueRcJson(
|
|||
val mergeBehavior: ConfigMergeType
|
||||
)
|
||||
|
||||
|
||||
interface IDE {
|
||||
suspend fun getIdeInfo(): IdeInfo
|
||||
|
||||
|
@ -208,17 +214,25 @@ data class AcceptRejectDiff(val accepted: Boolean, val stepIndex: Int)
|
|||
|
||||
data class DeleteAtIndex(val index: Int)
|
||||
|
||||
enum class ApplyStateStatus(val status: String) {
|
||||
NOT_STARTED("not-started"),
|
||||
STREAMING("streaming"),
|
||||
DONE("done"),
|
||||
CLOSED("closed");
|
||||
enum class ApplyStateStatus {
|
||||
NOT_STARTED, // Apply state created but not necessarily streaming
|
||||
STREAMING, // Changes are being applied to the file
|
||||
DONE, // All changes have been applied, awaiting user to accept/reject
|
||||
CLOSED; // All changes have been applied. Note that for new files, we immediately set the status to "closed"
|
||||
|
||||
companion object {
|
||||
fun toString(status: ApplyStateStatus): String = when (status) {
|
||||
NOT_STARTED -> "not-started"
|
||||
STREAMING -> "streaming"
|
||||
DONE -> "done"
|
||||
CLOSED -> "closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class UpdateApplyStatePayload(
|
||||
data class ApplyState(
|
||||
val streamId: String,
|
||||
val status: String,
|
||||
val status: ApplyStateStatus? = null,
|
||||
val numDiffs: Int? = null,
|
||||
val filepath: String? = null,
|
||||
val fileContent: String? = null,
|
||||
|
@ -231,17 +245,3 @@ data class HighlightedCodePayload(
|
|||
val shouldRun: Boolean? = null
|
||||
)
|
||||
|
||||
data class StreamDiffLinesPayload(
|
||||
val prefix: String,
|
||||
val highlighted: String,
|
||||
val suffix: String,
|
||||
val input: String,
|
||||
val language: String?,
|
||||
val modelTitle: String?,
|
||||
val includeRulesInSystemMessage: Boolean
|
||||
)
|
||||
|
||||
data class AcceptOrRejectDiffPayload(
|
||||
val filepath: String,
|
||||
val streamId: String? = null
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ package com.github.continuedev.continueintellijextension.unit
|
|||
|
||||
import com.github.continuedev.continueintellijextension.ApplyStateStatus
|
||||
import com.github.continuedev.continueintellijextension.IDE
|
||||
import com.github.continuedev.continueintellijextension.UpdateApplyStatePayload
|
||||
import com.github.continuedev.continueintellijextension.ApplyState
|
||||
import com.github.continuedev.continueintellijextension.`continue`.ApplyToFileHandler
|
||||
import com.github.continuedev.continueintellijextension.`continue`.CoreMessenger
|
||||
import com.github.continuedev.continueintellijextension.editor.DiffStreamService
|
||||
|
@ -26,6 +26,7 @@ class ApplyToFileHandlerTest {
|
|||
private val mockEditorUtils = mockk<EditorUtils>(relaxed = true)
|
||||
private val mockDiffStreamService = mockk<DiffStreamService>(relaxed = true)
|
||||
private val mockEditor = mockk<Editor>(relaxed = true)
|
||||
private val mockCoreMessenger = mockk<CoreMessenger>(relaxed = true)
|
||||
|
||||
// Test subject
|
||||
private lateinit var handler: ApplyToFileHandler
|
||||
|
@ -42,6 +43,7 @@ class ApplyToFileHandlerTest {
|
|||
fun setUp() {
|
||||
// Common setup
|
||||
every { mockEditorUtils.editor } returns mockEditor
|
||||
every { mockContinuePluginService.coreMessenger } returns mockCoreMessenger
|
||||
|
||||
// Create the handler with mocked dependencies
|
||||
handler = ApplyToFileHandler(
|
||||
|
@ -65,5 +67,28 @@ class ApplyToFileHandlerTest {
|
|||
// Then
|
||||
verify { mockEditorUtils.insertTextIntoEmptyDocument(testParams.text) }
|
||||
verify(exactly = 0) { mockDiffStreamService.register(any(), any()) } // Ensure no diff streaming happened
|
||||
|
||||
// Verify notifications sent
|
||||
verify {
|
||||
mockContinuePluginService.sendToWebview(
|
||||
eq("updateApplyState"),
|
||||
withArg { payload ->
|
||||
assert(payload is ApplyState)
|
||||
assert((payload as ApplyState).status == ApplyStateStatus.STREAMING)
|
||||
},
|
||||
any()
|
||||
)
|
||||
}
|
||||
|
||||
verify {
|
||||
mockContinuePluginService.sendToWebview(
|
||||
eq("updateApplyState"),
|
||||
withArg { payload ->
|
||||
assert(payload is ApplyState)
|
||||
assert((payload as ApplyState).status == ApplyStateStatus.CLOSED)
|
||||
},
|
||||
any()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package com.github.continuedev.continueintellijextension.unit
|
||||
|
||||
import com.github.continuedev.continueintellijextension.`continue`.UriUtils
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class UriUtilsTest {
|
||||
@Test
|
||||
fun testUriToFile() {
|
||||
val uri = "file:///path/to/file"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file"), file)
|
||||
}
|
||||
|
||||
@Disabled("Not working")
|
||||
@Test
|
||||
fun testUriToFileWithWindowsPath() {
|
||||
val uri = "file:///C:/path/to/file"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("C:/path/to/file"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldHandleAuthorityComponent() {
|
||||
val uri = "file://C:/path/to/file"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/C:/path/to/file"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithSpaces() {
|
||||
val uri = "file:///path/to/file%20with%20spaces"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file with spaces"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithSpecialCharacters() {
|
||||
val uri = "file:///path/to/file%23with%25special%26chars"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file#with%special&chars"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithQueryParams() {
|
||||
val uri = "file:///path/to/file?param=value"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithWSLPath() {
|
||||
val uri = "file:///wsl$/Ubuntu/home/user/file.txt"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/wsl$/Ubuntu/home/user/file.txt"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithWSLLocalhostPath() {
|
||||
val uri = "file:///wsl.localhost/Ubuntu/home/user/file.txt"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/wsl.localhost/Ubuntu/home/user/file.txt"), file)
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ export class GUISelectors {
|
|||
}
|
||||
|
||||
public static getToolCallStatusMessage(view: WebView) {
|
||||
return SelectorUtils.getElementByDataTestId(view, "toggle-div-title");
|
||||
return SelectorUtils.getElementByDataTestId(view, "tool-call-title");
|
||||
}
|
||||
|
||||
public static getToolButton(view: WebView) {
|
||||
|
@ -89,6 +89,14 @@ export class GUISelectors {
|
|||
);
|
||||
}
|
||||
|
||||
public static getRulesPeek(view: WebView) {
|
||||
return SelectorUtils.getElementByDataTestId(view, "rules-peek");
|
||||
}
|
||||
|
||||
public static getFirstRulesPeekItem(view: WebView) {
|
||||
return SelectorUtils.getElementByDataTestId(view, "rules-peek-item");
|
||||
}
|
||||
|
||||
public static getNthHistoryTableRow(view: WebView, index: number) {
|
||||
return SelectorUtils.getElementByDataTestId(view, `history-row-${index}`);
|
||||
}
|
||||
|
|
|
@ -245,6 +245,47 @@ describe("GUI Test", () => {
|
|||
await GUIActions.selectModeFromDropdown(view, "Agent");
|
||||
});
|
||||
|
||||
it("should display rules peek and show rule details", async () => {
|
||||
// Send a message to trigger the model response
|
||||
const [messageInput] = await GUISelectors.getMessageInputFields(view);
|
||||
await messageInput.sendKeys("Hello");
|
||||
await messageInput.sendKeys(Key.ENTER);
|
||||
|
||||
// Wait for the response to appear
|
||||
await TestUtils.waitForSuccess(() =>
|
||||
GUISelectors.getThreadMessageByText(view, "I'm going to call a tool:"),
|
||||
);
|
||||
|
||||
// Verify that "1 rule" text appears
|
||||
const rulesPeek = await TestUtils.waitForSuccess(() =>
|
||||
GUISelectors.getRulesPeek(view),
|
||||
);
|
||||
const rulesPeekText = await rulesPeek.getText();
|
||||
expect(rulesPeekText).to.include("1 rule");
|
||||
|
||||
// Click on the rules peek to expand it
|
||||
await rulesPeek.click();
|
||||
|
||||
// Wait for the rule details to appear
|
||||
const ruleItem = await TestUtils.waitForSuccess(() =>
|
||||
GUISelectors.getFirstRulesPeekItem(view),
|
||||
);
|
||||
|
||||
await TestUtils.waitForSuccess(async () => {
|
||||
const text = await ruleItem.getText();
|
||||
if (!text || text.trim() === "") {
|
||||
throw new Error("Rule item text is empty");
|
||||
}
|
||||
return ruleItem;
|
||||
});
|
||||
|
||||
// Verify the rule content
|
||||
const ruleItemText = await ruleItem.getText();
|
||||
expect(ruleItemText).to.include("Assistant rule");
|
||||
expect(ruleItemText).to.include("Always applied");
|
||||
expect(ruleItemText).to.include("TEST_SYS_MSG");
|
||||
}).timeout(DEFAULT_TIMEOUT.MD);
|
||||
|
||||
it("should render tool call", async () => {
|
||||
const [messageInput] = await GUISelectors.getMessageInputFields(view);
|
||||
await messageInput.sendKeys("Hello");
|
||||
|
@ -258,7 +299,9 @@ describe("GUI Test", () => {
|
|||
expect(await statusMessage.getText()).contain(
|
||||
"Continue viewed the git diff",
|
||||
);
|
||||
}).timeout(DEFAULT_TIMEOUT.MD);
|
||||
// wait for 30 seconds, promise
|
||||
await new Promise((resolve) => setTimeout(resolve, 30000));
|
||||
}).timeout(DEFAULT_TIMEOUT.MD * 100);
|
||||
|
||||
it("should call tool after approval", async () => {
|
||||
await GUIActions.toggleToolPolicy(view, "builtin_view_diff", 2);
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"svg-builder": "^2.0.0",
|
||||
"systeminformation": "^5.23.7",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"undici": "^6.2.0",
|
||||
"undici": "^6.21.3",
|
||||
"uuid": "^9.0.1",
|
||||
"uuidv4": "^6.2.13",
|
||||
"vectordb": "^0.4.20",
|
||||
|
@ -13095,9 +13095,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "continue",
|
||||
"icon": "media/icon.png",
|
||||
"author": "Continue Dev, Inc",
|
||||
"version": "1.1.34",
|
||||
"version": "1.1.35",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/continuedev/continue"
|
||||
|
@ -704,7 +704,7 @@
|
|||
"svg-builder": "^2.0.0",
|
||||
"systeminformation": "^5.23.7",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"undici": "^6.2.0",
|
||||
"undici": "^6.21.3",
|
||||
"uuid": "^9.0.1",
|
||||
"uuidv4": "^6.2.13",
|
||||
"vectordb": "^0.4.20",
|
||||
|
|
|
@ -9,16 +9,16 @@ import {
|
|||
indexDecorationType,
|
||||
} from "./decorations";
|
||||
|
||||
import type { UpdateApplyStatePayload, DiffLine } from "core";
|
||||
import type { ApplyState, DiffLine } from "core";
|
||||
import type { VerticalDiffCodeLens } from "./manager";
|
||||
|
||||
export interface VerticalDiffHandlerOptions {
|
||||
input?: string;
|
||||
instant?: boolean;
|
||||
onStatusUpdate: (
|
||||
status?: UpdateApplyStatePayload["status"],
|
||||
numDiffs?: UpdateApplyStatePayload["numDiffs"],
|
||||
fileContent?: UpdateApplyStatePayload["fileContent"],
|
||||
status?: ApplyState["status"],
|
||||
numDiffs?: ApplyState["numDiffs"],
|
||||
fileContent?: ApplyState["fileContent"],
|
||||
) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "eslint-plugin-react"],
|
||||
"parserOptions": {
|
||||
"project": "gui/tsconfig.json"
|
||||
"project": ["tsconfig.json", "tsconfig.node.json"]
|
||||
},
|
||||
"extends": [
|
||||
// "eslint:recommended",
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint --ext ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@continuedev/config-yaml": "file:../packages/config-yaml",
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { UpdateApplyStatePayload } from "core";
|
||||
import { ToIdeFromWebviewProtocol } from "core/protocol/ideWebview";
|
||||
import { ApplyState } from "core";
|
||||
import { useContext } from "react";
|
||||
import { IdeMessengerContext } from "../context/IdeMessenger";
|
||||
import { getMetaKeyLabel } from "../util";
|
||||
import { useFontSize } from "./ui/font";
|
||||
|
||||
export interface AcceptRejectAllButtonsProps {
|
||||
applyStates: UpdateApplyStatePayload[];
|
||||
applyStates: ApplyState[];
|
||||
onAcceptOrReject?: (outcome: AcceptOrRejectOutcome) => void;
|
||||
}
|
||||
|
||||
export type AcceptOrRejectOutcome = keyof Pick<
|
||||
ToIdeFromWebviewProtocol,
|
||||
"acceptDiff" | "rejectDiff"
|
||||
>;
|
||||
export type AcceptOrRejectOutcome = "acceptDiff" | "rejectDiff";
|
||||
|
||||
export default function AcceptRejectDiffButtons({
|
||||
export default function AcceptRejectAllButtons({
|
||||
applyStates,
|
||||
onAcceptOrReject,
|
||||
}: AcceptRejectAllButtonsProps) {
|
||||
|
@ -28,7 +24,6 @@ export default function AcceptRejectDiffButtons({
|
|||
const tinyFont = useFontSize(-3);
|
||||
|
||||
async function handleAcceptOrReject(status: AcceptOrRejectOutcome) {
|
||||
debugger;
|
||||
for (const { filepath = "", streamId } of pendingApplyStates) {
|
||||
ideMessenger.post(status, {
|
||||
filepath,
|
||||
|
@ -58,6 +53,7 @@ export default function AcceptRejectDiffButtons({
|
|||
<div className="flex flex-row items-center gap-1">
|
||||
<XMarkIcon className="h-4 w-4 text-red-600" />
|
||||
<span>Reject</span>
|
||||
<span className="xs:inline-block hidden">All</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
|
@ -77,6 +73,7 @@ export default function AcceptRejectDiffButtons({
|
|||
<div className="flex flex-row items-center gap-1">
|
||||
<CheckIcon className="h-4 w-4 text-green-600" />
|
||||
<span>Accept</span>
|
||||
<span className="xs:inline-block hidden">All</span>
|
||||
</div>
|
||||
<span
|
||||
className="xs:inline-block hidden text-gray-400"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CheckIcon, PlayIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { UpdateApplyStatePayload } from "core";
|
||||
import { ApplyState } from "core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getMetaKeyLabel } from "../../../util";
|
||||
import Spinner from "../../gui/Spinner";
|
||||
|
@ -9,7 +9,7 @@ import { ToolbarButtonWithTooltip } from "./ToolbarButtonWithTooltip";
|
|||
|
||||
interface ApplyActionsProps {
|
||||
disableManualApply?: boolean;
|
||||
applyState?: UpdateApplyStatePayload;
|
||||
applyState?: ApplyState;
|
||||
onClickAccept: () => void;
|
||||
onClickReject: () => void;
|
||||
onClickApply: () => void;
|
||||
|
|
|
@ -243,7 +243,7 @@ export function StepContainerPreToolbar({
|
|||
const plural = numLines === 1 ? "" : "s";
|
||||
if (isGeneratingCodeBlock) {
|
||||
return (
|
||||
<span className="text-lightgray inline-flex w-min items-center gap-2">
|
||||
<span className="text-lightgray inline-flex items-center gap-2 text-right">
|
||||
{!isExpanded ? `${numLines} line${plural}` : "Generating"}{" "}
|
||||
<div>
|
||||
<Spinner />
|
||||
|
@ -252,7 +252,7 @@ export function StepContainerPreToolbar({
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="text-lightgray inline-flex w-min items-center gap-2">
|
||||
<span className="text-lightgray inline-flex items-center gap-2 text-right">
|
||||
{`${numLines} line${plural} pending`}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { headerIsMarkdown } from './headerIsMarkdown';
|
||||
|
||||
describe('headerIsMarkdown', () => {
|
||||
// Test exact match with common Markdown identifiers
|
||||
it('should identify exact matches with common Markdown identifiers', () => {
|
||||
expect(headerIsMarkdown('md')).toBe(true);
|
||||
expect(headerIsMarkdown('markdown')).toBe(true);
|
||||
expect(headerIsMarkdown('gfm')).toBe(true);
|
||||
expect(headerIsMarkdown('github-markdown')).toBe(true);
|
||||
});
|
||||
|
||||
// Test identifiers preceded by a space
|
||||
it('should identify identifiers preceded by a space', () => {
|
||||
expect(headerIsMarkdown('lang md')).toBe(true);
|
||||
expect(headerIsMarkdown('something markdown')).toBe(true);
|
||||
expect(headerIsMarkdown('language gfm')).toBe(true);
|
||||
expect(headerIsMarkdown('spec github-markdown')).toBe(true);
|
||||
});
|
||||
|
||||
// Test file extensions
|
||||
it('should identify file names with markdown extensions', () => {
|
||||
expect(headerIsMarkdown('example.md')).toBe(true);
|
||||
expect(headerIsMarkdown('document.markdown')).toBe(true);
|
||||
expect(headerIsMarkdown('readme.gfm')).toBe(true);
|
||||
});
|
||||
|
||||
// Test more complex cases with extensions
|
||||
it('should identify file names with markdown extensions followed by other text', () => {
|
||||
expect(headerIsMarkdown('example.md additional text')).toBe(true);
|
||||
expect(headerIsMarkdown('document.markdown with some description')).toBe(true);
|
||||
expect(headerIsMarkdown('readme.gfm v2.0')).toBe(true);
|
||||
});
|
||||
|
||||
// Test non-markdown cases
|
||||
it('should not identify non-markdown headers', () => {
|
||||
expect(headerIsMarkdown('javascript')).toBe(false);
|
||||
expect(headerIsMarkdown('typescript')).toBe(false);
|
||||
expect(headerIsMarkdown('plain')).toBe(false);
|
||||
expect(headerIsMarkdown('python')).toBe(false);
|
||||
});
|
||||
|
||||
// Test edge cases
|
||||
it('should handle edge cases correctly', () => {
|
||||
expect(headerIsMarkdown('')).toBe(false);
|
||||
expect(headerIsMarkdown('mdx')).toBe(false); // Similar but not exactly "md"
|
||||
expect(headerIsMarkdown('readme md')).toBe(true);
|
||||
expect(headerIsMarkdown('md.js')).toBe(false); // "md" is not the extension
|
||||
});
|
||||
|
||||
// Test case sensitivity
|
||||
it('should respect case sensitivity', () => {
|
||||
expect(headerIsMarkdown('MD')).toBe(false);
|
||||
expect(headerIsMarkdown('MARKDOWN')).toBe(false);
|
||||
expect(headerIsMarkdown('example.MD')).toBe(false);
|
||||
expect(headerIsMarkdown('lang MD')).toBe(false);
|
||||
});
|
||||
|
||||
// Test with special characters and spacing
|
||||
it('should handle special characters and spacing correctly', () => {
|
||||
expect(headerIsMarkdown(' md')).toBe(true); // Space before "md"
|
||||
expect(headerIsMarkdown('md ')).toBe(true); // Space after "md"
|
||||
expect(headerIsMarkdown('hello-md')).toBe(false); // "md" with hyphen prefix
|
||||
expect(headerIsMarkdown('markdown:')).toBe(false); // "markdown" with suffix
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Determines if a given header string represents Markdown content.
|
||||
*
|
||||
* The function checks various patterns to identify Markdown headers:
|
||||
* - Exact match with common Markdown identifiers (md, markdown, gfm, github-markdown)
|
||||
* - Contains these identifiers preceded by a space
|
||||
* - First word has a file extension of md, markdown, or gfm
|
||||
*
|
||||
* @param header - The string to check for Markdown indicators
|
||||
* @returns True if the header represents Markdown content, false otherwise
|
||||
*/
|
||||
export function headerIsMarkdown(header: string): boolean {
|
||||
return (
|
||||
header === "md" ||
|
||||
header === "markdown" ||
|
||||
header === "gfm" ||
|
||||
header === "github-markdown" ||
|
||||
header.includes(" md") ||
|
||||
header.includes(" markdown") ||
|
||||
header.includes(" gfm") ||
|
||||
header.includes(" github-markdown") ||
|
||||
(header.split(" ")[0]?.split(".").pop() === "md") ||
|
||||
(header.split(" ")[0]?.split(".").pop() === "markdown") ||
|
||||
(header.split(" ")[0]?.split(".").pop() === "gfm")
|
||||
);
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import { patchNestedMarkdown } from './patchNestedMarkdown';
|
||||
|
||||
describe('patchNestedMarkdown', () => {
|
||||
it('should return unchanged content when no markdown codeblocks are present', () => {
|
||||
const source = 'Regular text\n```javascript\nconsole.log("hello");\n```';
|
||||
expect(patchNestedMarkdown(source)).toBe(source);
|
||||
});
|
||||
|
||||
it('should replace backticks with tildes for markdown codeblocks', () => {
|
||||
const source = '```markdown\n# Header\nSome text\n```';
|
||||
const expected = '~~~markdown\n# Header\nSome text\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle nested codeblocks within markdown blocks', () => {
|
||||
const source = '```markdown\n# Example\n```js\nconsole.log("nested");\n```\n```';
|
||||
const expected = '~~~markdown\n# Example\n```js\nconsole.log("nested");\n```\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle .md file extension', () => {
|
||||
const source = '```test.md\nContent\n```';
|
||||
const expected = '~~~test.md\nContent\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle multiple levels of nesting', () => {
|
||||
const source = '```markdown\n# Doc\n```js\nlet x = "```nested```";\n```\n```';
|
||||
const expected = '~~~markdown\n# Doc\n```js\nlet x = "```nested```";\n```\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle blocks that start with md', () => {
|
||||
const source = '```md\n# Header\nSome text\n```';
|
||||
const expected = '~~~md\n# Header\nSome text\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle blocks with language specifier followed by md', () => {
|
||||
const source = '```lang md\n# Header\nSome text\n```';
|
||||
const expected = '~~~lang md\n# Header\nSome text\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle blocks with language specifier followed by markdown', () => {
|
||||
const source = '```lang markdown\n# Header\nSome text\n```';
|
||||
const expected = '~~~lang markdown\n# Header\nSome text\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not replace backticks for non-markdown file extensions', () => {
|
||||
const source = '```test.js\nContent\n```';
|
||||
expect(patchNestedMarkdown(source)).toBe(source);
|
||||
});
|
||||
|
||||
it('should check the file extension branch when extension is not md/markdown/gfm', () => {
|
||||
// This tests the extension check branch with an unrecognized extension
|
||||
const source = '```test.txt\nContent with md keyword\n```';
|
||||
expect(patchNestedMarkdown(source)).toBe(source);
|
||||
});
|
||||
|
||||
it('should handle empty file name in code block header', () => {
|
||||
// This covers the branch where file is empty or undefined
|
||||
const source = '``` \nSome content\n```';
|
||||
expect(patchNestedMarkdown(source)).toBe(source);
|
||||
});
|
||||
|
||||
it('should handle file names with no extension', () => {
|
||||
// This covers the branch where ext might be undefined
|
||||
const source = '```filename\nContent\n```';
|
||||
expect(patchNestedMarkdown(source)).toBe(source);
|
||||
});
|
||||
|
||||
it('should correctly identify .markdown extension', () => {
|
||||
// This specifically tests the ext === "markdown" condition in the extension check
|
||||
const source = '```example.markdown\n# Some markdown content\n```';
|
||||
const expected = '~~~example.markdown\n# Some markdown content\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
// GitHub-specific tests
|
||||
it('should handle gfm language identifier', () => {
|
||||
const source = '```gfm\n# Header\nSome text\n```';
|
||||
const expected = '~~~gfm\n# Header\nSome text\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle github-markdown language identifier', () => {
|
||||
const source = '```github-markdown\n# Header\nSome text\n```';
|
||||
const expected = '~~~github-markdown\n# Header\nSome text\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle language specifier followed by gfm', () => {
|
||||
const source = '```lang gfm\n# Header\nSome text\n```';
|
||||
const expected = '~~~lang gfm\n# Header\nSome text\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle .gfm file extension', () => {
|
||||
const source = '```example.gfm\n# Some GitHub markdown content\n```';
|
||||
const expected = '~~~example.gfm\n# Some GitHub markdown content\n~~~';
|
||||
expect(patchNestedMarkdown(source)).toBe(expected);
|
||||
});
|
||||
});
|
|
@ -1,47 +1,52 @@
|
|||
/*
|
||||
This is a patch for outputing markdown code that contains codeblocks
|
||||
|
||||
It notices markdown blocks, keeps track of when that specific block is closed,
|
||||
It notices markdown blocks (including GitHub-specific variants),
|
||||
keeps track of when that specific block is closed,
|
||||
and uses ~~~ instead of ``` for that block
|
||||
|
||||
Note, this was benchmarked at sub-millisecond
|
||||
|
||||
// TODO support github-specific markdown as well, edge case
|
||||
*/
|
||||
import { headerIsMarkdown } from './headerIsMarkdown';
|
||||
|
||||
export const patchNestedMarkdown = (source: string): string => {
|
||||
if (!source.match(/```(\w+\.(md|markdown))/)) return source; // For performance
|
||||
// const start = Date.now();
|
||||
// Early return if no markdown codeblock pattern is found (including GitHub variants)
|
||||
if (!source.match(/```(\w*|.*)(md|markdown|gfm|github-markdown)/)) return source;
|
||||
|
||||
let nestCount = 0;
|
||||
const lines = source.split("\n");
|
||||
const trimmedLines = lines.map((l) => l.trim());
|
||||
|
||||
for (let i = 0; i < trimmedLines.length; i++) {
|
||||
const line = trimmedLines[i];
|
||||
if (nestCount) {
|
||||
|
||||
if (nestCount > 0) {
|
||||
// Inside a markdown block
|
||||
if (line.match(/^`+$/)) {
|
||||
// Ending a block
|
||||
if (nestCount === 1) lines[i] = "~~~"; // End of markdown block
|
||||
// Ending a block with just backticks (```)
|
||||
nestCount--;
|
||||
if (nestCount === 0) {
|
||||
lines[i] = "~~~"; // End of markdown block
|
||||
}
|
||||
} else if (line.startsWith("```")) {
|
||||
// Going into a nested codeblock
|
||||
nestCount++;
|
||||
}
|
||||
} else {
|
||||
// Enter the markdown block, start tracking nesting
|
||||
// Not inside a markdown block yet
|
||||
if (line.startsWith("```")) {
|
||||
const header = line.replaceAll("`", "");
|
||||
const file = header.split(" ")[0];
|
||||
|
||||
if (file) {
|
||||
const ext = file.split(".").at(-1);
|
||||
if (ext === "md" || ext === "markdown") {
|
||||
nestCount = 1;
|
||||
lines[i] = lines[i].replaceAll("`", "~"); // Replace backticks with tildes
|
||||
}
|
||||
// Check if this is a markdown codeblock using a consolidated approach (including GitHub-specific variants)
|
||||
const isMarkdown = headerIsMarkdown(header);
|
||||
|
||||
if (isMarkdown) {
|
||||
nestCount = 1;
|
||||
lines[i] = lines[i].replaceAll("`", "~");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const out = lines.join("\n");
|
||||
// console.log(`patched in ${Date.now() - start}ms`);
|
||||
return out;
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
|
|
@ -6,15 +6,21 @@ interface ToggleProps {
|
|||
children: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
icon?: ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
function ToggleDiv({ children, title, icon: Icon }: ToggleProps) {
|
||||
function ToggleDiv({
|
||||
children,
|
||||
title,
|
||||
icon: Icon,
|
||||
testId = "context-items-peek",
|
||||
}: ToggleProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`pl-2 pt-2`}
|
||||
className={`pl-2`}
|
||||
style={{
|
||||
backgroundColor: vscBackground,
|
||||
}}
|
||||
|
@ -24,7 +30,7 @@ function ToggleDiv({ children, title, icon: Icon }: ToggleProps) {
|
|||
onClick={() => setOpen((prev) => !prev)}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
data-testid="context-items-peek"
|
||||
data-testid={testId}
|
||||
>
|
||||
<div className="relative mr-1 h-4 w-4">
|
||||
{Icon && !isHovered && !open ? (
|
||||
|
@ -44,10 +50,7 @@ function ToggleDiv({ children, title, icon: Icon }: ToggleProps) {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="ml-1 text-xs text-gray-400 transition-colors duration-200"
|
||||
data-testid="toggle-div-title"
|
||||
>
|
||||
<span className="ml-1 text-xs text-gray-400 transition-colors duration-200">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Editor, JSONContent } from "@tiptap/react";
|
||||
import { ContextItemWithId, InputModifiers } from "core";
|
||||
import { ContextItemWithId, InputModifiers, RuleWithSource } from "core";
|
||||
import { useMemo } from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import { defaultBorderRadius, vscBackground } from "..";
|
||||
import { useAppSelector } from "../../redux/hooks";
|
||||
import { selectSlashCommandComboBoxInputs } from "../../redux/selectors";
|
||||
import { ContextItemsPeek } from "./belowMainInput/ContextItemsPeek";
|
||||
import { RulesPeek } from "./belowMainInput/RulesPeek";
|
||||
import { ToolbarOptions } from "./InputToolbar";
|
||||
import { Lump } from "./Lump";
|
||||
import { TipTapEditor } from "./TipTapEditor";
|
||||
|
@ -20,6 +21,7 @@ interface ContinueInputBoxProps {
|
|||
) => void;
|
||||
editorState?: JSONContent;
|
||||
contextItems?: ContextItemWithId[];
|
||||
appliedRules?: RuleWithSource[];
|
||||
hidden?: boolean;
|
||||
inputId: string; // used to keep track of things per input in redux
|
||||
}
|
||||
|
@ -116,6 +118,8 @@ function ContinueInputBox(props: ContinueInputBoxProps) {
|
|||
}
|
||||
: {};
|
||||
|
||||
const { appliedRules = [], contextItems = [] } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${props.hidden ? "hidden" : ""}`}
|
||||
|
@ -143,10 +147,15 @@ function ContinueInputBox(props: ContinueInputBoxProps) {
|
|||
/>
|
||||
</GradientBorder>
|
||||
</div>
|
||||
<ContextItemsPeek
|
||||
contextItems={props.contextItems}
|
||||
isCurrentContextPeek={props.isLastUserInput}
|
||||
/>
|
||||
{(appliedRules.length > 0 || contextItems.length > 0) && (
|
||||
<div className="mt-2 flex flex-col">
|
||||
<RulesPeek appliedRules={props.appliedRules} />
|
||||
<ContextItemsPeek
|
||||
contextItems={props.contextItems}
|
||||
isCurrentContextPeek={props.isLastUserInput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import { DocumentTextIcon, GlobeAltIcon } from "@heroicons/react/24/outline";
|
||||
import { RuleSource, RuleWithSource } from "core";
|
||||
import { ComponentType, useMemo, useState } from "react";
|
||||
import ToggleDiv from "../../ToggleDiv";
|
||||
|
||||
interface RulesPeekProps {
|
||||
appliedRules?: RuleWithSource[];
|
||||
icon?: ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||
}
|
||||
|
||||
interface RulesPeekItemProps {
|
||||
rule: RuleWithSource;
|
||||
}
|
||||
|
||||
// Convert technical source to user-friendly text
|
||||
const getSourceLabel = (source: RuleSource): string => {
|
||||
switch (source) {
|
||||
case "default-chat":
|
||||
return "Default Chat";
|
||||
case "default-agent":
|
||||
return "Default Agent";
|
||||
case "model-chat-options":
|
||||
return "Model Chat Options";
|
||||
case "model-agent-options":
|
||||
return "Model Agent Options";
|
||||
case "rules-block":
|
||||
return "Rules Block";
|
||||
case "json-systemMessage":
|
||||
return "System Message";
|
||||
case ".continuerules":
|
||||
return "Project Rules";
|
||||
default:
|
||||
return source;
|
||||
}
|
||||
};
|
||||
|
||||
export function RulesPeekItem({ rule }: RulesPeekItemProps) {
|
||||
const isGlobal = !rule.globs;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Define maximum length for rule text display
|
||||
const maxRuleLength = 100;
|
||||
const isRuleLong = rule.rule.length > maxRuleLength;
|
||||
|
||||
// Get the displayed rule text based on expanded state
|
||||
const displayedRule =
|
||||
isRuleLong && !expanded
|
||||
? `${rule.rule.slice(0, maxRuleLength)}...`
|
||||
: rule.rule;
|
||||
|
||||
const toggleExpand = () => {
|
||||
if (isRuleLong) {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group mr-2 flex flex-col overflow-hidden rounded px-1.5 py-1 text-xs hover:bg-white/10 ${isRuleLong ? "cursor-pointer hover:text-gray-200" : ""}`}
|
||||
data-testid="rules-peek-item"
|
||||
onClick={toggleExpand}
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
{isGlobal ? (
|
||||
<GlobeAltIcon className="mr-2 h-4 w-4 flex-shrink-0 text-gray-400" />
|
||||
) : (
|
||||
<DocumentTextIcon className="mr-2 h-4 w-4 flex-shrink-0 text-gray-400" />
|
||||
)}
|
||||
|
||||
<div className="flex min-w-0 flex-1 gap-2 text-xs">
|
||||
<div className="max-w-[50%] flex-shrink-0 truncate font-medium">
|
||||
{rule.name || "Assistant rule"}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-500">
|
||||
{isGlobal
|
||||
? "Always applied"
|
||||
: `Pattern: ${typeof rule.globs === "string" ? rule.globs : Array.isArray(rule.globs) ? rule.globs.join(", ") : ""}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-1 whitespace-pre-line pl-6 pr-2 text-xs italic text-gray-300`}
|
||||
title={
|
||||
isRuleLong ? (expanded ? "Click to collapse" : "Click to expand") : ""
|
||||
}
|
||||
>
|
||||
{displayedRule}
|
||||
{isRuleLong && (
|
||||
<span className="ml-1 text-gray-400 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{expanded ? "(collapse)" : "(expand)"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 pl-6 pr-2 text-xs text-gray-500">
|
||||
Source: {getSourceLabel(rule.source)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RulesPeek({ appliedRules, icon }: RulesPeekProps) {
|
||||
const rules = useMemo(() => {
|
||||
return appliedRules ?? [];
|
||||
}, [appliedRules]);
|
||||
|
||||
if (!rules || rules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleDiv
|
||||
icon={icon}
|
||||
title={`${rules.length} rule${rules.length > 1 ? "s" : ""}`}
|
||||
testId="rules-peek"
|
||||
>
|
||||
{rules.map((rule, idx) => (
|
||||
<RulesPeekItem key={`rule-${idx}`} rule={rule} />
|
||||
))}
|
||||
</ToggleDiv>
|
||||
);
|
||||
}
|
|
@ -173,17 +173,24 @@ function ModeSelect() {
|
|||
{mode === "chat" && <CheckIcon className="ml-auto h-3 w-3" />}
|
||||
</ListboxOption>
|
||||
|
||||
<ListboxOption value="agent" disabled={!agentModeSupported}>
|
||||
<ListboxOption
|
||||
value="agent"
|
||||
disabled={!agentModeSupported}
|
||||
className={"gap-1"}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<SparklesIcon className="h-3 w-3" />
|
||||
<span className="">Agent</span>
|
||||
</div>
|
||||
{mode === "agent" && <CheckIcon className="ml-auto h-3 w-3" />}
|
||||
{!agentModeSupported && <span> (Not supported)</span>}
|
||||
{agentModeSupported ? (
|
||||
mode === "agent" && <CheckIcon className="ml-auto h-3 w-3" />
|
||||
) : (
|
||||
<span>(Not supported)</span>
|
||||
)}
|
||||
</ListboxOption>
|
||||
|
||||
<div className="text-lightgray px-2 py-1">
|
||||
{metaKeyLabel}. for next mode
|
||||
{`${metaKeyLabel} . for next mode`}
|
||||
</div>
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
|
|
|
@ -166,7 +166,11 @@ function ModelSelect() {
|
|||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "'" && isMetaEquivalentKeyPressed(event as any)) {
|
||||
if (
|
||||
event.key === "'" &&
|
||||
isMetaEquivalentKeyPressed(event as any) &&
|
||||
!event.shiftKey // To prevent collisions w/ assistant toggle logic
|
||||
) {
|
||||
if (!selectedProfile) {
|
||||
return;
|
||||
}
|
||||
|
@ -179,7 +183,7 @@ function ModelSelect() {
|
|||
if (nextIndex < 0) nextIndex = options.length - 1;
|
||||
const newModelTitle = options[nextIndex].value;
|
||||
|
||||
dispatch(
|
||||
void dispatch(
|
||||
updateSelectedModelByRole({
|
||||
selectedProfile,
|
||||
role: "chat",
|
||||
|
@ -219,7 +223,7 @@ function ModelSelect() {
|
|||
<Listbox
|
||||
onChange={async (val: string) => {
|
||||
if (val === selectedModel?.title) return;
|
||||
dispatch(
|
||||
void dispatch(
|
||||
updateSelectedModelByRole({
|
||||
selectedProfile,
|
||||
role: mode === "edit" ? "edit" : "chat",
|
||||
|
|
|
@ -19,7 +19,6 @@ export const LocalStorageProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
const [values, setValues] = useState<LocalStorageType>(DEFAULT_LOCAL_STORAGE);
|
||||
|
||||
// TODO setvalue
|
||||
|
||||
useEffect(() => {
|
||||
const isJetbrains = getLocalStorage("ide") === "jetbrains";
|
||||
let fontSize = getLocalStorage("fontSize") ?? (isJetbrains ? 15 : 14);
|
||||
|
|
|
@ -20,6 +20,15 @@ export function HelpCenterSection() {
|
|||
<div className="py-5">
|
||||
<h3 className="mb-4 mt-0 text-xl">Help center</h3>
|
||||
<div className="-mx-4 flex flex-col">
|
||||
<MoreHelpRow
|
||||
title="Continue Hub"
|
||||
description="Visit hub.continue.dev to explore custom assistants and blocks"
|
||||
Icon={ArrowTopRightOnSquareIcon}
|
||||
onClick={() =>
|
||||
ideMessenger.post("openUrl", "https://hub.continue.dev/")
|
||||
}
|
||||
/>
|
||||
|
||||
<MoreHelpRow
|
||||
title="Documentation"
|
||||
description="Learn how to configure and use Continue"
|
||||
|
|
|
@ -132,6 +132,8 @@ export function Chat() {
|
|||
return isJetBrains();
|
||||
}, []);
|
||||
|
||||
useAutoScroll(stepsDivRef, history);
|
||||
|
||||
useEffect(() => {
|
||||
// Cmd + Backspace to delete current step
|
||||
const listener = (e: any) => {
|
||||
|
@ -282,8 +284,6 @@ export function Chat() {
|
|||
|
||||
const showScrollbar = showChatScrollbar ?? window.innerHeight > 5000;
|
||||
|
||||
useAutoScroll(stepsDivRef, history);
|
||||
|
||||
return (
|
||||
<>
|
||||
{widget}
|
||||
|
@ -299,7 +299,7 @@ export function Chat() {
|
|||
<div
|
||||
key={item.message.id}
|
||||
style={{
|
||||
minHeight: index === history.length - 1 ? "25vh" : 0,
|
||||
minHeight: index === history.length - 1 ? "200px" : 0,
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary
|
||||
|
@ -318,6 +318,7 @@ export function Chat() {
|
|||
isMainInput={false}
|
||||
editorState={item.editorState}
|
||||
contextItems={item.contextItems}
|
||||
appliedRules={item.appliedRules}
|
||||
inputId={item.message.id}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -8,6 +8,10 @@ interface CreateFileToolCallProps {
|
|||
}
|
||||
|
||||
export function CreateFile(props: CreateFileToolCallProps) {
|
||||
if (!props.fileContents) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const src = `\`\`\`${getMarkdownLanguageTagForFile(props.relativeFilepath ?? "output.txt")} ${props.relativeFilepath}\n${props.fileContents ?? ""}\n\`\`\``;
|
||||
|
||||
return props.relativeFilepath ? (
|
||||
|
|
|
@ -16,13 +16,12 @@ type EditToolCallProps = {
|
|||
};
|
||||
|
||||
export function EditFile(props: EditToolCallProps) {
|
||||
const src = `\`\`\`${getMarkdownLanguageTagForFile(props.relativeFilePath ?? "test.txt")} ${props.relativeFilePath}\n${props.changes ?? ""}\n\`\`\``;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const isStreaming = useAppSelector((state) => state.session.isStreaming);
|
||||
|
||||
const applyState = useAppSelector((state) =>
|
||||
selectApplyStateByToolCallId(state, props.toolCallId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!applyState) {
|
||||
dispatch(
|
||||
|
@ -35,10 +34,12 @@ export function EditFile(props: EditToolCallProps) {
|
|||
}
|
||||
}, [applyState, props.toolCallId]);
|
||||
|
||||
if (!props.relativeFilePath) {
|
||||
if (!props.relativeFilePath || !props.changes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const src = `\`\`\`${getMarkdownLanguageTagForFile(props.relativeFilePath ?? "test.txt")} ${props.relativeFilePath}\n${props.changes}\n\`\`\``;
|
||||
|
||||
return (
|
||||
<StyledMarkdownPreview
|
||||
isRenderingInStepContainer
|
||||
|
|
|
@ -61,7 +61,7 @@ export function SimpleToolCallUI({
|
|||
</div>
|
||||
<span
|
||||
className="ml-1 text-xs text-gray-400 transition-colors duration-200"
|
||||
data-testid="toggle-div-title"
|
||||
data-testid="tool-call-title"
|
||||
>
|
||||
<ToolCallStatusMessage tool={tool} toolCallState={toolCallState} />
|
||||
</span>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Tool, ToolCallState } from "core";
|
||||
import Mustache from "mustache";
|
||||
import { ReactNode } from "react";
|
||||
import { getFontSize } from "../../../util";
|
||||
import { useFontSize } from "../../../components/ui/font";
|
||||
|
||||
interface ToolCallStatusMessageProps {
|
||||
tool: Tool | undefined;
|
||||
|
@ -11,6 +13,7 @@ export function ToolCallStatusMessage({
|
|||
tool,
|
||||
toolCallState,
|
||||
}: ToolCallStatusMessageProps) {
|
||||
const fontSize = useFontSize();
|
||||
if (!tool) return "Agent tool use";
|
||||
|
||||
const defaultToolDescription = (
|
||||
|
@ -65,7 +68,7 @@ export function ToolCallStatusMessage({
|
|||
message = futureMessage;
|
||||
}
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="block" style={{ fontSize }}>
|
||||
<span>Continue</span> {intro} {message}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ChatHistoryItemWithMessageId } from "../../redux/slices/sessionSlice";
|
||||
|
||||
/**
|
||||
* Only reset scroll state when a new user message is added to the chat.
|
||||
* We don't want to auto-scroll on new tool response messages.
|
||||
*/
|
||||
function getNumUserMsgs(history: ChatHistoryItemWithMessageId[]) {
|
||||
return history.filter((msg) => msg.message.role === "user").length;
|
||||
}
|
||||
|
||||
export const useAutoScroll = (
|
||||
ref: React.RefObject<HTMLDivElement>,
|
||||
history: unknown[],
|
||||
history: ChatHistoryItemWithMessageId[],
|
||||
) => {
|
||||
const [userHasScrolled, setUserHasScrolled] = useState(false);
|
||||
const numUserMsgs = useMemo(() => getNumUserMsgs(history), [history.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (history.length) {
|
||||
setUserHasScrolled(false);
|
||||
}
|
||||
}, [history.length]);
|
||||
setUserHasScrolled(false);
|
||||
}, [numUserMsgs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || history.length === 0) return;
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import {
|
||||
UpdateApplyStatePayload,
|
||||
SetCodeToEditPayload,
|
||||
MessageModes,
|
||||
} from "core";
|
||||
import { ApplyState, SetCodeToEditPayload, MessageModes } from "core";
|
||||
import { EDIT_MODE_STREAM_ID } from "core/edit/constants";
|
||||
|
||||
export interface EditModeState {
|
||||
// Array because of previous multi-file edit functionality
|
||||
// Keeping array to not break persisted redux for now
|
||||
codeToEdit: SetCodeToEditPayload[];
|
||||
applyState: UpdateApplyStatePayload;
|
||||
applyState: ApplyState;
|
||||
returnToMode: MessageModes;
|
||||
lastNonEditSessionWasEmpty: boolean;
|
||||
}
|
||||
|
||||
export const INITIAL_EDIT_APPLY_STATE: UpdateApplyStatePayload = {
|
||||
export const INITIAL_EDIT_APPLY_STATE: ApplyState = {
|
||||
streamId: EDIT_MODE_STREAM_ID,
|
||||
status: "not-started",
|
||||
};
|
||||
|
@ -39,7 +35,7 @@ export const editModeStateSlice = createSlice({
|
|||
},
|
||||
updateEditStateApplyState: (
|
||||
state,
|
||||
{ payload }: PayloadAction<UpdateApplyStatePayload>,
|
||||
{ payload }: PayloadAction<ApplyState>,
|
||||
) => {
|
||||
state.applyState = {
|
||||
...state.applyState,
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "@reduxjs/toolkit";
|
||||
import { JSONContent } from "@tiptap/react";
|
||||
import {
|
||||
UpdateApplyStatePayload,
|
||||
ApplyState,
|
||||
ChatHistoryItem,
|
||||
ChatMessage,
|
||||
ContextItem,
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
FileSymbolMap,
|
||||
MessageModes,
|
||||
PromptLog,
|
||||
RuleWithSource,
|
||||
Session,
|
||||
SessionMetadata,
|
||||
ToolCallDelta,
|
||||
|
@ -31,7 +32,7 @@ import { findCurrentToolCall, findToolCall } from "../util";
|
|||
|
||||
// We need this to handle reorderings (e.g. a mid-array deletion) of the messages array.
|
||||
// The proper fix is adding a UUID to all chat messages, but this is the temp workaround.
|
||||
type ChatHistoryItemWithMessageId = ChatHistoryItem & {
|
||||
export type ChatHistoryItemWithMessageId = ChatHistoryItem & {
|
||||
message: ChatMessage & { id: string };
|
||||
};
|
||||
|
||||
|
@ -47,7 +48,7 @@ type SessionState = {
|
|||
symbols: FileSymbolMap;
|
||||
mode: MessageModes;
|
||||
codeBlockApplyStates: {
|
||||
states: UpdateApplyStatePayload[];
|
||||
states: ApplyState[];
|
||||
curIndex: number;
|
||||
};
|
||||
newestToolbarPreviewForInput: Record<string, string>;
|
||||
|
@ -247,6 +248,19 @@ export const sessionSlice = createSlice({
|
|||
...payload.contextItems,
|
||||
];
|
||||
},
|
||||
setAppliedRulesAtIndex: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: PayloadAction<{
|
||||
index: number;
|
||||
appliedRules: RuleWithSource[];
|
||||
}>,
|
||||
) => {
|
||||
if (state.history[payload.index]) {
|
||||
state.history[payload.index].appliedRules = payload.appliedRules;
|
||||
}
|
||||
},
|
||||
setInactive: (state) => {
|
||||
const curMessage = state.history.at(-1);
|
||||
|
||||
|
@ -521,10 +535,7 @@ export const sessionSlice = createSlice({
|
|||
|
||||
state.history[state.history.length - 1].contextItems = contextItems;
|
||||
},
|
||||
updateApplyState: (
|
||||
state,
|
||||
{ payload }: PayloadAction<UpdateApplyStatePayload>,
|
||||
) => {
|
||||
updateApplyState: (state, { payload }: PayloadAction<ApplyState>) => {
|
||||
const applyState = state.codeBlockApplyStates.states.find(
|
||||
(state) => state.streamId === payload.streamId,
|
||||
);
|
||||
|
@ -701,6 +712,7 @@ export const {
|
|||
updateFileSymbols,
|
||||
setContextItemsAtIndex,
|
||||
addContextItemsAtIndex,
|
||||
setAppliedRulesAtIndex,
|
||||
setInactive,
|
||||
streamUpdate,
|
||||
newSession,
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { createAsyncThunk, unwrapResult } from "@reduxjs/toolkit";
|
||||
import { JSONContent } from "@tiptap/core";
|
||||
import { InputModifiers } from "core";
|
||||
import { InputModifiers, ToolResultChatMessage, UserChatMessage } from "core";
|
||||
import { constructMessages } from "core/llm/constructMessages";
|
||||
import { getApplicableRules } from "core/llm/rules/getSystemMessageWithRules";
|
||||
import posthog from "posthog-js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getBaseSystemMessage } from "../../util";
|
||||
import { selectSelectedChatModel } from "../slices/configSlice";
|
||||
import {
|
||||
setAppliedRulesAtIndex,
|
||||
submitEditorAndInitAtIndex,
|
||||
updateHistoryItemAtIndex,
|
||||
} from "../slices/sessionSlice";
|
||||
|
@ -84,17 +86,41 @@ export const streamResponseThunk = createAsyncThunk<
|
|||
}),
|
||||
);
|
||||
|
||||
// Construct messages from updated history
|
||||
// Get updated history after the update
|
||||
const updatedHistory = getState().session.history;
|
||||
const messageMode = getState().session.mode
|
||||
|
||||
const baseChatOrAgentSystemMessage = getBaseSystemMessage(selectedChatModel, messageMode)
|
||||
// Determine which rules apply to this message
|
||||
const userMsg = updatedHistory[inputIndex].message;
|
||||
const rules = getState().config.config.rules;
|
||||
|
||||
// Calculate applicable rules once
|
||||
// We need to check the message type to match what getApplicableRules expects
|
||||
const applicableRules = getApplicableRules(
|
||||
userMsg.role === "user" || userMsg.role === "tool"
|
||||
? (userMsg as UserChatMessage | ToolResultChatMessage)
|
||||
: undefined,
|
||||
rules,
|
||||
);
|
||||
|
||||
// Store in history for UI display
|
||||
dispatch(
|
||||
setAppliedRulesAtIndex({
|
||||
index: inputIndex,
|
||||
appliedRules: applicableRules,
|
||||
}),
|
||||
);
|
||||
|
||||
const messageMode = getState().session.mode;
|
||||
const baseChatOrAgentSystemMessage = getBaseSystemMessage(
|
||||
selectedChatModel,
|
||||
messageMode,
|
||||
);
|
||||
|
||||
const messages = constructMessages(
|
||||
messageMode,
|
||||
[...updatedHistory],
|
||||
baseChatOrAgentSystemMessage,
|
||||
state.config.config.rules,
|
||||
applicableRules,
|
||||
);
|
||||
|
||||
posthog.capture("step run", {
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as http from "node:http";
|
|||
import { AddressInfo } from "node:net";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { pathToFileURL } from "url";
|
||||
import { PackageIdentifier } from "./interfaces/slugs.js";
|
||||
import { RegistryClient } from "./registryClient.js";
|
||||
|
||||
|
@ -134,6 +135,7 @@ describe("RegistryClient", () => {
|
|||
|
||||
describe("getContentFromFilePath", () => {
|
||||
let absoluteFilePath: string;
|
||||
let fileUrl: string;
|
||||
let relativeFilePath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -141,6 +143,11 @@ describe("RegistryClient", () => {
|
|||
absoluteFilePath = path.join(tempDir, "absolute-path.yaml");
|
||||
fs.writeFileSync(absoluteFilePath, "absolute file content", "utf8");
|
||||
|
||||
const urlFilePath = path.join(tempDir, "file-url-path.yaml");
|
||||
fs.writeFileSync(urlFilePath, "file:// file content", "utf8");
|
||||
const url = pathToFileURL(urlFilePath);
|
||||
fileUrl = url.toString();
|
||||
|
||||
// Create a subdirectory and file in the temp directory
|
||||
const subDir = path.join(tempDir, "sub");
|
||||
fs.mkdirSync(subDir, { recursive: true });
|
||||
|
@ -160,6 +167,14 @@ describe("RegistryClient", () => {
|
|||
expect(result).toBe("absolute file content");
|
||||
});
|
||||
|
||||
it("should read from local file url directly", () => {
|
||||
const client = new RegistryClient();
|
||||
|
||||
const result = (client as any).getContentFromFilePath(fileUrl);
|
||||
|
||||
expect(result).toBe("file:// file content");
|
||||
});
|
||||
|
||||
it("should use rootPath for relative paths when provided", () => {
|
||||
const client = new RegistryClient({ rootPath: tempDir });
|
||||
|
||||
|
|
|
@ -38,11 +38,9 @@ export class RegistryClient implements Registry {
|
|||
|
||||
private getContentFromFilePath(filepath: string): string {
|
||||
if (filepath.startsWith("file://")) {
|
||||
const pathWithoutProtocol = filepath.slice(7);
|
||||
|
||||
// For Windows file:///C:/path/to/file, we need to handle it properly
|
||||
// On other systems, we might have file:///path/to/file
|
||||
return fs.readFileSync(decodeURIComponent(pathWithoutProtocol), "utf8");
|
||||
return fs.readFileSync(new URL(filepath), "utf8");
|
||||
} else if (path.isAbsolute(filepath)) {
|
||||
return fs.readFileSync(filepath, "utf8");
|
||||
} else if (this.rootPath) {
|
||||
|
|
Loading…
Reference in New Issue