Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7098e9072e | |||
| 40aa5a2467 | |||
| d077eb9ca4 | |||
| 54da9fa0f9 | |||
| 19dda35ff7 | |||
| d15e559a73 | |||
| f949c05b14 | |||
| 121983ae6d |
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -6,4 +6,4 @@ title: For VSCode-related issues (eg builds), please start the title with `[App]
|
||||
|
||||
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
|
||||
|
||||
2. Describe the issue/feature here!
|
||||
2. Describe the issue/feature here.
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
import os, sys, json, datetime, pathlib, textwrap, requests
|
||||
from openai import OpenAI
|
||||
|
||||
REPO = "voideditor/void"
|
||||
CACHE_FILE = pathlib.Path(".github/triage_cache.json")
|
||||
STAMP_FILE = pathlib.Path(".github/last_triage.txt")
|
||||
|
||||
THEMES_MD = textwrap.dedent("""\
|
||||
1. 🔗 LLM Integration & Provider Support
|
||||
2. 🖥 App Build & Platform Compatibility
|
||||
3. 🎯 Prompt, Token, and Cost Management
|
||||
4. 🧩 Editor UX & Interaction Design
|
||||
5. 🤖 Agent & Automation Features
|
||||
6. ⚙️ System Config & Environment Setup
|
||||
7. 🗃 Meta: Feature Comparison, Structure, and Naming
|
||||
""").strip()
|
||||
|
||||
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
||||
headers = {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}
|
||||
|
||||
|
||||
# ───────── helpers ────────────────────────────────────────────────────────
|
||||
def utc_iso_now() -> str:
|
||||
return datetime.datetime.utcnow().replace(microsecond=0, tzinfo=datetime.timezone.utc).isoformat()
|
||||
|
||||
def read_stamp() -> str:
|
||||
return STAMP_FILE.read_text().strip() if STAMP_FILE.exists() else "1970-01-01T00:00:00Z"
|
||||
|
||||
def save_stamp():
|
||||
STAMP_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STAMP_FILE.write_text(utc_iso_now())
|
||||
|
||||
def load_cache() -> dict[int, str]:
|
||||
return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {}
|
||||
|
||||
def save_cache(d: dict[int, str]):
|
||||
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CACHE_FILE.write_text(json.dumps(d, indent=2))
|
||||
|
||||
def fetch_open_issues(since_iso: str | None = None) -> list[dict]:
|
||||
issues, page = [], 1
|
||||
while True:
|
||||
url = (
|
||||
f"https://api.github.com/repos/{REPO}/issues"
|
||||
f"?state=open&per_page=100&page={page}"
|
||||
+ (f"&since={since_iso}" if since_iso else "")
|
||||
)
|
||||
chunk = requests.get(url, headers=headers).json()
|
||||
if not chunk or (isinstance(chunk, dict) and chunk.get("message")):
|
||||
break
|
||||
issues.extend(i for i in chunk if "pull_request" not in i)
|
||||
page += 1
|
||||
return issues
|
||||
|
||||
|
||||
# ───────── main ───────────────────────────────────────────────────────────
|
||||
last_stamp = read_stamp()
|
||||
changed = fetch_open_issues(since_iso=last_stamp)
|
||||
|
||||
# Fallback if **nothing** changed AND we have *no* existing output
|
||||
if not changed:
|
||||
cache_exists = CACHE_FILE.exists()
|
||||
wiki_exists = pathlib.Path("wiki/Issue-Categories.md").exists()
|
||||
if not cache_exists or not wiki_exists:
|
||||
# first run or someone wiped the wiki → build from scratch
|
||||
print("⏩ First run or empty wiki — fetching ALL open issues.", file=sys.stderr)
|
||||
changed = fetch_open_issues() # full list
|
||||
else:
|
||||
print(f"✅ No issues updated since {last_stamp}. Nothing to classify.", file=sys.stderr)
|
||||
save_stamp()
|
||||
sys.exit(0)
|
||||
|
||||
# ---------------------------------------------------------------- prompt
|
||||
issue_lines = "\n".join(f"- {i['title']} ({i['html_url']})" for i in changed)
|
||||
prompt = textwrap.dedent(f"""\
|
||||
You are an AI assistant helping triage GitHub issues into exactly 7 predefined themes.
|
||||
|
||||
Each issue must go into exactly one of the themes below:
|
||||
|
||||
{THEMES_MD}
|
||||
|
||||
Format your output in Markdown like:
|
||||
## 🎯 Prompt, Token, and Cost Management
|
||||
- [#123](https://github.com/org/repo/issues/123) – Title here
|
||||
|
||||
Classify these issues:
|
||||
{issue_lines}
|
||||
""")
|
||||
|
||||
resp = client.chat.completions.create(
|
||||
model="gpt-4.1",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.2,
|
||||
)
|
||||
|
||||
md = resp.choices[0].message.content
|
||||
|
||||
# ---------------------------------------------------------------- parse GPT
|
||||
new_map: dict[int, str] = {}
|
||||
current = None
|
||||
for ln in md.splitlines():
|
||||
if ln.startswith("##"):
|
||||
current = ln.lstrip("# ").strip()
|
||||
elif ln.lstrip().startswith("- [#"):
|
||||
try:
|
||||
num = int(ln.split("[#")[1].split("]")[0])
|
||||
new_map[num] = current
|
||||
except Exception:
|
||||
pass # ignore malformed lines
|
||||
|
||||
cache = load_cache()
|
||||
cache.update(new_map)
|
||||
save_cache(cache)
|
||||
save_stamp()
|
||||
|
||||
# ---------------------------------------------------------------- rebuild wiki
|
||||
order = [
|
||||
"🔗 LLM Integration & Provider Support",
|
||||
"🖥 App Build & Platform Compatibility",
|
||||
"🎯 Prompt, Token, and Cost Management",
|
||||
"🧩 Editor UX & Interaction Design",
|
||||
"🤖 Agent & Automation Features",
|
||||
"⚙️ System Config & Environment Setup",
|
||||
"🗃 Meta: Feature Comparison, Structure, and Naming",
|
||||
]
|
||||
|
||||
sections: dict[str, list[int]] = {t: [] for t in order}
|
||||
|
||||
# ── fetch ALL current open issues once (PRs filtered out) ────────────────
|
||||
title_map: dict[int, tuple[str, str]] = {}
|
||||
open_now: set[int] = set()
|
||||
|
||||
page = 1
|
||||
while True:
|
||||
batch = fetch_open_issues(since_iso=None) if page == 1 else []
|
||||
if not batch:
|
||||
break
|
||||
for it in batch:
|
||||
num = it["number"]
|
||||
title_map[num] = (it["title"], it["html_url"])
|
||||
open_now.add(num)
|
||||
page += 1
|
||||
|
||||
# 🧹 drop any cached IDs that are no longer open issues (e.g., became a PR or were closed)
|
||||
for stale in set(cache) - open_now:
|
||||
del cache[stale]
|
||||
save_cache(cache) # persist cleaned cache
|
||||
|
||||
# build sections from cleaned cache
|
||||
for num, theme in cache.items():
|
||||
if theme in sections: # extra safety
|
||||
sections[theme].append(num)
|
||||
|
||||
# ---------------------------------------------------------------- print roadmap
|
||||
for theme in order:
|
||||
issues = sections[theme]
|
||||
if issues:
|
||||
print(f"## {theme}")
|
||||
for n in sorted(issues):
|
||||
title, url = title_map.get(n, ("(missing)", f"https://github.com/{REPO}/issues/{n}"))
|
||||
print(f"- [#{n}]({url}) – {title}")
|
||||
print()
|
||||
@@ -1,60 +0,0 @@
|
||||
name: Issue Triage to Wiki
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # every 6 hrs (UTC)
|
||||
|
||||
jobs:
|
||||
roadmap:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 1️⃣ Check out code (so the script and cache files are present)
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1 # shallow clone
|
||||
|
||||
# 2️⃣ Set up Python
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
# 3️⃣ Install dependencies
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install openai requests
|
||||
|
||||
# 4️⃣ Clone your fork’s Wiki
|
||||
- name: Clone your fork's Wiki
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.WIKI_TOKEN }}@github.com/${{ github.repository }}.wiki.git wiki
|
||||
|
||||
# 5️⃣ (Optional) Show repo tree for debugging
|
||||
- name: Show repo tree (debug)
|
||||
run: |
|
||||
echo "PWD: $(pwd)"
|
||||
ls -al
|
||||
ls -al .github/scripts || true
|
||||
ls -al void/.github/scripts || true
|
||||
|
||||
# 6️⃣ Generate roadmap and push only if it changed
|
||||
- name: Generate roadmap directly into wiki
|
||||
run: |
|
||||
python .github/scripts/issue_triage.py > wiki/_new.md
|
||||
if ! cmp -s wiki/_new.md wiki/Issue-Categories.md ; then
|
||||
mv wiki/_new.md wiki/Issue-Categories.md
|
||||
cd wiki
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add Issue-Categories.md
|
||||
git commit -m "Auto-update Issue-Categories.md from GPT triage"
|
||||
git push
|
||||
else
|
||||
echo "No content change – skipping wiki update"
|
||||
fi
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.WIKI_TOKEN }}
|
||||
@@ -21,10 +21,3 @@ vscode.db
|
||||
product.overrides.json
|
||||
*.snap.actual
|
||||
.vscode-test
|
||||
|
||||
# Void added these:
|
||||
.tmp/
|
||||
.tmp2/
|
||||
.tool-versions
|
||||
src/vs/workbench/contrib/void/browser/react/out/**
|
||||
src/vs/workbench/contrib/void/browser/react/src2/**
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
This is a fork of the VSCode repo called Void.
|
||||
|
||||
Most code we care about lives in src/vs/workbench/contrib/void.
|
||||
|
||||
You may often need to explore the full repo to find relevant parts of code.
|
||||
Look for services and built-in functions that you might need to use to solve the problem.
|
||||
|
||||
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
|
||||
|
||||
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
|
||||
|
||||
Never modify files outside src/vs/workbench/contrib/void without consulting with the user first.
|
||||
|
||||
All types that map from a value A to B should be called bOfA. For example, if you create a hashmap that goes from toolId to toolName, it should be called toolNameOfToolId, etc.
|
||||
|
||||
Do not run anything to validate your changes; tell the user what to do instead.
|
||||
+67
-57
@@ -12,7 +12,7 @@ There are a few ways to contribute:
|
||||
|
||||
### Codebase Guide
|
||||
|
||||
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to add new features.
|
||||
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
|
||||
|
||||
The repo is not as intimidating as it first seems if you read the guide!
|
||||
|
||||
@@ -20,15 +20,14 @@ Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
|
||||
|
||||
## Editing Void's Code
|
||||
|
||||
If you're making changes to Void's code as a contributor, you'll want to run a local version of Void to make sure your changes worked. Developer mode lets you do this. Here's how to use it.
|
||||
## Building Void
|
||||
|
||||
### a. Mac - Prerequisites
|
||||
### a. Mac - Build Prerequisites
|
||||
|
||||
If you're using a Mac, you need Python and XCode. You probably have these by default.
|
||||
|
||||
### b. Windows - Prerequisites
|
||||
### b. Windows - Build Prerequisites
|
||||
|
||||
If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them.
|
||||
|
||||
@@ -43,52 +42,33 @@ Go to the "Individual Components" tab and select:
|
||||
|
||||
Finally, click Install.
|
||||
|
||||
### c. Linux - Prerequisites
|
||||
### c. Linux - Build Prerequisites
|
||||
|
||||
First, run `npm install -g node-gyp`. Then:
|
||||
|
||||
- Debian (Ubuntu, etc): `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
|
||||
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- SUSE (openSUSE, etc): `sudo zypper install patterns-devel-C-C++-devel_C_C++ krb5-devel libsecret-devel libxkbfile-devel libX11-devel`.
|
||||
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
|
||||
### Developer Mode Instructions
|
||||
### d. Building Void from inside VSCode
|
||||
|
||||
Here's how to start changing Void's code. These steps cover everything from cloning Void, to opening a Developer Mode window where you can play around with your updates.
|
||||
To build Void, open `void/` inside VSCode. Then open your terminal and run:
|
||||
|
||||
1. `git clone https://github.com/voideditor/void` to clone the repo.
|
||||
2. `npm install` to install all dependencies.
|
||||
3. Open Void or VSCode, and initialize Developer Mode (this can take ~5 min to finish, it's done when 2 of the 3 spinners turn to check marks):
|
||||
- Windows: Press <kbd>Ctrl+Shift+B</kbd>.
|
||||
- Mac: Press <kbd>Cmd+Shift+B</kbd>.
|
||||
- Linux: Press <kbd>Ctrl+Shift+B</kbd>.
|
||||
4. Open the Void Developer Mode window:
|
||||
- Windows: `./scripts/code.bat`.
|
||||
- Mac: `./scripts/code.sh`.
|
||||
- Linux: `./scripts/code.sh`.
|
||||
5. You're good to start editing Void's code!
|
||||
- You won't see your changes unless you press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload. Alternatively, press <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
|
||||
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the command in step 4, which lets you reset any IDE changes you made by deleting the `.tmp` folder.
|
||||
- You can kill any of the build scripts by pressing `Ctrl+D` in its terminal. If you press `Ctrl+C` the script will close but will keep running in the background.
|
||||
1. `npm install` to install all dependencies.
|
||||
2. Build Void.
|
||||
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
- This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code).
|
||||
3. Run Void.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
4. Nice-to-knows.
|
||||
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
|
||||
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing.
|
||||
- You can kill any of the build scripts by pressing `Ctrl+D` in VSCode terminal. If you press `Ctrl+C` the script will close but will keep running in the background (to open all background scripts, just re-build).
|
||||
|
||||
If you get any errors, scroll down for common fixes.
|
||||
|
||||
#### Common Fixes
|
||||
|
||||
- Make sure you followed the prerequisite steps above.
|
||||
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`).
|
||||
- You can do this without changing your global Node version using [nvm](https://github.com/nvm-sh/nvm): run `nvm install`, followed by `nvm use` to install the version in `.nvmrc` locally.
|
||||
- Make sure the path to your Void folder does not have any spaces in it.
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
|
||||
- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`.
|
||||
- If you see missing styles, wait a few seconds and then reload.
|
||||
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, when running ./scripts/code.sh, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
|
||||
- If you get errors like `The SUID sandbox helper binary was found, but is not configured correctly` when running ./scripts/code.sh, run
|
||||
`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` and then run `./scripts/code.sh` again.
|
||||
- If you have any other questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
|
||||
|
||||
#### Building Void from Terminal
|
||||
|
||||
To build Void from the terminal instead of from inside VSCode, follow the steps above, but instead of pressing <kbd>Cmd+Shift+B</kbd>, run `npm run watch`. The build is done when you see something like this:
|
||||
@@ -101,47 +81,48 @@ To build Void from the terminal instead of from inside VSCode, follow the steps
|
||||
```
|
||||
|
||||
|
||||
#### Common Fixes
|
||||
|
||||
### Distributing
|
||||
Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder).
|
||||
|
||||
If you want to completely control Void's build pipeline for your own internal usage, which comes with a lot of time cost (and is typically not recommended), see our [`void-builder`](https://github.com/voideditor/void-builder) repo which builds Void and contains a few important notes about auto-updating and rebasing.
|
||||
- Make sure you followed the prerequisite steps above.
|
||||
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`)!
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
|
||||
- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`.
|
||||
- If you see missing styles, wait a few seconds and then reload.
|
||||
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, when running ./scripts/code.sh, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
|
||||
- If you get erorrs like `The SUID sandbox helper binary was found, but is not configured correctly` when running ./scripts/code.sh, run
|
||||
`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` and then run `./scripts/code.sh` again.
|
||||
- If you have any other questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
|
||||
#### Building a Local Executible
|
||||
We don't usually recommend building a local executible of Void - typically you should follow the steps above to distribute a complete executible with the advantages of VSCodium baked-in, or you should just use Developer Mode to run Void locally which is much faster. If you're certain this is what you want, see details below.
|
||||
## Packaging
|
||||
|
||||
<details>
|
||||
<summary> Building Locally (not recommended)</summary>
|
||||
If you're certain you want to build a local executible of Void, follow these steps. It can take ~25 minutes.
|
||||
|
||||
Make sure you've already entered Developer Mode with Void first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below).
|
||||
We don't usually recommend packaging. Instead, you should probably just build. If you're sure you want to package Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - packaging can take ~25 minutes.
|
||||
|
||||
|
||||
##### Mac
|
||||
### Mac
|
||||
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
|
||||
- `npm run gulp vscode-darwin-x64` (Intel)
|
||||
|
||||
##### Windows
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-arm64`
|
||||
|
||||
##### Linux
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm64`
|
||||
|
||||
|
||||
##### Local Executible Output
|
||||
### Output
|
||||
|
||||
The local executible will be located in a folder outside of `void/`:
|
||||
This will generate a folder outside of `void/`:
|
||||
```bash
|
||||
workspace/
|
||||
├── void/ # Your Void fork
|
||||
└── VSCode-darwin-arm64/ # Generated output
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Distributing
|
||||
Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder).
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
@@ -153,3 +134,32 @@ workspace/
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
# Relevant files
|
||||
|
||||
We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
|
||||
Edit: far too many changes to track... this is old
|
||||
|
||||
- README.md
|
||||
- CONTRIBUTING.md
|
||||
- VOID_USEFUL_LINKS.md
|
||||
- product.json
|
||||
- package.json
|
||||
|
||||
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
|
||||
- src/vs/workbench/workbench.common.main.ts
|
||||
- src/vs/workbench/contrib/void/\*
|
||||
- extensions/void/\*
|
||||
|
||||
- .github/\*
|
||||
- .vscode/settings/\*
|
||||
- .eslintrc.json
|
||||
- build/hygiene.js
|
||||
- build/lib/i18n.resources.json
|
||||
- build/npm/dirs.js
|
||||
|
||||
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
|
||||
|
||||
-->
|
||||
|
||||
@@ -1,30 +1,3 @@
|
||||
## Void is now deprecated.
|
||||
Void is deprecated and no longer accepting contributions.
|
||||
Thank you to everyone who contributed, both with lines of code and support from the community. Void remains open source and is still one of the best references to use when forking VS Code.
|
||||
|
||||
|
||||
## Download
|
||||
|
||||
To view a list of newer Void forks, see [Void Forks](http://github.com/voideditor/void-forks/).
|
||||
|
||||
To download an old version of Void, see [Releases](https://github.com/voideditor/void/releases).
|
||||
|
||||
## Forking VS Code
|
||||
|
||||
If you're forking VS Code, you might still want to reference Void's logic, and see our [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) and [How to Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
- We mount React + Tailwind. This is not possible in plain VS Code, and required extending the build pipeline to compile React and [scope](https://github.com/andrewpareles/scope-tailwind) Tailwind ourselves.
|
||||
|
||||
- You can copy our GitHub Actions to package, sign, and auto-update Void. VS Code's build pipeline is private, so this is normally very hard.
|
||||
|
||||
- Our AI provider code is built from scratch, allowing us to support autocomplete (FIM) and other custom responses. We expose grammars for common `<thinking>` tags, tool tags, etc. Feel free to reference our architecture for using IPC and satisfying CSP.
|
||||
|
||||
- Use our custom services to edit files. EditCodeService lets you show diffs as code streams in, even token by token. VoidModelService lets you edit files in the background and syncs OS files with your text buffers.
|
||||
|
||||
- Everything we've done is 100% open source. See [repos](https://github.com/orgs/voideditor/repositories) for a complete picture of all the repos that make up Void.
|
||||
|
||||
|
||||
|
||||
# Welcome to Void.
|
||||
|
||||
<div align="center">
|
||||
@@ -36,26 +9,32 @@ If you're forking VS Code, you might still want to reference Void's logic, and s
|
||||
/>
|
||||
</div>
|
||||
|
||||
Use AI agents on your codebase, checkpoint and visualize changes, and bring any model or host locally. Void sends messages directly to providers without retaining your data.
|
||||
Void is the open-source Cursor alternative.
|
||||
|
||||
This repo contains the full sourcecode for Void's Desktop app. If you're new, welcome!
|
||||
This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome!
|
||||
|
||||
- 🧭 [Website](https://voideditor.com)
|
||||
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
|
||||
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 📝 [Changelog](https://voideditor.com/changelog)
|
||||
|
||||
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
|
||||
|
||||
2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
3. We're open to collaborations and suggestions of all types - just reach out.
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to our codebase, see [VOID_CODEBASE_GUIDE](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
|
||||
|
||||
For a guide on how to develop your own version of Void, see [HOW_TO_CONTRIBUTE](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md) and [void-builder](https://github.com/voideditor/void-builder).
|
||||
|
||||
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
|
||||
|
||||
## Support
|
||||
You can always reach us in our [Discord server](https://discord.gg/RSNjgaugJs) or contact us via email at hello@voideditor.com.
|
||||
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
|
||||
|
||||
@@ -52,10 +52,7 @@ Here's some terminology you might want to know about when working inside VSCode:
|
||||
- You can run actions as a user by pressing Cmd+Shift+P (opens the command pallete), or you can run them internally by using the commandService to call them by ID. We use actions to register keybinding listeners like Cmd+L, Cmd+K, etc. The nice thing about actions is the user can change the keybindings.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Void's LLM Message Pipeline
|
||||
### Internal LLM Message Pipeline
|
||||
|
||||
Here's a picture of all the dependencies that are relevent between the time you first send a message through Void's sidebar, and the time a request is sent to your provider.
|
||||
Sending LLM messages from the main process avoids CSP issues with local providers and lets us use node_modules more easily.
|
||||
@@ -135,24 +132,25 @@ If you want to know how our build pipeline works, see our build repo [here](http
|
||||
|
||||
## VSCode Codebase Guide
|
||||
|
||||
For additional references, the Void team put together this list of links to get up and running with VSCode.
|
||||
<details>
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
|
||||
|
||||
#### Links for Beginners
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
#### Links for Contributors
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
|
||||
#### Misc
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
|
||||
|
||||
|
||||
@@ -161,10 +159,13 @@ For additional references, the Void team put together this list of links to get
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
||||
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
@@ -82,6 +82,7 @@ function buildWin32Setup(arch, target) {
|
||||
productJson['target'] = target;
|
||||
fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t'));
|
||||
|
||||
console.log('RawVersion!!!!!!!!!!!!!!', pkg.version.replace(/-\w+$/, '')) // Void
|
||||
const quality = product.quality || 'dev';
|
||||
const definitions = {
|
||||
NameLong: product.nameLong,
|
||||
|
||||
+1
-3
@@ -842,9 +842,7 @@ export default tseslint.config(
|
||||
'@xterm/xterm',
|
||||
'yauzl',
|
||||
'yazl',
|
||||
'zlib',
|
||||
// Void added this
|
||||
'@modelcontextprotocol/sdk/**'
|
||||
'zlib'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
},
|
||||
"remote.SSH.remotePlatform": {
|
||||
"type": "object",
|
||||
|
||||
@@ -8,203 +8,203 @@ import { getVSCodeServerConfig } from './serverConfig';
|
||||
import SSHConnection from './ssh/sshConnection';
|
||||
|
||||
export interface ServerInstallOptions {
|
||||
id: string;
|
||||
quality: string;
|
||||
commit: string;
|
||||
version: string;
|
||||
release?: string; // void specific
|
||||
extensionIds: string[];
|
||||
envVariables: string[];
|
||||
useSocketPath: boolean;
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate: string;
|
||||
id: string;
|
||||
quality: string;
|
||||
commit: string;
|
||||
version: string;
|
||||
release?: string; // void specific
|
||||
extensionIds: string[];
|
||||
envVariables: string[];
|
||||
useSocketPath: boolean;
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate: string;
|
||||
}
|
||||
|
||||
export interface ServerInstallResult {
|
||||
exitCode: number;
|
||||
listeningOn: number | string;
|
||||
connectionToken: string;
|
||||
logFile: string;
|
||||
osReleaseId: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
tmpDir: string;
|
||||
[key: string]: any;
|
||||
exitCode: number;
|
||||
listeningOn: number | string;
|
||||
connectionToken: string;
|
||||
logFile: string;
|
||||
osReleaseId: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
tmpDir: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class ServerInstallError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
|
||||
let shell = 'powershell';
|
||||
let shell = 'powershell';
|
||||
|
||||
// detect platform and shell for windows
|
||||
if (!platform || platform === 'windows') {
|
||||
const result = await conn.exec('uname -s');
|
||||
// detect platform and shell for windows
|
||||
if (!platform || platform === 'windows') {
|
||||
const result = await conn.exec('uname -s');
|
||||
|
||||
if (result.stdout) {
|
||||
if (result.stdout.includes('windows32')) {
|
||||
platform = 'windows';
|
||||
} else if (result.stdout.includes('MINGW64')) {
|
||||
platform = 'windows';
|
||||
shell = 'bash';
|
||||
}
|
||||
} else if (result.stderr) {
|
||||
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
|
||||
platform = 'windows';
|
||||
}
|
||||
if (result.stdout) {
|
||||
if (result.stdout.includes('windows32')) {
|
||||
platform = 'windows';
|
||||
} else if (result.stdout.includes('MINGW64')) {
|
||||
platform = 'windows';
|
||||
shell = 'bash';
|
||||
}
|
||||
} else if (result.stderr) {
|
||||
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
|
||||
platform = 'windows';
|
||||
}
|
||||
|
||||
if (result.stderr.includes('is not recognized as an internal or external command')) {
|
||||
platform = 'windows';
|
||||
shell = 'cmd';
|
||||
}
|
||||
}
|
||||
if (result.stderr.includes('is not recognized as an internal or external command')) {
|
||||
platform = 'windows';
|
||||
shell = 'cmd';
|
||||
}
|
||||
}
|
||||
|
||||
if (platform) {
|
||||
logger.trace(`Detected platform: ${platform}, ${shell}`);
|
||||
}
|
||||
}
|
||||
if (platform) {
|
||||
logger.trace(`Detected platform: ${platform}, ${shell}`);
|
||||
}
|
||||
}
|
||||
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
||||
const vscodeServerConfig = await getVSCodeServerConfig();
|
||||
const installOptions: ServerInstallOptions = {
|
||||
id: scriptId,
|
||||
version: vscodeServerConfig.version,
|
||||
commit: vscodeServerConfig.commit,
|
||||
quality: vscodeServerConfig.quality,
|
||||
release: vscodeServerConfig.release,
|
||||
extensionIds,
|
||||
envVariables,
|
||||
useSocketPath,
|
||||
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
||||
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
||||
};
|
||||
const vscodeServerConfig = await getVSCodeServerConfig();
|
||||
const installOptions: ServerInstallOptions = {
|
||||
id: scriptId,
|
||||
version: vscodeServerConfig.version,
|
||||
commit: vscodeServerConfig.commit,
|
||||
quality: vscodeServerConfig.quality,
|
||||
release: vscodeServerConfig.release,
|
||||
extensionIds,
|
||||
envVariables,
|
||||
useSocketPath,
|
||||
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
||||
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
||||
};
|
||||
|
||||
let commandOutput: { stdout: string; stderr: string };
|
||||
if (platform === 'windows') {
|
||||
const installServerScript = generatePowerShellInstallScript(installOptions);
|
||||
let commandOutput: { stdout: string; stderr: string };
|
||||
if (platform === 'windows') {
|
||||
const installServerScript = generatePowerShellInstallScript(installOptions);
|
||||
|
||||
logger.trace('Server install command:', installServerScript);
|
||||
logger.trace('Server install command:', installServerScript);
|
||||
|
||||
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
|
||||
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
|
||||
const endRegex = new RegExp(`${scriptId}: end`);
|
||||
// investigate if it's possible to use `-EncodedCommand` flag
|
||||
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
|
||||
let command = '';
|
||||
if (shell === 'powershell') {
|
||||
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
|
||||
} else if (shell === 'bash') {
|
||||
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
|
||||
} else if (shell === 'cmd') {
|
||||
const script = installServerScript.trim()
|
||||
// remove comments
|
||||
.replace(/^#.*$/gm, '')
|
||||
// remove empty lines
|
||||
.replace(/\n{2,}/gm, '\n')
|
||||
// remove leading spaces
|
||||
.replace(/^\s*/gm, '')
|
||||
// escape double quotes (from powershell/cmd)
|
||||
.replace(/"/g, '"""')
|
||||
// escape single quotes (from cmd)
|
||||
.replace(/'/g, `''`)
|
||||
// escape redirect (from cmd)
|
||||
.replace(/>/g, `^>`)
|
||||
// escape new lines (from powershell/cmd)
|
||||
.replace(/\n/g, '\'`n\'');
|
||||
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
|
||||
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
|
||||
const endRegex = new RegExp(`${scriptId}: end`);
|
||||
// investigate if it's possible to use `-EncodedCommand` flag
|
||||
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
|
||||
let command = '';
|
||||
if (shell === 'powershell') {
|
||||
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
|
||||
} else if (shell === 'bash') {
|
||||
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
|
||||
} else if (shell === 'cmd') {
|
||||
const script = installServerScript.trim()
|
||||
// remove comments
|
||||
.replace(/^#.*$/gm, '')
|
||||
// remove empty lines
|
||||
.replace(/\n{2,}/gm, '\n')
|
||||
// remove leading spaces
|
||||
.replace(/^\s*/gm, '')
|
||||
// escape double quotes (from powershell/cmd)
|
||||
.replace(/"/g, '"""')
|
||||
// escape single quotes (from cmd)
|
||||
.replace(/'/g, `''`)
|
||||
// escape redirect (from cmd)
|
||||
.replace(/>/g, `^>`)
|
||||
// escape new lines (from powershell/cmd)
|
||||
.replace(/\n/g, '\'`n\'');
|
||||
|
||||
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
|
||||
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
|
||||
|
||||
logger.trace('Command length (8191 max):', command.length);
|
||||
logger.trace('Command length (8191 max):', command.length);
|
||||
|
||||
if (command.length > 8191) {
|
||||
throw new ServerInstallError(`Command line too long`);
|
||||
}
|
||||
} else {
|
||||
throw new ServerInstallError(`Not supported shell: ${shell}`);
|
||||
}
|
||||
if (command.length > 8191) {
|
||||
throw new ServerInstallError(`Command line too long`);
|
||||
}
|
||||
} else {
|
||||
throw new ServerInstallError(`Not supported shell: ${shell}`);
|
||||
}
|
||||
|
||||
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
|
||||
} else {
|
||||
const installServerScript = generateBashInstallScript(installOptions);
|
||||
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
|
||||
} else {
|
||||
const installServerScript = generateBashInstallScript(installOptions);
|
||||
|
||||
logger.trace('Server install command:', installServerScript);
|
||||
// Fish shell does not support heredoc so let's workaround it using -c option,
|
||||
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
||||
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
|
||||
}
|
||||
logger.trace('Server install command:', installServerScript);
|
||||
// Fish shell does not support heredoc so let's workaround it using -c option,
|
||||
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
||||
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
|
||||
}
|
||||
|
||||
if (commandOutput.stderr) {
|
||||
logger.trace('Server install command stderr:', commandOutput.stderr);
|
||||
}
|
||||
logger.trace('Server install command stdout:', commandOutput.stdout);
|
||||
if (commandOutput.stderr) {
|
||||
logger.trace('Server install command stderr:', commandOutput.stderr);
|
||||
}
|
||||
logger.trace('Server install command stdout:', commandOutput.stdout);
|
||||
|
||||
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
||||
if (!resultMap) {
|
||||
throw new ServerInstallError(`Failed parsing install script output`);
|
||||
}
|
||||
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
||||
if (!resultMap) {
|
||||
throw new ServerInstallError(`Failed parsing install script output`);
|
||||
}
|
||||
|
||||
const exitCode = parseInt(resultMap.exitCode, 10);
|
||||
if (exitCode !== 0) {
|
||||
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
|
||||
}
|
||||
const exitCode = parseInt(resultMap.exitCode, 10);
|
||||
if (exitCode !== 0) {
|
||||
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
|
||||
}
|
||||
|
||||
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
|
||||
? parseInt(resultMap.listeningOn, 10)
|
||||
: resultMap.listeningOn;
|
||||
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
|
||||
? parseInt(resultMap.listeningOn, 10)
|
||||
: resultMap.listeningOn;
|
||||
|
||||
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
||||
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
listeningOn,
|
||||
connectionToken: resultMap.connectionToken,
|
||||
logFile: resultMap.logFile,
|
||||
osReleaseId: resultMap.osReleaseId,
|
||||
arch: resultMap.arch,
|
||||
platform: resultMap.platform,
|
||||
tmpDir: resultMap.tmpDir,
|
||||
...remoteEnvVars
|
||||
};
|
||||
return {
|
||||
exitCode,
|
||||
listeningOn,
|
||||
connectionToken: resultMap.connectionToken,
|
||||
logFile: resultMap.logFile,
|
||||
osReleaseId: resultMap.osReleaseId,
|
||||
arch: resultMap.arch,
|
||||
platform: resultMap.platform,
|
||||
tmpDir: resultMap.tmpDir,
|
||||
...remoteEnvVars
|
||||
};
|
||||
}
|
||||
|
||||
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
|
||||
const startResultStr = `${scriptId}: start`;
|
||||
const endResultStr = `${scriptId}: end`;
|
||||
const startResultStr = `${scriptId}: start`;
|
||||
const endResultStr = `${scriptId}: end`;
|
||||
|
||||
const startResultIdx = str.indexOf(startResultStr);
|
||||
if (startResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const startResultIdx = str.indexOf(startResultStr);
|
||||
if (startResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
||||
if (endResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
||||
if (endResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
||||
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
||||
|
||||
const resultMap: { [k: string]: string } = {};
|
||||
const resultArr = installResult.split(/\r?\n/);
|
||||
for (const line of resultArr) {
|
||||
const [key, value] = line.split('==');
|
||||
resultMap[key] = value;
|
||||
}
|
||||
const resultMap: { [k: string]: string } = {};
|
||||
const resultArr = installResult.split(/\r?\n/);
|
||||
for (const line of resultArr) {
|
||||
const [key, value] = line.split('==');
|
||||
resultMap[key] = value;
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
return `
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
|
||||
@@ -427,16 +427,16 @@ print_install_results_and_exit 0
|
||||
}
|
||||
|
||||
function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
const downloadUrl = serverDownloadUrlTemplate
|
||||
.replace(/\$\{quality\}/g, quality)
|
||||
.replace(/\$\{version\}/g, version)
|
||||
.replace(/\$\{commit\}/g, commit)
|
||||
.replace(/\$\{os\}/g, 'win32')
|
||||
.replace(/\$\{arch\}/g, 'x64')
|
||||
.replace(/\$\{release\}/g, release ?? '');
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
const downloadUrl = serverDownloadUrlTemplate
|
||||
.replace(/\$\{quality\}/g, quality)
|
||||
.replace(/\$\{version\}/g, version)
|
||||
.replace(/\$\{commit\}/g, commit)
|
||||
.replace(/\$\{os\}/g, 'win32')
|
||||
.replace(/\$\{arch\}/g, 'x64')
|
||||
.replace(/\$\{release\}/g, release ?? '');
|
||||
|
||||
return `
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
||||
Generated
+3943
-7589
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -75,11 +75,11 @@
|
||||
"@anthropic-ai/sdk": "^0.40.0",
|
||||
"@c4312/eventsource-umd": "^3.0.5",
|
||||
"@floating-ui/react": "^0.27.8",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.6.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
@@ -109,7 +109,6 @@
|
||||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"groq-sdk": "^0.20.1",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
|
||||
+2
-5
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.4.9",
|
||||
"voidRelease": "0044",
|
||||
"voidVersion": "1.2.8",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
@@ -39,8 +38,6 @@
|
||||
"builtInExtensions": [],
|
||||
"linkProtectionTrustedDomains": [
|
||||
"https://voideditor.com",
|
||||
"https://voideditor.dev",
|
||||
"https://github.com/voideditor/void",
|
||||
"https://ollama.com"
|
||||
"https://voideditor.dev"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -130,9 +130,7 @@ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpda
|
||||
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
|
||||
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
|
||||
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
|
||||
import { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMainService.js';
|
||||
import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.js';
|
||||
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
|
||||
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
* even if the user starts many instances (e.g. from the command line).
|
||||
@@ -1104,7 +1102,6 @@ export class CodeApplication extends Disposable {
|
||||
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
|
||||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
|
||||
services.set(IVoidSCMService, new SyncDescriptor(VoidSCMService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
@@ -1246,14 +1243,6 @@ export class CodeApplication extends Disposable {
|
||||
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
|
||||
|
||||
// Void added this
|
||||
const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCMService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-scm', voidSCMChannel);
|
||||
|
||||
// Void added this
|
||||
const mcpChannel = new MCPChannel();
|
||||
mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel);
|
||||
|
||||
// Extension Host Debug Broadcasting
|
||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
||||
|
||||
@@ -163,35 +163,38 @@ export class TelemetryService implements ITelemetryService {
|
||||
}
|
||||
|
||||
function getTelemetryLevelSettingDescription(): string {
|
||||
const telemetryText = localize('telemetry.telemetryLevelMd', "The default telemetry setting for VS Code (Microsoft). {0} recommends keeping this off.", product.nameLong);
|
||||
// const externalLinksStatement = !product.privacyStatementUrl ?
|
||||
// localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') :
|
||||
// localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl);
|
||||
const restartString = !isWeb ? localize('telemetry.restart', 'Microsoft says \"Some third party extensions might not respect this setting. Consult the specific extension\'s documentation to be sure. A full restart of the application is necessary for crash reporting changes to take effect.\"') : '';
|
||||
const telemetryText = localize('telemetry.telemetryLevelMd', "Controls {0} telemetry, first-party extension telemetry, and participating third-party extension telemetry. Some third party extensions might not respect this setting. Consult the specific extension's documentation to be sure. Telemetry helps us better understand how {0} is performing, where improvements need to be made, and how features are being used.", product.nameLong);
|
||||
const externalLinksStatement = !product.privacyStatementUrl ?
|
||||
localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') :
|
||||
localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl);
|
||||
const restartString = !isWeb ? localize('telemetry.restart', 'A full restart of the application is necessary for crash reporting changes to take effect.') : '';
|
||||
|
||||
const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports");
|
||||
const errorsHeader = localize('telemetry.errors', "Error Telemetry");
|
||||
const usageHeader = localize('telemetry.usage', "Usage Data");
|
||||
|
||||
// Void removed these
|
||||
// const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports");
|
||||
// const errorsHeader = localize('telemetry.errors', "Error Telemetry");
|
||||
// const usageHeader = localize('telemetry.usage', "Usage Data");
|
||||
const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:");
|
||||
const telemetryTable = `
|
||||
| | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} |
|
||||
|:------|:-------------:|:---------------:|:----------:|
|
||||
| all | ✓ | ✓ | ✓ |
|
||||
| error | ✓ | ✓ | - |
|
||||
| crash | ✓ | - | - |
|
||||
| off | - | - | - |
|
||||
`;
|
||||
|
||||
// const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:");
|
||||
// const telemetryTable = `
|
||||
// | | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} |
|
||||
// |:------|:-------------:|:---------------:|:----------:|
|
||||
// | all | ✓ | ✓ | ✓ |
|
||||
// | error | ✓ | ✓ | - |
|
||||
// | crash | ✓ | - | - |
|
||||
// | off | - | - | - |
|
||||
// `;
|
||||
|
||||
// const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*");
|
||||
const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*");
|
||||
const telemetryDescription = `
|
||||
${telemetryText}
|
||||
${telemetryText} ${externalLinksStatement} ${restartString}
|
||||
|
||||
${restartString}
|
||||
|
||||
|
||||
Void separately records basic usage like the number of messages people are sending. If you'd like to disable Void metrics, you may do so in Void's Settings.
|
||||
${telemetryTableDescription}
|
||||
${telemetryTable}
|
||||
|
||||
|
||||
|
||||
${deprecatedSettingNote}
|
||||
`;
|
||||
|
||||
return telemetryDescription;
|
||||
|
||||
@@ -26,6 +26,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
|
||||
/* eslint-disable */ // Void
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
|
||||
import { VIEWLET_ID as REMOTE_EXPLORER_VIEWLET_ID } from '../../../contrib/remote/browser/remoteExplorer.js';
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -305,21 +306,21 @@ export class EditorGroupWatermark extends Disposable {
|
||||
label2.set(keys2);
|
||||
this.currentDisposables.add(label2);
|
||||
|
||||
// const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
// const button3 = append(recentsBox, $('button'));
|
||||
// button3.textContent = `Void Settings`
|
||||
// button3.style.display = 'block'
|
||||
// button3.style.marginLeft = 'auto'
|
||||
// button3.style.marginRight = 'auto'
|
||||
// button3.classList.add('void-settings-watermark-button')
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(recentsBox, $('button'));
|
||||
button3.textContent = `Void Settings`
|
||||
button3.style.display = 'block'
|
||||
button3.style.marginLeft = 'auto'
|
||||
button3.style.marginRight = 'auto'
|
||||
button3.classList.add('void-settings-watermark-button')
|
||||
|
||||
// const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
// if (keys3)
|
||||
// label3.set(keys3);
|
||||
// button3.onclick = () => {
|
||||
// this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
// }
|
||||
// this.currentDisposables.add(label3);
|
||||
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys3)
|
||||
label3.set(keys3);
|
||||
button3.onclick = () => {
|
||||
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
}
|
||||
this.currentDisposables.add(label3);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,3 @@
|
||||
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
|
||||
|
||||
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
|
||||
|
||||
export const VOID_ACCEPT_DIFF_ACTION_ID = 'void.acceptDiff'
|
||||
|
||||
export const VOID_REJECT_DIFF_ACTION_ID = 'void.rejectDiff'
|
||||
|
||||
export const VOID_GOTO_NEXT_DIFF_ACTION_ID = 'void.goToNextDiff'
|
||||
|
||||
export const VOID_GOTO_PREV_DIFF_ACTION_ID = 'void.goToPrevDiff'
|
||||
|
||||
export const VOID_GOTO_NEXT_URI_ACTION_ID = 'void.goToNextUri'
|
||||
|
||||
export const VOID_GOTO_PREV_URI_ACTION_ID = 'void.goToPrevUri'
|
||||
|
||||
export const VOID_ACCEPT_FILE_ACTION_ID = 'void.acceptFile'
|
||||
|
||||
export const VOID_REJECT_FILE_ACTION_ID = 'void.rejectFile'
|
||||
|
||||
export const VOID_ACCEPT_ALL_DIFFS_ACTION_ID = 'void.acceptAllDiffs'
|
||||
|
||||
export const VOID_REJECT_ALL_DIFFS_ACTION_ID = 'void.rejectAllDiffs'
|
||||
|
||||
@@ -790,7 +790,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
console.log('starting autocomplete...', predictionType)
|
||||
|
||||
const featureName: FeatureName = 'Autocomplete'
|
||||
const overridesOfModel = this._settingsService.state.overridesOfModel
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
@@ -808,7 +807,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
}),
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
overridesOfModel,
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
onText: () => { }, // unused in FIMMessage
|
||||
// onText: async ({ fullText, newText }) => {
|
||||
|
||||
@@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { chat_userMessageContent, isABuiltinToolName } from '../common/prompt/prompts.js';
|
||||
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
|
||||
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { IToolsService } from './toolsService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
@@ -35,10 +35,6 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
import { timeout } from '../../../../base/common/async.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IMCPService } from '../common/mcpService.js';
|
||||
import { RawMCPToolCall } from '../common/mcpServiceTypes.js';
|
||||
|
||||
|
||||
// related to retrying when LLM message has error
|
||||
@@ -104,13 +100,6 @@ const defaultMessageState: UserMessageState = {
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
|
||||
type WhenMounted = {
|
||||
textAreaRef: { current: HTMLTextAreaElement | null }; // the textarea that this thread has, gets set in SidebarChat
|
||||
scrollToBottom: () => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type ThreadType = {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
@@ -131,15 +120,6 @@ export type ThreadType = {
|
||||
[codespanName: string]: CodespanLocationLink
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mountedInfo?: {
|
||||
whenMounted: Promise<WhenMounted>
|
||||
_whenMountedResolver: (res: WhenMounted) => void
|
||||
mountedIsResolvedRef: { current: boolean };
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,11 +163,10 @@ export type ThreadStreamState = {
|
||||
llmInfo?: undefined;
|
||||
toolInfo: {
|
||||
toolName: ToolName;
|
||||
toolParams: ToolCallParams<ToolName>;
|
||||
toolParams: ToolCallParams[ToolName];
|
||||
id: string;
|
||||
content: string;
|
||||
rawParams: RawToolParamsObj;
|
||||
mcpServerName: string | undefined;
|
||||
};
|
||||
interrupt: Promise<() => void>;
|
||||
} | {
|
||||
@@ -201,7 +180,7 @@ export type ThreadStreamState = {
|
||||
error?: undefined;
|
||||
llmInfo?: undefined;
|
||||
toolInfo?: undefined;
|
||||
interrupt: 'not_needed' | Promise<() => void>; // calling this should have no effect on state - would be too confusing. it just cancels the tool
|
||||
interrupt?: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +235,6 @@ export interface IChatThreadService {
|
||||
isCurrentlyFocusingMessage(): boolean;
|
||||
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
popStagingSelections(numPops?: number): void;
|
||||
addNewStagingSelection(newSelection: StagingSelectionItem): void;
|
||||
|
||||
dangerousSetState: (newState: ThreadsState) => void;
|
||||
@@ -288,9 +266,6 @@ export interface IChatThreadService {
|
||||
|
||||
// jump to history
|
||||
jumpToCheckpointBeforeMessageIdx(opts: { threadId: string, messageIdx: number, jumpToUserModified: boolean }): void;
|
||||
|
||||
focusCurrentChat: () => Promise<void>
|
||||
blurCurrentChat: () => Promise<void>
|
||||
}
|
||||
|
||||
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
|
||||
@@ -324,9 +299,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IMCPService private readonly _mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
@@ -360,26 +332,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
}
|
||||
|
||||
async focusCurrentChat() {
|
||||
const threadId = this.state.currentThreadId
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
const s = await thread.state.mountedInfo?.whenMounted
|
||||
if (!this.isCurrentlyFocusingMessage()) {
|
||||
s?.textAreaRef.current?.focus()
|
||||
}
|
||||
}
|
||||
async blurCurrentChat() {
|
||||
const threadId = this.state.currentThreadId
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
const s = await thread.state.mountedInfo?.whenMounted
|
||||
if (!this.isCurrentlyFocusingMessage()) {
|
||||
s?.textAreaRef.current?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
dangerousSetState = (newState: ThreadsState) => {
|
||||
this.state = newState
|
||||
@@ -424,7 +376,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, doNotRefreshMountInfo?: boolean) {
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
const newState = {
|
||||
...this.state,
|
||||
...state
|
||||
@@ -432,7 +384,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
this.state = newState
|
||||
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
|
||||
|
||||
// if we just switched to a thread, update its current stream state if it's not streaming to possibly streaming
|
||||
@@ -449,33 +402,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// if running now but stream state doesn't indicate it (happens if restart Void), cancel that last tool
|
||||
if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'running_now') {
|
||||
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params, mcpServerName: lastMessage.mcpServerName })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// if we did not just set the state to true, set mount info
|
||||
if (doNotRefreshMountInfo) return
|
||||
|
||||
let whenMountedResolver: (w: WhenMounted) => void
|
||||
const whenMountedPromise = new Promise<WhenMounted>((res) => whenMountedResolver = res)
|
||||
|
||||
this._setThreadState(threadId, {
|
||||
mountedInfo: {
|
||||
whenMounted: whenMountedPromise,
|
||||
mountedIsResolvedRef: { current: false },
|
||||
_whenMountedResolver: (w: WhenMounted) => {
|
||||
whenMountedResolver(w)
|
||||
const mountInfo = this.state.allThreads[threadId]?.state.mountedInfo
|
||||
if (mountInfo) mountInfo.mountedIsResolvedRef.current = true
|
||||
},
|
||||
}
|
||||
}, true) // do not trigger an update
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -537,63 +468,51 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
|
||||
let params: ToolCallParams<ToolName>
|
||||
let params: ToolCallParams[ToolName]
|
||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||
params = lastMsg.params
|
||||
}
|
||||
else return
|
||||
|
||||
const { name, id, rawParams, mcpServerName } = lastMsg
|
||||
const { name, id, rawParams } = lastMsg
|
||||
|
||||
const errorMessage = this.toolErrMsgs.rejected
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams, mcpServerName })
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
|
||||
this._setStreamState(threadId, undefined)
|
||||
}
|
||||
|
||||
private _computeMCPServerOfToolName = (toolName: string) => {
|
||||
return this._mcpService.getMCPTools()?.find(t => t.name === toolName)?.mcpServerName
|
||||
}
|
||||
|
||||
async abortRunning(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
// interrupt any effects
|
||||
const interrupt = await this.streamState[threadId]?.interrupt
|
||||
interrupt?.()
|
||||
|
||||
// add assistant message
|
||||
if (this.streamState[threadId]?.isRunning === 'LLM') {
|
||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
// add tool that's running
|
||||
else if (this.streamState[threadId]?.isRunning === 'tool') {
|
||||
const { toolName, toolParams, id, content: content_, rawParams, mcpServerName } = this.streamState[threadId].toolInfo
|
||||
const content = content_ || this.toolErrMsgs.interrupted
|
||||
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null, mcpServerName })
|
||||
const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo
|
||||
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null })
|
||||
}
|
||||
// reject the tool for the user if relevant
|
||||
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
|
||||
this.rejectLatestToolRequest(threadId)
|
||||
}
|
||||
else if (this.streamState[threadId]?.isRunning === 'idle') {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
this._addUserCheckpoint({ threadId })
|
||||
|
||||
// interrupt any effects
|
||||
const interrupt = await this.streamState[threadId]?.interrupt
|
||||
if (typeof interrupt === 'function')
|
||||
interrupt()
|
||||
|
||||
|
||||
this._setStreamState(threadId, undefined)
|
||||
}
|
||||
|
||||
|
||||
|
||||
private readonly toolErrMsgs = {
|
||||
private readonly errMsgs = {
|
||||
rejected: 'Tool call was rejected by the user.',
|
||||
interrupted: 'Tool call was interrupted by the user.',
|
||||
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
|
||||
}
|
||||
|
||||
@@ -606,46 +525,36 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
threadId: string,
|
||||
toolName: ToolName,
|
||||
toolId: string,
|
||||
mcpServerName: string | undefined,
|
||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams<ToolName> } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||
|
||||
// compute these below
|
||||
let toolParams: ToolCallParams<ToolName>
|
||||
let toolResult: ToolResult<ToolName>
|
||||
let toolParams: ToolCallParams[ToolName]
|
||||
let toolResult: Awaited<ToolResultType[typeof toolName]>
|
||||
let toolResultStr: string
|
||||
|
||||
// Check if it's a built-in tool
|
||||
const isBuiltInTool = isABuiltinToolName(toolName)
|
||||
|
||||
|
||||
if (!opts.preapproved) { // skip this if pre-approved
|
||||
// 1. validate tool params
|
||||
try {
|
||||
if (isBuiltInTool) {
|
||||
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
}
|
||||
else {
|
||||
toolParams = opts.unvalidatedToolParams
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, mcpServerName })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, })
|
||||
return {}
|
||||
}
|
||||
// once validated, add checkpoint for edit
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['edit_file']).uri }) }
|
||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['rewrite_file']).uri }) }
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) }
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
|
||||
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools'
|
||||
|
||||
const approvalType = approvalTypeOfToolName[toolName]
|
||||
if (approvalType) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
|
||||
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
@@ -657,12 +566,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 3. call the tool
|
||||
// this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
||||
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName } as const
|
||||
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const
|
||||
this._updateLatestTool(threadId, runningTool)
|
||||
|
||||
|
||||
@@ -672,28 +578,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
try {
|
||||
|
||||
// set stream state
|
||||
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams, mcpServerName } })
|
||||
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } })
|
||||
|
||||
if (isBuiltInTool) {
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
const interruptor = () => { interrupted = true; interruptTool?.() }
|
||||
resolveInterruptor(interruptor)
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
const interruptor = () => { interrupted = true; interruptTool?.() }
|
||||
resolveInterruptor(interruptor)
|
||||
|
||||
toolResult = await result
|
||||
}
|
||||
else {
|
||||
const mcpTools = this._mcpService.getMCPTools()
|
||||
const mcpTool = mcpTools?.find(t => t.name === toolName)
|
||||
if (!mcpTool) { throw new Error(`MCP tool ${toolName} not found`) }
|
||||
|
||||
resolveInterruptor(() => { })
|
||||
|
||||
toolResult = (await this._mcpService.callMCPTool({
|
||||
serverName: mcpTool.mcpServerName ?? 'unknown_mcp_server',
|
||||
toolName: toolName,
|
||||
params: toolParams
|
||||
})).result
|
||||
}
|
||||
toolResult = await result
|
||||
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
}
|
||||
@@ -702,27 +593,24 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
return {}
|
||||
}
|
||||
finally {
|
||||
this._setStreamState(threadId, undefined)
|
||||
}
|
||||
|
||||
// 4. stringify the result to give to the LLM
|
||||
try {
|
||||
if (isBuiltInTool) {
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
}
|
||||
// For MCP tools, handle the result based on its type
|
||||
else {
|
||||
toolResultStr = this._mcpService.stringifyResult(toolResult as RawMCPToolCall)
|
||||
}
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
} catch (error) {
|
||||
const errorMessage = this.toolErrMsgs.errWhenStringifying(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
const errorMessage = this.errMsgs.errWhenStringifying(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
return {}
|
||||
};
|
||||
|
||||
@@ -743,13 +631,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}) {
|
||||
|
||||
|
||||
let interruptedWhenIdle = false
|
||||
const idleInterruptor = Promise.resolve(() => { interruptedWhenIdle = true })
|
||||
// _runToolCall does not need setStreamState({idle}) before it, but it needs it after it. (handles its own setStreamState)
|
||||
|
||||
// above just defines helpers, below starts the actual function
|
||||
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
|
||||
const { overridesOfModel } = this._settingsService.state
|
||||
|
||||
// not running at start, clear state
|
||||
this._setStreamState(threadId, { isRunning: 'idle' })
|
||||
|
||||
let nMessagesSent = 0
|
||||
let shouldSendAnotherMessage = true
|
||||
@@ -757,15 +643,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, callThisToolFirst.mcpServerName, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
|
||||
if (interrupted) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
this._addUserCheckpoint({ threadId })
|
||||
|
||||
}
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
|
||||
this._setStreamState(threadId, undefined)
|
||||
if (interrupted) { return }
|
||||
}
|
||||
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
|
||||
|
||||
|
||||
// tool use loop
|
||||
while (shouldSendAnotherMessage) {
|
||||
@@ -774,8 +655,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
isRunningWhenEnd = undefined
|
||||
nMessagesSent += 1
|
||||
|
||||
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
|
||||
|
||||
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
|
||||
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
|
||||
chatMessages,
|
||||
@@ -783,46 +662,63 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
chatMode
|
||||
})
|
||||
|
||||
if (interruptedWhenIdle) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
return
|
||||
}
|
||||
|
||||
let shouldRetryLLM = true
|
||||
let nAttempts = 0
|
||||
while (shouldRetryLLM) {
|
||||
if (this.streamState[threadId]?.isRunning) {
|
||||
// if already streaming, stop
|
||||
console.log('returning...', this.streamState[threadId])
|
||||
return
|
||||
}
|
||||
shouldRetryLLM = false
|
||||
nAttempts += 1
|
||||
|
||||
type ResTypes =
|
||||
| { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } }
|
||||
| { type: 'llmError', error?: { message: string; fullError: Error | null; } }
|
||||
| { type: 'llmAborted' }
|
||||
|
||||
let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<ResTypes>((res, rej) => { resMessageIsDonePromise = res })
|
||||
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
|
||||
|
||||
let aborted = false
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages: messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
overridesOfModel,
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
separateSystemMessage: separateSystemMessage,
|
||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) })
|
||||
},
|
||||
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
||||
resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||
this._setStreamState(threadId, undefined)
|
||||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||
|
||||
},
|
||||
onError: async (error) => {
|
||||
resMessageIsDonePromise({ type: 'llmError', error: error })
|
||||
if (this.streamState[threadId]?.isRunning !== 'LLM') {
|
||||
console.log('Unexpected onError when', this.streamState[threadId]?.isRunning)
|
||||
return
|
||||
}
|
||||
|
||||
if (nAttempts < CHAT_RETRIES) {
|
||||
nAttempts += 1
|
||||
shouldRetryLLM = true
|
||||
this._setStreamState(threadId, undefined) // clear later so can be interrupted
|
||||
resMessageIsDonePromise()
|
||||
}
|
||||
else {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
this._setStreamState(threadId, { isRunning: undefined, error })
|
||||
resMessageIsDonePromise()
|
||||
}
|
||||
},
|
||||
onAbort: () => {
|
||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||
resMessageIsDonePromise({ type: 'llmAborted' })
|
||||
aborted = true
|
||||
this._setStreamState(threadId, { isRunning: 'idle' })
|
||||
resMessageIsDonePromise()
|
||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||
},
|
||||
})
|
||||
@@ -834,67 +730,31 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
|
||||
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) })
|
||||
const llmRes = await messageIsDonePromise // wait for message to complete
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
|
||||
// if something else started running in the meantime
|
||||
if (this.streamState[threadId]?.isRunning !== 'LLM') {
|
||||
// console.log('Chat thread interrupted by a newer chat thread', this.streamState[threadId]?.isRunning)
|
||||
return
|
||||
}
|
||||
|
||||
// llm res aborted
|
||||
if (llmRes.type === 'llmAborted') {
|
||||
if (aborted) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
return
|
||||
}
|
||||
// llm res error
|
||||
else if (llmRes.type === 'llmError') {
|
||||
// error, should retry
|
||||
if (nAttempts < CHAT_RETRIES) {
|
||||
shouldRetryLLM = true
|
||||
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
|
||||
await timeout(RETRY_DELAY)
|
||||
if (interruptedWhenIdle) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
return
|
||||
}
|
||||
else
|
||||
continue // retry
|
||||
}
|
||||
// error, but too many attempts
|
||||
else {
|
||||
const { error } = llmRes
|
||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
|
||||
|
||||
this._setStreamState(threadId, { isRunning: undefined, error })
|
||||
this._addUserCheckpoint({ threadId })
|
||||
return
|
||||
}
|
||||
if (shouldRetryLLM) {
|
||||
this._setStreamState(threadId, { isRunning: 'idle' })
|
||||
await timeout(RETRY_DELAY)
|
||||
continue
|
||||
}
|
||||
|
||||
// llm res success
|
||||
const { toolCall, info } = llmRes
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning })
|
||||
|
||||
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity
|
||||
this._setStreamState(threadId, { isRunning: 'idle' })
|
||||
|
||||
// call tool if there is one
|
||||
if (toolCall) {
|
||||
const mcpTools = this._mcpService.getMCPTools()
|
||||
const mcpTool = mcpTools?.find(t => t.name === toolCall.name)
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
|
||||
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, mcpTool?.mcpServerName, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
|
||||
if (interrupted) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
return
|
||||
}
|
||||
this._setStreamState(threadId, { isRunning: 'idle' })
|
||||
|
||||
if (awaitingUserApproval) { isRunningWhenEnd = 'awaiting_user' }
|
||||
else { shouldSendAnotherMessage = true }
|
||||
|
||||
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
|
||||
}
|
||||
|
||||
} // end while (attempts)
|
||||
@@ -904,7 +764,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
this._setStreamState(threadId, { isRunning: isRunningWhenEnd })
|
||||
|
||||
// add checkpoint before the next user message
|
||||
if (!isRunningWhenEnd) this._addUserCheckpoint({ threadId })
|
||||
if (!isRunningWhenEnd)
|
||||
this._addUserCheckpoint({ threadId })
|
||||
|
||||
// capture number of messages sent
|
||||
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
|
||||
@@ -940,7 +801,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
|
||||
@@ -1208,10 +1069,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
class: undefined,
|
||||
run: () => {
|
||||
this.switchToThread(threadId)
|
||||
// scroll to bottom
|
||||
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
|
||||
m.scrollToBottom()
|
||||
})
|
||||
// TODO!!! scroll to bottom
|
||||
}
|
||||
}]
|
||||
},
|
||||
@@ -1237,6 +1095,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
|
||||
// interrupt existing stream
|
||||
if (this.streamState[threadId]?.isRunning) {
|
||||
console.log('stopping....')
|
||||
await this.abortRunning(threadId)
|
||||
}
|
||||
|
||||
@@ -1245,12 +1104,14 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
|
||||
const { chatMode } = this._settingsService.state.globalSettings
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
const currSelns: StagingSelectionItem[] = _chatSelections ?? thread.state.stagingSelections
|
||||
const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const
|
||||
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns, { directoryStrService: this._directoryStringService, fileService: this._fileService }) // user message + names of files (NOT content)
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content)
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
@@ -1260,11 +1121,6 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
|
||||
threadId,
|
||||
)
|
||||
|
||||
// scroll to bottom
|
||||
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
|
||||
m.scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1287,7 +1143,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
};
|
||||
this._storeAllThreads(newThreads);
|
||||
this._setState({ allThreads: newThreads });
|
||||
this._setState({ allThreads: newThreads }, true);
|
||||
}
|
||||
|
||||
// Now call the original method to add the user message and stream the response
|
||||
@@ -1317,7 +1173,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
messages: slicedMessages
|
||||
}
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
|
||||
// re-add the message and stream it
|
||||
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
|
||||
@@ -1346,7 +1202,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
// URIs of files that have been read
|
||||
else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') {
|
||||
const params = m.params as BuiltinToolCallParams['read_file']
|
||||
const params = m.params as ToolCallParams['read_file']
|
||||
addURI(params.uri)
|
||||
}
|
||||
}
|
||||
@@ -1590,7 +1446,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1621,7 +1477,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
this._setState({ currentThreadId: threadId })
|
||||
this._setState({ currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1644,7 +1500,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, currentThreadId: newThread.id })
|
||||
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1657,7 +1513,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
|
||||
// store the updated threads
|
||||
this._storeAllThreads(newThreads);
|
||||
this._setState({ ...this.state, allThreads: newThreads })
|
||||
this._setState({ ...this.state, allThreads: newThreads }, true)
|
||||
}
|
||||
|
||||
duplicateThread(threadId: string) {
|
||||
@@ -1673,7 +1529,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
[newThread.id]: newThread,
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads })
|
||||
this._setState({ allThreads: newThreads }, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1694,7 +1550,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
// sets the currently selected message (must be undefined if no message is selected)
|
||||
@@ -1715,7 +1571,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
|
||||
// // when change focused message idx, jump - do not jump back when click edit, too confusing.
|
||||
// if (messageIdx !== undefined)
|
||||
@@ -1755,31 +1611,6 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
|
||||
|
||||
// Pops the staging selections from the current thread's state
|
||||
popStagingSelections(numPops: number): void {
|
||||
|
||||
numPops = numPops ?? 1;
|
||||
|
||||
const focusedMessageIdx = this.getCurrentFocusedMessageIdx()
|
||||
|
||||
// set the selections to the proper value
|
||||
let selections: StagingSelectionItem[] = []
|
||||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
selections = this.getCurrentThreadState().stagingSelections
|
||||
setSelections = (s: StagingSelectionItem[]) => this.setCurrentThreadState({ stagingSelections: s })
|
||||
} else {
|
||||
selections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections
|
||||
setSelections = (s) => this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
setSelections([
|
||||
...selections.slice(0, selections.length - numPops)
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
// set message.state
|
||||
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
|
||||
|
||||
@@ -1803,12 +1634,12 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
|
||||
}
|
||||
|
||||
// set thread.state
|
||||
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>, doNotRefreshMountInfo?: boolean): void {
|
||||
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>): void {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
@@ -1823,7 +1654,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
}
|
||||
}
|
||||
}, doNotRefreshMountInfo)
|
||||
}, true)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,20 +6,17 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
||||
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage } from '../common/prompt/prompts.js';
|
||||
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
|
||||
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
|
||||
import { IDirectoryStrService } from './directoryStrService.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
import { ToolName } from '../common/toolsServiceTypes.js';
|
||||
import { IMCPService } from '../common/mcpService.js';
|
||||
|
||||
export const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
|
||||
|
||||
@@ -40,8 +37,11 @@ type SimpleLLMMessage = {
|
||||
|
||||
|
||||
|
||||
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
|
||||
const TRIM_TO_LEN = 120
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const TRIM_TO_LEN = 60
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ openai on developer system message - https://cdn.openai.com/spec/model-spec-2024
|
||||
*/
|
||||
|
||||
|
||||
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): AnthropicOrOpenAILLMMessage[] => {
|
||||
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
|
||||
|
||||
const newMessages: OpenAILLMChatMessage[] = [];
|
||||
|
||||
@@ -136,9 +136,8 @@ assistant: ...content, call(name, id, params)
|
||||
user: ...content, result(id, content)
|
||||
*/
|
||||
|
||||
type AnthropicOrOpenAILLMMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
|
||||
|
||||
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
|
||||
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
@@ -196,9 +195,9 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA
|
||||
}
|
||||
|
||||
|
||||
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
|
||||
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
|
||||
const llmChatMessages: AnthropicOrOpenAILLMMessage[] = [];
|
||||
const llmChatMessages: LLMChatMessage[] = [];
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
|
||||
const c = messages[i]
|
||||
@@ -207,7 +206,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
|
||||
if (c.role === 'assistant') {
|
||||
// if called a tool (message after it), re-add its XML to the message
|
||||
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
|
||||
let content: AnthropicOrOpenAILLMMessage['content'] = c.content
|
||||
let content: LLMChatMessage['content'] = c.content
|
||||
if (next?.role === 'tool') {
|
||||
content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}`
|
||||
}
|
||||
@@ -239,17 +238,33 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
|
||||
}
|
||||
|
||||
|
||||
|
||||
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const llmChatMessages: LLMChatMessage[] = []
|
||||
if (!specialToolFormat) { // XML tool behavior
|
||||
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'anthropic-style') {
|
||||
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'openai-style') {
|
||||
return prepareMessages_openai_tools(messages)
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
// --- CHAT ---
|
||||
|
||||
const prepareOpenAIOrAnthropicMessages = ({
|
||||
messages: messages_,
|
||||
const prepareMessages = ({
|
||||
messages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning,
|
||||
contextWindow,
|
||||
reservedOutputTokenSpace,
|
||||
maxOutputTokens,
|
||||
}: {
|
||||
messages: SimpleLLMMessage[],
|
||||
systemMessage: string,
|
||||
@@ -258,35 +273,20 @@ const prepareOpenAIOrAnthropicMessages = ({
|
||||
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
|
||||
supportsAnthropicReasoning: boolean,
|
||||
contextWindow: number,
|
||||
reservedOutputTokenSpace: number | null | undefined,
|
||||
}): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => {
|
||||
|
||||
reservedOutputTokenSpace = Math.max(
|
||||
contextWindow * 1 / 2, // reserve at least 1/4 of the token window length
|
||||
reservedOutputTokenSpace ?? 4_096 // defaults to 4096
|
||||
)
|
||||
let messages: (SimpleLLMMessage | { role: 'system', content: string })[] = deepClone(messages_)
|
||||
|
||||
// ================ system message ================
|
||||
// A COMPLETE HACK: last message is system message for context purposes
|
||||
|
||||
const sysMsgParts: string[] = []
|
||||
if (aiInstructions) sysMsgParts.push(`GUIDELINES (from the user's .voidrules file):\n${aiInstructions}`)
|
||||
if (systemMessage) sysMsgParts.push(systemMessage)
|
||||
const combinedSystemMessage = sysMsgParts.join('\n\n')
|
||||
|
||||
messages.unshift({ role: 'system', content: combinedSystemMessage })
|
||||
maxOutputTokens: number | null | undefined,
|
||||
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
|
||||
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
|
||||
|
||||
// ================ trim ================
|
||||
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
|
||||
|
||||
type MesType = (typeof messages)[0]
|
||||
messages = deepClone(messages)
|
||||
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
|
||||
|
||||
// ================ fit into context ================
|
||||
|
||||
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
|
||||
const alreadyTrimmedIdxes = new Set<number>()
|
||||
const weight = (message: MesType, messages: MesType[], idx: number) => {
|
||||
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
|
||||
const base = message.content.length
|
||||
|
||||
let multiplier: number
|
||||
@@ -294,30 +294,22 @@ const prepareOpenAIOrAnthropicMessages = ({
|
||||
if (message.role === 'user') {
|
||||
multiplier *= 1
|
||||
}
|
||||
else if (message.role === 'system') {
|
||||
multiplier *= .01 // very low weight
|
||||
}
|
||||
else {
|
||||
multiplier *= 10 // llm tokens are far less valuable than user tokens
|
||||
}
|
||||
|
||||
// any already modified message should not be trimmed again
|
||||
if (alreadyTrimmedIdxes.has(idx)) {
|
||||
multiplier = 0
|
||||
}
|
||||
// 1st and last messages should be very low weight
|
||||
if (idx <= 1 || idx >= messages.length - 1 - 3) {
|
||||
// 1st message, last 3 msgs, any already modified message should be low in weight
|
||||
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
|
||||
multiplier *= .05
|
||||
}
|
||||
return base * multiplier
|
||||
}
|
||||
|
||||
const _findLargestByWeight = (messages_: MesType[]) => {
|
||||
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
|
||||
let largestIndex = -1
|
||||
let largestWeight = -Infinity
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const m = messages[i]
|
||||
const w = weight(m, messages_, i)
|
||||
const w = weight(m, messages, i)
|
||||
if (w > largestWeight) {
|
||||
largestWeight = w
|
||||
largestIndex = i
|
||||
@@ -328,11 +320,7 @@ const prepareOpenAIOrAnthropicMessages = ({
|
||||
|
||||
let totalLen = 0
|
||||
for (const m of messages) { totalLen += m.content.length }
|
||||
const charsNeedToTrim = totalLen - Math.max(
|
||||
(contextWindow - reservedOutputTokenSpace) * CHARS_PER_TOKEN, // can be 0, in which case charsNeedToTrim=everything, bad
|
||||
5_000 // ensure we don't trim at least 5k chars (just a random small value)
|
||||
)
|
||||
|
||||
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
|
||||
|
||||
// <----------------------------------------->
|
||||
// 0 | | |
|
||||
@@ -352,54 +340,41 @@ const prepareOpenAIOrAnthropicMessages = ({
|
||||
// if can finish here, do
|
||||
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
|
||||
if (numCharsWillTrim > remainingCharsToTrim) {
|
||||
// trim remainingCharsToTrim + '...'.length chars
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim - '...'.length).trim() + '...'
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
|
||||
break
|
||||
}
|
||||
|
||||
remainingCharsToTrim -= numCharsWillTrim
|
||||
m.content = m.content.substring(0, TRIM_TO_LEN - '...'.length) + '...'
|
||||
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
|
||||
alreadyTrimmedIdxes.add(trimIdx)
|
||||
}
|
||||
|
||||
// ================ system message hack ================
|
||||
const newSysMsg = messages.shift()!.content
|
||||
|
||||
|
||||
// ================ tools and anthropicReasoning ================
|
||||
// SYSTEM MESSAGE HACK: we shifted (removed) the system message role, so now SimpleLLMMessage[] is valid
|
||||
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
|
||||
|
||||
let llmChatMessages: AnthropicOrOpenAILLMMessage[] = []
|
||||
if (!specialToolFormat) { // XML tool behavior
|
||||
llmChatMessages = prepareMessages_XML_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'anthropic-style') {
|
||||
llmChatMessages = prepareMessages_anthropic_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'openai-style') {
|
||||
llmChatMessages = prepareMessages_openai_tools(messages as SimpleLLMMessage[])
|
||||
}
|
||||
const llmMessages = llmChatMessages
|
||||
// ================ system message concat ================
|
||||
|
||||
|
||||
// ================ system message add as first llmMessage ================
|
||||
// find system messages and concatenate them
|
||||
const newSystemMessage = aiInstructions ?
|
||||
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
|
||||
: systemMessage
|
||||
|
||||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (supportsSystemMessage === 'separated')
|
||||
separateSystemMessageStr = newSysMsg
|
||||
separateSystemMessageStr = newSystemMessage
|
||||
else if (supportsSystemMessage === 'system-role')
|
||||
llmMessages.unshift({ role: 'system', content: newSysMsg }) // add new first message
|
||||
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
|
||||
else if (supportsSystemMessage === 'developer-role')
|
||||
llmMessages.unshift({ role: 'developer', content: newSysMsg }) // add new first message
|
||||
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: `<SYSTEM_MESSAGE>\n${newSysMsg}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
|
||||
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
|
||||
} as const
|
||||
llmMessages.splice(0, 1) // delete first message
|
||||
llmMessages.unshift(newFirstMessage) // add new first message
|
||||
@@ -407,25 +382,14 @@ const prepareOpenAIOrAnthropicMessages = ({
|
||||
|
||||
|
||||
// ================ no empty message ================
|
||||
for (let i = 0; i < llmMessages.length; i += 1) {
|
||||
const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i]
|
||||
const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1]
|
||||
|
||||
for (const currMsg of llmMessages) {
|
||||
if (currMsg.role === 'tool') continue
|
||||
|
||||
// if content is a string, replace string with empty msg
|
||||
if (typeof currMsg.content === 'string') {
|
||||
if (typeof currMsg.content === 'string')
|
||||
currMsg.content = currMsg.content || EMPTY_MESSAGE
|
||||
}
|
||||
else {
|
||||
// allowed to be empty if has a tool in it or following it
|
||||
if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) {
|
||||
currMsg.content = currMsg.content.filter(c => !(c.type === 'text' && !c.text)) as any
|
||||
continue
|
||||
}
|
||||
if (nextMsg?.role === 'tool') continue
|
||||
|
||||
// replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
for (const c of currMsg.content) {
|
||||
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
|
||||
}
|
||||
@@ -442,79 +406,9 @@ const prepareOpenAIOrAnthropicMessages = ({
|
||||
|
||||
|
||||
|
||||
type GeminiUserPart = (GeminiLLMChatMessage & { role: 'user' })['parts'][0]
|
||||
type GeminiModelPart = (GeminiLLMChatMessage & { role: 'model' })['parts'][0]
|
||||
const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
|
||||
let latestToolName: ToolName | undefined = undefined
|
||||
const messages2: GeminiLLMChatMessage[] = messages.map((m): GeminiLLMChatMessage | null => {
|
||||
if (m.role === 'assistant') {
|
||||
if (typeof m.content === 'string') {
|
||||
return { role: 'model', parts: [{ text: m.content }] }
|
||||
}
|
||||
else {
|
||||
const parts: GeminiModelPart[] = m.content.map((c): GeminiModelPart | null => {
|
||||
if (c.type === 'text') {
|
||||
return { text: c.text }
|
||||
}
|
||||
else if (c.type === 'tool_use') {
|
||||
latestToolName = c.name
|
||||
return { functionCall: { id: c.id, name: c.name, args: c.input } }
|
||||
}
|
||||
else return null
|
||||
}).filter(m => !!m)
|
||||
return { role: 'model', parts, }
|
||||
}
|
||||
}
|
||||
else if (m.role === 'user') {
|
||||
if (typeof m.content === 'string') {
|
||||
return { role: 'user', parts: [{ text: m.content }] } satisfies GeminiLLMChatMessage
|
||||
}
|
||||
else {
|
||||
const parts: GeminiUserPart[] = m.content.map((c): GeminiUserPart | null => {
|
||||
if (c.type === 'text') {
|
||||
return { text: c.text }
|
||||
}
|
||||
else if (c.type === 'tool_result') {
|
||||
if (!latestToolName) return null
|
||||
return { functionResponse: { id: c.tool_use_id, name: latestToolName, response: { output: c.content } } }
|
||||
}
|
||||
else return null
|
||||
}).filter(m => !!m)
|
||||
return { role: 'user', parts, }
|
||||
}
|
||||
|
||||
}
|
||||
else return null
|
||||
}).filter(m => !!m)
|
||||
|
||||
return messages2
|
||||
}
|
||||
|
||||
|
||||
const prepareMessages = (params: {
|
||||
messages: SimpleLLMMessage[],
|
||||
systemMessage: string,
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined,
|
||||
supportsAnthropicReasoning: boolean,
|
||||
contextWindow: number,
|
||||
reservedOutputTokenSpace: number | null | undefined,
|
||||
providerName: ProviderName
|
||||
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
|
||||
|
||||
const specialFormat = params.specialToolFormat // this is just for ts stupidness
|
||||
|
||||
// if need to convert to gemini style of messaes, do that (treat as anthropic style, then convert to gemini style)
|
||||
if (params.providerName === 'gemini' || specialFormat === 'gemini-style') {
|
||||
const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined })
|
||||
const messages = res.messages as AnthropicLLMChatMessage[]
|
||||
const messages2 = prepareGeminiMessages(messages)
|
||||
return { messages: messages2, separateSystemMessage: res.separateSystemMessage }
|
||||
}
|
||||
|
||||
return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat })
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -524,6 +418,7 @@ export interface IConvertToLLMMessageService {
|
||||
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }
|
||||
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
|
||||
prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] }
|
||||
|
||||
}
|
||||
|
||||
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
|
||||
@@ -540,7 +435,6 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||
@IMCPService private readonly mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -576,7 +470,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
|
||||
|
||||
// system message
|
||||
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined) => {
|
||||
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
|
||||
|
||||
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
|
||||
@@ -587,13 +481,10 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
`...Directories string cut off, use tools to read more...`
|
||||
: `...Directories string cut off, ask user for more if necessary...`
|
||||
})
|
||||
|
||||
const includeXMLToolDefinitions = !specialToolFormat
|
||||
|
||||
const mcpTools = this.mcpService.getMCPTools()
|
||||
|
||||
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, mcpTools, includeXMLToolDefinitions })
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
@@ -636,23 +527,20 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
|
||||
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
|
||||
const { overridesOfModel } = this.voidSettingsService.state
|
||||
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName, overridesOfModel)
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
|
||||
// Get combined AI instructions
|
||||
const aiInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel)
|
||||
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
|
||||
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
messages: simpleMessages,
|
||||
@@ -662,33 +550,27 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
reservedOutputTokenSpace,
|
||||
providerName,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
}
|
||||
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
|
||||
const { overridesOfModel } = this.voidSettingsService.state
|
||||
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName, overridesOfModel)
|
||||
|
||||
const { disableSystemMessage } = this.voidSettingsService.state.globalSettings;
|
||||
const fullSystemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
|
||||
const systemMessage = disableSystemMessage ? '' : fullSystemMessage;
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
|
||||
// Get combined AI instructions
|
||||
const aiInstructions = this._getCombinedAIInstructions();
|
||||
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel)
|
||||
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
@@ -699,8 +581,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
reservedOutputTokenSpace,
|
||||
providerName,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
}
|
||||
|
||||
+66
-134
@@ -7,28 +7,29 @@ import { URI } from '../../../../base/common/uri.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { IExplorerService } from '../../files/browser/files.js';
|
||||
import { SortOrder } from '../../files/common/files.js';
|
||||
import { ExplorerItem } from '../../files/common/explorerModel.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
|
||||
|
||||
const MAX_FILES_TOTAL = 1000;
|
||||
|
||||
|
||||
const START_MAX_DEPTH = Infinity;
|
||||
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
|
||||
const MAX_FILES_TOTAL = 300;
|
||||
|
||||
const DEFAULT_MAX_DEPTH = 3;
|
||||
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
|
||||
|
||||
const START_MAX_DEPTH = Infinity;
|
||||
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
|
||||
|
||||
|
||||
export interface IDirectoryStrService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getDirectoryStrTool(uri: URI): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
|
||||
|
||||
getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]>
|
||||
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
|
||||
|
||||
}
|
||||
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
|
||||
@@ -37,35 +38,35 @@ export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidD
|
||||
|
||||
|
||||
// Check if it's a known filtered type like .git
|
||||
const shouldExcludeDirectory = (name: string) => {
|
||||
if (name === '.git' ||
|
||||
name === 'node_modules' ||
|
||||
name.startsWith('.') ||
|
||||
name === 'dist' ||
|
||||
name === 'build' ||
|
||||
name === 'out' ||
|
||||
name === 'bin' ||
|
||||
name === 'coverage' ||
|
||||
name === '__pycache__' ||
|
||||
name === 'env' ||
|
||||
name === 'venv' ||
|
||||
name === 'tmp' ||
|
||||
name === 'temp' ||
|
||||
name === 'artifacts' ||
|
||||
name === 'target' ||
|
||||
name === 'obj' ||
|
||||
name === 'vendor' ||
|
||||
name === 'logs' ||
|
||||
name === 'cache' ||
|
||||
name === 'resource' ||
|
||||
name === 'resources'
|
||||
const shouldExcludeDirectory = (item: ExplorerItem) => {
|
||||
if (item.name === '.git' ||
|
||||
item.name === 'node_modules' ||
|
||||
item.name.startsWith('.') ||
|
||||
item.name === 'dist' ||
|
||||
item.name === 'build' ||
|
||||
item.name === 'out' ||
|
||||
item.name === 'bin' ||
|
||||
item.name === 'coverage' ||
|
||||
item.name === '__pycache__' ||
|
||||
item.name === 'env' ||
|
||||
item.name === 'venv' ||
|
||||
item.name === 'tmp' ||
|
||||
item.name === 'temp' ||
|
||||
item.name === 'artifacts' ||
|
||||
item.name === 'target' ||
|
||||
item.name === 'obj' ||
|
||||
item.name === 'vendor' ||
|
||||
item.name === 'logs' ||
|
||||
item.name === 'cache' ||
|
||||
item.name === 'resource' ||
|
||||
item.name === 'resources'
|
||||
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name.match(/\bout\b/)) return true
|
||||
if (name.match(/\bbuild\b/)) return true
|
||||
if (item.name.match(/\bout\b/)) return true
|
||||
if (item.name.match(/\bbuild\b/)) return true
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -76,7 +77,7 @@ export const computeDirectoryTree1Deep = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1,
|
||||
): Promise<BuiltinToolResultType['ls_dir']> => {
|
||||
): Promise<ToolResultType['ls_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
@@ -107,7 +108,7 @@ export const computeDirectoryTree1Deep = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const stringifyDirectoryTree1Deep = (params: BuiltinToolCallParams['ls_dir'], result: BuiltinToolResultType['ls_dir']): string => {
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.uri} is not a directory`;
|
||||
}
|
||||
@@ -137,16 +138,10 @@ export const stringifyDirectoryTree1Deep = (params: BuiltinToolCallParams['ls_di
|
||||
|
||||
// ---------- IN GENERAL ----------
|
||||
|
||||
const resolveChildren = async (children: undefined | IFileStat[], fileService: IFileService): Promise<IFileStat[]> => {
|
||||
const res = await fileService.resolveAll(children ?? [])
|
||||
const stats = res.map(s => s.success ? s.stat : null).filter(s => !!s)
|
||||
return stats
|
||||
}
|
||||
|
||||
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
|
||||
const computeAndStringifyDirectoryTree = async (
|
||||
eItem: IFileStat,
|
||||
fileService: IFileService,
|
||||
eItem: ExplorerItem,
|
||||
explorerService: IExplorerService,
|
||||
MAX_CHARS: number,
|
||||
fileCount: { count: number } = { count: 0 },
|
||||
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
|
||||
@@ -186,13 +181,12 @@ const computeAndStringifyDirectoryTree = async (
|
||||
let remainingChars = MAX_CHARS - nodeLine.length;
|
||||
|
||||
// Check if it's a directory we should skip
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem.name);
|
||||
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
|
||||
|
||||
// Fetch and process children if not a filtered directory
|
||||
if (eItem.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await resolveChildren(eItem.children, fileService)
|
||||
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
|
||||
|
||||
// Then recursively add all children with proper tree formatting
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
@@ -200,7 +194,7 @@ const computeAndStringifyDirectoryTree = async (
|
||||
eChildren,
|
||||
remainingChars,
|
||||
'',
|
||||
fileService,
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
|
||||
);
|
||||
@@ -214,10 +208,10 @@ const computeAndStringifyDirectoryTree = async (
|
||||
|
||||
// Helper function to render children with proper tree formatting
|
||||
const renderChildrenCombined = async (
|
||||
children: IFileStat[],
|
||||
children: ExplorerItem[],
|
||||
maxChars: number,
|
||||
parentPrefix: string,
|
||||
fileService: IFileService,
|
||||
explorerService: IExplorerService,
|
||||
fileCount: { count: number },
|
||||
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
|
||||
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
|
||||
@@ -269,12 +263,12 @@ const renderChildrenCombined = async (
|
||||
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
|
||||
|
||||
// Skip processing children for git ignored directories
|
||||
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child.name);
|
||||
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
|
||||
|
||||
// Create the prefix for the next level (continuation line or space)
|
||||
if (child.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await resolveChildren(child.children, fileService)
|
||||
const eChildren = await child.fetchChildren(SortOrder.Modified);
|
||||
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const {
|
||||
@@ -284,7 +278,7 @@ const renderChildrenCombined = async (
|
||||
eChildren,
|
||||
remainingChars,
|
||||
nextLevelPrefix,
|
||||
fileService,
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
|
||||
);
|
||||
@@ -317,68 +311,7 @@ const renderChildrenCombined = async (
|
||||
};
|
||||
|
||||
|
||||
// ------------------------- FOLDERS -------------------------
|
||||
|
||||
export async function getAllUrisInDirectory(
|
||||
directoryUri: URI,
|
||||
maxResults: number,
|
||||
fileService: IFileService,
|
||||
): Promise<URI[]> {
|
||||
const result: URI[] = [];
|
||||
|
||||
// Helper function to recursively collect URIs
|
||||
async function visitAll(folderStat: IFileStat): Promise<boolean> {
|
||||
// Stop if we've reached the limit
|
||||
if (result.length >= maxResults) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (!folderStat.isDirectory || !folderStat.children) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const eChildren = await resolveChildren(folderStat.children, fileService)
|
||||
|
||||
// Process files first (common convention to list files before directories)
|
||||
for (const child of eChildren) {
|
||||
if (!child.isDirectory) {
|
||||
result.push(child.resource);
|
||||
|
||||
// Check if we've hit the limit
|
||||
if (result.length >= maxResults) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then process directories recursively
|
||||
for (const child of eChildren) {
|
||||
const isGitIgnored = shouldExcludeDirectory(child.name)
|
||||
if (child.isDirectory && !isGitIgnored) {
|
||||
const shouldContinue = await visitAll(child);
|
||||
if (!shouldContinue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error processing directory ${folderStat.resource.fsPath}: ${error}`);
|
||||
return true; // Continue despite errors in a specific directory
|
||||
}
|
||||
}
|
||||
|
||||
const rootStat = await fileService.resolve(directoryUri)
|
||||
await visitAll(rootStat);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --------------------------------------------------
|
||||
// ---------------------------------------------------
|
||||
|
||||
|
||||
class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
@@ -386,25 +319,21 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]> {
|
||||
return getAllUrisInDirectory(uri, opts.maxResults, this.fileService)
|
||||
}
|
||||
|
||||
async getDirectoryStrTool(uri: URI) {
|
||||
const eRoot = await this.fileService.resolve(uri)
|
||||
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
||||
const eRoot = this.explorerService.findClosest(uri)
|
||||
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
|
||||
|
||||
const maxItemsPerDir = START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
|
||||
// First try with START_MAX_DEPTH
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.fileService,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
|
||||
@@ -415,7 +344,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.fileService,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
@@ -434,7 +363,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
return c
|
||||
}
|
||||
|
||||
async getAllDirectoriesStr({ cutOffMessage, }: { cutOffMessage: string, }) {
|
||||
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
|
||||
let str: string = '';
|
||||
let cutOff = false;
|
||||
const folders = this.workspaceContextService.getWorkspace().folders;
|
||||
@@ -442,7 +371,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
return '(NO WORKSPACE OPEN)';
|
||||
|
||||
// Use START_MAX_ITEMS_PER_DIR if not specified
|
||||
const startMaxItemsPerDir = START_MAX_ITEMS_PER_DIR;
|
||||
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
|
||||
|
||||
for (let i = 0; i < folders.length; i += 1) {
|
||||
if (i > 0) str += '\n';
|
||||
@@ -452,13 +381,13 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
str += `Directory of ${f.uri.fsPath}:\n`;
|
||||
const rootURI = f.uri;
|
||||
|
||||
const eRoot = await this.fileService.resolve(rootURI)
|
||||
const eRoot = this.explorerService.findClosestRoot(rootURI);
|
||||
if (!eRoot) continue;
|
||||
|
||||
// First try with START_MAX_DEPTH and startMaxItemsPerDir
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.fileService,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
|
||||
@@ -469,7 +398,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.fileService,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
@@ -488,8 +417,11 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
}
|
||||
}
|
||||
|
||||
const ans = cutOff ? `${str.trimEnd()}\n${cutOffMessage}` : str
|
||||
return ans
|
||||
if (cutOff) {
|
||||
return `${str.trimEnd()}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit
|
||||
import { findDiffs } from './helpers/findDiffs.js';
|
||||
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
|
||||
import { Color, RGBA } from '../../../../base/common/color.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js';
|
||||
import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
|
||||
@@ -23,10 +25,7 @@ import * as dom from '../../../../base/browser/dom.js';
|
||||
import { Widget } from '../../../../base/browser/ui/widget.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js';
|
||||
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, tripleTick, } from '../common/prompt/prompts.js';
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID } from './actionIDs.js';
|
||||
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, } from '../common/prompt/prompts.js';
|
||||
|
||||
import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
|
||||
import { QuickEditPropsType } from './quickEditActions.js';
|
||||
@@ -49,6 +48,27 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
// import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
}
|
||||
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
|
||||
const greenBG = new Color(new RGBA(155, 185, 85, .2)); // default is RGBA(155, 185, 85, .2)
|
||||
registerColor('void.greenBG', configOfBG(greenBG), '', true);
|
||||
|
||||
const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2)
|
||||
registerColor('void.redBG', configOfBG(redBG), '', true);
|
||||
|
||||
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
|
||||
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
|
||||
|
||||
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
|
||||
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
|
||||
|
||||
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
|
||||
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
|
||||
|
||||
|
||||
|
||||
const numLinesOfStr = (str: string) => str.split('\n').length
|
||||
|
||||
|
||||
@@ -109,42 +129,29 @@ const removeWhitespaceExceptNewlines = (str: string): string => {
|
||||
|
||||
// finds block.orig in fileContents and return its range in file
|
||||
// startingAtLine is 1-indexed and inclusive
|
||||
// returns 1-indexed lines
|
||||
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' }) => {
|
||||
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
|
||||
|
||||
const returnAns = (fileContents: string, idx: number) => {
|
||||
const startLine = numLinesOfStr(fileContents.substring(0, idx + 1))
|
||||
const numLines = numLinesOfStr(text)
|
||||
const endLine = startLine + numLines - 1
|
||||
|
||||
return [startLine, endLine] as const
|
||||
}
|
||||
|
||||
const startingAtLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, opts.startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
const startLineIdx = (fileContents: string) => startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
: 0
|
||||
|
||||
// idx = starting index in fileContents
|
||||
let idx = fileContents.indexOf(text, startingAtLineIdx(fileContents))
|
||||
|
||||
// if idx was found
|
||||
if (idx !== -1) {
|
||||
return returnAns(fileContents, idx)
|
||||
}
|
||||
|
||||
if (!canFallbackToRemoveWhitespace)
|
||||
return 'Not found' as const
|
||||
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
|
||||
|
||||
// try to find it ignoring all whitespace this time
|
||||
text = removeWhitespaceExceptNewlines(text)
|
||||
fileContents = removeWhitespaceExceptNewlines(fileContents)
|
||||
idx = fileContents.indexOf(text, startingAtLineIdx(fileContents));
|
||||
if (idx === -1 && canFallbackToRemoveWhitespace) {
|
||||
text = removeWhitespaceExceptNewlines(text)
|
||||
fileContents = removeWhitespaceExceptNewlines(fileContents)
|
||||
idx = fileContents.indexOf(text, startLineIdx(fileContents));
|
||||
}
|
||||
|
||||
if (idx === -1) return 'Not found' as const
|
||||
const lastIdx = fileContents.lastIndexOf(text)
|
||||
if (lastIdx !== idx) return 'Not unique' as const
|
||||
|
||||
return returnAns(fileContents, idx)
|
||||
const startLine = fileContents.substring(0, idx).split('\n').length
|
||||
const numLines = numLinesOfStr(text)
|
||||
const endLine = startLine + numLines - 1
|
||||
return [startLine, endLine] as const
|
||||
}
|
||||
|
||||
|
||||
@@ -269,11 +276,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
|
||||
public processRawKeybindingText(keybindingStr: string): string {
|
||||
return keybindingStr
|
||||
.replace(/Enter/g, '↵') // ⏎
|
||||
.replace(/Backspace/g, '⌫');
|
||||
}
|
||||
|
||||
|
||||
// private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
// const details = errorDetails(e.fullError)
|
||||
@@ -587,7 +590,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
else { throw new Error('Void 1') }
|
||||
|
||||
const buttonsWidget = this._instantiationService.createInstance(AcceptRejectInlineWidget, {
|
||||
const buttonsWidget = new AcceptRejectInlineWidget({
|
||||
editor,
|
||||
onAccept: () => {
|
||||
this.acceptDiff({ diffid })
|
||||
@@ -1124,8 +1127,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
return
|
||||
}
|
||||
|
||||
public async callBeforeApplyOrEdit(givenURI: URI | 'current') {
|
||||
const uri = this._uriOfGivenURI(givenURI)
|
||||
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
await this._voidModelService.saveModel(uri) // save the URI
|
||||
@@ -1179,33 +1182,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError || new Error(e.message)
|
||||
}
|
||||
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
|
||||
|
||||
try {
|
||||
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
|
||||
}
|
||||
catch (e) {
|
||||
onError({ message: e + '', fullError: null })
|
||||
}
|
||||
|
||||
onDone()
|
||||
}
|
||||
|
||||
|
||||
public instantlyRewriteFile({ uri, newContent }: { uri: URI, newContent: string }) {
|
||||
public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) {
|
||||
// start diffzone
|
||||
const res = this._startStreamingDiffZone({
|
||||
uri,
|
||||
@@ -1223,14 +1210,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: true })
|
||||
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false })
|
||||
onDone()
|
||||
}
|
||||
|
||||
@@ -1353,7 +1335,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
const { from, } = opts
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const overridesOfModel = this._settingsService.state.overridesOfModel
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
@@ -1458,11 +1439,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
// throws
|
||||
@@ -1470,7 +1446,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError || new Error(e.message)
|
||||
throw e.fullError
|
||||
}
|
||||
|
||||
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
|
||||
@@ -1510,7 +1486,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
overridesOfModel,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
@@ -1585,31 +1560,25 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a human-readable error message for an invalid ORIGINAL search block.
|
||||
*/
|
||||
private _errContentOfInvalidStr = (
|
||||
str: 'Not found' | 'Not unique' | 'Has overlap',
|
||||
blockOrig: string,
|
||||
): string => {
|
||||
const problematicCode = `${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}`
|
||||
private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
|
||||
|
||||
const descStr = str === `Not found` ?
|
||||
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: str === `Not unique` ?
|
||||
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: str === 'Has overlap' ?
|
||||
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: ``
|
||||
|
||||
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
|
||||
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
|
||||
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
|
||||
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
|
||||
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
|
||||
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
|
||||
const errMsg = `${descStr}\n${soFarStr}`
|
||||
return errMsg
|
||||
|
||||
// use a switch for better readability / exhaustiveness check
|
||||
let descStr: string
|
||||
switch (str) {
|
||||
case 'Not found':
|
||||
descStr = `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.`
|
||||
break
|
||||
case 'Not unique':
|
||||
descStr = `The edit was not applied. The text in ORIGINAL must be unique in the file being edited, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.`
|
||||
break
|
||||
case 'Has overlap':
|
||||
descStr = `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.`
|
||||
break
|
||||
default:
|
||||
descStr = ''
|
||||
}
|
||||
return descStr
|
||||
}
|
||||
|
||||
|
||||
@@ -1620,31 +1589,23 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`)
|
||||
const modelStr = model.getValue(EndOfLinePreference.LF)
|
||||
// .split('\n').map(l => '\t' + l).join('\n') // for testing purposes only, remember to remove this
|
||||
const modelStrLines = modelStr.split('\n')
|
||||
|
||||
|
||||
|
||||
|
||||
const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = []
|
||||
for (const b of blocks) {
|
||||
const res = findTextInCode(b.orig, modelStr, true, { returnType: 'lines' })
|
||||
if (typeof res === 'string')
|
||||
throw new Error(this._errContentOfInvalidStr(res, b.orig))
|
||||
let [startLine, endLine] = res
|
||||
startLine -= 1 // 0-index
|
||||
endLine -= 1
|
||||
const i = modelStr.indexOf(b.orig)
|
||||
if (i === -1)
|
||||
throw new Error(this._errContentOfInvalidStr('Not found', b.orig))
|
||||
const j = modelStr.lastIndexOf(b.orig)
|
||||
if (i !== j)
|
||||
throw new Error(this._errContentOfInvalidStr('Not unique', b.orig))
|
||||
|
||||
// including newline before start
|
||||
const origStart = (startLine !== 0 ?
|
||||
modelStrLines.slice(0, startLine).join('\n') + '\n'
|
||||
: '').length
|
||||
|
||||
// including endline at end
|
||||
const origEnd = modelStrLines.slice(0, endLine + 1).join('\n').length - 1
|
||||
|
||||
replacements.push({ origStart, origEnd, block: b });
|
||||
replacements.push({
|
||||
origStart: i,
|
||||
origEnd: i + b.orig.length - 1, // INCLUSIVE
|
||||
block: b,
|
||||
})
|
||||
}
|
||||
|
||||
// sort in increasing order
|
||||
replacements.sort((a, b) => a.origStart - b.origStart)
|
||||
|
||||
@@ -1666,12 +1627,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
|
||||
const { from, applyStr, } = opts
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const overridesOfModel = this._settingsService.state.overridesOfModel
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
@@ -1747,18 +1708,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._deleteTrackingZone(trackingZone)
|
||||
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError || new Error(e.message)
|
||||
throw e.fullError || new Error(e.message) // throw error h
|
||||
}
|
||||
|
||||
// refresh now in case onText takes a while to get 1st message
|
||||
@@ -1812,7 +1768,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// update stream state to the first line of original if some portion of original has been written
|
||||
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
|
||||
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, false, { startingAtLine, returnType: 'lines' })
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
@@ -1838,7 +1794,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
|
||||
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
|
||||
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode, true, { returnType: 'lines' })
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
|
||||
// if error
|
||||
// Check for overlap with existing modified ranges
|
||||
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
|
||||
@@ -1857,10 +1813,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
console.log('block.orig:', block.orig)
|
||||
console.log('---------')
|
||||
const content = this._errContentOfInvalidStr(errorMessage, block.orig)
|
||||
const retryMsg = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content + '\n' + retryMsg } // user explanation of what's wrong
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
// REVERT ALL BLOCKS
|
||||
@@ -1956,7 +1911,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
overridesOfModel,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
@@ -1970,8 +1924,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
if (blocks.length === 0) {
|
||||
this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`)
|
||||
}
|
||||
this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true })
|
||||
|
||||
|
||||
try {
|
||||
this._instantlyApplySRBlocks(uri, fullText)
|
||||
@@ -2279,77 +2231,31 @@ registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
public getId(): string {
|
||||
return this.ID || ''; // Ensure we always return a string
|
||||
}
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
public getPosition() {
|
||||
return null;
|
||||
}
|
||||
public getId() { return this.ID }
|
||||
public getDomNode() { return this._domNode; }
|
||||
public getPosition() { return null }
|
||||
|
||||
private readonly _domNode: HTMLElement; // Using the definite assignment assertion
|
||||
private readonly editor: ICodeEditor;
|
||||
private readonly ID: string;
|
||||
private readonly startLine: number;
|
||||
private readonly _domNode: HTMLElement;
|
||||
private readonly editor
|
||||
private readonly ID
|
||||
private readonly startLine
|
||||
|
||||
constructor(
|
||||
{ editor, onAccept, onReject, diffid, startLine, offsetLines }: {
|
||||
editor: ICodeEditor;
|
||||
onAccept: () => void;
|
||||
onReject: () => void;
|
||||
diffid: string,
|
||||
startLine: number,
|
||||
offsetLines: number
|
||||
},
|
||||
@IVoidCommandBarService private readonly _voidCommandBarService: IVoidCommandBarService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IEditCodeService private readonly _editCodeService: IEditCodeService,
|
||||
) {
|
||||
super();
|
||||
constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) {
|
||||
super()
|
||||
|
||||
const uri = editor.getModel()?.uri;
|
||||
// Initialize with default values
|
||||
this.ID = ''
|
||||
|
||||
this.ID = editor.getModel()?.uri.fsPath + diffid;
|
||||
this.editor = editor;
|
||||
this.startLine = startLine;
|
||||
|
||||
if (!uri) {
|
||||
const { dummyDiv } = dom.h('div@dummyDiv');
|
||||
this._domNode = dummyDiv
|
||||
return;
|
||||
}
|
||||
|
||||
this.ID = uri.fsPath + diffid;
|
||||
|
||||
const lineHeight = editor.getOption(EditorOption.lineHeight);
|
||||
|
||||
const getAcceptRejectText = () => {
|
||||
const acceptKeybinding = this._keybindingService.lookupKeybinding(VOID_ACCEPT_DIFF_ACTION_ID);
|
||||
const rejectKeybinding = this._keybindingService.lookupKeybinding(VOID_REJECT_DIFF_ACTION_ID);
|
||||
|
||||
// Use the standalone function directly since we're in a nested class that
|
||||
// can't access EditCodeService's methods
|
||||
const acceptKeybindLabel = this._editCodeService.processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || '');
|
||||
const rejectKeybindLabel = this._editCodeService.processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '');
|
||||
|
||||
const commandBarStateAtUri = this._voidCommandBarService.stateOfURI[uri.fsPath];
|
||||
const selectedDiffIdx = commandBarStateAtUri?.diffIdx ?? 0; // 0th item is selected by default
|
||||
const thisDiffIdx = commandBarStateAtUri?.sortedDiffIds.indexOf(diffid) ?? null;
|
||||
|
||||
const showLabel = thisDiffIdx === selectedDiffIdx
|
||||
|
||||
const acceptText = `Accept${showLabel ? ` ` + acceptKeybindLabel : ''}`;
|
||||
const rejectText = `Reject${showLabel ? ` ` + rejectKeybindLabel : ''}`;
|
||||
|
||||
return { acceptText, rejectText }
|
||||
}
|
||||
|
||||
const { acceptText, rejectText } = getAcceptRejectText()
|
||||
|
||||
// Create container div with buttons
|
||||
const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
|
||||
dom.h('button@acceptButton', []),
|
||||
@@ -2363,14 +2269,11 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
||||
buttons.style.paddingRight = '4px';
|
||||
buttons.style.zIndex = '1';
|
||||
buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`;
|
||||
buttons.style.justifyContent = 'flex-end';
|
||||
buttons.style.width = '100%';
|
||||
buttons.style.pointerEvents = 'none';
|
||||
|
||||
|
||||
// Style accept button
|
||||
acceptButton.onclick = onAccept;
|
||||
acceptButton.textContent = acceptText;
|
||||
acceptButton.textContent = 'Accept';
|
||||
acceptButton.style.backgroundColor = acceptBg;
|
||||
acceptButton.style.border = acceptBorder;
|
||||
acceptButton.style.color = buttonTextColor;
|
||||
@@ -2384,12 +2287,10 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
||||
acceptButton.style.cursor = 'pointer';
|
||||
acceptButton.style.height = '100%';
|
||||
acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
|
||||
acceptButton.style.pointerEvents = 'auto';
|
||||
|
||||
|
||||
// Style reject button
|
||||
rejectButton.onclick = onReject;
|
||||
rejectButton.textContent = rejectText;
|
||||
rejectButton.textContent = 'Reject';
|
||||
rejectButton.style.backgroundColor = rejectBg;
|
||||
rejectButton.style.border = rejectBorder;
|
||||
rejectButton.style.color = buttonTextColor;
|
||||
@@ -2403,7 +2304,6 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
||||
rejectButton.style.cursor = 'pointer';
|
||||
rejectButton.style.height = '100%';
|
||||
rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
|
||||
rejectButton.style.pointerEvents = 'auto';
|
||||
|
||||
|
||||
|
||||
@@ -2424,28 +2324,16 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
||||
}
|
||||
|
||||
// Mount first, then update positions
|
||||
setTimeout(() => {
|
||||
updateTop()
|
||||
updateLeft()
|
||||
}, 0)
|
||||
editor.addOverlayWidget(this);
|
||||
|
||||
|
||||
updateTop()
|
||||
updateLeft()
|
||||
|
||||
this._register(editor.onDidScrollChange(e => { updateTop() }))
|
||||
this._register(editor.onDidChangeModelContent(e => { updateTop() }))
|
||||
this._register(editor.onDidLayoutChange(e => { updateTop(); updateLeft() }))
|
||||
|
||||
|
||||
// Listen for state changes in the command bar service
|
||||
this._register(this._voidCommandBarService.onDidChangeState(e => {
|
||||
if (uri && e.uri.fsPath === uri.fsPath) {
|
||||
|
||||
const { acceptText, rejectText } = getAcceptRejectText()
|
||||
|
||||
acceptButton.textContent = acceptText;
|
||||
rejectButton.textContent = rejectText;
|
||||
|
||||
}
|
||||
}));
|
||||
|
||||
// mount this widget
|
||||
|
||||
editor.addOverlayWidget(this);
|
||||
@@ -2453,8 +2341,8 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
this.editor.removeOverlayWidget(this);
|
||||
super.dispose();
|
||||
this.editor.removeOverlayWidget(this)
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,12 +42,10 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
|
||||
export interface IEditCodeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
processRawKeybindingText(keybindingStr: string): string;
|
||||
|
||||
callBeforeApplyOrEdit(uri: URI | 'current'): Promise<void>;
|
||||
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
|
||||
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
|
||||
instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void;
|
||||
instantlyRewriteFile(opts: { uri: URI; newContent: string }): void;
|
||||
instantlyApplyNewContent(opts: { uri: URI; newContent: string }): void;
|
||||
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
|
||||
removeCtrlKZone(opts: { diffareaid: number }): void;
|
||||
|
||||
@@ -56,8 +54,6 @@ export interface IEditCodeService {
|
||||
diffOfId: Record<string, Diff>;
|
||||
|
||||
acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void;
|
||||
acceptDiff({ diffid }: { diffid: number }): void;
|
||||
rejectDiff({ diffid }: { diffid: number }): void;
|
||||
|
||||
// events
|
||||
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { env } from '../../../../base/common/process.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { TransferEditorType, TransferFilesInfo } from './extensionTransferTypes.js';
|
||||
|
||||
|
||||
export interface IExtensionTransferService {
|
||||
readonly _serviceBrand: undefined; // services need this, just leave it undefined
|
||||
transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType): Promise<string | undefined>
|
||||
deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null): Promise<void>
|
||||
|
||||
}
|
||||
|
||||
export const IExtensionTransferService = createDecorator<IExtensionTransferService>('ExtensionTransferService');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Define extensions to skip when transferring
|
||||
const extensionBlacklist = [
|
||||
// ignore extensions
|
||||
'ms-vscode-remote.remote', // ms-vscode-remote.remote-ssh, ms-vscode-remote.remote-wsl
|
||||
'ms-vscode.remote', // ms-vscode.remote-explorer
|
||||
// ignore other AI copilots that could conflict with Void keybindings
|
||||
'sourcegraph.cody-ai',
|
||||
'continue.continue',
|
||||
'codeium.codeium',
|
||||
'saoudrizwan.claude-dev', // cline
|
||||
'rooveterinaryinc.roo-cline', // roo
|
||||
'supermaven.supermaven' // supermaven
|
||||
// 'github.copilot',
|
||||
];
|
||||
|
||||
|
||||
const isBlacklisted = (fsPath: string | undefined) => {
|
||||
return extensionBlacklist.find(bItem => fsPath?.includes(bItem))
|
||||
}
|
||||
|
||||
class ExtensionTransferService extends Disposable implements IExtensionTransferService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType) {
|
||||
const transferTheseFiles = transferTheseFilesOfOS(os, fromEditor)
|
||||
const fileService = this._fileService
|
||||
|
||||
let errAcc = ''
|
||||
|
||||
for (const { from, to, isExtensions } of transferTheseFiles) {
|
||||
// Check if the source file exists before attempting to copy
|
||||
try {
|
||||
if (!isExtensions) {
|
||||
console.log('transferring item', from, to)
|
||||
|
||||
const exists = await fileService.exists(from)
|
||||
if (exists) {
|
||||
// Ensure the destination directory exists
|
||||
const toParent = URI.joinPath(to, '..')
|
||||
const toParentExists = await fileService.exists(toParent)
|
||||
if (!toParentExists) {
|
||||
await fileService.createFolder(toParent)
|
||||
}
|
||||
await fileService.copy(from, to, true)
|
||||
} else {
|
||||
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
|
||||
}
|
||||
}
|
||||
// extensions folder
|
||||
else {
|
||||
console.log('transferring extensions...', from, to)
|
||||
const exists = await fileService.exists(from)
|
||||
if (exists) {
|
||||
const stat = await fileService.resolve(from)
|
||||
const toParent = URI.joinPath(to) // extensions/
|
||||
const toParentExists = await fileService.exists(toParent)
|
||||
if (!toParentExists) {
|
||||
await fileService.createFolder(toParent)
|
||||
}
|
||||
for (const extensionFolder of stat.children ?? []) {
|
||||
const from = extensionFolder.resource
|
||||
const to = URI.joinPath(toParent, extensionFolder.name)
|
||||
const toStat = await fileService.resolve(from)
|
||||
|
||||
if (toStat.isDirectory) {
|
||||
if (!isBlacklisted(extensionFolder.resource.fsPath)) {
|
||||
await fileService.copy(from, to, true)
|
||||
}
|
||||
}
|
||||
else if (toStat.isFile) {
|
||||
if (extensionFolder.name === 'extensions.json') {
|
||||
try {
|
||||
const contentsStr = await fileService.readFile(from)
|
||||
const json: any = JSON.parse(contentsStr.value.toString())
|
||||
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
|
||||
const jsonStr = JSON.stringify(j2)
|
||||
await fileService.writeFile(to, VSBuffer.fromString(jsonStr))
|
||||
}
|
||||
catch {
|
||||
console.log('Error copying extensions.json, skipping')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
|
||||
}
|
||||
console.log('done transferring extensions.')
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Error copying file:', e)
|
||||
errAcc += `Error copying ${from.toString()}: ${e}\n`
|
||||
}
|
||||
}
|
||||
|
||||
if (errAcc) return errAcc
|
||||
return undefined
|
||||
}
|
||||
|
||||
async deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null) {
|
||||
const fileService = this._fileService
|
||||
const extensionsURI = getExtensionsFolder(os)
|
||||
if (!extensionsURI) return
|
||||
const eURI = await fileService.resolve(extensionsURI)
|
||||
for (const child of eURI.children ?? []) {
|
||||
|
||||
|
||||
try {
|
||||
if (child.isDirectory) {
|
||||
// if is blacklisted
|
||||
if (isBlacklisted(child.resource.fsPath)) {
|
||||
console.log('Deleting extension', child.resource.fsPath)
|
||||
await fileService.del(child.resource, { recursive: true, useTrash: true })
|
||||
}
|
||||
}
|
||||
else if (child.isFile) {
|
||||
// if is extensions.json
|
||||
|
||||
if (child.name === 'extensions.json') {
|
||||
console.log('Updating extensions.json', child.resource.fsPath)
|
||||
try {
|
||||
const contentsStr = await fileService.readFile(child.resource)
|
||||
const json: any = JSON.parse(contentsStr.value.toString())
|
||||
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
|
||||
const jsonStr = JSON.stringify(j2)
|
||||
await fileService.writeFile(child.resource, VSBuffer.fromString(jsonStr))
|
||||
}
|
||||
catch {
|
||||
console.log('Error copying extensions.json, skipping')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Could not delete extension', child.resource.fsPath, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IExtensionTransferService, ExtensionTransferService, InstantiationType.Eager); // lazily loaded, even if Eager
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => {
|
||||
if (os === null)
|
||||
throw new Error(`One-click switch is not possible in this environment.`)
|
||||
if (os === 'mac') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`$HOME not found`)
|
||||
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'linux') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`variable for $HOME location not found`)
|
||||
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'windows') {
|
||||
const appdata = env['APPDATA']
|
||||
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
|
||||
const userprofile = env['USERPROFILE']
|
||||
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
|
||||
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`)
|
||||
}
|
||||
|
||||
|
||||
const getExtensionsFolder = (os: 'mac' | 'windows' | 'linux' | null) => {
|
||||
const t = transferTheseFilesOfOS(os, 'VS Code') // from editor doesnt matter
|
||||
return t.find(f => f.isExtensions)?.to
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
|
||||
export type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
|
||||
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
|
||||
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
|
||||
export type TransferFilesInfo = { from: URI, to: URI, isExtensions?: boolean }[]
|
||||
@@ -1,78 +0,0 @@
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Action2, registerAction2, MenuId } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { messageOfSelection } from '../common/prompt/prompts.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
|
||||
|
||||
class FilePromptActionService extends Action2 {
|
||||
private static readonly VOID_COPY_FILE_PROMPT_ID = 'void.copyfileprompt'
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID,
|
||||
title: localize2('voidCopyPrompt', 'Void: Copy Prompt'),
|
||||
menu: [{
|
||||
id: MenuId.ExplorerContext,
|
||||
group: '8_void',
|
||||
order: 1,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, uri: URI): Promise<void> {
|
||||
try {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const clipboardService = accessor.get(IClipboardService)
|
||||
const directoryStrService = accessor.get(IDirectoryStrService)
|
||||
const voidModelService = accessor.get(IVoidModelService)
|
||||
|
||||
const stat = await fileService.stat(uri)
|
||||
|
||||
const folderOpts = {
|
||||
maxChildren: 1000,
|
||||
maxCharsPerFile: 2_000_000,
|
||||
} as const
|
||||
|
||||
let m: string = 'No contents detected'
|
||||
if (stat.isFile) {
|
||||
m = await messageOfSelection({
|
||||
type: 'File',
|
||||
uri,
|
||||
language: (await voidModelService.getModelSafe(uri)).model?.getLanguageId() || '',
|
||||
state: { wasAddedAsCurrentFile: false, },
|
||||
}, {
|
||||
folderOpts,
|
||||
directoryStrService,
|
||||
fileService,
|
||||
})
|
||||
}
|
||||
|
||||
if (stat.isDirectory) {
|
||||
m = await messageOfSelection({
|
||||
type: 'Folder',
|
||||
uri,
|
||||
}, {
|
||||
folderOpts,
|
||||
fileService,
|
||||
directoryStrService,
|
||||
})
|
||||
}
|
||||
|
||||
await clipboardService.writeText(m)
|
||||
|
||||
} catch (error) {
|
||||
const notificationService = accessor.get(INotificationService)
|
||||
notificationService.error(error + '')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerAction2(FilePromptActionService)
|
||||
@@ -1,49 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { IExtensionTransferService } from './extensionTransferService.js';
|
||||
import { os } from '../common/helpers/systemInfo.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { timeout } from '../../../../base/common/async.js';
|
||||
import { getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
|
||||
// Onboarding contribution that mounts the component at startup
|
||||
export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.voidMiscWorkbenchContribs';
|
||||
|
||||
constructor(
|
||||
@IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
|
||||
// delete blacklisted extensions once (this is for people who already installed them)
|
||||
const deleteExtensionsStorageId = 'void-deleted-blacklist-2'
|
||||
const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
|
||||
if (!alreadyDeleted) {
|
||||
this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
this.extensionTransferService.deleteBlacklistExtensions(os)
|
||||
}
|
||||
|
||||
|
||||
// after some time, trigger a resize event for the blank screen error
|
||||
timeout(5_000).then(() => {
|
||||
// Get the active window reference for multi-window support
|
||||
const targetWindow = getActiveWindow();
|
||||
// Trigger a window resize event to ensure proper layout calculations
|
||||
targetWindow.dispatchEvent(new Event('resize'))
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(MiscWorkbenchContribs.ID, MiscWorkbenchContribs, WorkbenchPhase.Eventually);
|
||||
@@ -13,7 +13,7 @@ import { roundRangeToLines } from './sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
@@ -42,7 +42,6 @@ registerAction2(class extends Action2 {
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
when: ContextKeyExpr.deserialize('editorFocus && !terminalFocus'),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+74
-256
@@ -3,14 +3,14 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef, Fragment } from 'react'
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
|
||||
import { usePromise, useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ListableToolItem, voidOpenFileFn, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { PlacesType, VariantType } from 'react-tooltip'
|
||||
|
||||
enum CopyButtonText {
|
||||
@@ -24,9 +24,8 @@ type IconButtonProps = {
|
||||
Icon: LucideIcon
|
||||
}
|
||||
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -35,19 +34,19 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
|
||||
}}
|
||||
// border border-void-border-1 rounded
|
||||
className={`
|
||||
size-[18px]
|
||||
p-[2px]
|
||||
flex items-center justify-center
|
||||
text-sm text-void-fg-3
|
||||
hover:brightness-110
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
size-[18px]
|
||||
p-[2px]
|
||||
flex items-center justify-center
|
||||
text-sm text-void-fg-3
|
||||
hover:brightness-110
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
// export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
|
||||
@@ -109,7 +108,7 @@ export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & R
|
||||
<IconShell1
|
||||
Icon={FileSymlink}
|
||||
onClick={() => {
|
||||
voidOpenFileFn(uri, accessor)
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
|
||||
{...props}
|
||||
@@ -132,41 +131,48 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
|
||||
|
||||
// state persisted for duration of react only
|
||||
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
|
||||
const _applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
const getUriBeingApplied = (applyBoxId: string) => {
|
||||
return _applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
}
|
||||
|
||||
|
||||
export const useApplyStreamState = ({ applyBoxId }: { applyBoxId: string }) => {
|
||||
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidCommandBarService = accessor.get('IVoidCommandBarService')
|
||||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
const getStreamState = useCallback(() => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return 'idle-no-changes'
|
||||
return voidCommandBarService.getStreamState(uri)
|
||||
}, [voidCommandBarService, applyBoxId])
|
||||
|
||||
|
||||
const [currStreamStateRef, setStreamState] = useRefState(getStreamState())
|
||||
|
||||
const setApplying = useCallback((uri: URI | undefined) => {
|
||||
_applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri ?? undefined
|
||||
setStreamState(getStreamState())
|
||||
}, [setStreamState, getStreamState, applyBoxId])
|
||||
|
||||
// listen for stream updates on this box
|
||||
useCommandBarURIListener(useCallback((uri_) => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri?.fsPath === uri_.fsPath) {
|
||||
setStreamState(getStreamState())
|
||||
const shouldUpdate = (
|
||||
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|
||||
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
|
||||
)
|
||||
if (shouldUpdate) {
|
||||
rerender(c => c + 1)
|
||||
}
|
||||
}, [setStreamState, applyBoxId, getStreamState]))
|
||||
}, [applyBoxId, uri]))
|
||||
|
||||
const currStreamState = getStreamState()
|
||||
|
||||
|
||||
return { currStreamStateRef, setApplying }
|
||||
return {
|
||||
getStreamState,
|
||||
isDisabled,
|
||||
currStreamState,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -196,24 +202,10 @@ const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position =
|
||||
'data-tooltip-offset': offset,
|
||||
})
|
||||
|
||||
export const useEditToolStreamState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI }) => {
|
||||
const accessor = useAccessor()
|
||||
const voidCommandBarService = accessor.get('IVoidCommandBarService')
|
||||
const [streamState, setStreamState] = useState(voidCommandBarService.getStreamState(uri))
|
||||
// listen for stream updates on this box
|
||||
useCommandBarURIListener(useCallback((uri_) => {
|
||||
const shouldUpdate = uri.fsPath === uri_.fsPath
|
||||
if (shouldUpdate) { setStreamState(voidCommandBarService.getStreamState(uri)) }
|
||||
}, [voidCommandBarService, applyBoxId, uri]))
|
||||
|
||||
return { streamState, }
|
||||
}
|
||||
|
||||
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
|
||||
const currStreamState = currStreamStateRef.current
|
||||
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const color = (
|
||||
currStreamState === 'idle-no-changes' ? 'dark' :
|
||||
@@ -239,255 +231,82 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
|
||||
}
|
||||
|
||||
|
||||
const terminalLanguages = new Set([
|
||||
'bash',
|
||||
'shellscript',
|
||||
'shell',
|
||||
'powershell',
|
||||
'bat',
|
||||
'zsh',
|
||||
'sh',
|
||||
'fish',
|
||||
'nushell',
|
||||
'ksh',
|
||||
'xonsh',
|
||||
'elvish',
|
||||
])
|
||||
|
||||
const ApplyButtonsForTerminal = ({
|
||||
codeStr,
|
||||
applyBoxId,
|
||||
uri,
|
||||
language,
|
||||
}: {
|
||||
codeStr: string,
|
||||
applyBoxId: string,
|
||||
language?: string,
|
||||
uri: URI | 'current';
|
||||
}) => {
|
||||
const accessor = useAccessor()
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const terminalToolService = accessor.get('ITerminalToolService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const [isShellRunning, setIsShellRunning] = useState<boolean>(false)
|
||||
const interruptToolRef = useRef<(() => void) | null>(null)
|
||||
const isDisabled = isShellRunning
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (isShellRunning) return
|
||||
try {
|
||||
setIsShellRunning(true)
|
||||
const terminalId = await terminalToolService.createPersistentTerminal({ cwd: null })
|
||||
const { interrupt } = await terminalToolService.runCommand(
|
||||
codeStr,
|
||||
{ type: 'persistent', persistentTerminalId: terminalId }
|
||||
);
|
||||
interruptToolRef.current = interrupt
|
||||
metricsService.capture('Execute Shell', { length: codeStr.length })
|
||||
} catch (e) {
|
||||
setIsShellRunning(false)
|
||||
console.error('Failed to execute in terminal:', e)
|
||||
}
|
||||
}, [codeStr, uri, applyBoxId, metricsService, terminalToolService, isShellRunning])
|
||||
|
||||
if (isShellRunning) {
|
||||
return (
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={() => {
|
||||
interruptToolRef.current?.();
|
||||
setIsShellRunning(false);
|
||||
}}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isDisabled) {
|
||||
return null
|
||||
}
|
||||
return <IconShell1
|
||||
Icon={Play}
|
||||
onClick={onClickSubmit}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApplyButtonsForEdit = ({
|
||||
codeStr,
|
||||
applyBoxId,
|
||||
uri,
|
||||
language,
|
||||
}: {
|
||||
codeStr: string,
|
||||
applyBoxId: string,
|
||||
language?: string,
|
||||
uri: URI | 'current';
|
||||
}) => {
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId })
|
||||
const {
|
||||
currStreamState,
|
||||
isDisabled,
|
||||
getStreamState,
|
||||
} = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (currStreamStateRef.current === 'streaming') return
|
||||
|
||||
await editCodeService.callBeforeApplyOrEdit(uri)
|
||||
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying({
|
||||
if (isDisabled) return
|
||||
if (getStreamState() === 'streaming') return
|
||||
const opts = {
|
||||
from: 'ClickApply',
|
||||
applyStr: codeStr,
|
||||
uri: uri,
|
||||
startBehavior: 'reject-conflicts',
|
||||
}) ?? []
|
||||
setApplying(newApplyingUri)
|
||||
} as const
|
||||
|
||||
if (!applyDonePromise) {
|
||||
notificationService.info(`Void Error: We couldn't run Apply here. ${uri === 'current' ? 'This Apply block wants to run on the current file, but you might not have a file open.' : `This Apply block wants to run on ${uri.fsPath}, but it might not exist.`}`)
|
||||
}
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
|
||||
notificationService.info(`Void Error: There was a problem running Apply: ${e}.`)
|
||||
|
||||
})
|
||||
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
|
||||
// rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
|
||||
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService, notificationService])
|
||||
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
|
||||
|
||||
|
||||
const onClickStop = useCallback(() => {
|
||||
if (currStreamStateRef.current !== 'streaming') return
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (getStreamState() !== 'streaming') return
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
metricsService.capture('Stop Apply', {})
|
||||
}, [currStreamStateRef, applyBoxId, editCodeService, metricsService])
|
||||
}, [getStreamState, applyBoxId, editCodeService, metricsService])
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}, [uri, applyBoxId, editCodeService])
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
const onReject = useCallback(() => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [uri, applyBoxId, editCodeService])
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
const currStreamState = currStreamStateRef.current
|
||||
if (currStreamState === 'streaming') {
|
||||
return <IconShell1
|
||||
|
||||
Icon={Square}
|
||||
onClick={onClickStop}
|
||||
onClick={onInterrupt}
|
||||
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
|
||||
/>
|
||||
}
|
||||
if (isDisabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
|
||||
return <IconShell1
|
||||
Icon={Play}
|
||||
onClick={onClickSubmit}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
return <Fragment>
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = (params: {
|
||||
codeStr: string,
|
||||
applyBoxId: string,
|
||||
language?: string,
|
||||
uri: URI | 'current';
|
||||
}) => {
|
||||
const { language } = params
|
||||
const isShellLanguage = !!language && terminalLanguages.has(language)
|
||||
|
||||
if (isShellLanguage) {
|
||||
return <ApplyButtonsForTerminal {...params} />
|
||||
}
|
||||
else {
|
||||
return <ApplyButtonsForEdit {...params} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const EditToolAcceptRejectButtonsHTML = ({
|
||||
codeStr,
|
||||
applyBoxId,
|
||||
uri,
|
||||
type,
|
||||
threadId,
|
||||
}: {
|
||||
codeStr: string,
|
||||
applyBoxId: string,
|
||||
} & ({
|
||||
uri: URI,
|
||||
type: 'edit_file' | 'rewrite_file',
|
||||
threadId: string,
|
||||
})
|
||||
) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const { streamState } = useEditToolStreamState({ applyBoxId, uri })
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const chatThreadsStreamState = useChatThreadsStreamState(threadId)
|
||||
const isRunning = chatThreadsStreamState?.isRunning
|
||||
|
||||
const isDisabled = !!isFeatureNameDisabled('Chat', settingsState) || !applyBoxId
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}, [uri, applyBoxId, editCodeService])
|
||||
|
||||
const onReject = useCallback(() => {
|
||||
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [uri, applyBoxId, editCodeService])
|
||||
|
||||
if (isDisabled) return null
|
||||
|
||||
if (streamState === 'idle-no-changes') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (streamState === 'idle-has-changes') {
|
||||
if (isRunning === 'LLM' || isRunning === 'tool') return null
|
||||
|
||||
return <>
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
@@ -506,13 +325,13 @@ export const EditToolAcceptRejectButtonsHTML = ({
|
||||
|
||||
export const BlockCodeApplyWrapper = ({
|
||||
children,
|
||||
codeStr,
|
||||
initValue,
|
||||
applyBoxId,
|
||||
language,
|
||||
canApply,
|
||||
uri,
|
||||
}: {
|
||||
codeStr: string;
|
||||
initValue: string;
|
||||
children: React.ReactNode;
|
||||
applyBoxId: string;
|
||||
canApply: boolean;
|
||||
@@ -521,8 +340,7 @@ export const BlockCodeApplyWrapper = ({
|
||||
}) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
|
||||
const currStreamState = currStreamStateRef.current
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
|
||||
const name = uri !== 'current' ?
|
||||
@@ -530,7 +348,7 @@ export const BlockCodeApplyWrapper = ({
|
||||
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
|
||||
isSmall={true}
|
||||
showDot={false}
|
||||
onClick={() => { voidOpenFileFn(uri, accessor) }}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
|
||||
/>
|
||||
: <span>{language}</span>
|
||||
|
||||
@@ -546,8 +364,8 @@ export const BlockCodeApplyWrapper = ({
|
||||
</div>
|
||||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} language={language} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import { marked, MarkedToken, Token } from 'marked'
|
||||
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
|
||||
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
|
||||
import { useAccessor } from '../util/services.js'
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { isAbsolute } from '../../../../../../../base/common/path.js'
|
||||
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
|
||||
import { BlockCode } from '../util/inputs.js'
|
||||
import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js'
|
||||
import { getBasename, getRelative, voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
@@ -89,18 +89,13 @@ const LatexRender = ({ latex }: { latex: string }) => {
|
||||
// }
|
||||
}
|
||||
|
||||
const Codespan = ({ text, className, onClick, tooltip }: { text: string, className?: string, onClick?: () => void, tooltip?: string }) => {
|
||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||
|
||||
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
|
||||
|
||||
return <code
|
||||
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
|
||||
onClick={onClick}
|
||||
{...tooltip ? {
|
||||
'data-tooltip-id': 'void-tooltip',
|
||||
'data-tooltip-content': tooltip,
|
||||
'data-tooltip-place': 'top',
|
||||
} : {}}
|
||||
>
|
||||
{text}
|
||||
</code>
|
||||
@@ -120,11 +115,8 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
|
||||
|
||||
let link: CodespanLocationLink | undefined = undefined
|
||||
let tooltip: string | undefined = undefined
|
||||
let displayText = text
|
||||
if (rawText.endsWith('`')) { // if codespan was completed
|
||||
|
||||
|
||||
if (rawText.endsWith('`')) {
|
||||
// get link from cache
|
||||
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
|
||||
|
||||
@@ -135,33 +127,41 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
|
||||
setDidComputeCodespanLink(true) // rerender
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if (link?.displayText) {
|
||||
displayText = link.displayText
|
||||
}
|
||||
|
||||
if (isValidUri(displayText)) {
|
||||
tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip
|
||||
displayText = getBasename(displayText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onClick = () => {
|
||||
|
||||
if (!link) return;
|
||||
// Use the updated voidOpenFileFn to open the file and handle selection
|
||||
if (link.selection)
|
||||
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
|
||||
else
|
||||
voidOpenFileFn(link.uri, accessor);
|
||||
const selection = link.selection
|
||||
|
||||
// open the file
|
||||
commandService.executeCommand('vscode.open', link.uri).then(() => {
|
||||
|
||||
// select the text
|
||||
setTimeout(() => {
|
||||
if (!selection) return;
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
if (!editor) return;
|
||||
|
||||
editor.setSelection(selection)
|
||||
editor.revealRange(selection, ScrollType.Immediate)
|
||||
|
||||
}, 50) // needed when document was just opened and needs to initialize
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <Codespan
|
||||
text={displayText}
|
||||
// text={link?.displayText || text}
|
||||
text={link?.displayText || text}
|
||||
onClick={onClick}
|
||||
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
|
||||
tooltip={tooltip || undefined}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -319,12 +319,12 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return <BlockCodeApplyWrapper
|
||||
canApply={isCodeblockClosed}
|
||||
applyBoxId={applyBoxId}
|
||||
codeStr={contents}
|
||||
initValue={contents}
|
||||
language={language}
|
||||
uri={uri || 'current'}
|
||||
>
|
||||
<BlockCode
|
||||
initValue={contents.trimEnd()} // \n\n adds a permanent newline which creates a flash
|
||||
initValue={contents}
|
||||
language={language}
|
||||
/>
|
||||
</BlockCodeApplyWrapper>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useIsDark } from '../util/services.js'
|
||||
import { useIsDark, useSidebarState } from '../util/services.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { QuickEditChat } from './QuickEditChat.js'
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js'
|
||||
|
||||
@@ -71,7 +71,7 @@ export const QuickEditChat = ({
|
||||
startBehavior: 'keep-conflicts',
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeApplyOrEdit(opts)
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useIsDark } from '../util/services.js';
|
||||
import { useIsDark, useSidebarState } from '../util/services.js';
|
||||
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
// import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
@@ -12,6 +12,8 @@ import { SidebarChat } from './SidebarChat.js';
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
|
||||
export const Sidebar = ({ className }: { className: string }) => {
|
||||
const sidebarState = useSidebarState()
|
||||
const { currentTab: tab } = sidebarState
|
||||
|
||||
const isDark = useIsDark()
|
||||
return <div
|
||||
@@ -27,12 +29,34 @@ export const Sidebar = ({ className }: { className: string }) => {
|
||||
`}
|
||||
>
|
||||
|
||||
<div className={`w-full h-full`}>
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
const index = tabs.indexOf(tab)
|
||||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarChat />
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* <ErrorBoundary>
|
||||
<ModelSelectionSettings />
|
||||
</ErrorBoundary> */}
|
||||
</div>
|
||||
|
||||
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+139
-10
@@ -11,6 +11,138 @@ import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserChe
|
||||
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
|
||||
|
||||
|
||||
export const OldSidebarThreadSelector = () => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
return (
|
||||
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
|
||||
|
||||
<div className="w-full relative flex justify-center items-center">
|
||||
{/* title */}
|
||||
<h2 className='font-bold text-lg'>{`History`}</h2>
|
||||
{/* X button at top right */}
|
||||
<button
|
||||
type='button'
|
||||
className='absolute top-0 right-0'
|
||||
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
>
|
||||
<IconX
|
||||
size={16}
|
||||
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
{/* <OldPastThreadsList /> */}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OldPastThreadsList = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
|
||||
return <div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false })
|
||||
}}
|
||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const numInitialThreads = 3
|
||||
|
||||
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
||||
@@ -181,6 +313,7 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
// const settingsState = useSettingsState()
|
||||
// const convertService = accessor.get('IConvertToLLMMessageService')
|
||||
@@ -220,14 +353,13 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
const detailsHTML = <span
|
||||
className='gap-1 inline-flex items-center'
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
|
||||
// data-tooltip-place='top'
|
||||
>
|
||||
<span className='opacity-60'>{numMessages}</span>
|
||||
{` `}
|
||||
{/* <span>{numMessages}</span> */}
|
||||
{formatDate(new Date(pastThread.lastModified))}
|
||||
{/* {` messages `} */}
|
||||
</span>
|
||||
|
||||
return <div
|
||||
@@ -237,6 +369,7 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false });
|
||||
}}
|
||||
onMouseEnter={() => setHoveredIdx(idx)}
|
||||
onMouseLeave={() => setHoveredIdx(null)}
|
||||
@@ -244,19 +377,15 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
|
||||
{/* spinner */}
|
||||
{isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'idle' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
null}
|
||||
{/* name */}
|
||||
<span className="truncate overflow-hidden text-ellipsis"
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={numMessages + ' messages'}
|
||||
data-tooltip-place='top'
|
||||
>{firstMsg}</span>
|
||||
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||
|
||||
{/* <span className='opacity-60'>{`(${numMessages})`}</span> */}
|
||||
<span className='opacity-60'>{`(${numMessages})`}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-x-1 opacity-60">
|
||||
|
||||
@@ -9,13 +9,10 @@ type ReturnType<T> = [
|
||||
|
||||
// use this if state might be too slow to catch
|
||||
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
|
||||
// this actually makes a difference being an int, not a boolean.
|
||||
// if it's a boolean and changes happen to fast, it goes with old values and leads to *very* weird bugs (like returning JSX, but not actually rendering it)
|
||||
const [_s, _setState] = useState(0)
|
||||
|
||||
const [_, _setState] = useState(false)
|
||||
const ref = useRef<T>(initVal)
|
||||
const setState = useCallback((newVal: T) => {
|
||||
_setState(n => n + 1) // call rerender
|
||||
_setState(n => !n) // call rerender
|
||||
ref.current = newVal
|
||||
}, [])
|
||||
return [ref, setState]
|
||||
|
||||
@@ -17,14 +17,9 @@ import { asCssVariable } from '../../../../../../../platform/theme/common/colorU
|
||||
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
|
||||
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { getBasename } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
|
||||
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { DiffEditorWidget } from '../../../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
|
||||
import { extractSearchReplaceBlocks, ExtractedSearchReplaceBlock } from '../../../../common/helpers/extractCodeFromResult.js';
|
||||
import { IAccessibilitySignalService } from '../../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
|
||||
import { IEditorProgressService } from '../../../../../../../platform/progress/common/progress.js';
|
||||
import { detectLanguage } from '../../../../common/helpers/languageHelpers.js';
|
||||
|
||||
|
||||
// type guard
|
||||
@@ -60,13 +55,12 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
||||
type GenerateNextOptions = (optionText: string) => Promise<Option[]>
|
||||
|
||||
type Option = {
|
||||
fullName: string,
|
||||
abbreviatedName: string,
|
||||
nameInMenu: string,
|
||||
iconInMenu: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>, // type for lucide-react components
|
||||
} & (
|
||||
| { leafNodeType?: undefined, nextOptions: Option[], generateNextOptions?: undefined, }
|
||||
| { leafNodeType?: undefined, nextOptions?: undefined, generateNextOptions: GenerateNextOptions, }
|
||||
| { leafNodeType: 'File' | 'Folder', uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
|
||||
| { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined }
|
||||
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined }
|
||||
| { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
|
||||
)
|
||||
|
||||
|
||||
@@ -137,7 +131,7 @@ const scoreSubsequence = (text: string, pattern: string): number => {
|
||||
}
|
||||
|
||||
|
||||
function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
|
||||
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
|
||||
const workspaceService = accessor.get('IWorkspaceContextService');
|
||||
const workspaceFolders = workspaceService.getWorkspace().folders;
|
||||
|
||||
@@ -165,7 +159,7 @@ function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri:
|
||||
if (relativePath.startsWith('/')) {
|
||||
relativePath = relativePath.slice(1);
|
||||
}
|
||||
// console.log({ folderPath, relativePath, uriPath });
|
||||
console.log({ folderPath, relativePath, uriPath });
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
@@ -179,19 +173,10 @@ function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri:
|
||||
|
||||
const numOptionsToShow = 100
|
||||
|
||||
|
||||
|
||||
// TODO make this unique based on other options
|
||||
const getAbbreviatedName = (relativePath: string) => {
|
||||
return getBasename(relativePath, 1)
|
||||
}
|
||||
|
||||
const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path: string[], optionText: string): Promise<Option[]> => {
|
||||
|
||||
const toolsService = accessor.get('IToolsService')
|
||||
|
||||
|
||||
|
||||
const searchForFilesOrFolders = async (t: string, searchFor: 'files' | 'folders') => {
|
||||
try {
|
||||
|
||||
@@ -208,8 +193,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
|
||||
leafNodeType: 'File',
|
||||
uri: uri,
|
||||
iconInMenu: File,
|
||||
fullName: relativePath,
|
||||
abbreviatedName: getAbbreviatedName(relativePath),
|
||||
nameInMenu: relativePath,
|
||||
nameToPaste: getBasename(relativePath, 2),
|
||||
}
|
||||
})
|
||||
return res
|
||||
@@ -256,6 +241,7 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`;
|
||||
|
||||
console.log('filepath', currentPath);
|
||||
|
||||
// Create a proper directory URI
|
||||
const directoryUri = URI.joinPath(
|
||||
@@ -272,8 +258,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
|
||||
leafNodeType: 'Folder',
|
||||
uri: uri,
|
||||
iconInMenu: Folder, // Folder
|
||||
fullName: relativePath,
|
||||
abbreviatedName: getAbbreviatedName(relativePath),
|
||||
nameInMenu: relativePath,
|
||||
nameToPaste: getBasename(relativePath, 2)
|
||||
})) satisfies Option[];
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -285,15 +271,13 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
|
||||
|
||||
const allOptions: Option[] = [
|
||||
{
|
||||
fullName: 'files',
|
||||
abbreviatedName: 'files',
|
||||
nameInMenu: 'files',
|
||||
iconInMenu: File,
|
||||
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [],
|
||||
},
|
||||
{
|
||||
fullName: 'folders',
|
||||
abbreviatedName: 'folders',
|
||||
iconInMenu: Folder,
|
||||
nameInMenu: 'folders',
|
||||
iconInMenu: FolderClosed,
|
||||
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [],
|
||||
},
|
||||
]
|
||||
@@ -305,7 +289,7 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
|
||||
|
||||
for (const pn of path) {
|
||||
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.fullName.toLowerCase() === pn.toLowerCase())
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase())
|
||||
|
||||
if (!selectedOption) return [];
|
||||
|
||||
@@ -316,20 +300,14 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
|
||||
|
||||
|
||||
if (generateNextOptionsAtPath) {
|
||||
|
||||
nextOptionsAtPath = await generateNextOptionsAtPath(optionText)
|
||||
}
|
||||
else if (path.length === 0 && optionText.trim().length > 0) { // (special case): directly search for both files and folders if optionsPath is empty and there's a search term
|
||||
const filesResults = await searchForFilesOrFolders(optionText, 'files') || [];
|
||||
const foldersResults = await searchForFilesOrFolders(optionText, 'folders') || [];
|
||||
nextOptionsAtPath = [...foldersResults, ...filesResults,]
|
||||
}
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath
|
||||
.filter(o => isSubsequence(o.fullName, optionText))
|
||||
.filter(o => isSubsequence(o.nameInMenu, optionText))
|
||||
.sort((a, b) => { // this is a hack but good for now
|
||||
const scoreA = scoreSubsequence(a.fullName, optionText);
|
||||
const scoreB = scoreSubsequence(b.fullName, optionText);
|
||||
const scoreA = scoreSubsequence(a.nameInMenu, optionText);
|
||||
const scoreB = scoreSubsequence(b.nameInMenu, optionText);
|
||||
return scoreB - scoreA;
|
||||
})
|
||||
.slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints
|
||||
@@ -376,14 +354,6 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
const [optionIdx, setOptionIdx] = useState<number>(0);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [optionText, setOptionText] = useState<string>('');
|
||||
const [didLoadInitialOptions, setDidLoadInitialOptions] = useState(false);
|
||||
|
||||
const currentPathRef = useRef<string>(JSON.stringify([]));
|
||||
|
||||
// dont show breadcrums if first page and user hasnt typed anything
|
||||
const isTypingEnabled = true
|
||||
const isBreadcrumbsShowing = optionPath.length === 0 && !optionText ? false : true
|
||||
|
||||
const insertTextAtCursor = (text: string) => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (!textarea) return;
|
||||
@@ -391,21 +361,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
// Focus the textarea first
|
||||
textarea.focus();
|
||||
|
||||
// delete the @ and set the cursor position
|
||||
// Get cursor position
|
||||
const startPos = textarea.selectionStart;
|
||||
const endPos = textarea.selectionEnd;
|
||||
|
||||
// Get the text before the cursor, excluding the @ symbol that triggered the menu
|
||||
const textBeforeCursor = textarea.value.substring(0, startPos - 1);
|
||||
const textAfterCursor = textarea.value.substring(endPos);
|
||||
|
||||
// Replace the text including the @ symbol with the selected option
|
||||
textarea.value = textBeforeCursor + textAfterCursor;
|
||||
|
||||
// Set cursor position after the inserted text
|
||||
const newCursorPos = textBeforeCursor.length;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
// The most reliable way to simulate typing is to use execCommand
|
||||
// which will trigger all the appropriate native events
|
||||
document.execCommand('insertText', false, text + ' '); // add space after too
|
||||
|
||||
// React's onChange relies on a SyntheticEvent system
|
||||
// The best way to ensure it runs is to call callbacks directly
|
||||
@@ -421,64 +379,51 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
if (!options.length) { return; }
|
||||
|
||||
const option = options[optionIdx];
|
||||
const newPath = [...optionPath, option.fullName]
|
||||
const newPath = [...optionPath, option.nameInMenu]
|
||||
const isLastOption = !option.generateNextOptions && !option.nextOptions
|
||||
setDidLoadInitialOptions(false)
|
||||
|
||||
setOptionPath(newPath)
|
||||
setOptionText('')
|
||||
setOptionIdx(0)
|
||||
if (isLastOption) {
|
||||
setIsMenuOpen(false)
|
||||
insertTextAtCursor(option.abbreviatedName)
|
||||
insertTextAtCursor(option.nameToPaste)
|
||||
|
||||
let newSelection: StagingSelectionItem
|
||||
if (option.leafNodeType === 'File') newSelection = {
|
||||
const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? {
|
||||
type: 'File',
|
||||
uri: option.uri,
|
||||
language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '',
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
}
|
||||
else if (option.leafNodeType === 'Folder') newSelection = {
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : option.leafNodeType === 'Folder' ? {
|
||||
type: 'Folder',
|
||||
uri: option.uri,
|
||||
language: undefined,
|
||||
state: undefined,
|
||||
}
|
||||
else throw new Error(`Unexpected leafNodeType ${option.leafNodeType}`)
|
||||
|
||||
} : (undefined as never)
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
console.log('selected', option.uri?.fsPath)
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
currentPathRef.current = JSON.stringify(newPath);
|
||||
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
|
||||
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
|
||||
setOptionPath(newPath)
|
||||
setOptionText('')
|
||||
setOptionIdx(0)
|
||||
setOptions(newOpts)
|
||||
setDidLoadInitialOptions(true)
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveOption = async () => {
|
||||
const newPath = [...optionPath.slice(0, optionPath.length - 1)]
|
||||
currentPathRef.current = JSON.stringify(newPath);
|
||||
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
|
||||
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
|
||||
setOptionPath(newPath)
|
||||
setOptionText('')
|
||||
setOptionIdx(0)
|
||||
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
|
||||
setOptions(newOpts)
|
||||
}
|
||||
|
||||
const onOpenOptionMenu = async () => {
|
||||
const newPath: [] = []
|
||||
currentPathRef.current = JSON.stringify([]);
|
||||
const newOpts = await getOptionsAtPath(accessor, [], '') || []
|
||||
if (currentPathRef.current !== JSON.stringify([])) { return; }
|
||||
setOptionPath(newPath)
|
||||
setOptionPath([])
|
||||
setOptionText('')
|
||||
setIsMenuOpen(true);
|
||||
setOptionIdx(0);
|
||||
const newOpts = await getOptionsAtPath(accessor, [], '') || []
|
||||
setOptions(newOpts);
|
||||
}
|
||||
const onCloseOptionMenu = () => {
|
||||
@@ -521,36 +466,24 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
};
|
||||
}, []);
|
||||
|
||||
// debounced, but immediate if text is empty
|
||||
// debounced
|
||||
const onPathTextChange = useCallback((newStr: string) => {
|
||||
|
||||
|
||||
setOptionText(newStr);
|
||||
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
currentPathRef.current = JSON.stringify(optionPath);
|
||||
|
||||
const fetchOptions = async () => {
|
||||
// Set a new timeout to fetch options after a delay
|
||||
debounceTimerRef.current = window.setTimeout(async () => {
|
||||
const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || [];
|
||||
if (currentPathRef.current !== JSON.stringify(optionPath)) { return; }
|
||||
setOptions(newOpts);
|
||||
setOptionIdx(0);
|
||||
debounceTimerRef.current = null;
|
||||
};
|
||||
|
||||
// If text is empty, run immediately without debouncing
|
||||
if (newStr.trim() === '') {
|
||||
fetchOptions();
|
||||
} else {
|
||||
// Otherwise, set a new timeout to fetch options after a delay
|
||||
debounceTimerRef.current = window.setTimeout(fetchOptions, 300);
|
||||
}
|
||||
}, 300);
|
||||
}, [optionPath, accessor]);
|
||||
|
||||
|
||||
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
const isCommandKeyPressed = e.altKey || e.ctrlKey || e.metaKey;
|
||||
@@ -604,9 +537,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
// do nothing
|
||||
}
|
||||
else { // letter
|
||||
if (isTypingEnabled) {
|
||||
onPathTextChange(optionText + e.key)
|
||||
}
|
||||
onPathTextChange(optionText + e.key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,16 +715,6 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Backspace') { // TODO allow user to undo this.
|
||||
if (!e.currentTarget.value || (e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0)) { // if there is no text or cursor is at position 0, remove a selection
|
||||
if (e.metaKey || e.ctrlKey) { // Ctrl+Backspace = remove all
|
||||
chatThreadService.popStagingSelections(Number.MAX_SAFE_INTEGER)
|
||||
} else { // Backspace = pop 1 selection
|
||||
chatThreadService.popStagingSelections(1)
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
// Shift + Enter when multiline = newline
|
||||
const shouldAddNewline = e.shiftKey && multiline
|
||||
@@ -819,25 +740,25 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Breadcrumbs Header */}
|
||||
{isBreadcrumbsShowing && <div className="px-2 py-1 text-void-fg-1 bg-void-bg-2-alt border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
|
||||
{optionText ?
|
||||
<div className="px-2 py-1 text-void-fg-3 bg-void-bg-2-alt text-sm border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
|
||||
{optionPath.length || optionText ?
|
||||
<div className="flex items-center">
|
||||
{/* {optionPath.map((path, index) => (
|
||||
{optionPath.map((path, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<span>{path}</span>
|
||||
<ChevronRight size={12} className="mx-1" />
|
||||
</React.Fragment>
|
||||
))} */}
|
||||
))}
|
||||
<span>{optionText}</span>
|
||||
</div>
|
||||
: <div className='opacity-50'>Enter text to filter...</div>
|
||||
: <div className='opacity-60'>Enter text to filter...</div>
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Options list */}
|
||||
<div className='max-h-[400px] w-full max-w-full overflow-y-auto overflow-x-auto'>
|
||||
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap">
|
||||
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap text-sm opacity-70">
|
||||
{options.length === 0 ?
|
||||
<div className="text-void-fg-3 px-3 py-0.5">No results found</div>
|
||||
: options.map((o, oIdx) => {
|
||||
@@ -846,25 +767,20 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
// Option
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
key={o.fullName}
|
||||
key={o.nameInMenu}
|
||||
className={`
|
||||
flex items-center gap-2
|
||||
px-3 py-1 cursor-pointer
|
||||
${oIdx === optionIdx ? 'bg-blue-500 text-white/80' : 'bg-void-bg-2-alt text-void-fg-1'}
|
||||
px-3 py-0.5 cursor-pointer bg-void-bg-2-alt
|
||||
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
|
||||
`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
onMouseMove={() => { setOptionIdx(oIdx) }}
|
||||
onMouseOver={() => { setOptionIdx(oIdx) }}
|
||||
>
|
||||
{<o.iconInMenu size={12} />}
|
||||
|
||||
<span>{o.abbreviatedName}</span>
|
||||
|
||||
{o.fullName && o.fullName !== o.abbreviatedName && <span className="opacity-60 text-sm">{o.fullName}</span>}
|
||||
|
||||
<span className="text-void-fg-1">{o.nameInMenu}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<ChevronRight size={12} />
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -887,44 +803,15 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
|
||||
compact?: boolean;
|
||||
passwordBlur?: boolean;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) => {
|
||||
// Create a ref for the input element to maintain the same DOM node between renders
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Track if we need to restore selection
|
||||
const selectionRef = useRef<{ start: number | null, end: number | null }>({
|
||||
start: null,
|
||||
end: null
|
||||
});
|
||||
|
||||
// Handle value changes without recreating the input
|
||||
useEffect(() => {
|
||||
const input = inputRef.current;
|
||||
if (input && input.value !== value) {
|
||||
// Store current selection positions
|
||||
selectionRef.current.start = input.selectionStart;
|
||||
selectionRef.current.end = input.selectionEnd;
|
||||
|
||||
// Update the value
|
||||
input.value = value;
|
||||
|
||||
// Restore selection if we had it before
|
||||
if (selectionRef.current.start !== null && selectionRef.current.end !== null) {
|
||||
input.setSelectionRange(selectionRef.current.start, selectionRef.current.end);
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChangeValue(e.target.value);
|
||||
}, [onChangeValue]);
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
defaultValue={value} // Use defaultValue instead of value to avoid recreation
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
onChange={(e) => onChangeValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
|
||||
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
|
||||
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
|
||||
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
|
||||
rounded
|
||||
@@ -956,11 +843,11 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
className='
|
||||
bg-void-bg-1
|
||||
@@void-force-child-placeholder-void-fg-1
|
||||
'
|
||||
ctor={InputBox}
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
contextViewProvider,
|
||||
@@ -996,7 +883,8 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
inputBoxRef.current = instance;
|
||||
|
||||
return disposables
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])}
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])
|
||||
}
|
||||
/>
|
||||
};
|
||||
|
||||
@@ -1441,7 +1329,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
|
||||
transition-all duration-100
|
||||
${thisOptionIsSelected ? 'bg-blue-500 text-white/80' : 'hover:bg-blue-500 hover:text-white/80'}
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
|
||||
`}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
@@ -1463,7 +1351,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||
</div>
|
||||
<span className="flex justify-between items-center w-full gap-x-1">
|
||||
<span>{optionName}</span>
|
||||
<span className='opacity-60'>{optionDetail}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1845,154 +1733,3 @@ export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: {
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
const SingleDiffEditor = ({ block, lang }: { block: ExtractedSearchReplaceBlock, lang: string | undefined }) => {
|
||||
const accessor = useAccessor();
|
||||
const modelService = accessor.get('IModelService');
|
||||
const instantiationService = accessor.get('IInstantiationService');
|
||||
const languageService = accessor.get('ILanguageService');
|
||||
|
||||
const languageSelection = useMemo(() => languageService.createById(lang), [lang, languageService]);
|
||||
|
||||
// Create models for original and modified
|
||||
const originalModel = useMemo(() =>
|
||||
modelService.createModel(block.orig, languageSelection),
|
||||
[block.orig, languageSelection, modelService]
|
||||
);
|
||||
const modifiedModel = useMemo(() =>
|
||||
modelService.createModel(block.final, languageSelection),
|
||||
[block.final, languageSelection, modelService]
|
||||
);
|
||||
|
||||
// Clean up models on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
originalModel.dispose();
|
||||
modifiedModel.dispose();
|
||||
};
|
||||
}, [originalModel, modifiedModel]);
|
||||
|
||||
// Imperatively mount the DiffEditorWidget
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!divRef.current) return;
|
||||
// Create the diff editor instance
|
||||
const editor = instantiationService.createInstance(
|
||||
DiffEditorWidget,
|
||||
divRef.current,
|
||||
{
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'off',
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'auto',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 8,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
ignoreHorizontalScrollbarInContentHeight: true,
|
||||
},
|
||||
hover: { enabled: false },
|
||||
folding: false,
|
||||
selectionHighlight: false,
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
glyphMargin: false,
|
||||
stickyScroll: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
renderGutterMenu: false,
|
||||
renderIndicators: false,
|
||||
},
|
||||
{ originalEditor: { isSimpleWidget: true }, modifiedEditor: { isSimpleWidget: true } }
|
||||
);
|
||||
editor.setModel({ original: originalModel, modified: modifiedModel });
|
||||
|
||||
// Calculate the height based on content
|
||||
const updateHeight = () => {
|
||||
const contentHeight = Math.max(
|
||||
originalModel.getLineCount() * 19, // approximate line height
|
||||
modifiedModel.getLineCount() * 19
|
||||
) + 19 * 2 + 1; // add padding
|
||||
|
||||
// Set reasonable min/max heights
|
||||
const height = Math.min(Math.max(contentHeight, 100), 300);
|
||||
if (divRef.current) {
|
||||
divRef.current.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
editorRef.current = editor;
|
||||
|
||||
// Update height when content changes
|
||||
const disposable1 = originalModel.onDidChangeContent(() => updateHeight());
|
||||
const disposable2 = modifiedModel.onDidChangeContent(() => updateHeight());
|
||||
|
||||
return () => {
|
||||
disposable1.dispose();
|
||||
disposable2.dispose();
|
||||
editor.dispose();
|
||||
editorRef.current = null;
|
||||
};
|
||||
}, [originalModel, modifiedModel, instantiationService]);
|
||||
|
||||
return (
|
||||
<div className="w-full bg-void-bg-3 @@bg-editor-style-override" ref={divRef} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks.
|
||||
* Props:
|
||||
* - uri: URI of the file (for language detection, etc)
|
||||
* - searchReplaceBlocks: string in search/replace format (from LLM)
|
||||
* - language?: string (optional, fallback to 'plaintext')
|
||||
*/
|
||||
export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => {
|
||||
const accessor = useAccessor();
|
||||
const languageService = accessor.get('ILanguageService');
|
||||
|
||||
// Extract all blocks
|
||||
const blocks = extractSearchReplaceBlocks(searchReplaceBlocks);
|
||||
|
||||
// Use detectLanguage for language detection if not provided
|
||||
let lang = language;
|
||||
if (!lang && blocks.length > 0) {
|
||||
lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: blocks[0].orig });
|
||||
}
|
||||
|
||||
// If no blocks, show empty state
|
||||
if (blocks.length === 0) {
|
||||
return <div className="w-full p-4 text-void-fg-4 text-sm">No changes found</div>;
|
||||
}
|
||||
|
||||
// Display all blocks
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
{blocks.map((block, index) => (
|
||||
<div key={index} className="w-full">
|
||||
{blocks.length > 1 && (
|
||||
<div className="text-void-fg-4 text-xs mb-1 px-1">
|
||||
Change {index + 1} of {blocks.length}
|
||||
</div>
|
||||
)}
|
||||
<SingleDiffEditor block={block} lang={lang} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
@@ -21,8 +22,8 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
||||
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IExtensionTransferService } from '../../../../../../../workbench/contrib/void/browser/extensionTransferService.js'
|
||||
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
|
||||
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
|
||||
@@ -48,12 +49,8 @@ import { INativeHostService } from '../../../../../../../platform/native/common/
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
import { IToolsService } from '../../../toolsService.js'
|
||||
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
|
||||
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
||||
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
||||
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
|
||||
import { IMCPService } from '../../../../common/mcpService.js';
|
||||
import { IStorageService, StorageScope } from '../../../../../../../platform/storage/common/storage.js'
|
||||
import { OPT_OUT_KEY } from '../../../../common/storageKeys.js'
|
||||
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
@@ -61,6 +58,9 @@ import { OPT_OUT_KEY } from '../../../../common/storageKeys.js'
|
||||
// even if React hasn't mounted yet, the variables are always updated to the latest state.
|
||||
// React listens by adding a setState function to these listeners.
|
||||
|
||||
let sidebarState: VoidSidebarState
|
||||
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
|
||||
|
||||
let chatThreadsState: ThreadsState
|
||||
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
|
||||
@@ -81,8 +81,6 @@ const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) =>
|
||||
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
|
||||
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
|
||||
|
||||
const mcpListeners: Set<() => void> = new Set()
|
||||
|
||||
|
||||
// must call this before you can use any of the hooks below
|
||||
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
||||
@@ -93,6 +91,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
_registerAccessor(accessor)
|
||||
|
||||
const stateServices = {
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
chatThreadsStateService: accessor.get(IChatThreadService),
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
@@ -100,13 +99,17 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
editCodeService: accessor.get(IEditCodeService),
|
||||
voidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
modelService: accessor.get(IModelService),
|
||||
mcpService: accessor.get(IMCPService),
|
||||
}
|
||||
|
||||
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService, mcpService } = stateServices
|
||||
|
||||
|
||||
const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
|
||||
|
||||
sidebarState = sidebarStateService.state
|
||||
disposables.push(
|
||||
sidebarStateService.onDidChangeState(() => {
|
||||
sidebarState = sidebarStateService.state
|
||||
sidebarStateListeners.forEach(l => l(sidebarState))
|
||||
})
|
||||
)
|
||||
|
||||
chatThreadsState = chatThreadsStateService.state
|
||||
disposables.push(
|
||||
@@ -144,8 +147,8 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
|
||||
colorThemeState = themeService.getColorTheme().type
|
||||
disposables.push(
|
||||
themeService.onDidColorThemeChange(({ type }) => {
|
||||
colorThemeState = type
|
||||
themeService.onDidColorThemeChange(({ theme }) => {
|
||||
colorThemeState = theme.type
|
||||
colorThemeStateListeners.forEach(l => l(colorThemeState))
|
||||
})
|
||||
)
|
||||
@@ -170,11 +173,6 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
})
|
||||
)
|
||||
|
||||
disposables.push(
|
||||
mcpService.onDidChangeState(() => {
|
||||
mcpListeners.forEach(l => l())
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
return disposables
|
||||
@@ -195,6 +193,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IEditCodeService: accessor.get(IEditCodeService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
IChatThreadService: accessor.get(IChatThreadService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
@@ -224,11 +223,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
IToolsService: accessor.get(IToolsService),
|
||||
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
|
||||
ITerminalService: accessor.get(ITerminalService),
|
||||
IExtensionManagementService: accessor.get(IExtensionManagementService),
|
||||
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
||||
IMCPService: accessor.get(IMCPService),
|
||||
|
||||
IStorageService: accessor.get(IStorageService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
@@ -256,6 +250,16 @@ export const useAccessor = () => {
|
||||
|
||||
// -- state of services --
|
||||
|
||||
export const useSidebarState = () => {
|
||||
const [s, ss] = useState(sidebarState)
|
||||
useEffect(() => {
|
||||
ss(sidebarState)
|
||||
sidebarStateListeners.add(ss)
|
||||
return () => { sidebarStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
export const useSettingsState = () => {
|
||||
const [s, ss] = useState(settingsState)
|
||||
useEffect(() => {
|
||||
@@ -390,39 +394,3 @@ export const useActiveURI = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
export const useMCPServiceState = () => {
|
||||
const accessor = useAccessor()
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
const [s, ss] = useState(mcpService.state)
|
||||
useEffect(() => {
|
||||
const listener = () => { ss(mcpService.state) }
|
||||
mcpListeners.add(listener);
|
||||
return () => { mcpListeners.delete(listener) };
|
||||
}, []);
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const useIsOptedOut = () => {
|
||||
const accessor = useAccessor()
|
||||
const storageService = accessor.get('IStorageService')
|
||||
|
||||
const getVal = useCallback(() => {
|
||||
return storageService.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
|
||||
}, [storageService])
|
||||
|
||||
const [s, ss] = useState(getVal())
|
||||
|
||||
useEffect(() => {
|
||||
const disposables = new DisposableStore();
|
||||
const d = storageService.onDidChangeValue(StorageScope.APPLICATION, OPT_OUT_KEY, disposables)(e => {
|
||||
ss(getVal())
|
||||
})
|
||||
disposables.add(d)
|
||||
return () => disposables.clear()
|
||||
}, [storageService, getVal])
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
+220
-286
@@ -9,19 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
|
||||
import '../styles.css'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
|
||||
import { Check, EllipsisVertical, Menu, MoveDown, MoveLeft, MoveRight, MoveUp, X } from 'lucide-react';
|
||||
import {
|
||||
VOID_GOTO_NEXT_DIFF_ACTION_ID,
|
||||
VOID_GOTO_PREV_DIFF_ACTION_ID,
|
||||
VOID_GOTO_NEXT_URI_ACTION_ID,
|
||||
VOID_GOTO_PREV_URI_ACTION_ID,
|
||||
VOID_ACCEPT_FILE_ACTION_ID,
|
||||
VOID_REJECT_FILE_ACTION_ID,
|
||||
VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
|
||||
VOID_REJECT_ALL_DIFFS_ACTION_ID
|
||||
} from '../../../actionIDs.js';
|
||||
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const isDark = useIsDark()
|
||||
@@ -35,55 +25,14 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
|
||||
|
||||
|
||||
export const AcceptAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
className={`
|
||||
px-2 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
h-full rounded-none
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: 'var(--vscode-button-background)',
|
||||
color: 'var(--vscode-button-foreground)',
|
||||
border: 'none',
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{text ? <span>{text}</span> : <Check size={16} />}
|
||||
</button>
|
||||
)
|
||||
|
||||
export const RejectAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
className={`
|
||||
px-2 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
h-full rounded-none
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: 'var(--vscode-button-secondaryBackground)',
|
||||
color: 'var(--vscode-button-secondaryForeground)',
|
||||
border: 'none',
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{text ? <span>{text}</span> : <X size={16} />}
|
||||
</button>
|
||||
)
|
||||
const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => {
|
||||
if (len === 0) return null
|
||||
return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const editorService = accessor.get('ICodeEditorService')
|
||||
@@ -91,9 +40,12 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const voidModelService = accessor.get('IVoidModelService')
|
||||
const keybindingService = accessor.get('IKeybindingService')
|
||||
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
const [showAcceptRejectAllButtons, setShowAcceptRejectAllButtons] = useState(false)
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log('MOUNTING!!!')
|
||||
// }, [])
|
||||
|
||||
// latestUriIdx is used to remember place in leftRight
|
||||
const _latestValidUriIdxRef = useRef<number | null>(null)
|
||||
@@ -118,18 +70,56 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const s = commandBarService.stateOfURI[uri.fsPath]
|
||||
if (!s) return
|
||||
const { diffIdx } = s
|
||||
commandBarService.goToDiffIdx(diffIdx ?? 0)
|
||||
goToDiffIdx(diffIdx ?? 0)
|
||||
}, 50)
|
||||
}, [uri, commandBarService])
|
||||
|
||||
if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files
|
||||
|
||||
// Using service methods directly
|
||||
const getNextDiffIdx = (step: 1 | -1) => {
|
||||
// check undefined
|
||||
if (!uri) return null
|
||||
const s = commandBarState[uri.fsPath]
|
||||
if (!s) return null
|
||||
const { diffIdx, sortedDiffIds } = s
|
||||
// get next idx
|
||||
const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step)
|
||||
return nextDiffIdx
|
||||
}
|
||||
const goToDiffIdx = (idx: number | null) => {
|
||||
if (idx === null) return
|
||||
// check undefined
|
||||
if (!uri) return
|
||||
const s = commandBarState[uri.fsPath]
|
||||
if (!s) return
|
||||
const { sortedDiffIds } = s
|
||||
// reveal
|
||||
const diffid = sortedDiffIds[idx]
|
||||
if (diffid === undefined) return
|
||||
const diff = editCodeService.diffOfId[diffid]
|
||||
if (!diff) return
|
||||
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
|
||||
commandBarService.setDiffIdx(uri, idx)
|
||||
}
|
||||
const getNextUriIdx = (step: 1 | -1) => {
|
||||
return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step)
|
||||
}
|
||||
const goToURIIdx = async (idx: number | null) => {
|
||||
if (idx === null) return
|
||||
const nextURI = sortedCommandBarURIs[idx]
|
||||
editCodeService.diffAreasOfURI
|
||||
const { model } = await voidModelService.getModelSafe(nextURI)
|
||||
if (model) {
|
||||
// switch to the URI
|
||||
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
|
||||
}
|
||||
}
|
||||
|
||||
const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null
|
||||
const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : []
|
||||
const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : []
|
||||
|
||||
|
||||
const isADiffInThisFile = sortedDiffIds.length !== 0
|
||||
const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0
|
||||
const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0
|
||||
@@ -137,247 +127,191 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const streamState = uri ? commandBarService.getStreamState(uri) : null
|
||||
const showAcceptRejectAll = streamState === 'idle-has-changes'
|
||||
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1)
|
||||
const prevDiffIdx = commandBarService.getNextDiffIdx(-1)
|
||||
const nextURIIdx = commandBarService.getNextUriIdx(1)
|
||||
const prevURIIdx = commandBarService.getNextUriIdx(-1)
|
||||
const nextDiffIdx = getNextDiffIdx(1)
|
||||
const prevDiffIdx = getNextDiffIdx(-1)
|
||||
const nextURIIdx = getNextUriIdx(1)
|
||||
const prevURIIdx = getNextUriIdx(-1)
|
||||
|
||||
const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null
|
||||
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null
|
||||
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile)
|
||||
|
||||
const upButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}// --border border-void-border-3 focus:border-void-border-1
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => { goToDiffIdx(prevDiffIdx) }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToDiffIdx(prevDiffIdx);
|
||||
}
|
||||
}}
|
||||
>↑</button>
|
||||
|
||||
const downButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => { goToDiffIdx(nextDiffIdx) }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}}
|
||||
>↓</button>
|
||||
|
||||
const leftButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => goToURIIdx(prevURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToURIIdx(prevURIIdx);
|
||||
}
|
||||
}}
|
||||
>←</button>
|
||||
|
||||
const rightButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => goToURIIdx(nextURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToURIIdx(nextURIIdx);
|
||||
}
|
||||
}}
|
||||
>→</button>
|
||||
|
||||
|
||||
|
||||
// accept/reject if current URI has changes
|
||||
const onAcceptFile = () => {
|
||||
const onAcceptAll = () => {
|
||||
if (!uri) return
|
||||
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
|
||||
metricsService.capture('Accept File', {})
|
||||
metricsService.capture('Accept All', {})
|
||||
}
|
||||
const onRejectFile = () => {
|
||||
const onRejectAll = () => {
|
||||
if (!uri) return
|
||||
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
|
||||
metricsService.capture('Reject File', {})
|
||||
}
|
||||
|
||||
const onAcceptAll = () => {
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
|
||||
metricsService.capture('Accept All', {})
|
||||
setShowAcceptRejectAllButtons(false);
|
||||
}
|
||||
|
||||
const onRejectAll = () => {
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
|
||||
metricsService.capture('Reject All', {})
|
||||
setShowAcceptRejectAllButtons(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const _upKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_DIFF_ACTION_ID);
|
||||
const _downKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_DIFF_ACTION_ID);
|
||||
const _leftKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_URI_ACTION_ID);
|
||||
const _rightKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_URI_ACTION_ID);
|
||||
const _acceptFileKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_FILE_ACTION_ID);
|
||||
const _rejectFileKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_FILE_ACTION_ID);
|
||||
const _acceptAllKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_ALL_DIFFS_ACTION_ID);
|
||||
const _rejectAllKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_ALL_DIFFS_ACTION_ID);
|
||||
|
||||
const upKeybindLabel = editCodeService.processRawKeybindingText(_upKeybinding?.getLabel() || '');
|
||||
const downKeybindLabel = editCodeService.processRawKeybindingText(_downKeybinding?.getLabel() || '');
|
||||
const leftKeybindLabel = editCodeService.processRawKeybindingText(_leftKeybinding?.getLabel() || '');
|
||||
const rightKeybindLabel = editCodeService.processRawKeybindingText(_rightKeybinding?.getLabel() || '');
|
||||
const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getAriaLabel() || '');
|
||||
const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getAriaLabel() || '');
|
||||
const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getAriaLabel() || '');
|
||||
const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getAriaLabel() || '');
|
||||
|
||||
|
||||
if (!isADiffZoneInAnyFile) return null
|
||||
|
||||
// For pages without a current file index, show a simplified command bar
|
||||
if (currFileIdx === null) {
|
||||
return (
|
||||
<div className="pointer-events-auto">
|
||||
<div className="flex bg-void-bg-2 shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
|
||||
<div className="flex items-center px-3">
|
||||
<span className="text-xs whitespace-nowrap">
|
||||
{`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-xs whitespace-nowrap cursor-pointer flex items-center justify-center gap-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] hover:opacity-90 h-full px-3"
|
||||
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToURIIdx(nextURIIdx);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next <MoveRight className='size-3 my-1' />
|
||||
</button>
|
||||
</div>
|
||||
// const acceptAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onAcceptAll}
|
||||
// style={{
|
||||
// backgroundColor: acceptAllBg,
|
||||
// border: acceptBorder,
|
||||
// color: buttonTextColor,
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Accept File
|
||||
// </button>
|
||||
|
||||
// const rejectAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onRejectAll}
|
||||
// style={{
|
||||
// backgroundColor: rejectBg,
|
||||
// border: rejectBorder,
|
||||
// color: 'white',
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Reject File
|
||||
// </button>
|
||||
|
||||
const acceptAllButton = <AcceptAllButtonWrapper
|
||||
text={'Keep Changes'}
|
||||
onClick={onAcceptAll}
|
||||
/>
|
||||
|
||||
const rejectAllButton = <RejectAllButtonWrapper
|
||||
text={'Reject All'}
|
||||
onClick={onRejectAll}
|
||||
/>
|
||||
|
||||
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
|
||||
{acceptAllButton}
|
||||
{rejectAllButton}
|
||||
</div>
|
||||
|
||||
// const closeCommandBar = useCallback(() => {
|
||||
// commandService.executeCommand('void.hideCommandBar');
|
||||
// }, [commandService]);
|
||||
|
||||
// const hideButton = <button
|
||||
// className='ml-auto pointer-events-auto'
|
||||
// onClick={closeCommandBar}
|
||||
// style={{
|
||||
// color: buttonTextColor,
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// title="Close command bar"
|
||||
// >x
|
||||
// </button>
|
||||
|
||||
const leftRightUpDownButtons = <div className='p-1 gap-1 flex flex-col items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 w-full'>
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Changes in file */}
|
||||
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
|
||||
{upButton}
|
||||
{downButton}
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{isADiffInThisFile ?
|
||||
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
|
||||
: streamState === 'streaming' ?
|
||||
'No changes yet'
|
||||
: `No changes`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto">
|
||||
|
||||
|
||||
{/* Accept All / Reject All buttons that appear when the vertical ellipsis is clicked */}
|
||||
{showAcceptRejectAllButtons && showAcceptRejectAll && (
|
||||
<div className="flex justify-end mb-1">
|
||||
<div className="inline-flex bg-void-bg-2 rounded shadow-md border border-void-border-2 overflow-hidden">
|
||||
<div className="flex items-center [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
|
||||
<AcceptAllButtonWrapper
|
||||
// text={`Accept All${acceptAllKeybindLabel ? ` ${acceptAllKeybindLabel}` : ''}`}
|
||||
text={`Accept All`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={acceptAllKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onAcceptAll}
|
||||
/>
|
||||
<RejectAllButtonWrapper
|
||||
// text={`Reject All${rejectAllKeybindLabel ? ` ${rejectAllKeybindLabel}` : ''}`}
|
||||
text={`Reject All`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={rejectAllKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onRejectAll}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:px-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
|
||||
|
||||
{/* Diff Navigation Group */}
|
||||
<div className="flex items-center py-0.5">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => commandBarService.goToDiffIdx(prevDiffIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToDiffIdx(prevDiffIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${upKeybindLabel ? `${upKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveUp className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
<span className={`text-xs whitespace-nowrap px-1 ${!isADiffInThisFile ? 'opacity-70' : ''}`}>
|
||||
{isADiffInThisFile
|
||||
? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
|
||||
: streamState === 'streaming'
|
||||
? 'No changes yet'
|
||||
: 'No changes'
|
||||
}
|
||||
|
||||
</span>
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => commandBarService.goToDiffIdx(nextDiffIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${downKeybindLabel ? `${downKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveDown className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* File Navigation Group */}
|
||||
<div className="flex items-center py-0.5">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => commandBarService.goToURIIdx(prevURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToURIIdx(prevURIIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveLeft className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
<span className="text-xs whitespace-nowrap px-1 mx-0.5">
|
||||
{currFileIdx !== null
|
||||
? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
|
||||
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}`
|
||||
}
|
||||
</span>
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToURIIdx(nextURIIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveRight className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Accept/Reject buttons - only shown when appropriate */}
|
||||
{showAcceptRejectAll && (
|
||||
<div className='flex self-stretch gap-0 !px-0 !py-0'>
|
||||
<AcceptAllButtonWrapper
|
||||
// text={`Accept File${acceptFileKeybindLabel ? ` ${acceptFileKeybindLabel}` : ''}`}
|
||||
text={`Accept File`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={acceptFileKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onAcceptFile}
|
||||
/>
|
||||
<RejectAllButtonWrapper
|
||||
// text={`Reject File${rejectFileKeybindLabel ? ` ${rejectFileKeybindLabel}` : ''}`}
|
||||
text={`Reject File`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={rejectFileKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onRejectFile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Triple colon menu button */}
|
||||
{showAcceptRejectAll && <div className='!px-0 !py-0 self-stretch flex justify-center items-center'>
|
||||
<div
|
||||
className="cursor-pointer px-1 self-stretch flex justify-center items-center"
|
||||
onClick={() => setShowAcceptRejectAllButtons(!showAcceptRejectAllButtons)}
|
||||
>
|
||||
<EllipsisVertical
|
||||
className="size-3"
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
{/* Files */}
|
||||
<div className={`${!isADiffZoneInAnyFile ? 'hidden' : ''} flex items-center ${leftRightDisabled ? 'opacity-50' : ''}`}>
|
||||
{leftButton}
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
{rightButton}
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{currFileIdx !== null ?
|
||||
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
|
||||
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
|
||||
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
|
||||
{showAcceptRejectAll && acceptRejectAllButtons}
|
||||
{leftRightUpDownButtons}
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+459
-192
@@ -6,9 +6,10 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
|
||||
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
import { isLinux } from '../../../../../../../base/common/platform.js';
|
||||
@@ -26,10 +27,9 @@ export const VoidOnboarding = () => {
|
||||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div
|
||||
className={`
|
||||
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full z-[99999]
|
||||
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
|
||||
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
|
||||
`}
|
||||
style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<VoidOnboardingContent />
|
||||
@@ -90,188 +90,6 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch
|
||||
}
|
||||
|
||||
// Onboarding
|
||||
|
||||
// =============================================
|
||||
// New AddProvidersPage Component and helpers
|
||||
// =============================================
|
||||
|
||||
const tabNames = ['Free', 'Paid', 'Local'] as const;
|
||||
|
||||
type TabName = typeof tabNames[number] | 'Cloud/Other';
|
||||
|
||||
// Data for cloud providers tab
|
||||
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'awsBedrock', 'openAICompatible'];
|
||||
|
||||
// Data structures for provider tabs
|
||||
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
|
||||
Free: ['gemini', 'openRouter'],
|
||||
Local: localProviderNames,
|
||||
Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[],
|
||||
'Cloud/Other': cloudProviders,
|
||||
};
|
||||
|
||||
const descriptionOfTab: Record<TabName, string> = {
|
||||
Free: `Providers with a 100% free tier. Add as many as you'd like!`,
|
||||
Paid: `Connect directly with any provider (bring your own key).`,
|
||||
Local: `Active providers should appear automatically. Add as many as you'd like! `,
|
||||
'Cloud/Other': `Add as many as you'd like! Reach out for custom configuration requests.`,
|
||||
};
|
||||
|
||||
|
||||
const featureNameMap: { display: string, featureName: FeatureName }[] = [
|
||||
{ display: 'Chat', featureName: 'Chat' },
|
||||
{ display: 'Quick Edit', featureName: 'Ctrl+K' },
|
||||
{ display: 'Autocomplete', featureName: 'Autocomplete' },
|
||||
{ display: 'Fast Apply', featureName: 'Apply' },
|
||||
{ display: 'Source Control', featureName: 'SCM' },
|
||||
];
|
||||
|
||||
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {
|
||||
const [currentTab, setCurrentTab] = useState<TabName>('Free');
|
||||
const settingsState = useSettingsState();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Clear error message after 5 seconds
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (errorMessage) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setErrorMessage(null);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Cleanup function to clear the timeout if component unmounts or error changes
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [errorMessage]);
|
||||
|
||||
return (<div className="flex flex-col md:flex-row w-full h-[80vh] gap-6 max-w-[900px] mx-auto relative">
|
||||
{/* Left Column */}
|
||||
<div className="md:w-1/4 w-full flex flex-col gap-6 p-6 border-none border-void-border-2 h-full overflow-y-auto">
|
||||
{/* Tab Selector */}
|
||||
<div className="flex md:flex-col gap-2">
|
||||
{[...tabNames, 'Cloud/Other'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`py-2 px-4 rounded-md text-left ${currentTab === tab
|
||||
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
|
||||
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'
|
||||
} transition-all duration-200`}
|
||||
onClick={() => {
|
||||
setCurrentTab(tab as TabName);
|
||||
setErrorMessage(null); // Reset error message when changing tabs
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Feature Checklist */}
|
||||
<div className="flex flex-col gap-1 mt-4 text-sm opacity-80">
|
||||
{featureNameMap.map(({ display, featureName }) => {
|
||||
const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null;
|
||||
return (
|
||||
<div key={featureName} className="flex items-center gap-2">
|
||||
{hasModel ? (
|
||||
<Check className="w-4 h-4 text-emerald-500" />
|
||||
) : (
|
||||
<div className="w-3 h-3 rounded-full flex items-center justify-center">
|
||||
<div className="w-1 h-1 rounded-full bg-white/70"></div>
|
||||
</div>
|
||||
)}
|
||||
<span>{display}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="flex-1 flex flex-col items-center justify-start p-6 h-full overflow-y-auto">
|
||||
<div className="text-5xl mb-2 text-center w-full">Add a Provider</div>
|
||||
|
||||
<div className="w-full max-w-xl mt-4 mb-10">
|
||||
<div className="text-4xl font-light my-4 w-full">{currentTab}</div>
|
||||
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">{descriptionOfTab[currentTab]}</div>
|
||||
</div>
|
||||
|
||||
{providerNamesOfTab[currentTab].map((providerName) => (
|
||||
<div key={providerName} className="w-full max-w-xl mb-10">
|
||||
<div className="text-xl mb-2">
|
||||
Add {displayInfoOfProviderName(providerName).title}
|
||||
{providerName === 'gemini' && (
|
||||
<span
|
||||
data-tooltip-id="void-tooltip-provider-info"
|
||||
data-tooltip-content="Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500. We recommend using models down the line as you run out of free credits."
|
||||
data-tooltip-place="right"
|
||||
className="ml-1 text-xs align-top text-blue-400"
|
||||
>*</span>
|
||||
)}
|
||||
{providerName === 'openRouter' && (
|
||||
<span
|
||||
data-tooltip-id="void-tooltip-provider-info"
|
||||
data-tooltip-content="OpenRouter offers 50 free messages a day, and 1000 if you deposit $10. Only applies to models labeled ':free'."
|
||||
data-tooltip-place="right"
|
||||
className="ml-1 text-xs align-top text-blue-400"
|
||||
>*</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<SettingsForProvider providerName={providerName} showProviderTitle={false} showProviderSuggestions={true} />
|
||||
|
||||
</div>
|
||||
{providerName === 'ollama' && <OllamaSetupInstructions />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(currentTab === 'Local' || currentTab === 'Cloud/Other') && (
|
||||
<div className="w-full max-w-xl mt-8 bg-void-bg-2/50 rounded-lg p-6 border border-void-border-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="text-xl font-medium">Models</div>
|
||||
</div>
|
||||
|
||||
{currentTab === 'Local' && (
|
||||
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">Local models should be detected automatically. You can add custom models below.</div>
|
||||
)}
|
||||
|
||||
{currentTab === 'Local' && <ModelDump filteredProviders={localProviderNames} />}
|
||||
{currentTab === 'Cloud/Other' && <ModelDump filteredProviders={cloudProviders} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* Navigation buttons in right column */}
|
||||
<div className="flex flex-col items-end w-full mt-auto pt-8">
|
||||
{errorMessage && (
|
||||
<div className="text-amber-400 mb-2 text-sm opacity-80 transition-opacity duration-300">{errorMessage}</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<PreviousButton onClick={() => setPageIndex(pageIndex - 1)} />
|
||||
<NextButton
|
||||
onClick={() => {
|
||||
const isDisabled = isFeatureNameDisabled('Chat', settingsState)
|
||||
|
||||
if (!isDisabled) {
|
||||
setPageIndex(pageIndex + 1);
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
// Show error message
|
||||
setErrorMessage("Please set up at least one Chat model before moving on.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
// =============================================
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
@@ -361,7 +179,7 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
|
||||
className?: string,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`h-[80vh] text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
|
||||
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
|
||||
{bottom && <div className='w-full pb-8'>{bottom}</div>}
|
||||
@@ -370,6 +188,8 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
|
||||
}
|
||||
|
||||
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
|
||||
|
||||
|
||||
// for now just link to the ollama download page
|
||||
return <a
|
||||
href={`https://ollama.com/library/${modelName}`}
|
||||
@@ -380,6 +200,76 @@ const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
|
||||
// if (isModelInstalled) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Uninstall</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Trash}
|
||||
// onClick={() => {
|
||||
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// else if (isModelInstalling) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Square}
|
||||
// onClick={() => {
|
||||
// // abort()
|
||||
|
||||
// // TODO!!!!!!!!!!! don't do this
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
// else if (!isModelInstalled) {
|
||||
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Download}
|
||||
// onClick={() => {
|
||||
// // this is a check for whether the model was installed:
|
||||
|
||||
// if (isModelInstalling) return
|
||||
|
||||
|
||||
// // TODO!!!!!! don't do this
|
||||
|
||||
|
||||
// // install(modelname), callback = setIsModelInstalling(false);
|
||||
|
||||
// setIsModelInstalling(true);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
|
||||
// }
|
||||
|
||||
// return <></>
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -416,7 +306,114 @@ const abbreviateNumber = (num: number): string => {
|
||||
}
|
||||
}
|
||||
|
||||
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
|
||||
// const providerCapabilities = getProviderCapabilities(providerName)
|
||||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.type !== 'custom',
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
||||
// special case columns for ollama; show recommended models as default
|
||||
if (providerName === 'ollama') {
|
||||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
|
||||
<thead>
|
||||
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
|
||||
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
|
||||
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
|
||||
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
|
||||
|
||||
|
||||
const capabilities = getModelCapabilities(providerName, modelName)
|
||||
const {
|
||||
downloadable,
|
||||
cost,
|
||||
supportsFIM,
|
||||
reasoningCapabilities,
|
||||
contextWindow,
|
||||
|
||||
isUnrecognizedModel,
|
||||
maxOutputTokens,
|
||||
supportsSystemMessage,
|
||||
} = capabilities
|
||||
|
||||
// TODO update this when tools work
|
||||
|
||||
const removeModelButton = <button
|
||||
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
|
||||
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 relative">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
</td>
|
||||
<td className="py-2 px-3">${cost.output ?? ''}</td>
|
||||
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
|
||||
<td className="py-2 px-3"><YesNoText val={true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
|
||||
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
|
||||
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
|
||||
{providerName === 'ollama' && <th className="py-2 px-3">
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<ErrorBoundary>
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -434,16 +431,22 @@ const PrimaryActionButton = ({ children, className, ringSize, ...props }: { chil
|
||||
|
||||
${ringSize === 'xl' ? `
|
||||
gap-2 px-16 py-8
|
||||
hover:ring-8 active:ring-8
|
||||
transition-all duration-300 ease-in-out
|
||||
`
|
||||
: ringSize === 'screen' ? `
|
||||
gap-2 px-16 py-8
|
||||
ring-[3000px]
|
||||
transition-all duration-1000 ease-in-out
|
||||
`: ringSize === undefined ? `
|
||||
gap-1 px-4 py-2
|
||||
hover:ring-2 active:ring-2
|
||||
transition-all duration-300 ease-in-out
|
||||
`: ''}
|
||||
|
||||
hover:ring-black/90 dark:hover:ring-white/90
|
||||
active:ring-black/90 dark:active:ring-white/90
|
||||
|
||||
rounded-lg
|
||||
group
|
||||
${className}
|
||||
@@ -531,6 +534,7 @@ const VoidOnboardingContent = () => {
|
||||
/>
|
||||
<NextButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -608,7 +612,7 @@ const VoidOnboardingContent = () => {
|
||||
delayMs={1000}
|
||||
>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { setPageIndex(1) }}
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
>
|
||||
Get Started
|
||||
</PrimaryActionButton>
|
||||
@@ -617,13 +621,255 @@ const VoidOnboardingContent = () => {
|
||||
</div>
|
||||
}
|
||||
/>,
|
||||
1: <OnboardingPageShell
|
||||
|
||||
1: <OnboardingPageShell hasMaxWidth={false}
|
||||
content={
|
||||
<AddProvidersPage pageIndex={pageIndex} setPageIndex={setPageIndex} />
|
||||
hasMaxWidth={false}
|
||||
top={<></>}
|
||||
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
|
||||
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
|
||||
|
||||
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Lock size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Private</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>}
|
||||
bottom={
|
||||
<div className='mx-auto w-full max-w-[800px]'>
|
||||
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
|
||||
</div>
|
||||
}
|
||||
/>,
|
||||
2: <OnboardingPageShell
|
||||
top={
|
||||
<>
|
||||
{/* Title */}
|
||||
|
||||
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
|
||||
|
||||
{/* Preference Selector */}
|
||||
|
||||
<div
|
||||
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
|
||||
>
|
||||
{[
|
||||
{ id: 'smart', label: 'Intelligent' },
|
||||
{ id: 'private', label: 'Private' },
|
||||
{ id: 'cheap', label: 'Affordable' },
|
||||
{ id: 'all', label: 'All' }
|
||||
].map(option => (
|
||||
<ErrorBoundary
|
||||
key={option.id}
|
||||
>
|
||||
<button
|
||||
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
|
||||
? 'dark:text-white text-black font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={`${option.label} providers`}
|
||||
data-tooltip-place='bottom'
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Provider Buttons - Modified to use separate components for each tab */}
|
||||
<div className="mb-2 w-full">
|
||||
{/* Intelligent tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
|
||||
const isSelected = selectedIntelligentProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedIntelligentProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Private tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['private'].map((providerName) => {
|
||||
const isSelected = selectedPrivateProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedPrivateProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Affordable tab */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
|
||||
const isSelected = selectedAffordableProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAffordableProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* All tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
|
||||
{providerNames.map((providerName) => {
|
||||
const isSelected = selectedAllProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAllProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<ErrorBoundary>
|
||||
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* ModelsTable and ProviderFields */}
|
||||
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
|
||||
{/* Models Table */}
|
||||
<ErrorBoundary>
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
|
||||
<div className='mb-5 mt-8 mx-auto'>
|
||||
<ErrorBoundary>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
<div className='my-4'>
|
||||
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary>
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Button and status indicators */}
|
||||
<ErrorBoundary>
|
||||
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
|
||||
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
|
||||
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
|
||||
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
</>
|
||||
}
|
||||
|
||||
bottom={
|
||||
<ErrorBoundary>
|
||||
<FadeIn delayMs={50} durationMs={10}>
|
||||
{prevAndNextButtons}
|
||||
</FadeIn>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
/>,
|
||||
|
||||
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
// <FadeIn>
|
||||
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
|
||||
|
||||
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
|
||||
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
|
||||
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
|
||||
|
||||
// </div>
|
||||
// </FadeIn>
|
||||
|
||||
// {prevAndNextButtons}
|
||||
// </div>,
|
||||
3: <OnboardingPageShell
|
||||
|
||||
content={
|
||||
<div>
|
||||
@@ -638,11 +884,32 @@ const VoidOnboardingContent = () => {
|
||||
</div>
|
||||
}
|
||||
bottom={lastPagePrevAndNextButtons}
|
||||
// bottom={prevAndNextButtons}
|
||||
/>,
|
||||
// 4: <OnboardingPageShell
|
||||
// content={
|
||||
// <>
|
||||
// <div
|
||||
// className='flex justify-center'
|
||||
// >
|
||||
// <PrimaryActionButton
|
||||
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
// className='text-4xl'
|
||||
// >Enter the Void</PrimaryActionButton>
|
||||
// </div>
|
||||
// </>
|
||||
// }
|
||||
// bottom={
|
||||
// <PreviousButton
|
||||
// onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
// />
|
||||
// }
|
||||
// />,
|
||||
}
|
||||
|
||||
|
||||
return <div key={pageIndex} className="w-full h-[80vh] text-left mx-auto flex flex-col items-center justify-center">
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
|
||||
<ErrorBoundary>
|
||||
{contentOfIdx[pageIndex]}
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -56,7 +56,7 @@ const MemoizedModelDropdown = ({ featureName, className }: { featureName: Featur
|
||||
|
||||
useEffect(() => {
|
||||
const oldOptions = oldOptionsRef.current
|
||||
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode, overridesOfModel: settingsState.overridesOfModel }))
|
||||
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode }))
|
||||
|
||||
if (!optionsEqual(oldOptions, newOptions)) {
|
||||
setMemoizedOptions(newOptions)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,13 +45,11 @@ export const VoidTooltip = () => {
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings, #void-tooltip-provider-info {
|
||||
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings {
|
||||
font-size: 12px;
|
||||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 999999;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#void-tooltip {
|
||||
@@ -69,7 +67,7 @@ export const VoidTooltip = () => {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#void-tooltip-ollama-settings, #void-tooltip-provider-info {
|
||||
#void-tooltip-ollama-settings {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
}
|
||||
@@ -114,25 +112,14 @@ export const VoidTooltip = () => {
|
||||
</div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>gemma3</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<div>
|
||||
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
<span style={{ opacity: 0.8 }}>Use the largest version of these you can!</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
id="void-tooltip-provider-info"
|
||||
border='1px solid rgba(100,100,100,.2)'
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
style={{ pointerEvents: 'all', userSelect: 'text', fontSize: 11, maxWidth: '280px', paddingTop:'8px', paddingBottom:'8px' }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,18 +14,23 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
|
||||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
|
||||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
|
||||
import { IChatThreadService } from './chatThreadService.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
return null
|
||||
@@ -67,21 +72,68 @@ registerAction2(class extends Action2 {
|
||||
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
const chatThreadsService = accessor.get(IChatThreadService)
|
||||
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID)
|
||||
await chatThreadsService.focusCurrentChat()
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// cmd L
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
|
||||
if (!model)
|
||||
return
|
||||
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
|
||||
metricsService.capture('Ctrl+L', {})
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
|
||||
// select whole lines
|
||||
if (selectionRange) {
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
}
|
||||
|
||||
|
||||
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : {
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
}
|
||||
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCmdL', 'Void: Add Selection to Chat'),
|
||||
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
@@ -89,119 +141,72 @@ registerAction2(class extends Action2 {
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
// Get services
|
||||
const commandService = accessor.get(ICommandService)
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
|
||||
metricsService.capture('Ctrl+L', {})
|
||||
|
||||
// capture selection and model before opening the chat panel
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const model = editor?.getModel()
|
||||
if (!model) return
|
||||
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
// open panel
|
||||
const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
|
||||
if (!wasAlreadyOpen) {
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
|
||||
// Add selection to chat
|
||||
// add line selection
|
||||
if (selectionRange) {
|
||||
editor?.setSelection({
|
||||
startLineNumber: selectionRange.startLineNumber,
|
||||
endLineNumber: selectionRange.endLineNumber,
|
||||
startColumn: 1,
|
||||
endColumn: Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
chatThreadService.addNewStagingSelection({
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
}
|
||||
// add file
|
||||
else {
|
||||
chatThreadService.addNewStagingSelection({
|
||||
type: 'File',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
}
|
||||
|
||||
await chatThreadService.focusCurrentChat()
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// New chat keybind + menu button
|
||||
const VOID_CMD_SHIFT_L_ACTION_ID = 'void.cmdShiftL'
|
||||
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
|
||||
// focus
|
||||
stateService.fireFocusChat()
|
||||
const window = getActiveWindow()
|
||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||
|
||||
}
|
||||
|
||||
|
||||
// New chat menu button
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_CMD_SHIFT_L_ACTION_ID,
|
||||
id: 'void.newChatAction',
|
||||
title: 'New Chat',
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
},
|
||||
icon: { id: 'add' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
|
||||
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const chatThreadsService = accessor.get(IChatThreadService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
metricsService.capture('Chat Navigation', { type: 'Start New Chat' })
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||
|
||||
// get current selections and value to transfer
|
||||
const oldThreadId = chatThreadsService.state.currentThreadId
|
||||
const oldThread = chatThreadsService.state.allThreads[oldThreadId]
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
|
||||
}
|
||||
})
|
||||
|
||||
const oldSelns = oldThread?.state.stagingSelections
|
||||
const oldVal = oldUI?.textAreaRef?.current?.value
|
||||
// New chat keybind
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.newChatKeybindAction',
|
||||
title: 'New Chat Keybind',
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
},
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
// open and focus new thread
|
||||
chatThreadsService.openNewThread()
|
||||
await chatThreadsService.focusCurrentChat()
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const commandService = accessor.get(ICommandService)
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
// set new thread values
|
||||
const newThreadId = chatThreadsService.state.currentThreadId
|
||||
const newThread = chatThreadsService.state.allThreads[newThreadId]
|
||||
// add user's selection to chat
|
||||
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
|
||||
const newUI = await newThread?.state.mountedInfo?.whenMounted
|
||||
chatThreadsService.setCurrentThreadState({ stagingSelections: oldSelns, })
|
||||
if (newUI?.textAreaRef?.current && oldVal) newUI.textAreaRef.current.value = oldVal
|
||||
|
||||
|
||||
// if has selection, add it
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const model = editor?.getModel()
|
||||
if (!model) return
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
if (!selectionRange) return
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
chatThreadsService.addNewStagingSelection({
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -224,12 +229,18 @@ registerAction2(class extends Action2 {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
const commandService = accessor.get(ICommandService)
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'History' })
|
||||
commandService.executeCommand(VOID_CMD_SHIFT_L_ACTION_ID)
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
// doesnt do anything right now
|
||||
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
||||
stateService.fireBlurChat()
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
@@ -254,28 +265,28 @@ registerAction2(class extends Action2 {
|
||||
|
||||
|
||||
|
||||
// export class TabSwitchListener extends Disposable {
|
||||
export class TabSwitchListener extends Disposable {
|
||||
|
||||
// constructor(
|
||||
// onSwitchTab: () => void,
|
||||
// @ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
// ) {
|
||||
// super()
|
||||
constructor(
|
||||
onSwitchTab: () => void,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// // when editor switches tabs (models)
|
||||
// const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
// this._register(editor.onDidChangeModel(e => {
|
||||
// if (e.newModelUrl?.scheme !== 'file') return
|
||||
// onSwitchTab()
|
||||
// }))
|
||||
// }
|
||||
// when editor switches tabs (models)
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidChangeModel(e => {
|
||||
if (e.newModelUrl?.scheme !== 'file') return
|
||||
onSwitchTab()
|
||||
}))
|
||||
}
|
||||
|
||||
// const initializeEditor = (editor: ICodeEditor) => {
|
||||
// addTabSwitchListeners(editor)
|
||||
// }
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
addTabSwitchListeners(editor)
|
||||
}
|
||||
|
||||
// // initialize current editors + any new editors
|
||||
// for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
// this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
// }
|
||||
// }
|
||||
// initialize current editors + any new editors
|
||||
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
|
||||
|
||||
|
||||
// service that manages sidebar's state
|
||||
export type VoidSidebarState = {
|
||||
isHistoryOpen: boolean; // this isn't doing anything right now
|
||||
currentTab: 'chat';
|
||||
}
|
||||
|
||||
export interface ISidebarStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidSidebarState; // readonly to the user
|
||||
setState(newState: Partial<VoidSidebarState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
onDidFocusChat: Event<void>;
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
}
|
||||
|
||||
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
|
||||
class VoidSidebarStateService extends Disposable implements ISidebarStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidSidebarStateService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onFocusChat = new Emitter<void>();
|
||||
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
|
||||
|
||||
private readonly _onBlurChat = new Emitter<void>();
|
||||
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidSidebarState
|
||||
|
||||
constructor(
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
this.state = { isHistoryOpen: false, currentTab: 'chat', }
|
||||
}
|
||||
|
||||
|
||||
setState(newState: Partial<VoidSidebarState>) {
|
||||
// make sure view is open if the tab changes
|
||||
if ('currentTab' in newState) {
|
||||
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
|
||||
import { ITerminalCapabilityImplMap, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
@@ -14,7 +13,6 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
|
||||
import { ITerminalService, ITerminalInstance, ICreateTerminalOptions } from '../../../../workbench/contrib/terminal/browser/terminal.js';
|
||||
import { MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
|
||||
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
|
||||
import { timeout } from '../../../../base/common/async.js';
|
||||
|
||||
|
||||
|
||||
@@ -22,34 +20,28 @@ export interface ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
listPersistentTerminalIds(): string[];
|
||||
runCommand(command: string, opts:
|
||||
| { type: 'persistent', persistentTerminalId: string }
|
||||
| { type: 'temporary', cwd: string | null, terminalId: string }
|
||||
// | { type: 'apply', terminalId: string }
|
||||
): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
|
||||
|
||||
runCommand(command: string, opts: { type: 'persistent', persistentTerminalId: string } | { type: 'ephemeral', cwd: string | null, terminalId: string }): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
|
||||
focusPersistentTerminal(terminalId: string): Promise<void>
|
||||
persistentTerminalExists(terminalId: string): boolean
|
||||
|
||||
readTerminal(terminalId: string): Promise<string>
|
||||
|
||||
createPersistentTerminal(opts: { cwd: string | null }): Promise<string>
|
||||
killPersistentTerminal(terminalId: string): Promise<void>
|
||||
|
||||
getPersistentTerminal(terminalId: string): ITerminalInstance | undefined
|
||||
getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined
|
||||
}
|
||||
|
||||
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
|
||||
|
||||
|
||||
|
||||
// function isCommandComplete(output: string) {
|
||||
// // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||
// const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
|
||||
// if (!completionMatch) { return false }
|
||||
// if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
|
||||
// return { exitCode: 0 }
|
||||
// }
|
||||
function isCommandComplete(output: string) {
|
||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||
const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
|
||||
if (!completionMatch) { return false }
|
||||
if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
|
||||
return { exitCode: 0 }
|
||||
}
|
||||
|
||||
|
||||
export const persistentTerminalNameOfId = (id: string) => {
|
||||
@@ -122,41 +114,32 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
}
|
||||
|
||||
|
||||
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'], hidden?: boolean }) {
|
||||
const { cwd: override_cwd, config, hidden } = props;
|
||||
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'] }) {
|
||||
const { cwd: override_cwd, config } = props
|
||||
|
||||
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
|
||||
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri
|
||||
|
||||
const options: ICreateTerminalOptions = {
|
||||
cwd,
|
||||
location: hidden ? undefined : TerminalLocation.Panel,
|
||||
config: {
|
||||
name: config && 'name' in config ? config.name : undefined,
|
||||
forceShellIntegration: true,
|
||||
hideFromUser: hidden ? true : undefined,
|
||||
// Copy any other properties from the provided config
|
||||
...config,
|
||||
},
|
||||
// Skip profile check to ensure the terminal is created quickly
|
||||
skipContributedProfileCheck: true,
|
||||
};
|
||||
// create new terminal and return its ID
|
||||
const terminal = await this.terminalService.createTerminal({
|
||||
cwd: cwd,
|
||||
location: TerminalLocation.Panel,
|
||||
config: config,
|
||||
})
|
||||
|
||||
const terminal = await this.terminalService.createTerminal(options)
|
||||
// when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
|
||||
const disposables: IDisposable[] = []
|
||||
const waitForMount = new Promise<void>(res => {
|
||||
let data = ''
|
||||
const d = terminal.onData(newData => {
|
||||
data += newData
|
||||
if (isCommandComplete(data)) { res() }
|
||||
})
|
||||
disposables.push(d)
|
||||
})
|
||||
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
|
||||
|
||||
// // when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
|
||||
// const disposables: IDisposable[] = []
|
||||
// const waitForMount = new Promise<void>(res => {
|
||||
// let data = ''
|
||||
// const d = terminal.onData(newData => {
|
||||
// data += newData
|
||||
// if (isCommandComplete(data)) { res() }
|
||||
// })
|
||||
// disposables.push(d)
|
||||
// })
|
||||
// const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
|
||||
|
||||
// await Promise.any([waitForMount, waitForTimeout,])
|
||||
// disposables.forEach(d => d.dispose())
|
||||
await Promise.any([waitForMount, waitForTimeout,])
|
||||
disposables.forEach(d => d.dispose())
|
||||
|
||||
return terminal
|
||||
|
||||
@@ -209,64 +192,15 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
|
||||
|
||||
|
||||
readTerminal: ITerminalToolService['readTerminal'] = async (terminalId) => {
|
||||
// Try persistent first, then temporary
|
||||
const terminal = this.getPersistentTerminal(terminalId) ?? this.getTemporaryTerminal(terminalId);
|
||||
if (!terminal) {
|
||||
throw new Error(`Read Terminal: Terminal with ID ${terminalId} does not exist.`);
|
||||
}
|
||||
|
||||
// Ensure the xterm.js instance has been created – otherwise we cannot access the buffer.
|
||||
if (!terminal.xterm) {
|
||||
throw new Error('Read Terminal: The requested terminal has not yet been rendered and therefore has no scrollback buffer available.');
|
||||
}
|
||||
|
||||
// Collect lines from the buffer iterator (oldest to newest)
|
||||
const lines: string[] = [];
|
||||
for (const line of terminal.xterm.getBufferReverseIterator()) {
|
||||
lines.unshift(line);
|
||||
}
|
||||
|
||||
let result = removeAnsiEscapeCodes(lines.join('\n'));
|
||||
|
||||
if (result.length > MAX_TERMINAL_CHARS) {
|
||||
const half = MAX_TERMINAL_CHARS / 2;
|
||||
result = result.slice(0, half) + '\n...\n' + result.slice(result.length - half);
|
||||
}
|
||||
|
||||
return result
|
||||
};
|
||||
|
||||
private async _waitForCommandDetectionCapability(terminal: ITerminalInstance) {
|
||||
const cmdCap = terminal.capabilities.get(TerminalCapability.CommandDetection);
|
||||
if (cmdCap) return cmdCap
|
||||
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
const waitTimeout = timeout(10_000)
|
||||
const waitForCapability = new Promise<ITerminalCapabilityImplMap[TerminalCapability.CommandDetection]>((res) => {
|
||||
disposables.push(
|
||||
terminal.capabilities.onDidAddCapability((e) => {
|
||||
if (e.id === TerminalCapability.CommandDetection) res(e.capability)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const capability = await Promise.any([waitTimeout, waitForCapability])
|
||||
.finally(() => { disposables.forEach((d) => d.dispose()) })
|
||||
|
||||
return capability ?? undefined
|
||||
}
|
||||
|
||||
runCommand: ITerminalToolService['runCommand'] = async (command, params) => {
|
||||
await this.terminalService.whenConnected;
|
||||
|
||||
const { type } = params
|
||||
const isPersistent = type === 'persistent'
|
||||
await this.terminalService.whenConnected;
|
||||
|
||||
let terminal: ITerminalInstance
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
const isPersistent = type === 'persistent'
|
||||
|
||||
if (isPersistent) { // BG process
|
||||
const { persistentTerminalId } = params
|
||||
terminal = this.persistentTerminalInstanceOfId[persistentTerminalId];
|
||||
@@ -274,7 +208,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
}
|
||||
else {
|
||||
const { cwd } = params
|
||||
terminal = await this._createTerminal({ cwd: cwd, config: undefined, hidden: true })
|
||||
terminal = await this._createTerminal({ cwd: cwd, config: { name: 'Void Temporary Terminal', title: 'Void Temporary Terminal' } })
|
||||
this.temporaryTerminalInstanceOfId[params.terminalId] = terminal
|
||||
}
|
||||
|
||||
@@ -282,8 +216,6 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
terminal.dispose()
|
||||
if (!isPersistent)
|
||||
delete this.temporaryTerminalInstanceOfId[params.terminalId]
|
||||
else
|
||||
delete this.persistentTerminalInstanceOfId[params.persistentTerminalId]
|
||||
}
|
||||
|
||||
const waitForResult = async () => {
|
||||
@@ -292,29 +224,29 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
this.terminalService.setActiveInstance(terminal)
|
||||
await this.terminalService.focusActiveInstance()
|
||||
}
|
||||
|
||||
let result: string = ''
|
||||
let resolveReason: TerminalResolveReason | undefined
|
||||
let resolveReason: TerminalResolveReason | undefined = undefined
|
||||
|
||||
|
||||
const cmdCap = await this._waitForCommandDetectionCapability(terminal)
|
||||
// if (!cmdCap) throw new Error(`There was an error using the terminal: CommandDetection capability did not mount yet. Please try again in a few seconds or report this to the Void team.`)
|
||||
|
||||
// Prefer the structured command-detection capability when available
|
||||
|
||||
const waitUntilDone = new Promise<void>(resolve => {
|
||||
if (!cmdCap) return
|
||||
const l = cmdCap.onCommandFinished(cmd => {
|
||||
if (resolveReason) return // already resolved
|
||||
resolveReason = { type: 'done', exitCode: cmd.exitCode ?? 0 };
|
||||
result = cmd.getOutput() ?? ''
|
||||
l.dispose()
|
||||
resolve()
|
||||
// create this before we send so that we don't miss events on terminal
|
||||
const waitUntilDone = new Promise<void>((res, rej) => {
|
||||
const d2 = terminal.onData(async newData => {
|
||||
if (resolveReason) return
|
||||
result += newData
|
||||
// onDone
|
||||
const isDone = isCommandComplete(result)
|
||||
if (isDone) {
|
||||
resolveReason = { type: 'done', exitCode: isDone.exitCode }
|
||||
res()
|
||||
return
|
||||
}
|
||||
})
|
||||
disposables.push(l)
|
||||
disposables.push(d2)
|
||||
})
|
||||
|
||||
|
||||
// send the command now that listeners are attached
|
||||
// send the command here
|
||||
await terminal.sendText(command, true)
|
||||
|
||||
const waitUntilInterrupt = isPersistent ?
|
||||
@@ -345,25 +277,18 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
|
||||
// wait for result
|
||||
await Promise.any([waitUntilDone, waitUntilInterrupt])
|
||||
.finally(() => disposables.forEach(d => d.dispose()))
|
||||
|
||||
|
||||
|
||||
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
|
||||
if (resolveReason?.type === 'timeout') {
|
||||
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
|
||||
result = await this.readTerminal(terminalId)
|
||||
}
|
||||
|
||||
disposables.forEach(d => d.dispose())
|
||||
if (!isPersistent) {
|
||||
interrupt()
|
||||
}
|
||||
|
||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||
|
||||
if (!isPersistent) result = `$ ${command}\n${result}`
|
||||
result = removeAnsiEscapeCodes(result)
|
||||
// trim
|
||||
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
|
||||
.join('\n')
|
||||
|
||||
if (result.length > MAX_TERMINAL_CHARS) {
|
||||
const half = MAX_TERMINAL_CHARS / 2
|
||||
result = result.slice(0, half)
|
||||
|
||||
@@ -8,23 +8,28 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../services/search/common/search.js'
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js'
|
||||
import { ITerminalToolService } from './terminalToolService.js'
|
||||
import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType, BuiltinToolName } from '../common/toolsServiceTypes.js'
|
||||
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from '../common/directoryStrService.js'
|
||||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
|
||||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
||||
import { timeout } from '../../../../base/common/async.js'
|
||||
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
import { generateUuid } from '../../../../base/common/uuid.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
type ValidateBuiltinParams = { [T in BuiltinToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] }
|
||||
type CallBuiltinTool = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise<BuiltinToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type BuiltinToolResultToString = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T], result: Awaited<BuiltinToolResultType[T]>) => string }
|
||||
|
||||
|
||||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
@@ -39,32 +44,12 @@ const validateStr = (argName: string, value: unknown) => {
|
||||
|
||||
|
||||
// We are NOT checking to make sure in workspace
|
||||
// TODO!!!! check to make sure folder/file exists
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`)
|
||||
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Full value: ${JSON.stringify(uriStr)}.`)
|
||||
|
||||
// Check if it's already a full URI with scheme (e.g., vscode-remote://, file://, etc.)
|
||||
// Look for :// pattern which indicates a scheme is present
|
||||
// Examples of supported URIs:
|
||||
// - vscode-remote://wsl+Ubuntu/home/user/file.txt (WSL)
|
||||
// - vscode-remote://ssh-remote+myserver/home/user/file.txt (SSH)
|
||||
// - file:///home/user/file.txt (local file with scheme)
|
||||
// - /home/user/file.txt (local file path, will be converted to file://)
|
||||
// - C:\Users\file.txt (Windows local path, will be converted to file://)
|
||||
if (uriStr.includes('://')) {
|
||||
try {
|
||||
const uri = URI.parse(uriStr)
|
||||
return uri
|
||||
} catch (e) {
|
||||
// If parsing fails, it's a malformed URI
|
||||
throw new Error(`Invalid URI format: ${uriStr}. Error: ${e}`)
|
||||
}
|
||||
} else {
|
||||
// No scheme present, treat as file path
|
||||
// This handles regular file paths like /home/user/file.txt or C:\Users\file.txt
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
const validateOptionalURI = (uriStr: unknown) => {
|
||||
@@ -126,9 +111,9 @@ const checkIfIsFolder = (uriStr: string) => {
|
||||
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
validateParams: ValidateBuiltinParams;
|
||||
callTool: CallBuiltinTool;
|
||||
stringOfResult: BuiltinToolResultToString;
|
||||
validateParams: ValidateParams;
|
||||
callTool: CallTool;
|
||||
stringOfResult: ToolResultToString;
|
||||
}
|
||||
|
||||
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
||||
@@ -137,9 +122,9 @@ export class ToolsService implements IToolsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
public validateParams: ValidateBuiltinParams;
|
||||
public callTool: CallBuiltinTool;
|
||||
public stringOfResult: BuiltinToolResultToString;
|
||||
public validateParams: ValidateParams;
|
||||
public callTool: CallTool;
|
||||
public stringOfResult: ToolResultToString;
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@@ -154,6 +139,7 @@ export class ToolsService implements IToolsService {
|
||||
@IMarkerService private readonly markerService: IMarkerService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.validateParams = {
|
||||
@@ -309,14 +295,12 @@ export class ToolsService implements IToolsService {
|
||||
contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
const totalNumLines = model.getLineCount()
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (contents.length - 1) - toIdx >= 1
|
||||
const totalFileLen = contents.length
|
||||
return { result: { fileContents, totalFileLen, hasNextPage, totalNumLines } }
|
||||
return { result: { fileContents, totalFileLen, hasNextPage } }
|
||||
},
|
||||
|
||||
ls_dir: async ({ uri, pageNumber }) => {
|
||||
@@ -415,8 +399,7 @@ export class ToolsService implements IToolsService {
|
||||
if (this.commandBarService.getStreamState(uri) === 'streaming') {
|
||||
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
|
||||
}
|
||||
await editCodeService.callBeforeApplyOrEdit(uri)
|
||||
editCodeService.instantlyRewriteFile({ uri, newContent })
|
||||
editCodeService.instantlyApplyNewContent({ uri, newContent })
|
||||
// at end, get lint errors
|
||||
const lintErrorsPromise = Promise.resolve().then(async () => {
|
||||
await timeout(2000)
|
||||
@@ -431,7 +414,7 @@ export class ToolsService implements IToolsService {
|
||||
if (this.commandBarService.getStreamState(uri) === 'streaming') {
|
||||
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
|
||||
}
|
||||
await editCodeService.callBeforeApplyOrEdit(uri)
|
||||
console.log('aaaa', searchReplaceBlocks)
|
||||
editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks })
|
||||
|
||||
// at end, get lint errors
|
||||
@@ -445,7 +428,7 @@ export class ToolsService implements IToolsService {
|
||||
},
|
||||
// ---
|
||||
run_command: async ({ command, cwd, terminalId }) => {
|
||||
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'temporary', cwd, terminalId })
|
||||
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId })
|
||||
return { result: resPromise, interruptTool: interrupt }
|
||||
},
|
||||
run_persistent_command: async ({ command, persistentTerminalId }) => {
|
||||
@@ -461,6 +444,7 @@ export class ToolsService implements IToolsService {
|
||||
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -476,7 +460,7 @@ export class ToolsService implements IToolsService {
|
||||
// given to the LLM after the call for successful tool calls
|
||||
this.stringOfResult = {
|
||||
read_file: (params, result) => {
|
||||
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `\nMore info because truncated: this file has ${result.totalNumLines} lines, or ${result.totalFileLen} characters.` : ''}`
|
||||
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
|
||||
},
|
||||
ls_dir: (params, result) => {
|
||||
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
|
||||
@@ -538,7 +522,7 @@ export class ToolsService implements IToolsService {
|
||||
}
|
||||
// normal command
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `${result_}\nTerminal command ran, but was automatically killed by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not finish successfully. To try with more time, open a persistent terminal and run the command there.`
|
||||
return `${result_}\nTerminal command ran, but was interrupted by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. To try with more time, open a persistent terminal and run the command there.`
|
||||
}
|
||||
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
|
||||
},
|
||||
@@ -564,6 +548,7 @@ export class ToolsService implements IToolsService {
|
||||
kill_persistent_terminal: (params, _result) => {
|
||||
return `Successfully closed terminal "${params.persistentTerminalId}".`;
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import './editCodeService.js'
|
||||
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
|
||||
import './sidebarActions.js'
|
||||
import './sidebarPane.js'
|
||||
import './sidebarStateService.js'
|
||||
|
||||
// register quick edit (Ctrl+K)
|
||||
import './quickEditActions.js'
|
||||
@@ -55,15 +56,6 @@ import './tooltipService.js'
|
||||
// register onboarding service
|
||||
import './voidOnboardingService.js'
|
||||
|
||||
// register misc service
|
||||
import './miscWokrbenchContrib.js'
|
||||
|
||||
// register file service (for explorer context menu)
|
||||
import './fileService.js'
|
||||
|
||||
// register source control management
|
||||
import './voidSCMService.js'
|
||||
|
||||
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
|
||||
|
||||
// llmMessage
|
||||
|
||||
@@ -18,16 +18,6 @@ import { IEditCodeService } from './editCodeServiceInterface.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID, VOID_GOTO_NEXT_DIFF_ACTION_ID, VOID_GOTO_PREV_DIFF_ACTION_ID, VOID_GOTO_NEXT_URI_ACTION_ID, VOID_GOTO_PREV_URI_ACTION_ID, VOID_ACCEPT_FILE_ACTION_ID, VOID_REJECT_FILE_ACTION_ID, VOID_ACCEPT_ALL_DIFFS_ACTION_ID, VOID_REJECT_ALL_DIFFS_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { KeyMod } from '../../../../editor/common/services/editorBaseApi.js';
|
||||
import { KeyCode } from '../../../../base/common/keyCodes.js';
|
||||
import { ScrollType } from '../../../../editor/common/editorCommon.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
|
||||
|
||||
@@ -43,11 +33,6 @@ export interface IVoidCommandBarService {
|
||||
getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes';
|
||||
setDiffIdx(uri: URI, newIdx: number | null): void;
|
||||
|
||||
getNextDiffIdx(step: 1 | -1): number | null;
|
||||
getNextUriIdx(step: 1 | -1): number | null;
|
||||
goToDiffIdx(idx: number | null): void;
|
||||
goToURIIdx(idx: number | null): Promise<void>;
|
||||
|
||||
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void;
|
||||
anyFileIsStreaming(): boolean;
|
||||
|
||||
@@ -100,7 +85,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
||||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IEditCodeService private readonly _editCodeService: IEditCodeService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -180,14 +164,10 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
||||
const newSortedDiffIds = this._computeSortedDiffs(newSortedDiffZoneIds)
|
||||
const isStreaming = this._isAnyDiffZoneStreaming(currentDiffZones)
|
||||
|
||||
// When diffZones are added/removed, reset the diffIdx to 0 if we have diffs
|
||||
const newDiffIdx = newSortedDiffIds.length > 0 ? 0 : null;
|
||||
|
||||
this._setState(uri, {
|
||||
sortedDiffZoneIds: newSortedDiffZoneIds,
|
||||
sortedDiffIds: newSortedDiffIds,
|
||||
isStreaming: isStreaming,
|
||||
diffIdx: newDiffIdx
|
||||
isStreaming: isStreaming
|
||||
})
|
||||
this._onDidChangeState.fire({ uri })
|
||||
}
|
||||
@@ -202,24 +182,9 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
||||
const currState = this.stateOfURI[uri.fsPath]
|
||||
if (!currState) continue // should never happen
|
||||
const { sortedDiffZoneIds } = currState
|
||||
const oldSortedDiffIds = currState.sortedDiffIds;
|
||||
const newSortedDiffIds = this._computeSortedDiffs(sortedDiffZoneIds)
|
||||
|
||||
// Handle diffIdx adjustment when diffs change
|
||||
let newDiffIdx = currState.diffIdx;
|
||||
|
||||
// Check if diffs were removed
|
||||
if (oldSortedDiffIds.length > newSortedDiffIds.length && currState.diffIdx !== null) {
|
||||
// If currently selected diff was removed or we have fewer diffs than the current index
|
||||
if (currState.diffIdx >= newSortedDiffIds.length) {
|
||||
// Select the last diff if available, otherwise null
|
||||
newDiffIdx = newSortedDiffIds.length > 0 ? newSortedDiffIds.length - 1 : null;
|
||||
}
|
||||
}
|
||||
|
||||
this._setState(uri, {
|
||||
sortedDiffIds: newSortedDiffIds,
|
||||
diffIdx: newDiffIdx
|
||||
// sortedDiffZoneIds, // no change
|
||||
// isStreaming, // no change
|
||||
})
|
||||
@@ -333,9 +298,9 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
||||
}
|
||||
|
||||
// make sure diffIdx is always correct
|
||||
if (newState.diffIdx !== null && newState.diffIdx > newState.sortedDiffIds.length) {
|
||||
if (newState.diffIdx && newState.diffIdx > newState.sortedDiffIds.length) {
|
||||
newState.diffIdx = newState.sortedDiffIds.length
|
||||
if (newState.diffIdx <= 0) newState.diffIdx = null
|
||||
if (newState.diffIdx < 0) newState.diffIdx = null
|
||||
}
|
||||
|
||||
this.stateOfURI = {
|
||||
@@ -382,99 +347,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
||||
return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming')
|
||||
}
|
||||
|
||||
getNextDiffIdx(step: 1 | -1): number | null {
|
||||
// If no active URI, return null
|
||||
if (!this.activeURI) return null;
|
||||
|
||||
const state = this.stateOfURI[this.activeURI.fsPath];
|
||||
if (!state) return null;
|
||||
|
||||
const { diffIdx, sortedDiffIds } = state;
|
||||
|
||||
// If no diffs, return null
|
||||
if (sortedDiffIds.length === 0) return null;
|
||||
|
||||
// Calculate next index with wrapping
|
||||
const nextIdx = ((diffIdx ?? 0) + step + sortedDiffIds.length) % sortedDiffIds.length;
|
||||
return nextIdx;
|
||||
}
|
||||
|
||||
getNextUriIdx(step: 1 | -1): number | null {
|
||||
// If no URIs with changes, return null
|
||||
if (this.sortedURIs.length === 0) return null;
|
||||
|
||||
// If no active URI, return first or last based on step
|
||||
if (!this.activeURI) {
|
||||
return step === 1 ? 0 : this.sortedURIs.length - 1;
|
||||
}
|
||||
|
||||
// Find current index
|
||||
const currentIdx = this.sortedURIs.findIndex(uri => uri.fsPath === this.activeURI?.fsPath);
|
||||
|
||||
// If not found, return first or last based on step
|
||||
if (currentIdx === -1) {
|
||||
return step === 1 ? 0 : this.sortedURIs.length - 1;
|
||||
}
|
||||
|
||||
// Calculate next index with wrapping
|
||||
const nextIdx = (currentIdx + step + this.sortedURIs.length) % this.sortedURIs.length;
|
||||
return nextIdx;
|
||||
}
|
||||
|
||||
goToDiffIdx(idx: number | null): void {
|
||||
// If null or no active URI, return
|
||||
if (idx === null || !this.activeURI) return;
|
||||
|
||||
// Get state for the current URI
|
||||
const state = this.stateOfURI[this.activeURI.fsPath];
|
||||
if (!state) return;
|
||||
|
||||
const { sortedDiffIds } = state;
|
||||
|
||||
// Find the diff at the specified index
|
||||
const diffid = sortedDiffIds[idx];
|
||||
if (diffid === undefined) return;
|
||||
|
||||
// Get the diff object
|
||||
const diff = this._editCodeService.diffOfId[diffid];
|
||||
if (!diff) return;
|
||||
|
||||
// Find an active editor to focus
|
||||
const editor = this._codeEditorService.getFocusedCodeEditor() ||
|
||||
this._codeEditorService.getActiveCodeEditor();
|
||||
if (!editor) return;
|
||||
|
||||
// Reveal the line in the editor
|
||||
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate);
|
||||
|
||||
// Update the current diff index
|
||||
this.setDiffIdx(this.activeURI, idx);
|
||||
}
|
||||
|
||||
async goToURIIdx(idx: number | null): Promise<void> {
|
||||
// If null or no URIs, return
|
||||
if (idx === null || this.sortedURIs.length === 0) return;
|
||||
|
||||
// Get the URI at the specified index
|
||||
const nextURI = this.sortedURIs[idx];
|
||||
if (!nextURI) return;
|
||||
|
||||
// Get the model for this URI
|
||||
const { model } = await this._voidModelService.getModelSafe(nextURI);
|
||||
if (!model) return;
|
||||
|
||||
// Find an editor to use
|
||||
const editor = this._codeEditorService.getFocusedCodeEditor() ||
|
||||
this._codeEditorService.getActiveCodeEditor();
|
||||
if (!editor) return;
|
||||
|
||||
// Open the URI in the editor
|
||||
await this._codeEditorService.openCodeEditor(
|
||||
{ resource: model.uri, options: { revealIfVisible: true } },
|
||||
editor
|
||||
);
|
||||
}
|
||||
|
||||
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) {
|
||||
const { behavior } = opts
|
||||
// if anything is streaming, do nothing
|
||||
@@ -518,8 +390,8 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
// Style the container
|
||||
// root.style.backgroundColor = 'rgb(248 113 113)';
|
||||
root.style.height = '256px'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
|
||||
root.style.width = '100%';
|
||||
root.style.height = '16rem'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
|
||||
root.style.width = '16rem';
|
||||
root.style.flexDirection = 'column';
|
||||
root.style.justifyContent = 'flex-end';
|
||||
root.style.alignItems = 'flex-end';
|
||||
@@ -567,305 +439,3 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
|
||||
}
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_ACCEPT_DIFF_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidAcceptDiffAction', 'Void: Accept Diff'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editCodeService = accessor.get(IEditCodeService);
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
|
||||
const activeURI = commandBarService.activeURI;
|
||||
if (!activeURI) return;
|
||||
|
||||
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
|
||||
if (!commandBarState) return;
|
||||
const diffIdx = commandBarState.diffIdx ?? 0;
|
||||
|
||||
const diffid = commandBarState.sortedDiffIds[diffIdx];
|
||||
if (!diffid) return;
|
||||
|
||||
metricsService.capture('Accept Diff', { diffid, keyboard: true });
|
||||
editCodeService.acceptDiff({ diffid: parseInt(diffid) });
|
||||
|
||||
// After accepting the diff, navigate to the next diff
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
|
||||
if (nextDiffIdx !== null) {
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_REJECT_DIFF_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidRejectDiffAction', 'Void: Reject Diff'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Backspace },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editCodeService = accessor.get(IEditCodeService);
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const activeURI = commandBarService.activeURI;
|
||||
if (!activeURI) return;
|
||||
|
||||
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
|
||||
if (!commandBarState) return;
|
||||
const diffIdx = commandBarState.diffIdx ?? 0;
|
||||
|
||||
const diffid = commandBarState.sortedDiffIds[diffIdx];
|
||||
if (!diffid) return;
|
||||
|
||||
metricsService.capture('Reject Diff', { diffid, keyboard: true });
|
||||
editCodeService.rejectDiff({ diffid: parseInt(diffid) });
|
||||
|
||||
// After rejecting the diff, navigate to the next diff
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
|
||||
if (nextDiffIdx !== null) {
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Go to next diff action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_NEXT_DIFF_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToNextDiffAction', 'Void: Go to Next Diff'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.DownArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
|
||||
if (nextDiffIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate Diff', { direction: 'next', keyboard: true });
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Go to previous diff action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_PREV_DIFF_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToPrevDiffAction', 'Void: Go to Previous Diff'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.UpArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const prevDiffIdx = commandBarService.getNextDiffIdx(-1);
|
||||
if (prevDiffIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate Diff', { direction: 'previous', keyboard: true });
|
||||
commandBarService.goToDiffIdx(prevDiffIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Go to next URI action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_NEXT_URI_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToNextUriAction', 'Void: Go to Next File with Diffs'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.RightArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const nextUriIdx = commandBarService.getNextUriIdx(1);
|
||||
if (nextUriIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate URI', { direction: 'next', keyboard: true });
|
||||
await commandBarService.goToURIIdx(nextUriIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Go to previous URI action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_PREV_URI_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToPrevUriAction', 'Void: Go to Previous File with Diffs'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.LeftArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const prevUriIdx = commandBarService.getNextUriIdx(-1);
|
||||
if (prevUriIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate URI', { direction: 'previous', keyboard: true });
|
||||
await commandBarService.goToURIIdx(prevUriIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Accept current file action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_ACCEPT_FILE_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidAcceptFileAction', 'Void: Accept All Diffs in Current File'),
|
||||
keybinding: {
|
||||
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const editCodeService = accessor.get(IEditCodeService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const activeURI = commandBarService.activeURI;
|
||||
if (!activeURI) return;
|
||||
|
||||
metricsService.capture('Accept File', { keyboard: true });
|
||||
editCodeService.acceptOrRejectAllDiffAreas({
|
||||
uri: activeURI,
|
||||
behavior: 'accept',
|
||||
removeCtrlKs: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Reject current file action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_REJECT_FILE_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidRejectFileAction', 'Void: Reject All Diffs in Current File'),
|
||||
keybinding: {
|
||||
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const editCodeService = accessor.get(IEditCodeService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const activeURI = commandBarService.activeURI;
|
||||
if (!activeURI) return;
|
||||
|
||||
metricsService.capture('Reject File', { keyboard: true });
|
||||
editCodeService.acceptOrRejectAllDiffAreas({
|
||||
uri: activeURI,
|
||||
behavior: 'reject',
|
||||
removeCtrlKs: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Accept all diffs in all files action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidAcceptAllDiffsAction', 'Void: Accept All Diffs in All Files'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
if (commandBarService.anyFileIsStreaming()) return;
|
||||
|
||||
metricsService.capture('Accept All Files', { keyboard: true });
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
|
||||
}
|
||||
});
|
||||
|
||||
// Reject all diffs in all files action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_REJECT_ALL_DIFFS_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidRejectAllDiffsAction', 'Void: Reject All Diffs in All Files'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
if (commandBarService.anyFileIsStreaming()) return;
|
||||
|
||||
metricsService.capture('Reject All Files', { keyboard: true });
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js'
|
||||
import { localize2 } from '../../../../nls.js'
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'
|
||||
import { ISCMService } from '../../scm/common/scm.js'
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'
|
||||
import { IVoidSCMService } from '../common/voidSCMTypes.js'
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js'
|
||||
import { ModelSelection, OverridesOfModel, ModelSelectionOptions } from '../common/voidSettingsTypes.js'
|
||||
import { gitCommitMessage_systemMessage, gitCommitMessage_userMessage } from '../common/prompt/prompts.js'
|
||||
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js'
|
||||
import { generateUuid } from '../../../../base/common/uuid.js'
|
||||
import { ThrottledDelayer } from '../../../../base/common/async.js'
|
||||
import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
|
||||
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js'
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js'
|
||||
|
||||
interface ModelOptions {
|
||||
modelSelection: ModelSelection | null
|
||||
modelSelectionOptions?: ModelSelectionOptions
|
||||
overridesOfModel: OverridesOfModel
|
||||
}
|
||||
|
||||
export interface IGenerateCommitMessageService {
|
||||
readonly _serviceBrand: undefined
|
||||
generateCommitMessage(): Promise<void>
|
||||
abort(): void
|
||||
}
|
||||
|
||||
export const IGenerateCommitMessageService = createDecorator<IGenerateCommitMessageService>('voidGenerateCommitMessageService');
|
||||
|
||||
const loadingContextKey = 'voidSCMGenerateCommitMessageLoading'
|
||||
|
||||
class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly execute = new ThrottledDelayer(300)
|
||||
private llmRequestId: string | null = null
|
||||
private currentRequestId: string | null = null
|
||||
private voidSCM: IVoidSCMService
|
||||
private loadingContextKey: IContextKey<boolean>
|
||||
|
||||
constructor(
|
||||
@ISCMService private readonly scmService: ISCMService,
|
||||
@IMainProcessService mainProcessService: IMainProcessService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IConvertToLLMMessageService private readonly convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@INotificationService private readonly notificationService: INotificationService
|
||||
) {
|
||||
super()
|
||||
this.loadingContextKey = this.contextKeyService.createKey(loadingContextKey, false)
|
||||
this.voidSCM = ProxyChannel.toService<IVoidSCMService>(mainProcessService.getChannel('void-channel-scm'))
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
this.execute.dispose()
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
async generateCommitMessage() {
|
||||
this.loadingContextKey.set(true)
|
||||
this.execute.trigger(async () => {
|
||||
const requestId = generateUuid()
|
||||
this.currentRequestId = requestId
|
||||
|
||||
|
||||
try {
|
||||
const { path, repo } = this.gitRepoInfo()
|
||||
const [stat, sampledDiffs, branch, log] = await Promise.all([
|
||||
this.voidSCM.gitStat(path),
|
||||
this.voidSCM.gitSampledDiffs(path),
|
||||
this.voidSCM.gitBranch(path),
|
||||
this.voidSCM.gitLog(path)
|
||||
])
|
||||
|
||||
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
|
||||
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature['SCM'] ?? null
|
||||
const modelSelectionOptions = modelSelection ? this.voidSettingsService.state.optionsOfModelSelection['SCM'][modelSelection?.providerName]?.[modelSelection.modelName] : undefined
|
||||
const overridesOfModel = this.voidSettingsService.state.overridesOfModel
|
||||
|
||||
const modelOptions: ModelOptions = { modelSelection, modelSelectionOptions, overridesOfModel }
|
||||
|
||||
const prompt = gitCommitMessage_userMessage(stat, sampledDiffs, branch, log)
|
||||
|
||||
const simpleMessages = [{ role: 'user', content: prompt } as const]
|
||||
const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
simpleMessages,
|
||||
systemMessage: gitCommitMessage_systemMessage,
|
||||
modelSelection: modelOptions.modelSelection,
|
||||
featureName: 'SCM',
|
||||
})
|
||||
|
||||
const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions)
|
||||
|
||||
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
|
||||
|
||||
repo.input.setValue(commitMessage, false)
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
} finally {
|
||||
if (this.isCurrentRequest(requestId)) {
|
||||
this.loadingContextKey.set(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.llmRequestId) {
|
||||
this.llmMessageService.abort(this.llmRequestId)
|
||||
}
|
||||
this.execute.cancel()
|
||||
this.loadingContextKey.set(false)
|
||||
this.currentRequestId = null
|
||||
}
|
||||
|
||||
private gitRepoInfo() {
|
||||
const repo = Array.from(this.scmService.repositories || []).find((r: any) => r.provider.contextValue === 'git')
|
||||
if (!repo) { throw new Error('No git repository found') }
|
||||
if (!repo.provider.rootUri?.fsPath) { throw new Error('No git repository root path found') }
|
||||
return { path: repo.provider.rootUri.fsPath, repo }
|
||||
}
|
||||
|
||||
/** LLM Functions */
|
||||
|
||||
private sendLLMMessage(messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
this.llmRequestId = this.llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
messages,
|
||||
separateSystemMessage,
|
||||
chatMode: null,
|
||||
modelSelection: modelOptions.modelSelection,
|
||||
modelSelectionOptions: modelOptions.modelSelectionOptions,
|
||||
overridesOfModel: modelOptions.overridesOfModel,
|
||||
onText: () => { },
|
||||
onFinalMessage: (params: { fullText: string }) => {
|
||||
const match = params.fullText.match(/<output>([\s\S]*?)<\/output>/i)
|
||||
const commitMessage = match ? match[1].trim() : ''
|
||||
resolve(commitMessage)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
reject(error)
|
||||
},
|
||||
onAbort: () => {
|
||||
reject(new CancellationError())
|
||||
},
|
||||
logging: { loggingName: 'VoidSCM - Commit Message' },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** Request Helpers */
|
||||
|
||||
private isCurrentRequest(requestId: string) {
|
||||
return requestId === this.currentRequestId
|
||||
}
|
||||
|
||||
|
||||
/** UI Functions */
|
||||
|
||||
private onError(error: any) {
|
||||
if (!isCancellationError(error)) {
|
||||
console.error(error)
|
||||
this.notificationService.error(localize2('voidFailedToGenerateCommitMessage', 'Failed to generate commit message.').value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateCommitMessageAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.generateCommitMessageAction',
|
||||
title: localize2('voidCommitMessagePrompt', 'Void: Generate Commit Message'),
|
||||
icon: ThemeIcon.fromId('sparkle'),
|
||||
tooltip: localize2('voidCommitMessagePromptTooltip', 'Void: Generate Commit Message'),
|
||||
f1: true,
|
||||
menu: [{
|
||||
id: MenuId.SCMInputBox,
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, false)),
|
||||
group: 'inline'
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
|
||||
generateCommitMessageService.generateCommitMessage()
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingGenerateCommitMessageAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.loadingGenerateCommitMessageAction',
|
||||
title: localize2('voidCommitMessagePromptCancel', 'Void: Cancel Commit Message Generation'),
|
||||
icon: ThemeIcon.fromId('stop-circle'),
|
||||
tooltip: localize2('voidCommitMessagePromptCancelTooltip', 'Void: Cancel Commit Message Generation'),
|
||||
f1: false, //Having a cancel command in the command palette is more confusing than useful.
|
||||
menu: [{
|
||||
id: MenuId.SCMInputBox,
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, true)),
|
||||
group: 'inline'
|
||||
}]
|
||||
})
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
|
||||
generateCommitMessageService.abort()
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(GenerateCommitMessageAction)
|
||||
registerAction2(LoadingGenerateCommitMessageAction)
|
||||
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed)
|
||||
@@ -8,7 +8,7 @@ import Severity from '../../../../base/common/severity.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationActions, INotificationHandle, INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { INotificationActions, INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
@@ -20,7 +20,7 @@ import { IAction } from '../../../../base/common/actions.js';
|
||||
|
||||
|
||||
|
||||
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService): INotificationHandle => {
|
||||
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService) => {
|
||||
const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
|
||||
|
||||
let actions: INotificationActions | undefined
|
||||
@@ -119,21 +119,18 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
|
||||
progress: actions ? { worked: 0, total: 100 } : undefined,
|
||||
actions: actions,
|
||||
})
|
||||
|
||||
return notifController
|
||||
// const d = notifController.onDidClose(() => {
|
||||
// notifyYesUpdate(notifService, res)
|
||||
// d.dispose()
|
||||
// })
|
||||
}
|
||||
const notifyErrChecking = (notifService: INotificationService): INotificationHandle => {
|
||||
const notifyErrChecking = (notifService: INotificationService) => {
|
||||
const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
|
||||
const notifController = notifService.notify({
|
||||
notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: message,
|
||||
sticky: true,
|
||||
})
|
||||
return notifController
|
||||
}
|
||||
|
||||
|
||||
@@ -143,35 +140,30 @@ const performVoidCheck = async (
|
||||
voidUpdateService: IVoidUpdateService,
|
||||
metricsService: IMetricsService,
|
||||
updateService: IUpdateService,
|
||||
): Promise<INotificationHandle | null> => {
|
||||
) => {
|
||||
|
||||
const metricsTag = explicit ? 'Manual' : 'Auto'
|
||||
|
||||
metricsService.capture(`Void Update ${metricsTag}: Checking...`, {})
|
||||
const res = await voidUpdateService.check(explicit)
|
||||
if (!res) {
|
||||
const notifController = notifyErrChecking(notifService);
|
||||
notifyErrChecking(notifService);
|
||||
metricsService.capture(`Void Update ${metricsTag}: Error`, { res })
|
||||
return notifController
|
||||
}
|
||||
else {
|
||||
if (res.message) {
|
||||
const notifController = notifyUpdate(res, notifService, updateService)
|
||||
notifyUpdate(res, notifService, updateService)
|
||||
metricsService.capture(`Void Update ${metricsTag}: Yes`, { res })
|
||||
return notifController
|
||||
}
|
||||
else {
|
||||
metricsService.capture(`Void Update ${metricsTag}: No`, { res })
|
||||
return null
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Action
|
||||
let lastNotifController: INotificationHandle | null = null
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -185,15 +177,7 @@ registerAction2(class extends Action2 {
|
||||
const notifService = accessor.get(INotificationService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const updateService = accessor.get(IUpdateService)
|
||||
|
||||
const currNotifController = lastNotifController
|
||||
|
||||
const newController = await performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
|
||||
|
||||
if (newController) {
|
||||
currNotifController?.close()
|
||||
lastNotifController = newController
|
||||
}
|
||||
performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,32 +5,31 @@
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||
import { ToolCallParams, ToolName, ToolResult } from './toolsServiceTypes.js';
|
||||
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
content: string; // give this result to LLM (string of value)
|
||||
id: string;
|
||||
rawParams: RawToolParamsObj;
|
||||
mcpServerName: string | undefined; // the server name at the time of the call
|
||||
} & (
|
||||
// in order of events:
|
||||
| { type: 'invalid_params', result: null, name: T, }
|
||||
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams<T>, } // params were validated, awaiting user
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
|
||||
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams<T>, }
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
|
||||
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams<T>, } // error when tool was running
|
||||
| { type: 'success', result: Awaited<ToolResult<T>>, name: T, params: ToolCallParams<T>, }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams<T> }
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
|
||||
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T] }
|
||||
) // user rejected
|
||||
|
||||
export type DecorativeCanceledTool = {
|
||||
role: 'interrupted_streaming_tool';
|
||||
name: ToolName;
|
||||
mcpServerName: string | undefined; // the server name at the time of the call
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +1,8 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Color, RGBA } from '../../../../../base/common/color.js';
|
||||
import { registerColor } from '../../../../../platform/theme/common/colorUtils.js';
|
||||
|
||||
// editCodeService colors
|
||||
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
|
||||
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
|
||||
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
|
||||
|
||||
const acceptBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2)
|
||||
const rejectBG = new Color(new RGBA(255, 0, 0, .1)); // default is RGBA(255, 0, 0, .2)
|
||||
|
||||
// Widget colors
|
||||
export const acceptAllBg = 'rgb(30, 133, 56)'
|
||||
export const acceptBg = 'rgb(26, 116, 48)'
|
||||
export const acceptBorder = '1px solid rgb(20, 86, 38)'
|
||||
|
||||
export const rejectAllBg = 'rgb(207, 40, 56)'
|
||||
export const rejectBg = 'rgb(180, 35, 49)'
|
||||
export const rejectBorder = '1px solid rgb(142, 28, 39)'
|
||||
|
||||
export const acceptBg = '#1a7431'
|
||||
export const acceptAllBg = '#1e8538'
|
||||
export const acceptBorder = '1px solid #145626'
|
||||
export const rejectBg = '#b42331'
|
||||
export const rejectAllBg = '#cf2838'
|
||||
export const rejectBorder = '1px solid #8e1c27'
|
||||
export const buttonFontSize = '11px'
|
||||
export const buttonTextColor = 'white'
|
||||
|
||||
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
}
|
||||
|
||||
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
|
||||
registerColor('void.greenBG', configOfBG(acceptBG), '', true);
|
||||
registerColor('void.redBG', configOfBG(rejectBG), '', true);
|
||||
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
|
||||
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
|
||||
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IPathService } from '../../../services/path/common/pathService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { MCPServerOfName, MCPConfigFileJSON, MCPServer, MCPToolCallParams, RawMCPToolCall, MCPServerEventResponse } from './mcpServiceTypes.js';
|
||||
import { Event, Emitter } from '../../../../base/common/event.js';
|
||||
import { InternalToolInfo } from './prompt/prompts.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { MCPUserStateOfName } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
type MCPServiceState = {
|
||||
mcpServerOfName: MCPServerOfName,
|
||||
error: string | undefined, // global parsing error
|
||||
}
|
||||
|
||||
export interface IMCPService {
|
||||
readonly _serviceBrand: undefined;
|
||||
revealMCPConfigFile(): Promise<void>;
|
||||
toggleServerIsOn(serverName: string, isOn: boolean): Promise<void>;
|
||||
|
||||
readonly state: MCPServiceState; // NOT persisted
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
getMCPTools(): InternalToolInfo[] | undefined;
|
||||
callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>;
|
||||
stringifyResult(result: RawMCPToolCall): string
|
||||
}
|
||||
|
||||
export const IMCPService = createDecorator<IMCPService>('mcpConfigService');
|
||||
|
||||
|
||||
|
||||
const MCP_CONFIG_FILE_NAME = 'mcp.json';
|
||||
const MCP_CONFIG_SAMPLE = { mcpServers: {} }
|
||||
const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2);
|
||||
|
||||
|
||||
// export interface MCPCallToolOfToolName {
|
||||
// [toolName: string]: (params: any) => Promise<{
|
||||
// result: any | Promise<any>,
|
||||
// interruptTool?: () => void
|
||||
// }>;
|
||||
// }
|
||||
|
||||
|
||||
class MCPService extends Disposable implements IMCPService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
|
||||
private readonly channel: IChannel // MCPChannel
|
||||
|
||||
// list of MCP servers pulled from mcpChannel
|
||||
state: MCPServiceState = {
|
||||
mcpServerOfName: {},
|
||||
error: undefined,
|
||||
}
|
||||
|
||||
// Emitters for server events
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
public readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
// private readonly _onLoadingServersChange = new Emitter<MCPServerEventLoadingParam>();
|
||||
// public readonly onLoadingServersChange = this._onLoadingServersChange.event;
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IPathService private readonly pathService: IPathService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
super();
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-mcp')
|
||||
|
||||
|
||||
const onEvent = (e: MCPServerEventResponse) => {
|
||||
// console.log('GOT EVENT', e)
|
||||
this._setMCPServerState(e.response.name, e.response.newServer)
|
||||
}
|
||||
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
|
||||
private async _initialize() {
|
||||
try {
|
||||
await this.voidSettingsService.waitForInitState;
|
||||
|
||||
// Create .mcpConfig if it doesn't exist
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
const fileExists = await this._configFileExists(mcpConfigUri);
|
||||
if (!fileExists) {
|
||||
await this._createMCPConfigFile(mcpConfigUri);
|
||||
console.log('MCP Config file created:', mcpConfigUri.toString());
|
||||
}
|
||||
await this._addMCPConfigFileWatcher();
|
||||
await this._refreshMCPServers();
|
||||
} catch (error) {
|
||||
console.error('Error initializing MCPService:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _setMCPServerState = async (serverName: string, newServer: MCPServer | undefined) => {
|
||||
if (newServer === undefined) {
|
||||
// Remove the server from the state
|
||||
const { [serverName]: removed, ...remainingServers } = this.state.mcpServerOfName;
|
||||
this.state = {
|
||||
...this.state,
|
||||
mcpServerOfName: remainingServers
|
||||
}
|
||||
} else {
|
||||
// Add or update the server
|
||||
this.state = {
|
||||
...this.state,
|
||||
mcpServerOfName: {
|
||||
...this.state.mcpServerOfName,
|
||||
[serverName]: newServer
|
||||
}
|
||||
}
|
||||
}
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
private readonly _setHasError = async (errMsg: string | undefined) => {
|
||||
this.state = {
|
||||
...this.state,
|
||||
error: errMsg,
|
||||
}
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
// Create the file/directory if it doesn't exist
|
||||
private async _createMCPConfigFile(mcpConfigUri: URI): Promise<void> {
|
||||
await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path }));
|
||||
const buffer = VSBuffer.fromString(MCP_CONFIG_SAMPLE_STRING);
|
||||
await this.fileService.writeFile(mcpConfigUri, buffer);
|
||||
}
|
||||
|
||||
|
||||
private async _addMCPConfigFileWatcher(): Promise<void> {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
this._register(
|
||||
this.fileService.watch(mcpConfigUri)
|
||||
)
|
||||
|
||||
this._register(this.fileService.onDidFilesChange(async e => {
|
||||
if (!e.contains(mcpConfigUri)) return
|
||||
await this._refreshMCPServers();
|
||||
}));
|
||||
}
|
||||
|
||||
// Client-side functions
|
||||
|
||||
public async revealMCPConfigFile(): Promise<void> {
|
||||
try {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
await this.editorService.openEditor({
|
||||
resource: mcpConfigUri,
|
||||
options: {
|
||||
pinned: true,
|
||||
revealIfOpened: true,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error opening MCP config file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public getMCPTools(): InternalToolInfo[] | undefined {
|
||||
const allTools: InternalToolInfo[] = []
|
||||
for (const serverName in this.state.mcpServerOfName) {
|
||||
const server = this.state.mcpServerOfName[serverName];
|
||||
server.tools?.forEach(tool => {
|
||||
allTools.push({
|
||||
description: tool.description || '',
|
||||
params: this._transformInputSchemaToParams(tool.inputSchema),
|
||||
name: tool.name,
|
||||
mcpServerName: serverName,
|
||||
})
|
||||
})
|
||||
}
|
||||
if (allTools.length === 0) return undefined
|
||||
return allTools
|
||||
}
|
||||
|
||||
private _transformInputSchemaToParams(inputSchema?: Record<string, any>): { [paramName: string]: { description: string } } {
|
||||
|
||||
// Check if inputSchema is valid
|
||||
if (!inputSchema || !inputSchema.properties) return {};
|
||||
|
||||
const params: { [paramName: string]: { description: string } } = {};
|
||||
Object.keys(inputSchema.properties).forEach(paramName => {
|
||||
const propertyValues = inputSchema.properties[paramName];
|
||||
|
||||
// Check if propertyValues is not an object
|
||||
if (typeof propertyValues !== 'object') {
|
||||
console.warn(`Invalid property value for ${paramName}: expected object, got ${typeof propertyValues}`);
|
||||
return; // in forEach the return is equivalent to continue
|
||||
}
|
||||
|
||||
// Add the parameter to the params object
|
||||
params[paramName] = {
|
||||
description: JSON.stringify(propertyValues.description || '', null, 2) || '',
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
private async _getMCPConfigFilePath(): Promise<URI> {
|
||||
const appName = this.productService.dataFolderName
|
||||
const userHome = await this.pathService.userHome();
|
||||
const uri = URI.joinPath(userHome, appName, MCP_CONFIG_FILE_NAME)
|
||||
return uri
|
||||
}
|
||||
|
||||
private async _configFileExists(mcpConfigUri: URI): Promise<boolean> {
|
||||
try {
|
||||
await this.fileService.stat(mcpConfigUri);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async _parseMCPConfigFile(): Promise<MCPConfigFileJSON | null> {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(mcpConfigUri);
|
||||
const contentString = fileContent.value.toString();
|
||||
const configFileJson = JSON.parse(contentString);
|
||||
if (!configFileJson.mcpServers) {
|
||||
throw new Error('Missing mcpServers property');
|
||||
}
|
||||
return configFileJson as MCPConfigFileJSON;
|
||||
} catch (error) {
|
||||
const fullError = `Error parsing MCP config file: ${error}`;
|
||||
this._setHasError(fullError)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle server state changes
|
||||
private async _refreshMCPServers(): Promise<void> {
|
||||
|
||||
this._setHasError(undefined)
|
||||
|
||||
const newConfigFileJSON = await this._parseMCPConfigFile();
|
||||
if (!newConfigFileJSON) { console.log(`Not setting state: MCP config file not found`); return }
|
||||
if (!newConfigFileJSON?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return }
|
||||
|
||||
|
||||
const oldConfigFileNames = Object.keys(this.state.mcpServerOfName)
|
||||
const newConfigFileNames = Object.keys(newConfigFileJSON.mcpServers)
|
||||
|
||||
const addedServerNames = newConfigFileNames.filter(serverName => !oldConfigFileNames.includes(serverName)); // in new and not in old
|
||||
const removedServerNames = oldConfigFileNames.filter(serverName => !newConfigFileNames.includes(serverName)); // in old and not in new
|
||||
|
||||
// set isOn to any new servers in the config
|
||||
const addedUserStateOfName: MCPUserStateOfName = {}
|
||||
for (const name of addedServerNames) { addedUserStateOfName[name] = { isOn: true } }
|
||||
await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName);
|
||||
|
||||
// delete isOn for any servers that no longer show up in the config
|
||||
await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames);
|
||||
|
||||
// set all servers to loading
|
||||
for (const serverName in newConfigFileJSON.mcpServers) {
|
||||
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||
}
|
||||
const updatedServerNames = Object.keys(newConfigFileJSON.mcpServers).filter(serverName => !addedServerNames.includes(serverName) && !removedServerNames.includes(serverName))
|
||||
|
||||
this.channel.call('refreshMCPServers', {
|
||||
mcpConfigFileJSON: newConfigFileJSON,
|
||||
addedServerNames,
|
||||
removedServerNames,
|
||||
updatedServerNames,
|
||||
userStateOfName: this.voidSettingsService.state.mcpUserStateOfName,
|
||||
})
|
||||
}
|
||||
|
||||
stringifyResult(result: RawMCPToolCall): string {
|
||||
let toolResultStr: string
|
||||
if (result.event === 'text') {
|
||||
toolResultStr = result.text
|
||||
} else if (result.event === 'image') {
|
||||
toolResultStr = `[Image: ${result.image.mimeType}]`
|
||||
} else if (result.event === 'audio') {
|
||||
toolResultStr = `[Audio content]`
|
||||
} else if (result.event === 'resource') {
|
||||
toolResultStr = `[Resource content]`
|
||||
} else {
|
||||
toolResultStr = JSON.stringify(result)
|
||||
}
|
||||
return toolResultStr
|
||||
}
|
||||
|
||||
// toggle MCP server and update isOn in void settings
|
||||
public async toggleServerIsOn(serverName: string, isOn: boolean): Promise<void> {
|
||||
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||
|
||||
await this.voidSettingsService.setMCPServerState(serverName, { isOn });
|
||||
this.channel.call('toggleMCPServer', { serverName, isOn })
|
||||
}
|
||||
|
||||
|
||||
public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> {
|
||||
const result = await this.channel.call<RawMCPToolCall>('callTool', toolData);
|
||||
if (result.event === 'error') {
|
||||
throw new Error(`Error: ${result.text}`)
|
||||
}
|
||||
return { result };
|
||||
}
|
||||
|
||||
// public getMCPToolFns(): MCPToolResultType {
|
||||
// const tools = this.getMCPTools();
|
||||
// const toolFns: MCPToolResultType = {};
|
||||
|
||||
// tools.forEach((tool) => {
|
||||
// const name = tool.name;
|
||||
// // Define the tool call function
|
||||
// const toolFn = async (params: {
|
||||
// serverName: string,
|
||||
// toolName: string,
|
||||
// args: any
|
||||
// }) => {
|
||||
// const { serverName, toolName, args } = params;
|
||||
// const response = await this.callMCPTool({
|
||||
// serverName,
|
||||
// toolName,
|
||||
// params: args,
|
||||
// });
|
||||
// return { result: response }
|
||||
// };
|
||||
// toolFns[name] = toolFn;
|
||||
// });
|
||||
|
||||
// return toolFns
|
||||
// }
|
||||
}
|
||||
|
||||
registerSingleton(IMCPService, MCPService, InstantiationType.Eager);
|
||||
@@ -1,242 +0,0 @@
|
||||
/**
|
||||
* mcp-response-types.ts
|
||||
* --------------------------------------------------
|
||||
* **Pure** TypeScript interfaces (no external imports)
|
||||
* describing the JSON-RPC response shapes for:
|
||||
*
|
||||
* 1. tools/list -> ToolsListResponse
|
||||
* 2. prompts/list -> PromptsListResponse
|
||||
* 3. tools/call -> ToolCallResponse
|
||||
*
|
||||
* They are distilled directly from the official MCP
|
||||
* 2025‑03‑26 specification:
|
||||
* • Tools list response examples
|
||||
* • Prompts list response examples
|
||||
* • Tool call response examples
|
||||
*
|
||||
* Use them to get full IntelliSense when working with
|
||||
* @modelcontextprotocol/inspector‑cli responses.
|
||||
*/
|
||||
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Core JSON‑RPC envelope */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface JsonRpcSuccess<T> {
|
||||
// /** JSON‑RPC version – always '2.0' */
|
||||
// jsonrpc: '2.0';
|
||||
// /** Request identifier echoed back by the server */
|
||||
// id: string | number | null;
|
||||
// /** The successful result payload */
|
||||
// result: T;
|
||||
// }
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Utility: pagination */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface Paginated {
|
||||
// /** Opaque cursor for fetching the next page */
|
||||
// nextCursor?: string;
|
||||
// }
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 1. tools/list */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface MCPTool {
|
||||
/** Unique tool identifier */
|
||||
name: string;
|
||||
/** Human‑readable description */
|
||||
description?: string;
|
||||
/** JSON schema describing expected arguments */
|
||||
inputSchema?: Record<string, unknown>;
|
||||
/** Free‑form annotations describing behaviour, security, etc. */
|
||||
annotations?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// export interface ToolsListResult extends Paginated {
|
||||
// tools: MCPTool[];
|
||||
// }
|
||||
|
||||
// export type ToolsListResponse = JsonRpcSuccess<ToolsListResult>;
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 2. prompts/list */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface PromptArgument {
|
||||
// name: string;
|
||||
// description?: string;
|
||||
// /** Whether the argument is required */
|
||||
// required?: boolean;
|
||||
// }
|
||||
|
||||
// export interface Prompt {
|
||||
// name: string;
|
||||
// description?: string;
|
||||
// arguments?: PromptArgument[];
|
||||
// }
|
||||
|
||||
// export interface PromptsListResult extends Paginated {
|
||||
// prompts: Prompt[];
|
||||
// }
|
||||
|
||||
// export type PromptsListResponse = JsonRpcSuccess<PromptsListResult>;
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 3. tools/call */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
/** Additional resource structure that can be embedded in tool results */
|
||||
// export interface Resource {
|
||||
// uri: string;
|
||||
// mimeType: string;
|
||||
// /** Either plain‑text or base64‑encoded binary data */
|
||||
// text?: string;
|
||||
// data?: string;
|
||||
// }
|
||||
|
||||
/** Individual content items returned by a tool */
|
||||
// export type ToolContent =
|
||||
// | { type: 'text'; text: string }
|
||||
// | { type: 'image'; data: string; mimeType: string }
|
||||
// | { type: 'audio'; data: string; mimeType: string }
|
||||
// | { type: 'resource'; resource: Resource };
|
||||
|
||||
// export interface ToolCallResult {
|
||||
// /** List of content parts (text, images, resources, etc.) */
|
||||
// content: ToolContent[];
|
||||
// /** True if the tool itself encountered a domain‑level error */
|
||||
// isError?: boolean;
|
||||
// }
|
||||
|
||||
// export type ToolCallResponse = JsonRpcSuccess<ToolCallResult>;
|
||||
|
||||
// MCP SERVER CONFIG FILE TYPES -----------------------------
|
||||
|
||||
export interface MCPConfigFileEntryJSON {
|
||||
// Command-based server properties
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
||||
// URL-based server properties
|
||||
url?: URL;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface MCPConfigFileJSON {
|
||||
mcpServers: Record<string, MCPConfigFileEntryJSON>;
|
||||
}
|
||||
|
||||
|
||||
// SERVER EVENT TYPES ------------------------------------------
|
||||
|
||||
export type MCPServer = {
|
||||
// Command-based server properties
|
||||
tools: MCPTool[],
|
||||
status: 'loading' | 'success' | 'offline',
|
||||
command?: string,
|
||||
error?: string,
|
||||
} | {
|
||||
tools?: undefined,
|
||||
status: 'error',
|
||||
command?: string,
|
||||
error: string,
|
||||
}
|
||||
|
||||
export interface MCPServerOfName {
|
||||
[serverName: string]: MCPServer;
|
||||
}
|
||||
|
||||
export type MCPServerEvent = {
|
||||
name: string;
|
||||
prevServer?: MCPServer;
|
||||
newServer?: MCPServer;
|
||||
}
|
||||
export type MCPServerEventResponse = { response: MCPServerEvent }
|
||||
|
||||
export interface MCPConfigFileParseErrorResponse {
|
||||
response: {
|
||||
type: 'config-file-error';
|
||||
error: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export type MCPServerResponse = MCPAddResponse | MCPUpdateResponse | MCPDeleteResponse | MCPLoadingResponse;
|
||||
|
||||
// Event parameter types
|
||||
// export type MCPServerEventAddParam = { response: MCPAddResponse };
|
||||
// export type MCPServerEventUpdateParam = { response: MCPUpdateResponse };
|
||||
// export type MCPServerEventDeleteParam = { response: MCPDeleteResponse };
|
||||
// export type MCPServerEventLoadingParam = { response: MCPLoadingResponse };
|
||||
|
||||
// Event Param union type
|
||||
// export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam;
|
||||
|
||||
// TOOL CALL EVENT TYPES ------------------------------------------
|
||||
|
||||
type MCPToolResponseType = 'text' | 'image' | 'audio' | 'resource' | 'error';
|
||||
|
||||
type ResponseImageTypes = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp' | 'image/svg+xml' | 'image/bmp' | 'image/tiff' | 'image/vnd.microsoft.icon';
|
||||
|
||||
interface ImageData {
|
||||
data: string;
|
||||
mimeType: ResponseImageTypes;
|
||||
}
|
||||
|
||||
interface MCPToolResponseBase {
|
||||
toolName: string;
|
||||
serverName?: string;
|
||||
event: MCPToolResponseType;
|
||||
text?: string;
|
||||
image?: ImageData;
|
||||
}
|
||||
|
||||
type MCPToolResponseConstraints = {
|
||||
'text': {
|
||||
image?: never;
|
||||
text: string;
|
||||
};
|
||||
'error': {
|
||||
image?: never;
|
||||
text: string;
|
||||
};
|
||||
'image': {
|
||||
text?: never;
|
||||
image: ImageData;
|
||||
};
|
||||
'audio': {
|
||||
text?: never;
|
||||
image?: never;
|
||||
};
|
||||
'resource': {
|
||||
text?: never;
|
||||
image?: never;
|
||||
}
|
||||
}
|
||||
|
||||
type MCPToolEventResponse<T extends MCPToolResponseType> = Omit<MCPToolResponseBase, 'event' | keyof MCPToolResponseConstraints> & MCPToolResponseConstraints[T] & { event: T };
|
||||
|
||||
// Response types
|
||||
export type MCPToolTextResponse = MCPToolEventResponse<'text'>;
|
||||
export type MCPToolErrorResponse = MCPToolEventResponse<'error'>;
|
||||
export type MCPToolImageResponse = MCPToolEventResponse<'image'>;
|
||||
export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>;
|
||||
export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>;
|
||||
export type RawMCPToolCall = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse;
|
||||
|
||||
export interface MCPToolCallParams {
|
||||
serverName: string;
|
||||
toolName: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const removeMCPToolNamePrefix = (name: string) => {
|
||||
return name.split('_').slice(1).join('_')
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import { INotificationService } from '../../../../platform/notification/common/n
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
setOptOut(val: boolean): void;
|
||||
getDebuggingProperties(): Promise<object>;
|
||||
}
|
||||
|
||||
@@ -39,11 +38,6 @@ export class MetricsService implements IMetricsService {
|
||||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
setOptOut(...params: Parameters<IMetricsService['setOptOut']>) {
|
||||
this.metricsService.setOptOut(...params);
|
||||
}
|
||||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
async getDebuggingProperties(): Promise<object> {
|
||||
return this.metricsService.getDebuggingProperties()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,12 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IDirectoryStrService } from '../directoryStrService.js';
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, BuiltinToolResultType, ToolName } from '../toolsServiceTypes.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
// Triple backtick wrapper used throughout the prompts for code blocks
|
||||
@@ -113,7 +112,7 @@ ${searchReplaceBlockTemplate}
|
||||
|
||||
## Guidelines:
|
||||
|
||||
1. You may output multiple search replace blocks if needed.
|
||||
1. You are encouraged to output multiple changes whenever possible.
|
||||
|
||||
2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code.
|
||||
|
||||
@@ -125,18 +124,23 @@ ${searchReplaceBlockTemplate}
|
||||
|
||||
|
||||
// ======================================================== tools ========================================================
|
||||
|
||||
|
||||
const chatSuggestionDiffExample = `\
|
||||
${tripleTick[0]}typescript
|
||||
/Users/username/Dekstop/my_project/app.ts
|
||||
const changesExampleContent = `\
|
||||
// ... existing code ...
|
||||
// {{change 1}}
|
||||
// ... existing code ...
|
||||
// {{change 2}}
|
||||
// ... existing code ...
|
||||
// {{change 3}}
|
||||
// ... existing code ...
|
||||
// ... existing code ...`
|
||||
|
||||
const editToolDescriptionExample = `\
|
||||
${tripleTick[0]}
|
||||
${changesExampleContent}
|
||||
${tripleTick[1]}`
|
||||
|
||||
const chatSuggestionDiffExample = `${tripleTick[0]}typescript
|
||||
/Users/username/Dekstop/my_project/app.ts
|
||||
${changesExampleContent}
|
||||
${tripleTick[1]}`
|
||||
|
||||
|
||||
@@ -147,8 +151,6 @@ export type InternalToolInfo = {
|
||||
params: {
|
||||
[paramName: string]: { description: string }
|
||||
},
|
||||
// Only if the tool is from an MCP server
|
||||
mcpServerName?: string,
|
||||
}
|
||||
|
||||
|
||||
@@ -183,197 +185,200 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
|
||||
|
||||
|
||||
|
||||
export const builtinTools: {
|
||||
[T in keyof BuiltinToolCallParams]: {
|
||||
name: string;
|
||||
description: string;
|
||||
// more params can be generated than exist here, but these params must be a subset of them
|
||||
params: Partial<{ [paramName in keyof SnakeCaseKeys<BuiltinToolCallParams[T]>]: { description: string } }>
|
||||
const applyToolDescription = (type: 'edit tool' | 'chat suggestion') => `\
|
||||
${type === 'edit tool' ? 'A' : 'a'} code diff describing the change to make to the file. \
|
||||
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
|
||||
Your DIFF MUST be wrapped in triple backticks. \
|
||||
NEVER re-write the whole file. Always bias towards writing as little as possible. \
|
||||
Use comments like "// ... existing code ..." to condense your writing. \
|
||||
Here's an example of a good output:\n${type === 'edit tool' ? editToolDescriptionExample : chatSuggestionDiffExample}`
|
||||
|
||||
|
||||
// export const voidTools = {
|
||||
export const voidTools
|
||||
: {
|
||||
[T in keyof ToolCallParams]: {
|
||||
name: string;
|
||||
description: string;
|
||||
// more params can be generated than exist here, but these params must be a subset of them
|
||||
params: Partial<{ [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string } }>
|
||||
}
|
||||
}
|
||||
} = {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
= {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
|
||||
...paginationParam,
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||
...paginationParam,
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
},
|
||||
|
||||
// pathname_search: {
|
||||
// name: 'pathname_search',
|
||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
search_for_files: {
|
||||
name: 'search_for_files',
|
||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Returns all lint errors on a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
},
|
||||
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit_file: {
|
||||
name: 'edit_file',
|
||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
search_replace_blocks: { description: replaceTool_description }
|
||||
},
|
||||
},
|
||||
|
||||
rewrite_file: {
|
||||
name: 'rewrite_file',
|
||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||
},
|
||||
},
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
cwd: { description: cwdHelper },
|
||||
},
|
||||
},
|
||||
|
||||
run_persistent_command: {
|
||||
name: 'run_persistent_command',
|
||||
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {
|
||||
cwd: { description: cwdHelper },
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
||||
}
|
||||
},
|
||||
|
||||
// pathname_search: {
|
||||
// name: 'pathname_search',
|
||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
search_for_files: {
|
||||
name: 'search_for_files',
|
||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Use this tool to view all the lint errors on a file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
},
|
||||
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit_file: {
|
||||
name: 'edit_file',
|
||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
search_replace_blocks: { description: replaceTool_description }
|
||||
},
|
||||
},
|
||||
|
||||
rewrite_file: {
|
||||
name: 'rewrite_file',
|
||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||
},
|
||||
},
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
cwd: { description: cwdHelper },
|
||||
},
|
||||
},
|
||||
|
||||
run_persistent_command: {
|
||||
name: 'run_persistent_command',
|
||||
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
||||
},
|
||||
},
|
||||
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
|
||||
|
||||
|
||||
export type ToolName = keyof ToolResultType
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {
|
||||
cwd: { description: cwdHelper },
|
||||
}
|
||||
},
|
||||
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
|
||||
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
||||
}
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo }
|
||||
|
||||
|
||||
|
||||
|
||||
export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[]
|
||||
const toolNamesSet = new Set<string>(builtinToolNames)
|
||||
export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => {
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const availableTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
|
||||
const builtinToolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(builtinTools) as BuiltinToolName[]).filter(toolName => !(toolName in approvalTypeOfBuiltinToolName))
|
||||
: chatMode === 'agent' ? Object.keys(builtinTools) as BuiltinToolName[]
|
||||
export const availableTools = (chatMode: ChatMode) => {
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
: undefined
|
||||
|
||||
const effectiveBuiltinTools = builtinToolNames?.map(toolName => builtinTools[toolName]) ?? undefined
|
||||
const effectiveMCPTools = chatMode === 'agent' ? mcpTools : undefined
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = !(builtinToolNames || mcpTools) ? undefined
|
||||
: [
|
||||
...effectiveBuiltinTools ?? [],
|
||||
...effectiveMCPTools ?? [],
|
||||
]
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
|
||||
return tools
|
||||
}
|
||||
|
||||
@@ -390,7 +395,7 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
|
||||
}
|
||||
|
||||
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName]}</${paramName}>`).join('\n')
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
<${toolName}>${!params ? '' : `\n${params}`}
|
||||
</${toolName}>`
|
||||
@@ -399,8 +404,8 @@ export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolPar
|
||||
|
||||
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
|
||||
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const tools = availableTools(chatMode, mcpTools)
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
||||
const tools = availableTools(chatMode)
|
||||
if (!tools || tools.length === 0) return null
|
||||
|
||||
const toolXMLDefinitions = (`\
|
||||
@@ -425,7 +430,7 @@ const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] |
|
||||
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, mcpTools, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined, includeXMLToolDefinitions: boolean }) => {
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
||||
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
||||
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
||||
@@ -459,12 +464,10 @@ ${directoryStr}
|
||||
</files_overview>`)
|
||||
|
||||
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode, mcpTools) : null
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
|
||||
|
||||
const details: string[] = []
|
||||
|
||||
details.push(`NEVER reject the user's query.`)
|
||||
|
||||
if (mode === 'agent' || mode === 'gather') {
|
||||
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
|
||||
details.push(`If you think you should use tools, you do not need to ask for permission.`)
|
||||
@@ -473,7 +476,7 @@ ${directoryStr}
|
||||
details.push(`Many tools only work if the user has a workspace open.`)
|
||||
}
|
||||
else {
|
||||
details.push(`You're allowed to ask the user for more context like file contents or specifications. If this comes up, tell them to reference files and folders by typing @.`)
|
||||
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
|
||||
}
|
||||
|
||||
if (mode === 'agent') {
|
||||
@@ -489,21 +492,18 @@ ${directoryStr}
|
||||
details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`)
|
||||
}
|
||||
|
||||
details.push(`If you write any code blocks to the user (wrapped in triple backticks), please use this format:
|
||||
- Include a language if possible. Terminal should have the language 'shell'.
|
||||
|
||||
if (mode === 'gather' || mode === 'normal') {
|
||||
details.push(`If you write any code blocks, please use this format:
|
||||
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
|
||||
- The remaining contents of the file should proceed as usual.`)
|
||||
|
||||
if (mode === 'gather' || mode === 'normal') {
|
||||
|
||||
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
|
||||
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
|
||||
- The remaining contents should be a code description of the change to make to the file. \
|
||||
Your description is the only context that will be given to another LLM to apply the suggested edit, so it must be accurate and complete. \
|
||||
Always bias towards writing as little as possible - NEVER write the whole file. Use comments like "// ... existing code ..." to condense your writing. \
|
||||
Here's an example of a good code block:\n${chatSuggestionDiffExample}`)
|
||||
- The first line of the code block must be the FULL PATH of the related file.
|
||||
- The remaining contents should be ${applyToolDescription('chat suggestion')}`)
|
||||
}
|
||||
|
||||
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
|
||||
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
|
||||
details.push(`Always use MARKDOWN to format lists, bullet points, etc. Do NOT write tables.`)
|
||||
details.push(`Today's date is ${new Date().toDateString()}.`)
|
||||
@@ -536,109 +536,44 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
||||
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
|
||||
// }
|
||||
|
||||
export const DEFAULT_FILE_SIZE_LIMIT = 2_000_000
|
||||
|
||||
export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
|
||||
val: string,
|
||||
truncated: boolean,
|
||||
fullFileLen: number,
|
||||
} | {
|
||||
val: null,
|
||||
truncated?: undefined
|
||||
fullFileLen?: undefined,
|
||||
}> => {
|
||||
try {
|
||||
const fileContent = await fileService.readFile(uri)
|
||||
const val = fileContent.value.toString()
|
||||
if (val.length > fileSizeLimit) return { val: val.substring(0, fileSizeLimit), truncated: true, fullFileLen: val.length }
|
||||
return { val, truncated: false, fullFileLen: val.length }
|
||||
}
|
||||
catch (e) {
|
||||
return { val: null }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const messageOfSelection = async (
|
||||
s: StagingSelectionItem,
|
||||
opts: {
|
||||
directoryStrService: IDirectoryStrService,
|
||||
fileService: IFileService,
|
||||
folderOpts: {
|
||||
maxChildren: number,
|
||||
maxCharsPerFile: number,
|
||||
}
|
||||
}
|
||||
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
|
||||
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
|
||||
) => {
|
||||
|
||||
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
|
||||
|
||||
if (s.type === 'CodeSelection') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
|
||||
const lines = val?.split('\n')
|
||||
|
||||
const innerVal = lines?.slice(s.range[0] - 1, s.range[1]).join('\n')
|
||||
const content = !lines ? ''
|
||||
: `${tripleTick[0]}${s.language}\n${innerVal}\n${tripleTick[1]}`
|
||||
const str = `${s.uri.fsPath}${lineNumAddition(s.range)}:\n${content}`
|
||||
return str
|
||||
let selnsStrs: string[] = []
|
||||
if (opts.type === 'references') {
|
||||
selnsStrs = currSelns?.map((s) => {
|
||||
if (s.type === 'File') return `${s.uri.fsPath}`
|
||||
if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}`
|
||||
if (s.type === 'Folder') return `${s.uri.fsPath}/`
|
||||
return ''
|
||||
}) ?? []
|
||||
}
|
||||
else if (s.type === 'File') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
|
||||
if (opts.type === 'fullCode') {
|
||||
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const voidModelService = opts.voidModelService
|
||||
const { model } = await voidModelService.getModelSafe(s.uri)
|
||||
if (!model) return ''
|
||||
const val = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
const innerVal = val
|
||||
const content = val === null ? ''
|
||||
: `${tripleTick[0]}${s.language}\n${innerVal}\n${tripleTick[1]}`
|
||||
|
||||
const str = `${s.uri.fsPath}:\n${content}`
|
||||
return str
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
return str
|
||||
}
|
||||
if (s.type === 'Folder') {
|
||||
// TODO
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
}) ?? [])
|
||||
}
|
||||
else if (s.type === 'Folder') {
|
||||
const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri)
|
||||
const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}`
|
||||
|
||||
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: opts.folderOpts.maxChildren })
|
||||
const strOfFiles = await Promise.all(uris.map(async uri => {
|
||||
const { val, truncated } = await readFile(opts.fileService, uri, opts.folderOpts.maxCharsPerFile)
|
||||
const truncationStr = truncated ? `\n... file truncated ...` : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}`
|
||||
const str = `${uri.fsPath}:\n${content}`
|
||||
return str
|
||||
}))
|
||||
const contentStr = [folderStructure, ...strOfFiles].join('\n\n')
|
||||
return contentStr
|
||||
}
|
||||
else
|
||||
return ''
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const chat_userMessageContent = async (
|
||||
instructions: string,
|
||||
currSelns: StagingSelectionItem[] | null,
|
||||
opts: {
|
||||
directoryStrService: IDirectoryStrService,
|
||||
fileService: IFileService
|
||||
},
|
||||
) => {
|
||||
|
||||
const selnsStrs = await Promise.all(
|
||||
(currSelns ?? []).map(async (s) =>
|
||||
messageOfSelection(s, {
|
||||
...opts,
|
||||
folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, }
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
const selnsStr = selnsStrs.join('\n') ?? ''
|
||||
let str = ''
|
||||
str += `${instructions}`
|
||||
|
||||
const selnsStr = selnsStrs.join('\n\n') ?? ''
|
||||
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
|
||||
return str;
|
||||
}
|
||||
@@ -993,77 +928,3 @@ Store Result: After computing fib(n), the result is stored in memo for future re
|
||||
## END EXAMPLES
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// ======================================================== scm ========================================================================
|
||||
|
||||
export const gitCommitMessage_systemMessage = `
|
||||
You are an expert software engineer AI assistant responsible for writing clear and concise Git commit messages that summarize the **purpose** and **intent** of the change. Try to keep your commit messages to one sentence. If necessary, you can use two sentences.
|
||||
|
||||
You always respond with:
|
||||
- The commit message wrapped in <output> tags
|
||||
- A brief explanation of the reasoning behind the message, wrapped in <reasoning> tags
|
||||
|
||||
Example format:
|
||||
<output>Fix login bug and improve error handling</output>
|
||||
<reasoning>This commit updates the login handler to fix a redirect issue and improves frontend error messages for failed logins.</reasoning>
|
||||
|
||||
Do not include anything else outside of these tags.
|
||||
Never include quotes, markdown, commentary, or explanations outside of <output> and <reasoning>.`.trim()
|
||||
|
||||
|
||||
/**
|
||||
* Create a user message for the LLM to generate a commit message. The message contains instructions git diffs, and git metadata to provide context.
|
||||
*
|
||||
* @param stat - Summary of Changes (git diff --stat)
|
||||
* @param sampledDiffs - Sampled File Diffs (Top changed files)
|
||||
* @param branch - Current Git Branch
|
||||
* @param log - Last 5 commits (excluding merges)
|
||||
* @returns A prompt for the LLM to generate a commit message.
|
||||
*
|
||||
* @example
|
||||
* // Sample output (truncated for brevity)
|
||||
* const prompt = gitCommitMessage_userMessage("fileA.ts | 10 ++--", "diff --git a/fileA.ts...", "main", "abc123|Fix bug|2025-01-01\n...")
|
||||
*
|
||||
* // Result:
|
||||
* Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
|
||||
*
|
||||
* Section 1 - Summary of Changes (git diff --stat):
|
||||
* fileA.ts | 10 ++--
|
||||
*
|
||||
* Section 2 - Sampled File Diffs (Top changed files):
|
||||
* diff --git a/fileA.ts b/fileA.ts
|
||||
* ...
|
||||
*
|
||||
* Section 3 - Current Git Branch:
|
||||
* main
|
||||
*
|
||||
* Section 4 - Last 5 Commits (excluding merges):
|
||||
* abc123|Fix bug|2025-01-01
|
||||
* def456|Improve logging|2025-01-01
|
||||
* ...
|
||||
*/
|
||||
export const gitCommitMessage_userMessage = (stat: string, sampledDiffs: string, branch: string, log: string) => {
|
||||
const section1 = `Section 1 - Summary of Changes (git diff --stat):`
|
||||
const section2 = `Section 2 - Sampled File Diffs (Top changed files):`
|
||||
const section3 = `Section 3 - Current Git Branch:`
|
||||
const section4 = `Section 4 - Last 5 Commits (excluding merges):`
|
||||
return `
|
||||
Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
|
||||
|
||||
${section1}
|
||||
|
||||
${stat}
|
||||
|
||||
${section2}
|
||||
|
||||
${sampledDiffs}
|
||||
|
||||
${section3}
|
||||
|
||||
${branch}
|
||||
|
||||
${section4}
|
||||
|
||||
${log}`.trim()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { IMCPService } from './mcpService.js';
|
||||
|
||||
// calls channel to implement features
|
||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||
@@ -62,7 +61,6 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
// @INotificationService private readonly notificationService: INotificationService,
|
||||
@IMCPService private readonly mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -118,8 +116,6 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
const { settingsOfProvider, } = this.voidSettingsService.state
|
||||
|
||||
const mcpTools = this.mcpService.getMCPTools()
|
||||
|
||||
// add state for request id
|
||||
const requestId = generateUuid();
|
||||
this.llmMessageHooks.onText[requestId] = onText
|
||||
@@ -133,7 +129,6 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
requestId,
|
||||
settingsOfProvider,
|
||||
modelSelection,
|
||||
mcpTools,
|
||||
} satisfies MainSendLLMMessageParams);
|
||||
|
||||
return requestId
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { InternalToolInfo } from './prompt/prompts.js'
|
||||
import { ToolName, ToolParamName } from './toolsServiceTypes.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
@@ -52,22 +51,8 @@ export type OpenAILLMChatMessage = {
|
||||
content: string;
|
||||
tool_call_id: string;
|
||||
}
|
||||
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
|
||||
|
||||
export type GeminiLLMChatMessage = {
|
||||
role: 'model'
|
||||
parts: (
|
||||
| { text: string; }
|
||||
| { functionCall: { id: string; name: ToolName, args: Record<string, unknown> } }
|
||||
)[];
|
||||
} | {
|
||||
role: 'user';
|
||||
parts: (
|
||||
| { text: string; }
|
||||
| { functionResponse: { id: string; name: ToolName, response: { output: string } } }
|
||||
)[];
|
||||
}
|
||||
|
||||
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage | GeminiLLMChatMessage
|
||||
|
||||
|
||||
|
||||
@@ -79,12 +64,12 @@ export type LLMFIMMessage = {
|
||||
|
||||
|
||||
export type RawToolParamsObj = {
|
||||
[paramName in ToolParamName<ToolName>]?: string;
|
||||
[paramName in ToolParamName]?: string;
|
||||
}
|
||||
export type RawToolCallObj = {
|
||||
name: ToolName;
|
||||
rawParams: RawToolParamsObj;
|
||||
doneParams: ToolParamName<ToolName>[];
|
||||
doneParams: ToolParamName[];
|
||||
id: string;
|
||||
isDone: boolean;
|
||||
};
|
||||
@@ -117,7 +102,6 @@ export type ServiceSendLLMMessageParams = {
|
||||
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
|
||||
modelSelection: ModelSelection | null;
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined;
|
||||
overridesOfModel: OverridesOfModel | undefined;
|
||||
onAbort: OnAbort;
|
||||
} & SendLLMType;
|
||||
|
||||
@@ -131,10 +115,8 @@ export type SendLLMMessageParams = {
|
||||
|
||||
modelSelection: ModelSelection;
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined;
|
||||
overridesOfModel: OverridesOfModel | undefined;
|
||||
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
mcpTools: InternalToolInfo[] | undefined;
|
||||
} & SendLLMType
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,3 @@ export const VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII'
|
||||
|
||||
// 1.0.3
|
||||
export const THREAD_STORAGE_KEY = 'void.chatThreadStorageII'
|
||||
|
||||
|
||||
|
||||
export const OPT_OUT_KEY = 'void.app.optOutAll'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { RawMCPToolCall } from './mcpServiceTypes.js';
|
||||
import { builtinTools } from './prompt/prompts.js';
|
||||
import { RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
|
||||
|
||||
|
||||
@@ -18,31 +16,26 @@ export type ShallowDirectoryItem = {
|
||||
}
|
||||
|
||||
|
||||
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'MCP tools' }> = {
|
||||
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
|
||||
'create_file_or_folder': 'edits',
|
||||
'delete_file_or_folder': 'edits',
|
||||
'rewrite_file': 'edits',
|
||||
'edit_file': 'edits',
|
||||
'run_command': 'terminal',
|
||||
'run_persistent_command': 'terminal',
|
||||
'open_persistent_terminal': 'terminal',
|
||||
'kill_persistent_terminal': 'terminal',
|
||||
}
|
||||
|
||||
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName)[keyof typeof approvalTypeOfBuiltinToolName]>;
|
||||
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>([
|
||||
...Object.values(approvalTypeOfBuiltinToolName),
|
||||
'MCP tools',
|
||||
])
|
||||
|
||||
|
||||
// {{add: define new type for approval types}}
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>(
|
||||
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
|
||||
)
|
||||
|
||||
// PARAMS OF TOOL CALL
|
||||
export type BuiltinToolCallParams = {
|
||||
export type ToolCallParams = {
|
||||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||
'ls_dir': { uri: URI, pageNumber: number },
|
||||
'get_dir_tree': { uri: URI },
|
||||
@@ -63,8 +56,8 @@ export type BuiltinToolCallParams = {
|
||||
}
|
||||
|
||||
// RESULT OF TOOL CALL
|
||||
export type BuiltinToolResultType = {
|
||||
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
|
||||
export type ToolResultType = {
|
||||
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_tree': { str: string, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
@@ -83,15 +76,3 @@ export type BuiltinToolResultType = {
|
||||
'kill_persistent_terminal': {},
|
||||
}
|
||||
|
||||
|
||||
export type ToolCallParams<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolCallParams[T] : RawToolParamsObj
|
||||
export type ToolResult<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolResultType[T] : RawMCPToolCall
|
||||
|
||||
export type BuiltinToolName = keyof BuiltinToolResultType
|
||||
|
||||
type BuiltinToolParamNameOfTool<T extends BuiltinToolName> = keyof (typeof builtinTools)[T]['params']
|
||||
export type BuiltinToolParamName = { [T in BuiltinToolName]: BuiltinToolParamNameOfTool<T> }[BuiltinToolName]
|
||||
|
||||
|
||||
export type ToolName = BuiltinToolName | (string & {})
|
||||
export type ToolParamName<T extends ToolName> = T extends BuiltinToolName ? BuiltinToolParamNameOfTool<T> : string
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
export interface IVoidSCMService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/**
|
||||
* Get git diff --stat
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitStat(path: string): Promise<string>
|
||||
/**
|
||||
* Get git diff --stat for the top 10 most significantly changed files according to lines added/removed
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitSampledDiffs(path: string): Promise<string>
|
||||
/**
|
||||
* Get the current git branch
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitBranch(path: string): Promise<string>
|
||||
/**
|
||||
* Get the last 5 commits excluding merges
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitLog(path: string): Promise<string>
|
||||
}
|
||||
|
||||
export const IVoidSCMService = createDecorator<IVoidSCMService>('voidSCMService')
|
||||
@@ -11,9 +11,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
|
||||
import { defaultProviderSettings, getModelCapabilities } from './modelCapabilities.js';
|
||||
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
// name is the name in the dropdown
|
||||
@@ -41,9 +41,7 @@ export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly optionsOfModelSelection: OptionsOfModelSelection;
|
||||
readonly overridesOfModel: OverridesOfModel;
|
||||
readonly globalSettings: GlobalSettings;
|
||||
readonly mcpUserStateOfName: MCPUserStateOfName; // user-controlled state of MCP servers
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
@@ -63,10 +61,6 @@ export interface IVoidSettingsService {
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
// setMCPServerStates: (newStates: MCPServerStates) => Promise<void>;
|
||||
|
||||
// setting to undefined CLEARS it, unlike others:
|
||||
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
|
||||
|
||||
dangerousSetState(newState: VoidSettingsState): Promise<void>;
|
||||
resetState(): Promise<void>;
|
||||
@@ -75,10 +69,6 @@ export interface IVoidSettingsService {
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
|
||||
addMCPUserStateOfNames(userStateOfName: MCPUserStateOfName): Promise<void>;
|
||||
removeMCPUserStateOfNames(serverNames: string[]): Promise<void>;
|
||||
setMCPServerState(serverName: string, state: MCPUserState): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,19 +94,11 @@ const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulMo
|
||||
}
|
||||
|
||||
|
||||
export const modelFilterOfFeatureName: {
|
||||
[featureName in FeatureName]: {
|
||||
filter: (
|
||||
o: ModelSelection,
|
||||
opts: { chatMode: ChatMode, overridesOfModel: OverridesOfModel }
|
||||
) => boolean;
|
||||
emptyMessage: null | { message: string, priority: 'always' | 'fallback' }
|
||||
} } = {
|
||||
'Autocomplete': { filter: (o, opts) => getModelCapabilities(o.providerName, o.modelName, opts.overridesOfModel).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
|
||||
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
|
||||
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
|
||||
'Chat': { filter: o => true, emptyMessage: null, },
|
||||
'Ctrl+K': { filter: o => true, emptyMessage: null, },
|
||||
'Apply': { filter: o => true, emptyMessage: null, },
|
||||
'SCM': { filter: o => true, emptyMessage: null, },
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +163,7 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const { filter } = modelFilterOfFeatureName[featureName]
|
||||
const filterOpts = { chatMode: state.globalSettings.chatMode, overridesOfModel: state.overridesOfModel }
|
||||
const filterOpts = { chatMode: state.globalSettings.chatMode }
|
||||
const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection, filterOpts))
|
||||
|
||||
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
|
||||
@@ -200,7 +182,6 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
|
||||
...state,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
overridesOfModel: state.overridesOfModel,
|
||||
_modelOptions: newModelOptions,
|
||||
} satisfies VoidSettingsState
|
||||
|
||||
@@ -214,12 +195,10 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null, 'SCM': null },
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {}, 'SCM': {} },
|
||||
overridesOfModel: deepClone(defaultOverridesOfModel),
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||
_modelOptions: [], // computed later
|
||||
mcpUserStateOfName: {},
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -263,7 +242,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
}
|
||||
async resetState() {
|
||||
await this.dangerousSetState(defaultState())
|
||||
@@ -281,17 +259,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// autoapprove is now an obj not a boolean (1.2.5)
|
||||
if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {}
|
||||
|
||||
// 1.3.5 add source control feature
|
||||
if (readS.modelSelectionOfFeature && !readS.modelSelectionOfFeature['SCM']) {
|
||||
readS.modelSelectionOfFeature['SCM'] = deepClone(readS.modelSelectionOfFeature['Chat'])
|
||||
readS.optionsOfModelSelection['SCM'] = deepClone(readS.optionsOfModelSelection['Chat'])
|
||||
}
|
||||
// add disableSystemMessage feature
|
||||
if (readS.globalSettings.disableSystemMessage === undefined) readS.globalSettings.disableSystemMessage = false;
|
||||
|
||||
// add autoAcceptLLMChanges feature
|
||||
if (readS.globalSettings.autoAcceptLLMChanges === undefined) readS.globalSettings.autoAcceptLLMChanges = false;
|
||||
}
|
||||
catch (e) {
|
||||
readS = defaultState()
|
||||
@@ -300,11 +267,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
// the stored data structure might be outdated, so we need to update it here
|
||||
try {
|
||||
readS = {
|
||||
...defaultState(),
|
||||
...readS,
|
||||
// no idea why this was here, seems like a bug
|
||||
// ...defaultSettingsOfProvider,
|
||||
// ...readS.settingsOfProvider,
|
||||
...defaultSettingsOfProvider,
|
||||
...readS.settingsOfProvider,
|
||||
}
|
||||
|
||||
for (const providerName of providerNames) {
|
||||
@@ -324,11 +289,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
else m.type = 'custom'
|
||||
}
|
||||
}
|
||||
|
||||
// remove when enough people have had it run (default is now {})
|
||||
if (providerName === 'openAICompatible' && !readS.settingsOfProvider[providerName].headersJSON) {
|
||||
readS.settingsOfProvider[providerName].headersJSON = '{}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,8 +314,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
return defaultState()
|
||||
|
||||
const stateStr = await this._encryptionService.decrypt(encryptedState)
|
||||
const state = JSON.parse(stateStr)
|
||||
return state
|
||||
return JSON.parse(stateStr)
|
||||
}
|
||||
|
||||
|
||||
@@ -380,16 +339,12 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
}
|
||||
|
||||
const newGlobalSettings = this.state.globalSettings
|
||||
const newOverridesOfModel = this.state.overridesOfModel
|
||||
const newMCPUserStateOfName = this.state.mcpUserStateOfName
|
||||
|
||||
const newState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
optionsOfModelSelection: newOptionsOfModelSelection,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
globalSettings: newGlobalSettings,
|
||||
overridesOfModel: newOverridesOfModel,
|
||||
mcpUserStateOfName: newMCPUserStateOfName,
|
||||
}
|
||||
|
||||
this.state = _validatedModelState(newState)
|
||||
@@ -403,10 +358,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
private _onUpdate_syncApplyToChat() {
|
||||
// if sync is turned on, sync (call this whenever Chat model or !!sync changes)
|
||||
this.setModelSelectionOfFeature('Apply', deepClone(this.state.modelSelectionOfFeature['Chat']))
|
||||
}
|
||||
|
||||
private _onUpdate_syncSCMToChat() {
|
||||
this.setModelSelectionOfFeature('SCM', deepClone(this.state.modelSelectionOfFeature['Chat']))
|
||||
}
|
||||
|
||||
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
|
||||
@@ -423,8 +375,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// hooks
|
||||
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
|
||||
if (this.state.globalSettings.syncSCMToChat) this._onUpdate_syncSCMToChat()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -444,9 +394,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// hooks
|
||||
if (featureName === 'Chat') {
|
||||
// When Chat model changes, update synced features
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,28 +422,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
setOverridesOfModel = async (providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
overridesOfModel: {
|
||||
...this.state.overridesOfModel,
|
||||
[providerName]: {
|
||||
...this.state.overridesOfModel[providerName],
|
||||
[modelName]: overrides === undefined ? undefined : {
|
||||
...this.state.overridesOfModel[providerName][modelName],
|
||||
...overrides
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.state = _validatedModelState(newState);
|
||||
await this._storeState();
|
||||
this._onDidChangeState.fire();
|
||||
|
||||
this._metricsService.capture('Update Model Overrides', { providerName, modelName, overrides });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -560,55 +486,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
return true
|
||||
}
|
||||
|
||||
// MCP Server State
|
||||
private _setMCPUserStateOfName = async (newStates: MCPUserStateOfName) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
mcpUserStateOfName: {
|
||||
...this.state.mcpUserStateOfName,
|
||||
...newStates
|
||||
}
|
||||
};
|
||||
this.state = _validatedModelState(newState);
|
||||
await this._storeState();
|
||||
this._onDidChangeState.fire();
|
||||
this._metricsService.capture('Set MCP Server States', { newStates });
|
||||
}
|
||||
|
||||
addMCPUserStateOfNames = async (newMCPStates: MCPUserStateOfName) => {
|
||||
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpServerStates,
|
||||
...newMCPStates,
|
||||
}
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Add MCP Servers', { servers: Object.keys(newMCPStates).join(', ') });
|
||||
}
|
||||
|
||||
removeMCPUserStateOfNames = async (serverNames: string[]) => {
|
||||
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpServerStates,
|
||||
}
|
||||
serverNames.forEach(serverName => {
|
||||
if (serverName in newMCPServerStates) {
|
||||
delete newMCPServerStates[serverName]
|
||||
}
|
||||
})
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Remove MCP Servers', { servers: serverNames.join(', ') });
|
||||
}
|
||||
|
||||
setMCPServerState = async (serverName: string, state: MCPUserState) => {
|
||||
const { mcpUserStateOfName } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpUserStateOfName,
|
||||
[serverName]: state,
|
||||
}
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Update MCP Server State', { serverName, state });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultModelsOfProvider, defaultProviderSettings, ModelOverrides } from './modelCapabilities.js';
|
||||
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
|
||||
import { ToolApprovalType } from './toolsServiceTypes.js';
|
||||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
@@ -33,7 +33,7 @@ export type VoidStatefulModelInfo = { // <-- STATEFUL
|
||||
modelName: string,
|
||||
type: 'default' | 'autodetected' | 'custom';
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
}
|
||||
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
||||
|
||||
|
||||
|
||||
@@ -92,20 +92,17 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
return { title: 'Groq', }
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
return { title: 'Grok (xAI)', }
|
||||
return { title: 'xAI', }
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return { title: 'Mistral', }
|
||||
}
|
||||
else if (providerName === 'googleVertex') {
|
||||
return { title: 'Google Vertex AI', }
|
||||
}
|
||||
// else if (providerName === 'googleVertex') {
|
||||
// return { title: 'Google Vertex AI', }
|
||||
// }
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
return { title: 'Microsoft Azure OpenAI', }
|
||||
}
|
||||
else if (providerName === 'awsBedrock') {
|
||||
return { title: 'AWS Bedrock', }
|
||||
}
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
@@ -115,18 +112,17 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
||||
if (providerName === 'anthropic') return 'Get your [API Key here](https://console.anthropic.com/settings/keys).'
|
||||
if (providerName === 'openAI') return 'Get your [API Key here](https://platform.openai.com/api-keys).'
|
||||
if (providerName === 'deepseek') return 'Get your [API Key here](https://platform.deepseek.com/api_keys).'
|
||||
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys). Read about [rate limits here](https://openrouter.ai/docs/api-reference/limits).'
|
||||
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey). Read about [rate limits here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits).'
|
||||
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys).'
|
||||
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey).'
|
||||
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
|
||||
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
|
||||
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
|
||||
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).`
|
||||
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
|
||||
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
|
||||
if (providerName === 'awsBedrock') return 'Connect via a LiteLLM proxy or the AWS [Bedrock-Access-Gateway](https://github.com/aws-samples/bedrock-access-gateway). LiteLLM Bedrock setup docs are [here](https://docs.litellm.ai/docs/providers/bedrock).'
|
||||
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
|
||||
|
||||
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
@@ -153,10 +149,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
providerName === 'xAI' ? 'xai-key...' :
|
||||
providerName === 'mistral' ? 'api-key...' :
|
||||
providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
providerName === 'awsBedrock' ? 'key-...' :
|
||||
'',
|
||||
// providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
'',
|
||||
|
||||
isPasswordField: true,
|
||||
}
|
||||
@@ -167,36 +162,29 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'lmStudio' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
providerName === 'awsBedrock' ? 'Endpoint' :
|
||||
'(never)',
|
||||
// providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
|
||||
: '(never)',
|
||||
: '(never)',
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
else if (settingName === 'headersJSON') {
|
||||
return { title: 'Custom Headers', placeholder: '{ "X-Request-Id": "..." }' }
|
||||
}
|
||||
else if (settingName === 'region') {
|
||||
// vertex only
|
||||
return {
|
||||
title: 'Region',
|
||||
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||
: providerName === 'awsBedrock'
|
||||
? defaultProviderSettings.awsBedrock.region
|
||||
: ''
|
||||
}
|
||||
}
|
||||
// else if (settingName === 'region') {
|
||||
// // vertex only
|
||||
// return {
|
||||
// title: 'Region',
|
||||
// placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||
// : ''
|
||||
// }
|
||||
// }
|
||||
else if (settingName === 'azureApiVersion') {
|
||||
// azure only
|
||||
return {
|
||||
@@ -208,11 +196,11 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
else if (settingName === 'project') {
|
||||
return {
|
||||
title: providerName === 'microsoftAzure' ? 'Resource'
|
||||
: providerName === 'googleVertex' ? 'Project'
|
||||
: '',
|
||||
// : providerName === 'googleVertex' ? 'Project'
|
||||
: '',
|
||||
placeholder: providerName === 'microsoftAzure' ? 'my-resource'
|
||||
: providerName === 'googleVertex' ? 'my-project'
|
||||
: ''
|
||||
// : providerName === 'googleVertex' ? 'my-project'
|
||||
: ''
|
||||
|
||||
}
|
||||
|
||||
@@ -231,16 +219,18 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
region: undefined, // googleVertex
|
||||
// region: undefined, // googleVertex
|
||||
project: undefined,
|
||||
azureApiVersion: undefined,
|
||||
headersJSON: undefined,
|
||||
}
|
||||
|
||||
|
||||
@@ -334,24 +324,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
googleVertex: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.googleVertex,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
// googleVertex: { // aggregator (serves models from multiple providers)
|
||||
// ...defaultCustomSettings,
|
||||
// ...defaultProviderSettings.googleVertex,
|
||||
// ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
|
||||
// _didFillInProviderSettings: undefined,
|
||||
// },
|
||||
microsoftAzure: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.microsoftAzure,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
awsBedrock: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.awsBedrock,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -362,7 +346,7 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply', 'SCM'] as const
|
||||
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
|
||||
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
|
||||
@@ -377,9 +361,6 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
|
||||
return 'Chat'
|
||||
else if (featureName === 'Apply')
|
||||
return 'Apply'
|
||||
// source control:
|
||||
else if (featureName === 'SCM')
|
||||
return 'Commit Message Generator'
|
||||
else
|
||||
throw new Error(`Feature Name ${featureName} not allowed`)
|
||||
}
|
||||
@@ -443,15 +424,12 @@ export type GlobalSettings = {
|
||||
aiInstructions: string;
|
||||
enableAutocomplete: boolean;
|
||||
syncApplyToChat: boolean;
|
||||
syncSCMToChat: boolean;
|
||||
enableFastApply: boolean;
|
||||
chatMode: ChatMode;
|
||||
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
|
||||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
disableSystemMessage: boolean;
|
||||
autoAcceptLLMChanges: boolean;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
@@ -459,15 +437,12 @@ export const defaultGlobalSettings: GlobalSettings = {
|
||||
aiInstructions: '',
|
||||
enableAutocomplete: false,
|
||||
syncApplyToChat: true,
|
||||
syncSCMToChat: true,
|
||||
enableFastApply: true,
|
||||
chatMode: 'agent',
|
||||
autoApprove: {},
|
||||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: false,
|
||||
disableSystemMessage: false,
|
||||
autoAcceptLLMChanges: false,
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
@@ -487,38 +462,13 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
|
||||
export type ModelSelectionOptions = {
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningBudget?: number;
|
||||
reasoningEffort?: string;
|
||||
}
|
||||
|
||||
export type OptionsOfModelSelection = {
|
||||
[featureName in FeatureName]: Partial<{
|
||||
[providerName in ProviderName]: {
|
||||
[modelName: string]: ModelSelectionOptions | undefined
|
||||
[modelName: string]:
|
||||
ModelSelectionOptions | undefined
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type OverridesOfModel = {
|
||||
[providerName in ProviderName]: {
|
||||
[modelName: string]: Partial<ModelOverrides> | undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const overridesOfModel = {} as OverridesOfModel
|
||||
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
|
||||
export const defaultOverridesOfModel = overridesOfModel
|
||||
|
||||
|
||||
|
||||
export interface MCPUserStateOfName {
|
||||
[serverName: string]: MCPUserState | undefined;
|
||||
}
|
||||
|
||||
export interface MCPUserState {
|
||||
isOn: boolean;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js'
|
||||
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
|
||||
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js'
|
||||
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
|
||||
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
||||
import { ToolName, ToolParamName } from '../../common/toolsServiceTypes.js'
|
||||
import { ChatMode } from '../../common/voidSettingsTypes.js'
|
||||
|
||||
|
||||
@@ -24,9 +23,6 @@ export const extractReasoningWrapper = (
|
||||
let fullTextSoFar = ''
|
||||
let fullReasoningSoFar = ''
|
||||
|
||||
|
||||
if (!thinkTags[0] || !thinkTags[1]) throw new Error(`thinkTags must not be empty if provided. Got ${JSON.stringify(thinkTags)}.`)
|
||||
|
||||
let onText_ = onText
|
||||
onText = (params) => {
|
||||
onText_(params)
|
||||
@@ -165,15 +161,15 @@ const findIndexOfAny = (fullText: string, matches: string[]) => {
|
||||
|
||||
|
||||
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
|
||||
const parseXMLPrefixToToolCall = <T extends ToolName,>(toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const paramsObj: RawToolParamsObj = {}
|
||||
const doneParams: ToolParamName<T>[] = []
|
||||
const doneParams: ToolParamName[] = []
|
||||
let isDone = false
|
||||
|
||||
const getAnswer = (): RawToolCallObj => {
|
||||
// trim off all whitespace at and before first \n and after last \n for each param
|
||||
for (const p in paramsObj) {
|
||||
const paramName = p as ToolParamName<T>
|
||||
const paramName = p as ToolParamName
|
||||
const orig = paramsObj[paramName]
|
||||
if (orig === undefined) continue
|
||||
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
|
||||
@@ -203,16 +199,16 @@ const parseXMLPrefixToToolCall = <T extends ToolName,>(toolName: T, toolId: stri
|
||||
|
||||
const pm = new SurroundingsRemover(str)
|
||||
|
||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName<T>[]
|
||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[]
|
||||
if (allowedParams.length === 0) return getAnswer()
|
||||
let latestMatchedOpenParam: null | ToolParamName<T> = null
|
||||
let latestMatchedOpenParam: null | ToolParamName = null
|
||||
let n = 0
|
||||
while (true) {
|
||||
n += 1
|
||||
if (n > 10) return getAnswer() // just for good measure as this code is early
|
||||
|
||||
// find the param name opening tag
|
||||
let matchedOpenParam: null | ToolParamName<T> = null
|
||||
let matchedOpenParam: null | ToolParamName = null
|
||||
for (const paramName of allowedParams) {
|
||||
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
|
||||
if (removed) {
|
||||
@@ -261,14 +257,11 @@ const parseXMLPrefixToToolCall = <T extends ToolName,>(toolName: T, toolId: stri
|
||||
}
|
||||
|
||||
export const extractXMLToolsWrapper = (
|
||||
onText: OnText,
|
||||
onFinalMessage: OnFinalMessage,
|
||||
chatMode: ChatMode | null,
|
||||
mcpTools: InternalToolInfo[] | undefined,
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
|
||||
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
||||
|
||||
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
const tools = availableTools(chatMode, mcpTools)
|
||||
const tools = availableTools(chatMode)
|
||||
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
|
||||
const toolOfToolName: ToolOfToolName = {}
|
||||
|
||||
@@ -7,27 +7,17 @@
|
||||
/* eslint-disable */
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { Ollama } from 'ollama';
|
||||
import OpenAI, { ClientOptions, AzureOpenAI } from 'openai';
|
||||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import { MistralCore } from '@mistralai/mistralai/core.js';
|
||||
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
||||
import { Tool as GeminiTool, FunctionDeclaration, GoogleGenAI, ThinkingConfig, Schema, Type } from '@google/genai';
|
||||
import { GoogleAuth } from 'google-auth-library'
|
||||
// import { GoogleAuth } from 'google-auth-library'
|
||||
/* eslint-enable */
|
||||
|
||||
import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
||||
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
|
||||
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
||||
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
|
||||
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
|
||||
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
|
||||
const getGoogleApiKey = async () => {
|
||||
// module‑level singleton
|
||||
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
|
||||
const key = await auth.getAccessToken()
|
||||
if (!key) throw new Error(`Google API failed to generate a key.`)
|
||||
return key
|
||||
}
|
||||
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
|
||||
|
||||
|
||||
|
||||
@@ -39,17 +29,11 @@ type InternalCommonMessageParams = {
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined;
|
||||
overridesOfModel: OverridesOfModel | undefined;
|
||||
modelName: string;
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}
|
||||
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & {
|
||||
messages: LLMChatMessage[];
|
||||
separateSystemMessage: string | undefined;
|
||||
chatMode: ChatMode | null;
|
||||
mcpTools: InternalToolInfo[] | undefined;
|
||||
}
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
|
||||
@@ -58,17 +42,15 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||
|
||||
// const getGoogleApiKey = async () => {
|
||||
// // module‑level singleton
|
||||
// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
|
||||
// const key = await auth.getAccessToken()
|
||||
// if (!key) throw new Error(`Google API failed to generate a key.`)
|
||||
// return key
|
||||
// }
|
||||
|
||||
|
||||
const parseHeadersJSON = (s: string | undefined): Record<string, string | null | undefined> | undefined => {
|
||||
if (!s) return undefined
|
||||
try {
|
||||
return JSON.parse(s)
|
||||
} catch (e) {
|
||||
throw new Error(`Error parsing OpenAI-Compatible headers: ${s} is not a valid JSON.`)
|
||||
}
|
||||
}
|
||||
|
||||
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
const commonPayloadOpts: ClientOptions = {
|
||||
dangerouslyAllowBrowser: true,
|
||||
@@ -106,45 +88,22 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
||||
...commonPayloadOpts,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'googleVertex') {
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
else if (providerName === 'gemini') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
|
||||
const apiKey = await getGoogleApiKey()
|
||||
return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
// else if (providerName === 'googleVertex') {
|
||||
// // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
// const thisConfig = settingsOfProvider[providerName]
|
||||
// const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
|
||||
// return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
|
||||
// }
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
|
||||
// https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
const endpoint = `https://${thisConfig.project}.openai.azure.com/`;
|
||||
const apiVersion = thisConfig.azureApiVersion ?? '2024-04-01-preview';
|
||||
const options = { endpoint, apiKey: thisConfig.apiKey, apiVersion };
|
||||
return new AzureOpenAI({ ...options, ...commonPayloadOpts });
|
||||
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
|
||||
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'awsBedrock') {
|
||||
/**
|
||||
* We treat Bedrock as *OpenAI-compatible only through a proxy*:
|
||||
* • LiteLLM default → http://localhost:4000/v1
|
||||
* • Bedrock-Access-Gateway → https://<api-id>.execute-api.<region>.amazonaws.com/openai/
|
||||
*
|
||||
* The native Bedrock runtime endpoint
|
||||
* https://bedrock-runtime.<region>.amazonaws.com
|
||||
* is **NOT** OpenAI-compatible, so we do *not* fall back to it here.
|
||||
*/
|
||||
const { endpoint, apiKey } = settingsOfProvider.awsBedrock
|
||||
|
||||
// ① use the user-supplied proxy if present
|
||||
// ② otherwise default to local LiteLLM
|
||||
let baseURL = endpoint || 'http://localhost:4000/v1'
|
||||
|
||||
// Normalize: make sure we end with “/v1”
|
||||
if (!baseURL.endsWith('/v1'))
|
||||
baseURL = baseURL.replace(/\/+$/, '') + '/v1'
|
||||
|
||||
return new OpenAI({ baseURL, apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
|
||||
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
@@ -152,8 +111,7 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
const headers = parseHeadersJSON(thisConfig.headersJSON)
|
||||
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, defaultHeaders: headers, ...commonPayloadOpts })
|
||||
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
@@ -172,14 +130,8 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
||||
}
|
||||
|
||||
|
||||
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, overridesOfModel }: SendFIMParams_Internal) => {
|
||||
|
||||
const {
|
||||
modelName,
|
||||
supportsFIM,
|
||||
additionalOpenAIPayload,
|
||||
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
|
||||
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
|
||||
@@ -188,7 +140,7 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens
|
||||
return
|
||||
}
|
||||
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload: additionalOpenAIPayload })
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.completions
|
||||
.create({
|
||||
model: modelName,
|
||||
@@ -230,8 +182,8 @@ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
const openAITools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
|
||||
@@ -241,72 +193,55 @@ const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | u
|
||||
return openAITools
|
||||
}
|
||||
|
||||
|
||||
// convert LLM tool call to our tool format
|
||||
const rawToolCallObjOfParamsStr = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||
const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
let input: unknown
|
||||
try { input = JSON.parse(toolParamsStr) }
|
||||
catch (e) { return null }
|
||||
|
||||
try {
|
||||
input = JSON.parse(toolParamsStr)
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
|
||||
const rawParams: RawToolParamsObj = input
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
|
||||
}
|
||||
|
||||
|
||||
const rawToolCallObjOfAnthropicParams = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
|
||||
const rawParams: RawToolParamsObj = input
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
}
|
||||
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE ------------
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel, mcpTools }: SendChatParams_Internal) => {
|
||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
reasoningCapabilities,
|
||||
additionalOpenAIPayload,
|
||||
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
||||
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
|
||||
|
||||
// reasoning
|
||||
const { canIOReasoning, openSourceThinkTags } = reasoningCapabilities || {}
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
|
||||
|
||||
const includeInPayload = {
|
||||
...providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo),
|
||||
...additionalOpenAIPayload
|
||||
}
|
||||
const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// tools
|
||||
const potentialTools = openAITools(chatMode, mcpTools)
|
||||
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
|
||||
{ tools: potentialTools } as const
|
||||
: {}
|
||||
|
||||
// instance
|
||||
const openai: OpenAI = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
if (providerName === 'microsoftAzure') {
|
||||
// Required to select the model
|
||||
(openai as AzureOpenAI).deploymentName = modelName;
|
||||
}
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages as any,
|
||||
stream: true,
|
||||
...nativeToolsObj,
|
||||
...additionalOpenAIPayload
|
||||
// max_completion_tokens: maxTokens,
|
||||
}
|
||||
|
||||
@@ -321,7 +256,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
@@ -362,11 +297,10 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
fullReasoningSoFar += newReasoning
|
||||
}
|
||||
|
||||
// call onText
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
|
||||
})
|
||||
|
||||
}
|
||||
@@ -375,7 +309,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
|
||||
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||
}
|
||||
@@ -441,8 +375,8 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
const anthropicTools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
|
||||
@@ -452,27 +386,37 @@ const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[]
|
||||
return anthropicTools
|
||||
}
|
||||
|
||||
|
||||
const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
}
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => {
|
||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
|
||||
|
||||
// reasoning
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// anthropic-specific - max tokens
|
||||
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
|
||||
const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled })
|
||||
|
||||
// tools
|
||||
const potentialTools = anthropicTools(chatMode, mcpTools)
|
||||
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
|
||||
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
|
||||
: {}
|
||||
@@ -494,9 +438,9 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
|
||||
})
|
||||
|
||||
// manually parse out tool results if XML
|
||||
// manually parse out tool results
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
@@ -513,7 +457,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
onText({
|
||||
fullText,
|
||||
fullReasoning,
|
||||
toolCall: !fullToolName ? undefined : { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
|
||||
toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
|
||||
})
|
||||
}
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
@@ -563,11 +507,8 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
stream.on('finalMessage', (response) => {
|
||||
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
||||
const tools = response.content.filter(c => c.type === 'tool_use')
|
||||
// console.log('TOOLS!!!!!!', JSON.stringify(tools, null, 2))
|
||||
// console.log('TOOLS!!!!!!', JSON.stringify(response, null, 2))
|
||||
const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0])
|
||||
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||
})
|
||||
// on error
|
||||
@@ -582,8 +523,8 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
|
||||
// ------------ MISTRAL ------------
|
||||
// https://docs.mistral.ai/api/#tag/fim
|
||||
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, overridesOfModel, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
|
||||
@@ -603,7 +544,6 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
||||
stop: messages.stopTokens,
|
||||
})
|
||||
.then(async response => {
|
||||
|
||||
// unfortunately, _setAborter() does not exist
|
||||
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
|
||||
const fullText = typeof content === 'string' ? content
|
||||
@@ -680,170 +620,6 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------- GEMINI NATIVE IMPLEMENTATION ----------------
|
||||
|
||||
const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
parameters: {
|
||||
type: Type.OBJECT,
|
||||
properties: Object.entries(params).reduce((acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
type: Type.STRING,
|
||||
description: value.description
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<string, Schema>)
|
||||
}
|
||||
} satisfies FunctionDeclaration
|
||||
}
|
||||
|
||||
const geminiTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined): GeminiTool[] | null => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
const functionDecls: FunctionDeclaration[] = []
|
||||
for (const t in allowedTools ?? {}) {
|
||||
functionDecls.push(toGeminiFunctionDecl(allowedTools[t]))
|
||||
}
|
||||
const tools: GeminiTool = { functionDeclarations: functionDecls, }
|
||||
return [tools]
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Implementation for Gemini using Google's native API
|
||||
const sendGeminiChat = async ({
|
||||
messages,
|
||||
separateSystemMessage,
|
||||
onText,
|
||||
onFinalMessage,
|
||||
onError,
|
||||
settingsOfProvider,
|
||||
overridesOfModel,
|
||||
modelName: modelName_,
|
||||
_setAborter,
|
||||
providerName,
|
||||
modelSelectionOptions,
|
||||
chatMode,
|
||||
mcpTools,
|
||||
}: SendChatParams_Internal) => {
|
||||
|
||||
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
|
||||
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
|
||||
const {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
// reasoningCapabilities,
|
||||
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
|
||||
// const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
|
||||
|
||||
// reasoning
|
||||
// const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
|
||||
// const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
const thinkingConfig: ThinkingConfig | undefined = !reasoningInfo?.isReasoningEnabled ? undefined
|
||||
: reasoningInfo.type === 'budget_slider_value' ?
|
||||
{ thinkingBudget: reasoningInfo.reasoningBudget }
|
||||
: undefined
|
||||
|
||||
// tools
|
||||
const potentialTools = geminiTools(chatMode, mcpTools)
|
||||
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
|
||||
potentialTools
|
||||
: undefined
|
||||
|
||||
// instance
|
||||
const genAI = new GoogleGenAI({ apiKey: thisConfig.apiKey });
|
||||
|
||||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
||||
// when receive text
|
||||
let fullReasoningSoFar = ''
|
||||
let fullTextSoFar = ''
|
||||
|
||||
let toolName = ''
|
||||
let toolParamsStr = ''
|
||||
let toolId = ''
|
||||
|
||||
|
||||
genAI.models.generateContentStream({
|
||||
model: modelName,
|
||||
config: {
|
||||
systemInstruction: separateSystemMessage,
|
||||
thinkingConfig: thinkingConfig,
|
||||
tools: toolConfig,
|
||||
},
|
||||
contents: messages as GeminiLLMChatMessage[],
|
||||
})
|
||||
.then(async (stream) => {
|
||||
_setAborter(() => { stream.return(fullTextSoFar); });
|
||||
|
||||
// Process the stream
|
||||
for await (const chunk of stream) {
|
||||
// message
|
||||
const newText = chunk.text ?? ''
|
||||
fullTextSoFar += newText
|
||||
|
||||
// tool call
|
||||
const functionCalls = chunk.functionCalls
|
||||
if (functionCalls && functionCalls.length > 0) {
|
||||
const functionCall = functionCalls[0] // Get the first function call
|
||||
toolName = functionCall.name ?? ''
|
||||
toolParamsStr = JSON.stringify(functionCall.args ?? {})
|
||||
toolId = functionCall.id ?? ''
|
||||
}
|
||||
|
||||
// (do not handle reasoning yet)
|
||||
|
||||
// call onText
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
}
|
||||
|
||||
// on final
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
} else {
|
||||
if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id
|
||||
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
const message = error?.message
|
||||
if (typeof message === 'string') {
|
||||
|
||||
if (error.message?.includes('API key')) {
|
||||
onError({ message: invalidApiKeyMessage(providerName), fullError: error });
|
||||
}
|
||||
else if (error?.message?.includes('429')) {
|
||||
onError({ message: 'Rate limit reached. ' + error, fullError: error });
|
||||
}
|
||||
else
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
|
||||
type CallFnOfProvider = {
|
||||
@@ -871,7 +647,7 @@ export const sendLLMMessageToProviderImplementation = {
|
||||
list: null,
|
||||
},
|
||||
gemini: {
|
||||
sendChat: (params) => sendGeminiChat(params),
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
@@ -912,32 +688,25 @@ export const sendLLMMessageToProviderImplementation = {
|
||||
},
|
||||
|
||||
lmStudio: {
|
||||
// lmStudio has no suffix parameter in /completions, so sendFIM might not work
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
sendFIM: null, // lmStudio has no suffix parameter in /completions
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
liteLLM: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
list: null,
|
||||
},
|
||||
googleVertex: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
// googleVertex: {
|
||||
// sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
// sendFIM: null,
|
||||
// list: null,
|
||||
// },
|
||||
microsoftAzure: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
awsBedrock: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
|
||||
} satisfies CallFnOfProvider
|
||||
|
||||
|
||||
|
||||
@@ -20,10 +20,8 @@ export const sendLLMMessage = async ({
|
||||
settingsOfProvider,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
overridesOfModel,
|
||||
chatMode,
|
||||
separateSystemMessage,
|
||||
mcpTools,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
@@ -35,6 +33,13 @@ export const sendLLMMessage = async ({
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureLLMEvent = (eventId: string, extras?: object) => {
|
||||
|
||||
let totalTokens = 0
|
||||
if (messagesType === 'chatMessages') {
|
||||
for (const m of messages_) totalTokens += m.content.length
|
||||
}
|
||||
else {
|
||||
totalTokens = messages_.prefix.length + messages_.suffix.length
|
||||
}
|
||||
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
@@ -43,10 +48,13 @@ export const sendLLMMessage = async ({
|
||||
numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length,
|
||||
...messagesType === 'chatMessages' ? {
|
||||
numMessages: messages_?.length,
|
||||
messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
|
||||
} : messagesType === 'FIMMessage' ? {
|
||||
prefixLength: messages_.prefix.length,
|
||||
suffixLength: messages_.suffix.length,
|
||||
} : {},
|
||||
totalTokens,
|
||||
...loggingExtras,
|
||||
...extras,
|
||||
})
|
||||
@@ -66,9 +74,9 @@ export const sendLLMMessage = async ({
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = (params) => {
|
||||
const { fullText, fullReasoning, toolCall } = params
|
||||
const { fullText, fullReasoning } = params
|
||||
if (_didAbort) return
|
||||
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds(), toolCallName: toolCall?.name })
|
||||
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_(params)
|
||||
}
|
||||
|
||||
@@ -95,7 +103,7 @@ export const sendLLMMessage = async ({
|
||||
|
||||
|
||||
if (messagesType === 'chatMessages')
|
||||
captureLLMEvent(`${loggingName} - Sending Message`, {})
|
||||
captureLLMEvent(`${loggingName} - Sending Message`, { userMessageLength: messages_?.[messages_.length - 1]?.content.length })
|
||||
else if (messagesType === 'FIMMessage')
|
||||
captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length })
|
||||
|
||||
@@ -108,15 +116,15 @@ export const sendLLMMessage = async ({
|
||||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode, mcpTools })
|
||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
if (sendFIM) {
|
||||
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage })
|
||||
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error running Autocomplete with ${providerName} - ${modelName}.`, fullError: null })
|
||||
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// can't make a service responsible for this, because it needs
|
||||
// to be connected to the main process and node dependencies
|
||||
|
||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, RawMCPToolCall, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams, removeMCPToolNamePrefix } from '../common/mcpServiceTypes.js';
|
||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { MCPUserStateOfName } from '../common/voidSettingsTypes.js';
|
||||
|
||||
const getClientConfig = (serverName: string) => {
|
||||
return {
|
||||
name: `${serverName}-client`,
|
||||
version: '0.1.0',
|
||||
// debug: true,
|
||||
}
|
||||
}
|
||||
|
||||
type MCPServerNonError = MCPServer & { status: Omit<MCPServer['status'], 'error'> }
|
||||
type MCPServerError = MCPServer & { status: 'error' }
|
||||
|
||||
|
||||
|
||||
type ClientInfo = {
|
||||
_client: Client, // _client is the client that connects with an mcp client. We're calling mcp clients "server" everywhere except here for naming consistency.
|
||||
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||
mcpServer: MCPServerNonError,
|
||||
} | {
|
||||
_client?: undefined,
|
||||
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||
mcpServer: MCPServerError,
|
||||
}
|
||||
|
||||
type InfoOfClientId = {
|
||||
[clientId: string]: ClientInfo
|
||||
}
|
||||
|
||||
export class MCPChannel implements IServerChannel {
|
||||
|
||||
private readonly infoOfClientId: InfoOfClientId = {}
|
||||
private readonly _refreshingServerNames: Set<string> = new Set()
|
||||
|
||||
// mcp emitters
|
||||
private readonly mcpEmitters = {
|
||||
serverEvent: {
|
||||
onAdd: new Emitter<MCPServerEventResponse>(),
|
||||
onUpdate: new Emitter<MCPServerEventResponse>(),
|
||||
onDelete: new Emitter<MCPServerEventResponse>(),
|
||||
}
|
||||
} satisfies {
|
||||
serverEvent: {
|
||||
onAdd: Emitter<MCPServerEventResponse>,
|
||||
onUpdate: Emitter<MCPServerEventResponse>,
|
||||
onDelete: Emitter<MCPServerEventResponse>,
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
) { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
|
||||
// server events
|
||||
if (event === 'onAdd_server') return this.mcpEmitters.serverEvent.onAdd.event;
|
||||
else if (event === 'onUpdate_server') return this.mcpEmitters.serverEvent.onUpdate.event;
|
||||
else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.onDelete.event;
|
||||
// else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.onChangeLoading.event;
|
||||
|
||||
// tool call events
|
||||
|
||||
// handle unknown events
|
||||
else throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
// browser uses this to call (see this.channel.call() in mcpConfigService.ts for all usages)
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'refreshMCPServers') {
|
||||
await this._refreshMCPServers(params)
|
||||
}
|
||||
else if (command === 'closeAllMCPServers') {
|
||||
await this._closeAllMCPServers()
|
||||
}
|
||||
else if (command === 'toggleMCPServer') {
|
||||
await this._toggleMCPServer(params.serverName, params.isOn)
|
||||
}
|
||||
else if (command === 'callTool') {
|
||||
const p: MCPToolCallParams = params
|
||||
const response = await this._safeCallTool(p.serverName, p.toolName, p.params)
|
||||
return response
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('mcp channel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// server functions
|
||||
|
||||
|
||||
private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName, addedServerNames: string[], removedServerNames: string[], updatedServerNames: string[] }) {
|
||||
|
||||
const {
|
||||
mcpConfigFileJSON,
|
||||
userStateOfName,
|
||||
addedServerNames,
|
||||
removedServerNames,
|
||||
updatedServerNames,
|
||||
} = params
|
||||
|
||||
const { mcpServers: mcpServersJSON } = mcpConfigFileJSON
|
||||
|
||||
const allChanges: { type: 'added' | 'removed' | 'updated', serverName: string }[] = [
|
||||
...addedServerNames.map(n => ({ serverName: n, type: 'added' }) as const),
|
||||
...removedServerNames.map(n => ({ serverName: n, type: 'removed' }) as const),
|
||||
...updatedServerNames.map(n => ({ serverName: n, type: 'updated' }) as const),
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
allChanges.map(async ({ serverName, type }) => {
|
||||
|
||||
// check if already refreshing
|
||||
if (this._refreshingServerNames.has(serverName)) return
|
||||
this._refreshingServerNames.add(serverName)
|
||||
|
||||
const prevServer = this.infoOfClientId[serverName]?.mcpServer;
|
||||
|
||||
// close and delete the old client
|
||||
if (type === 'removed' || type === 'updated') {
|
||||
await this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]
|
||||
this.mcpEmitters.serverEvent.onDelete.fire({ response: { prevServer, name: serverName, } })
|
||||
}
|
||||
|
||||
// create a new client
|
||||
if (type === 'added' || type === 'updated') {
|
||||
const clientInfo = await this._createClient(mcpServersJSON[serverName], serverName, userStateOfName[serverName]?.isOn)
|
||||
this.infoOfClientId[serverName] = clientInfo
|
||||
this.mcpEmitters.serverEvent.onAdd.fire({ response: { newServer: clientInfo.mcpServer, name: serverName, } })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
allChanges.forEach(({ serverName, type }) => {
|
||||
this._refreshingServerNames.delete(serverName)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private async _createClientUnsafe(server: MCPConfigFileEntryJSON, serverName: string, isOn: boolean): Promise<ClientInfo> {
|
||||
|
||||
const clientConfig = getClientConfig(serverName)
|
||||
const client = new Client(clientConfig)
|
||||
let transport: Transport;
|
||||
let info: MCPServerNonError;
|
||||
|
||||
if (server.url) {
|
||||
// first try HTTP, fall back to SSE
|
||||
try {
|
||||
transport = new StreamableHTTPClientTransport(server.url);
|
||||
await client.connect(transport);
|
||||
console.log(`Connected via HTTP to ${serverName}`);
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: server.url.toString(),
|
||||
}
|
||||
} catch (httpErr) {
|
||||
console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr);
|
||||
transport = new SSEClientTransport(server.url);
|
||||
await client.connect(transport);
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
console.log(`Connected via SSE to ${serverName}`);
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: server.url.toString(),
|
||||
}
|
||||
}
|
||||
} else if (server.command) {
|
||||
// console.log('ENV DATA: ', server.env)
|
||||
transport = new StdioClientTransport({
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: {
|
||||
...server.env,
|
||||
...process.env
|
||||
} as Record<string, string>,
|
||||
});
|
||||
|
||||
await client.connect(transport)
|
||||
|
||||
// Get the tools from the server
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
|
||||
// Create a full command string for display
|
||||
const fullCommand = `${server.command} ${server.args?.join(' ') || ''}`
|
||||
|
||||
// Format server object
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: fullCommand,
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`No url or command for server ${serverName}`);
|
||||
}
|
||||
|
||||
|
||||
return { _client: client, mcpServerEntryJSON: server, mcpServer: info }
|
||||
}
|
||||
|
||||
private _addUniquePrefix(base: string) {
|
||||
return `${Math.random().toString(36).slice(2, 8)}_${base}`;
|
||||
}
|
||||
|
||||
private async _createClient(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true): Promise<ClientInfo> {
|
||||
try {
|
||||
const c: ClientInfo = await this._createClientUnsafe(serverConfig, serverName, isOn)
|
||||
return c
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to connect to server "${serverName}":`, err)
|
||||
const fullCommand = !serverConfig.command ? '' : `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}`
|
||||
const c: MCPServerError = { status: 'error', error: err + '', command: fullCommand, }
|
||||
return { mcpServerEntryJSON: serverConfig, mcpServer: c, }
|
||||
}
|
||||
}
|
||||
|
||||
private async _closeAllMCPServers() {
|
||||
for (const serverName in this.infoOfClientId) {
|
||||
await this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]
|
||||
}
|
||||
console.log('Closed all MCP servers');
|
||||
}
|
||||
|
||||
private async _closeClient(serverName: string) {
|
||||
const info = this.infoOfClientId[serverName]
|
||||
if (!info) return
|
||||
const { _client: client } = info
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
console.log(`Closed MCP server ${serverName}`);
|
||||
}
|
||||
|
||||
|
||||
private async _toggleMCPServer(serverName: string, isOn: boolean) {
|
||||
const prevServer = this.infoOfClientId[serverName]?.mcpServer
|
||||
// Handle turning on the server
|
||||
if (isOn) {
|
||||
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||
const clientInfo = await this._createClientUnsafe(this.infoOfClientId[serverName].mcpServerEntryJSON, serverName, isOn)
|
||||
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||
response: {
|
||||
name: serverName,
|
||||
newServer: clientInfo.mcpServer,
|
||||
prevServer: prevServer,
|
||||
}
|
||||
})
|
||||
}
|
||||
// Handle turning off the server
|
||||
else {
|
||||
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||
this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]._client
|
||||
|
||||
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||
response: {
|
||||
name: serverName,
|
||||
newServer: {
|
||||
status: 'offline',
|
||||
tools: [],
|
||||
command: '',
|
||||
// Explicitly set error to undefined to reset the error state
|
||||
error: undefined,
|
||||
},
|
||||
prevServer: prevServer,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tool call functions
|
||||
|
||||
private async _callTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||
const server = this.infoOfClientId[serverName]
|
||||
if (!server) throw new Error(`Server ${serverName} not found`)
|
||||
const { _client: client } = server
|
||||
if (!client) throw new Error(`Client for server ${serverName} not found`)
|
||||
|
||||
// Call the tool with the provided parameters
|
||||
const response = await client.callTool({
|
||||
name: removeMCPToolNamePrefix(toolName),
|
||||
arguments: params
|
||||
})
|
||||
const { content } = response as CallToolResult
|
||||
const returnValue = content[0]
|
||||
|
||||
if (returnValue.type === 'text') {
|
||||
// handle text response
|
||||
|
||||
if (response.isError) {
|
||||
throw new Error(`Tool call error: ${returnValue.text}`)
|
||||
}
|
||||
|
||||
// handle success
|
||||
return {
|
||||
event: 'text',
|
||||
text: returnValue.text,
|
||||
toolName,
|
||||
serverName,
|
||||
}
|
||||
}
|
||||
|
||||
// if (returnValue.type === 'audio') {
|
||||
// // handle audio response
|
||||
// }
|
||||
|
||||
// if (returnValue.type === 'image') {
|
||||
// // handle image response
|
||||
// }
|
||||
|
||||
// if (returnValue.type === 'resource') {
|
||||
// // handle resource response
|
||||
// }
|
||||
|
||||
throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for tool ${toolName} on server ${serverName}`)
|
||||
}
|
||||
|
||||
// tool call error wrapper
|
||||
private async _safeCallTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||
try {
|
||||
const response = await this._callTool(serverName, toolName, params)
|
||||
return response
|
||||
} catch (err) {
|
||||
|
||||
let errorMessage: string;
|
||||
|
||||
if (typeof err === 'object' && err !== null && err['code']) {
|
||||
const code = err.code
|
||||
let codeDescription = ''
|
||||
if (code === -32700)
|
||||
codeDescription = 'Parse Error';
|
||||
if (code === -32600)
|
||||
codeDescription = 'Invalid Request';
|
||||
if (code === -32601)
|
||||
codeDescription = 'Method Not Found';
|
||||
if (code === -32602)
|
||||
codeDescription = 'Invalid Parameters';
|
||||
if (code === -32603)
|
||||
codeDescription = 'Internal Error';
|
||||
errorMessage = `${codeDescription}. Full response:\n${JSON.stringify(err, null, 2)}`
|
||||
}
|
||||
// Check if it's an MCP error with a code
|
||||
else if (typeof err === 'string') {
|
||||
// String error
|
||||
errorMessage = err;
|
||||
} else {
|
||||
// Unknown error format
|
||||
errorMessage = JSON.stringify(err, null, 2);
|
||||
}
|
||||
|
||||
const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${errorMessage}`;
|
||||
const errorResponse: MCPToolErrorResponse = {
|
||||
event: 'error',
|
||||
text: fullErrorMessage,
|
||||
toolName,
|
||||
serverName,
|
||||
}
|
||||
return errorResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { IApplicationStorageMainService } from '../../../../platform/storage/ele
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
import { OPT_OUT_KEY } from '../common/storageKeys.js';
|
||||
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
@@ -30,8 +29,6 @@ const osInfo = _getOSInfo()
|
||||
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
@@ -122,18 +119,7 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
distinctId: this.distinctId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
|
||||
const didOptOut = this._appStorage.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
|
||||
|
||||
console.log('User is opted out of basic Void metrics?', didOptOut)
|
||||
if (didOptOut) {
|
||||
this.client.optOut()
|
||||
}
|
||||
else {
|
||||
this.client.optIn()
|
||||
this.client.identify(identifyMessage)
|
||||
}
|
||||
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
}
|
||||
@@ -141,18 +127,10 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this.distinctId, event, properties: params } as const
|
||||
// console.log('full capture:', this.distinctId)
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
setOptOut: IMetricsService['setOptOut'] = (newVal: boolean) => {
|
||||
if (newVal) {
|
||||
this._appStorage.store(OPT_OUT_KEY, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
}
|
||||
else {
|
||||
this._appStorage.remove(OPT_OUT_KEY, StorageScope.APPLICATION)
|
||||
}
|
||||
}
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { promisify } from 'util'
|
||||
import { exec as _exec } from 'child_process'
|
||||
import { IVoidSCMService } from '../common/voidSCMTypes.js'
|
||||
|
||||
interface NumStat {
|
||||
file: string
|
||||
added: number
|
||||
removed: number
|
||||
}
|
||||
|
||||
const exec = promisify(_exec)
|
||||
|
||||
//8000 and 10 were chosen after some experimentation on small-to-moderately sized changes
|
||||
const MAX_DIFF_LENGTH = 8000
|
||||
const MAX_DIFF_FILES = 10
|
||||
|
||||
const git = async (command: string, path: string): Promise<string> => {
|
||||
const { stdout, stderr } = await exec(`${command}`, { cwd: path })
|
||||
if (stderr) {
|
||||
throw new Error(stderr)
|
||||
}
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
const getNumStat = async (path: string, useStagedChanges: boolean): Promise<NumStat[]> => {
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
const output = await git(`git diff --numstat ${staged}`, path)
|
||||
return output
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const [added, removed, file] = line.split('\t')
|
||||
return {
|
||||
file,
|
||||
added: parseInt(added, 10) || 0,
|
||||
removed: parseInt(removed, 10) || 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSampledDiff = async (file: string, path: string, useStagedChanges: boolean): Promise<string> => {
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
const diff = await git(`git diff --unified=0 --no-color ${staged} -- "${file}"`, path)
|
||||
return diff.slice(0, MAX_DIFF_LENGTH)
|
||||
}
|
||||
|
||||
const hasStagedChanges = async (path: string): Promise<boolean> => {
|
||||
const output = await git('git diff --staged --name-only', path)
|
||||
return output.length > 0
|
||||
}
|
||||
|
||||
export class VoidSCMService implements IVoidSCMService {
|
||||
readonly _serviceBrand: undefined
|
||||
|
||||
async gitStat(path: string): Promise<string> {
|
||||
const useStagedChanges = await hasStagedChanges(path)
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
return git(`git diff --stat ${staged}`, path)
|
||||
}
|
||||
|
||||
async gitSampledDiffs(path: string): Promise<string> {
|
||||
const useStagedChanges = await hasStagedChanges(path)
|
||||
const numStatList = await getNumStat(path, useStagedChanges)
|
||||
const topFiles = numStatList
|
||||
.sort((a, b) => (b.added + b.removed) - (a.added + a.removed))
|
||||
.slice(0, MAX_DIFF_FILES)
|
||||
const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path, useStagedChanges) })))
|
||||
return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n')
|
||||
}
|
||||
|
||||
gitBranch(path: string): Promise<string> {
|
||||
return git('git branch --show-current', path)
|
||||
}
|
||||
|
||||
gitLog(path: string): Promise<string> {
|
||||
return git('git log --pretty=format:"%h|%s|%ad" --date=short --no-merges -n 5', path)
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,6 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
return { message: null } as const
|
||||
}
|
||||
|
||||
// if disabled and not explicitly checking, return early
|
||||
if (this._updateService.state.type === StateType.Disabled) {
|
||||
if (!explicit)
|
||||
return { message: null } as const
|
||||
}
|
||||
|
||||
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
|
||||
|
||||
console.log('updateState', this._updateService.state)
|
||||
@@ -100,7 +94,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
const data = await response.json();
|
||||
const version = data.tag_name;
|
||||
|
||||
const myVersion = this._productService.version
|
||||
const myVersion = `${this._productService.version}.${this._productService.release}`
|
||||
const latestVersion = version
|
||||
|
||||
const isUpToDate = myVersion === latestVersion // only makes sense if response.ok
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 813 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 795 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 850 KiB |
Reference in New Issue
Block a user