Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3166e7ef2 | |||
| 17e7a5b152 | |||
| f100db9175 | |||
| 1ee8980fe1 | |||
| 74650cb867 | |||
| 5aaa1604e1 | |||
| ff717cad16 | |||
| 7a974f2dff | |||
| cbb52dec28 | |||
| d948fc5950 | |||
| ee39ee94b1 | |||
| a7eb54a09b | |||
| 34e3413dca | |||
| 607a61c068 | |||
| 9732f74ba7 | |||
| 9031215001 | |||
| 3fa95be625 | |||
| 1183cd3ad4 | |||
| ac1aba825f | |||
| 867df38364 | |||
| c90f707070 | |||
| 0424e7d0d3 | |||
| 42c50413f5 | |||
| b5ae6d7a96 | |||
| 453f392f01 | |||
| b5a41840a0 | |||
| f0ad8e2e2e | |||
| d732d6ba05 | |||
| 24add9286c | |||
| 67e89fd979 | |||
| 5a702f98bc | |||
| 8b3b118319 | |||
| c3125804f3 | |||
| b739e99a1e | |||
| ec3b7caa8e | |||
| ffdbb214f9 | |||
| ec5b7636be | |||
| d1adb8b0fc | |||
| a475c80f4f | |||
| 8233c59f2b | |||
| 7c2192a7dc | |||
| 6aa2409302 | |||
| b48c3aaf35 | |||
| 722d0c5b3b | |||
| 7727b19b3c | |||
| 0816694210 | |||
| c6d210902b | |||
| b00d77610d | |||
| ad8fedc307 | |||
| 940155ac80 | |||
| ffbfba8d4f | |||
| da425ab0fe | |||
| 1191abf745 | |||
| 1a922133da | |||
| 40a27cd1ee | |||
| bf062146c9 | |||
| 97cb63c7fc | |||
| 2f283b503a | |||
| 7b78f962d3 | |||
| 93ee1731b5 | |||
| f5680a5c62 | |||
| 88288dba46 | |||
| 5b7580c3da | |||
| 9bf0646abe | |||
| 1737c7bfc8 | |||
| 584dc18ede | |||
| 3ec6973a0f | |||
| 42ef028c1e | |||
| 7b14f0ca73 | |||
| 9a5580d32e | |||
| c41838afc6 | |||
| f32743d759 | |||
| 9e53dfef33 | |||
| 844784445c | |||
| 9920bd3366 | |||
| 483cfb29a0 | |||
| 282800e394 | |||
| aacae4728f | |||
| 77c9284df5 | |||
| 7eac4aad6f | |||
| 797c8e7096 | |||
| 406d0dc297 | |||
| e34e38cb3a | |||
| 95f2aa70a1 | |||
| b74772e6e2 | |||
| 374e6c7109 | |||
| 8e6bb7194d | |||
| 2b4283a0f0 | |||
| b0617700aa | |||
| 53f7aa47f6 | |||
| e324257e1a | |||
| 7feb9154da | |||
| 857eda72fd | |||
| 3bc0a34e83 | |||
| 9c642763f0 | |||
| 17e44e192d | |||
| a24d097264 | |||
| 2880549d3f | |||
| 22fa093928 | |||
| 31950ae01e | |||
| 12a1014534 | |||
| 5815c0180a | |||
| 822430535e | |||
| 020fb60d74 | |||
| 615f8c3315 | |||
| 980d7937c3 | |||
| 57d94d2d44 | |||
| 382ecaae1b | |||
| a9abdf6351 | |||
| 27c40d8428 | |||
| c067d9fbf0 | |||
| 83c4a05642 | |||
| bd13208513 | |||
| 8350fbd2d1 | |||
| 22e08e6a06 | |||
| b50079709d | |||
| e2863d3002 | |||
| 06d20d705e | |||
| f034be08c3 | |||
| f74b94e3ba | |||
| d09601191d | |||
| 189e207499 | |||
| f88dba7610 | |||
| 5e100fcf1d | |||
| 9e745e2b87 | |||
| 3a39551205 | |||
| 033a23aa28 | |||
| 38f06202ed | |||
| 327d2dfe06 | |||
| 23bb28f3c0 | |||
| 5995591296 | |||
| 1d15d29a19 | |||
| 20dba6f04e | |||
| 8e0d29ff75 | |||
| 117feaa063 | |||
| 4300df420c | |||
| 74d5c86593 |
@@ -0,0 +1,164 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,60 @@
|
||||
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 }}
|
||||
@@ -10,3 +10,7 @@ In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any
|
||||
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.
|
||||
|
||||
+49
-62
@@ -20,14 +20,15 @@ Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
|
||||
|
||||
## Editing Void's Code
|
||||
|
||||
## Building Void
|
||||
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.
|
||||
|
||||
### a. Mac - Build Prerequisites
|
||||
### a. Mac - Prerequisites
|
||||
|
||||
If you're using a Mac, you need Python and XCode. You probably have these by default.
|
||||
|
||||
### b. Windows - Build Prerequisites
|
||||
### b. Windows - 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.
|
||||
|
||||
@@ -42,34 +43,52 @@ Go to the "Individual Components" tab and select:
|
||||
|
||||
Finally, click Install.
|
||||
|
||||
### c. Linux - Build Prerequisites
|
||||
### c. Linux - 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).
|
||||
|
||||
### d. Building Void from inside VSCode
|
||||
### Developer Mode Instructions
|
||||
|
||||
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.
|
||||
|
||||
1. `git clone https://github.com/voideditor/void` to clone the repo.
|
||||
2. `npm install` to install all dependencies.
|
||||
3. To build Void, open VSCode. Then:
|
||||
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>.
|
||||
- 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).
|
||||
4. To run Void:
|
||||
4. Open the Void Developer Mode window:
|
||||
- Windows: `./scripts/code.bat`.
|
||||
- Mac: `./scripts/code.sh`.
|
||||
- Linux: `./scripts/code.sh`.
|
||||
5. 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).
|
||||
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.
|
||||
|
||||
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:
|
||||
@@ -82,50 +101,47 @@ To build Void from the terminal instead of from inside VSCode, follow the steps
|
||||
```
|
||||
|
||||
|
||||
#### 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 easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc`.
|
||||
- Make sure that 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 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.
|
||||
### 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.
|
||||
|
||||
|
||||
## Packaging
|
||||
#### 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.
|
||||
|
||||
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.
|
||||
<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).
|
||||
|
||||
|
||||
### 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`
|
||||
|
||||
|
||||
### Output
|
||||
##### Local Executible Output
|
||||
|
||||
This will generate a folder outside of `void/`:
|
||||
The local executible will be located in a folder outside of `void/`:
|
||||
```bash
|
||||
workspace/
|
||||
├── void/ # Your Void fork
|
||||
└── VSCode-darwin-arm64/ # Generated output
|
||||
```
|
||||
|
||||
### 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).
|
||||
</details>
|
||||
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
@@ -137,32 +153,3 @@ Void's maintainers distribute Void on our website and in releases. Our build pip
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
# 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,3 +1,30 @@
|
||||
## 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">
|
||||
@@ -9,29 +36,26 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
Void is the open-source Cursor alternative.
|
||||
|
||||
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.
|
||||
|
||||
This repo contains the full sourcecode for Void. If you're new, welcome!
|
||||
This repo contains the full sourcecode for Void's Desktop app. If you're new, welcome!
|
||||
|
||||
- 🧭 [Website](https://voideditor.com)
|
||||
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 🚙 [Project Board](https://github.com/orgs/voideditor/projects/2)
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
1. To get started working on Void, check out our Project Board! You can also see [HOW_TO_CONTRIBUTE](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
2. Feel free to attend a casual weekly meeting in our Discord channel!
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the codebase, see [VOID_CODEBASE_GUIDE](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
|
||||
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).
|
||||
|
||||
|
||||
|
||||
## Support
|
||||
You can always reach us in our Discord server or contact us via email: hello@voideditor.com.
|
||||
You can always reach us in our [Discord server](https://discord.gg/RSNjgaugJs) or contact us via email at hello@voideditor.com.
|
||||
|
||||
@@ -52,7 +52,10 @@ 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.
|
||||
|
||||
|
||||
### Internal LLM Message Pipeline
|
||||
|
||||
|
||||
|
||||
### Void's 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.
|
||||
@@ -134,7 +137,7 @@ If you want to know how our build pipeline works, see our build repo [here](http
|
||||
|
||||
For additional references, the Void team put together this list of links to get up and running with VSCode.
|
||||
<details>
|
||||
|
||||
|
||||
|
||||
#### Links for Beginners
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.3.10",
|
||||
"voidRelease": "0034",
|
||||
"voidVersion": "1.4.9",
|
||||
"voidRelease": "0044",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
||||
@@ -130,8 +130,9 @@ 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).
|
||||
@@ -1103,6 +1104,7 @@ 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));
|
||||
@@ -1244,6 +1246,11 @@ 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);
|
||||
|
||||
|
||||
@@ -163,38 +163,35 @@ export class TelemetryService implements ITelemetryService {
|
||||
}
|
||||
|
||||
function getTelemetryLevelSettingDescription(): string {
|
||||
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 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 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 | - | - | - |
|
||||
`;
|
||||
// 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 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 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 telemetryDescription = `
|
||||
${telemetryText} ${externalLinksStatement} ${restartString}
|
||||
${telemetryText}
|
||||
|
||||
|
||||
${restartString}
|
||||
|
||||
${telemetryTableDescription}
|
||||
${telemetryTable}
|
||||
|
||||
|
||||
|
||||
${deprecatedSettingNote}
|
||||
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.
|
||||
`;
|
||||
|
||||
return telemetryDescription;
|
||||
|
||||
@@ -26,7 +26,6 @@ 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 */
|
||||
|
||||
@@ -306,21 +305,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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -641,7 +641,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
|
||||
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
|
||||
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools'
|
||||
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)
|
||||
|
||||
@@ -1179,6 +1179,11 @@ 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' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1218,6 +1223,11 @@ 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 })
|
||||
@@ -1448,6 +1458,11 @@ 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
|
||||
@@ -1732,6 +1747,11 @@ 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; }) => {
|
||||
|
||||
@@ -34,6 +34,7 @@ import ErrorBoundary from './ErrorBoundary.js';
|
||||
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
||||
|
||||
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
|
||||
import { removeMCPToolNamePrefix } from '../../../../common/mcpServiceTypes.js';
|
||||
|
||||
|
||||
|
||||
@@ -663,81 +664,89 @@ export const SelectedFiles = (
|
||||
key={thisKey}
|
||||
className={`flex flex-col space-y-[1px]`}
|
||||
>
|
||||
{/* summarybox */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-1 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
text-xs text-nowrap
|
||||
border rounded-sm
|
||||
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-1 hover:brightness-95 text-void-fg-1'}
|
||||
${isThisSelectionProspective
|
||||
? 'border-void-border-2'
|
||||
: 'border-void-border-1'
|
||||
}
|
||||
hover:border-void-border-1
|
||||
transition-all duration-150
|
||||
`}
|
||||
onClick={() => {
|
||||
if (type !== 'staging') return; // (never)
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
setSelections([...selections, selection])
|
||||
}
|
||||
else if (selection.type === 'File') { // open files
|
||||
voidOpenFileFn(selection.uri, accessor);
|
||||
|
||||
const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile
|
||||
if (wasAddedAsCurrentFile) {
|
||||
// make it so the file is added permanently, not just as the current file
|
||||
const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } }
|
||||
setSelections([
|
||||
...selections.slice(0, i),
|
||||
newSelection,
|
||||
...selections.slice(i + 1)
|
||||
])
|
||||
}
|
||||
}
|
||||
else if (selection.type === 'CodeSelection') {
|
||||
voidOpenFileFn(selection.uri, accessor, selection.range);
|
||||
}
|
||||
else if (selection.type === 'Folder') {
|
||||
// TODO!!! reveal in tree
|
||||
}
|
||||
}}
|
||||
{/* tooltip for file path */}
|
||||
<span className="truncate overflow-hidden text-ellipsis"
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={getRelative(selection.uri, accessor)}
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-delay-show={3000}
|
||||
>
|
||||
{<SelectionIcon size={10} />}
|
||||
{/* summarybox */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-1 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
text-xs text-nowrap
|
||||
border rounded-sm
|
||||
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-1 hover:brightness-95 text-void-fg-1'}
|
||||
${isThisSelectionProspective
|
||||
? 'border-void-border-2'
|
||||
: 'border-void-border-1'
|
||||
}
|
||||
hover:border-void-border-1
|
||||
transition-all duration-150
|
||||
`}
|
||||
onClick={() => {
|
||||
if (type !== 'staging') return; // (never)
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
setSelections([...selections, selection])
|
||||
}
|
||||
else if (selection.type === 'File') { // open files
|
||||
voidOpenFileFn(selection.uri, accessor);
|
||||
|
||||
{ // file name and range
|
||||
getBasename(selection.uri.fsPath)
|
||||
+ (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '')
|
||||
}
|
||||
const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile
|
||||
if (wasAddedAsCurrentFile) {
|
||||
// make it so the file is added permanently, not just as the current file
|
||||
const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } }
|
||||
setSelections([
|
||||
...selections.slice(0, i),
|
||||
newSelection,
|
||||
...selections.slice(i + 1)
|
||||
])
|
||||
}
|
||||
}
|
||||
else if (selection.type === 'CodeSelection') {
|
||||
voidOpenFileFn(selection.uri, accessor, selection.range);
|
||||
}
|
||||
else if (selection.type === 'Folder') {
|
||||
// TODO!!! reveal in tree
|
||||
}
|
||||
}}
|
||||
>
|
||||
{<SelectionIcon size={10} />}
|
||||
|
||||
{selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ?
|
||||
<span className={`text-[8px] 'void-opacity-60 text-void-fg-4`}>
|
||||
{`(Current File)`}
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
{ // file name and range
|
||||
getBasename(selection.uri.fsPath)
|
||||
+ (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '')
|
||||
}
|
||||
|
||||
{type === 'staging' && !isThisSelectionProspective ? // X button
|
||||
<div // box for making it easier to click
|
||||
className='cursor-pointer z-1 self-stretch flex items-center justify-center'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX
|
||||
className='stroke-[2]'
|
||||
size={10}
|
||||
/>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
{selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ?
|
||||
<span className={`text-[8px] 'void-opacity-60 text-void-fg-4`}>
|
||||
{`(Current File)`}
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
|
||||
{type === 'staging' && !isThisSelectionProspective ? // X button
|
||||
<div // box for making it easier to click
|
||||
className='cursor-pointer z-1 self-stretch flex items-center justify-center'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX
|
||||
className='stroke-[2]'
|
||||
size={10}
|
||||
/>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
})}
|
||||
@@ -1120,7 +1129,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
|
||||
if (e.key === 'Escape') {
|
||||
onCloseEdit()
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
||||
onSubmit()
|
||||
}
|
||||
}
|
||||
@@ -1616,7 +1625,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
|
||||
</button>
|
||||
)
|
||||
|
||||
const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
|
||||
const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools'
|
||||
const approvalToggle = approvalType ? <div key={approvalType} className="flex items-center ml-2 gap-x-1">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div> : null
|
||||
@@ -1857,7 +1866,7 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps<string>) => {
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
|
||||
const title = getTitle(toolMessage)
|
||||
const desc1 = toolMessage.name
|
||||
const desc1 = removeMCPToolNamePrefix(toolMessage.name)
|
||||
const icon = null
|
||||
|
||||
|
||||
@@ -3047,7 +3056,7 @@ export const SidebarChat = () => {
|
||||
setInstructionsAreEmpty(!newStr)
|
||||
}, [setInstructionsAreEmpty])
|
||||
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
||||
onSubmit()
|
||||
} else if (e.key === 'Escape' && isRunning) {
|
||||
onAbort()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.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'
|
||||
@@ -52,6 +52,8 @@ 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'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
@@ -226,6 +228,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
||||
IMCPService: accessor.get(IMCPService),
|
||||
|
||||
IStorageService: accessor.get(IStorageService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
}
|
||||
@@ -399,3 +403,26 @@ export const useMCPServiceState = () => {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ 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', 'openAICompatible'];
|
||||
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'awsBedrock', 'openAICompatible'];
|
||||
|
||||
// Data structures for provider tabs
|
||||
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
|
||||
@@ -123,6 +123,7 @@ const featureNameMap: { display: string, featureName: FeatureName }[] = [
|
||||
{ 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 }) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { useAccessor, useIsDark, useIsOptedOut, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check, Asterisk, Plus } from 'lucide-react'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
@@ -21,6 +21,18 @@ import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../.
|
||||
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
|
||||
import { MCPServer } from '../../../../common/mcpServiceTypes.js';
|
||||
import { useMCPServiceState } from '../util/services.js';
|
||||
import { OPT_OUT_KEY } from '../../../../common/storageKeys.js';
|
||||
import { StorageScope, StorageTarget } from '../../../../../../../platform/storage/common/storage.js';
|
||||
|
||||
type Tab =
|
||||
| 'models'
|
||||
| 'localProviders'
|
||||
| 'providers'
|
||||
| 'featureOptions'
|
||||
| 'mcp'
|
||||
| 'general'
|
||||
| 'all';
|
||||
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
|
||||
@@ -205,10 +217,10 @@ const SimpleModelSettingsDialog = ({
|
||||
if (!isOpen || !modelInfo) return null;
|
||||
|
||||
const { modelName, providerName, type } = modelInfo;
|
||||
const accessor = useAccessor();
|
||||
const settingsState = useSettingsState();
|
||||
const accessor = useAccessor()
|
||||
const settingsState = useSettingsState()
|
||||
const mouseDownInsideModal = useRef(false); // Ref to track mousedown origin
|
||||
const settingsStateService = accessor.get('IVoidSettingsService');
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// current overrides and defaults
|
||||
const defaultModelCapabilities = getModelCapabilities(providerName, modelName, undefined);
|
||||
@@ -678,6 +690,7 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam
|
||||
// </div >
|
||||
// }
|
||||
|
||||
|
||||
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
@@ -919,66 +932,72 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer
|
||||
const removeUniquePrefix = (name: string) => name.split('_').slice(1).join('_')
|
||||
|
||||
return (
|
||||
<div className="border-b border-gray-800 bg-gray-300/10 py-4 rounded-lg ">
|
||||
<div className="flex items-center mx-4">
|
||||
{/* Status indicator */}
|
||||
<div className={`w-2 h-2 rounded-full mr-2
|
||||
${server.status === 'success' ? 'bg-green-500'
|
||||
: server.status === 'error' ? 'bg-red-500'
|
||||
: server.status === 'loading' ? 'bg-yellow-500'
|
||||
: server.status === 'offline' ? 'bg-gray-500'
|
||||
: ''}
|
||||
<div className="border border-void-border-2 bg-void-bg-1 py-3 px-4 rounded-sm my-2">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left side - status and name */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Status indicator */}
|
||||
<div className={`w-2 h-2 rounded-full
|
||||
${server.status === 'success' ? 'bg-green-500'
|
||||
: server.status === 'error' ? 'bg-red-500'
|
||||
: server.status === 'loading' ? 'bg-yellow-500'
|
||||
: server.status === 'offline' ? 'bg-void-fg-3'
|
||||
: ''}
|
||||
`}></div>
|
||||
|
||||
`}></div>
|
||||
|
||||
{/* Server name */}
|
||||
<div className="text-sm font-medium mr-2">{name}</div>
|
||||
|
||||
{/* Power toggle switch */}
|
||||
<div className="ml-auto mb-2">
|
||||
<VoidSwitch
|
||||
value={isOn ?? false}
|
||||
size='sm'
|
||||
disabled={server.status === 'error'}
|
||||
onChange={() => mcpService.toggleServerIsOn(name, !isOn)}
|
||||
/>
|
||||
{/* Server name */}
|
||||
<div className="text-sm font-medium text-void-fg-1">{name}</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - power toggle switch */}
|
||||
<VoidSwitch
|
||||
value={isOn ?? false}
|
||||
size='xs'
|
||||
disabled={server.status === 'error'}
|
||||
onChange={() => mcpService.toggleServerIsOn(name, !isOn)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tools section */}
|
||||
<div className="mt-1 mx-4">
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto pb-1">
|
||||
{isOn && (server.tools ?? []).length > 0 ? (
|
||||
(server.tools ?? []).map((tool: { name: string; description?: string }) => (
|
||||
<span
|
||||
key={tool.name}
|
||||
className="px-2 py-0.5 bg-black/5 dark:bg-white/5 rounded text-xs"
|
||||
{isOn && (
|
||||
<div className="mt-3">
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
||||
{(server.tools ?? []).length > 0 ? (
|
||||
(server.tools ?? []).map((tool: { name: string; description?: string }) => (
|
||||
<span
|
||||
key={tool.name}
|
||||
className="px-2 py-0.5 bg-void-bg-2 text-void-fg-3 rounded-sm text-xs"
|
||||
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={tool.description || ''}
|
||||
data-tooltip-class-name='void-max-w-[300px]'
|
||||
>
|
||||
{removeUniquePrefix(tool.name)}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-gray-500">No tools available</span>
|
||||
)}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={tool.description || ''}
|
||||
data-tooltip-class-name='void-max-w-[300px]'
|
||||
>
|
||||
{removeUniquePrefix(tool.name)}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-void-fg-3">No tools available</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Command badge */}
|
||||
{isOn && server.command && (
|
||||
<div className="mt-2 mx-4">
|
||||
<div className="text-xs text-gray-400">Command:</div>
|
||||
<div className="px-2 py-1 bg-void-bg-3 text-xs font-mono overflow-x-auto whitespace-nowrap">
|
||||
<div className="mt-3">
|
||||
<div className="text-xs text-void-fg-3 mb-1">Command:</div>
|
||||
<div className="px-2 py-1 bg-void-bg-2 text-xs font-mono overflow-x-auto whitespace-nowrap text-void-fg-2 rounded-sm">
|
||||
{server.command}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message if present */}
|
||||
{server.error && (<WarningBox className='ml-4' text={server.error} />)}
|
||||
{server.error && (
|
||||
<div className="mt-3">
|
||||
<WarningBox text={server.error} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -989,31 +1008,43 @@ const MCPServersList = () => {
|
||||
|
||||
let content: React.ReactNode
|
||||
if (mcpServiceState.error) {
|
||||
content = <div className="text-red-500 text-sm font-medium">
|
||||
content = <div className="text-void-fg-3 text-sm mt-2">
|
||||
{mcpServiceState.error}
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
const entries = Object.entries(mcpServiceState.mcpServerOfName)
|
||||
if (entries.length === 0) {
|
||||
content = <div className="text-red-500 text-sm font-medium">
|
||||
content = <div className="text-void-fg-3 text-sm mt-2">
|
||||
No servers found
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
content = entries.map(([name, server]) => (
|
||||
<div className="py-2" key={name}>
|
||||
<MCPServerComponent name={name} server={server} />
|
||||
</div>
|
||||
<MCPServerComponent key={name} name={name} server={server} />
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
return <div className="my-2">{content}</div>
|
||||
};
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
// ─── sidebar nav ──────────────────────────
|
||||
const [selectedSection, setSelectedSection] =
|
||||
useState<Tab>('models');
|
||||
|
||||
const navItems: { tab: Tab; label: string }[] = [
|
||||
{ tab: 'models', label: 'Models' },
|
||||
{ tab: 'localProviders', label: 'Local Providers' },
|
||||
{ tab: 'providers', label: 'Main Providers' },
|
||||
{ tab: 'featureOptions', label: 'Feature Options' },
|
||||
{ tab: 'general', label: 'General' },
|
||||
{ tab: 'mcp', label: 'MCP' },
|
||||
{ tab: 'all', label: 'All Settings' },
|
||||
];
|
||||
const shouldShowTab = (tab: Tab) => selectedSection === 'all' || selectedSection === tab;
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const environmentService = accessor.get('IEnvironmentService')
|
||||
@@ -1023,6 +1054,9 @@ export const Settings = () => {
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
const storageService = accessor.get('IStorageService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const isOptedOut = useIsOptedOut()
|
||||
|
||||
const onDownload = (t: 'Chats' | 'Settings') => {
|
||||
let dataStr: string
|
||||
@@ -1088,317 +1122,442 @@ export const Settings = () => {
|
||||
}
|
||||
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
return (
|
||||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%', overflow: 'auto' }}>
|
||||
<div className="flex flex-col md:flex-row w-full gap-6 max-w-[900px] mx-auto mb-32" style={{ minHeight: '80vh' }}>
|
||||
{/* ────────────── SIDEBAR ────────────── */}
|
||||
|
||||
<div className='max-w-xl mx-auto'>
|
||||
<aside className="md:w-1/4 w-full p-6 shrink-0">
|
||||
{/* vertical tab list */}
|
||||
<div className="flex flex-col gap-2 mt-12">
|
||||
{navItems.map(({ tab, label }) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => {
|
||||
if (tab === 'all') {
|
||||
setSelectedSection('all');
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
setSelectedSection(tab);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
py-2 px-4 rounded-md text-left transition-all duration-200
|
||||
${selectedSection === tab
|
||||
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
|
||||
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'}
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
<div className='w-full h-[1px] my-2' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<RedoOnboardingButton />
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ModelDump />
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
<OllamaSetupInstructions sayWeAutoDetect={true} />
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
{/* ───────────── MAIN PANE ───────────── */}
|
||||
<main className="flex-1 p-6 select-none">
|
||||
|
||||
|
||||
|
||||
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
|
||||
<div className='max-w-3xl'>
|
||||
|
||||
<div className='flex flex-col gap-y-8 my-4'>
|
||||
<ErrorBoundary>
|
||||
{/* FIM */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>
|
||||
<span>
|
||||
Experimental.{' '}
|
||||
</span>
|
||||
<span
|
||||
className='hover:brightness-110'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
|
||||
data-tooltip-class-name='void-max-w-[300px]'
|
||||
>
|
||||
Only works with FIM models.*
|
||||
</span>
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
<div className='w-full h-[1px] my-2' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<RedoOnboardingButton />
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
{/* All sections in flex container with gap-12 */}
|
||||
<div className='flex flex-col gap-12'>
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<div className={shouldShowTab('models') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ModelDump />
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
{/* Local Providers section */}
|
||||
<div className={shouldShowTab('localProviders') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.enableAutocomplete}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
|
||||
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
<OllamaSetupInstructions sayWeAutoDetect={true} />
|
||||
</div>
|
||||
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Main Providers section */}
|
||||
<div className={shouldShowTab('providers') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Main Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Feature Options section */}
|
||||
<div className={shouldShowTab('featureOptions') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Feature Options</h2>
|
||||
|
||||
<div className='flex flex-col gap-y-8 my-4'>
|
||||
<ErrorBoundary>
|
||||
{/* FIM */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>
|
||||
<span>
|
||||
Experimental.{' '}
|
||||
</span>
|
||||
<span
|
||||
className='hover:brightness-110'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
|
||||
data-tooltip-class-name='void-max-w-[20px]'
|
||||
>
|
||||
Only works with FIM models.*
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.enableAutocomplete}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<ErrorBoundary>
|
||||
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Apply */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.syncApplyToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Fast Apply Method Dropdown */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<FastApplyMethodDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Tools Section */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>Tools</h4>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
{[...toolApprovalTypes].map((approvalType) => {
|
||||
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div>
|
||||
})}
|
||||
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.includeToolLintErrors}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Auto Accept LLM Changes Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.autoAcceptLLMChanges}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoAcceptLLMChanges', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>Auto-accept LLM changes</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SCM */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('SCM')}</h4>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>Settings that control the behavior of the commit message generator.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.syncSCMToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncSCMToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncSCMToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${settingsState.globalSettings.syncSCMToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'SCM'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<ErrorBoundary>
|
||||
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
{/* General section */}
|
||||
<div className={`${shouldShowTab('general') ? `` : 'hidden'} flex flex-col gap-12`}>
|
||||
{/* One-Click Switch section */}
|
||||
<div>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2'>One-Click Switch</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer your editor settings into Void.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Import/Export section */}
|
||||
<div>
|
||||
<h2 className='text-3xl mb-2'>Import/Export</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer Void's settings and chats in and out of Void.`}</h4>
|
||||
<div className='flex flex-col gap-8'>
|
||||
{/* Settings Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Import Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
|
||||
Export Settings
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
|
||||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
|
||||
Export Chats
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
|
||||
Reset Chats
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Apply */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.syncApplyToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
|
||||
|
||||
{/* Built-in Settings section */}
|
||||
<div>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<ErrorBoundary>
|
||||
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Fast Apply Method Dropdown */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<FastApplyMethodDropdown />
|
||||
</div>
|
||||
</div>
|
||||
{/* Metrics section */}
|
||||
<div className='max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>Metrics</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>Very basic anonymous usage tracking helps us keep Void running smoothly. You may opt out below. Regardless of this setting, Void never sees your code, messages, or API keys.</h4>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Tools Section */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>Tools</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
{[...toolApprovalTypes].map((approvalType) => {
|
||||
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
<div className='my-2'>
|
||||
{/* Disable All Metrics Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={isOptedOut}
|
||||
onChange={(newVal) => {
|
||||
storageService.store(OPT_OUT_KEY, newVal, StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
metricsService.capture(`Set metrics opt-out to ${newVal}`, {}) // this only fires if it's enabled, so it's fine to have here
|
||||
}}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{'Opt-out (requires restart)'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
})}
|
||||
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.includeToolLintErrors}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* General section (formerly GeneralTab) */}
|
||||
<div className='mt-12'>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer your editor settings into Void.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Import/Export section, as its own block right after One-Click Switch */}
|
||||
<div className='mt-12'>
|
||||
<h2 className='text-3xl mb-2'>Import/Export</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer Void's settings and chats in and out of Void.`}</h4>
|
||||
<div className='flex flex-col gap-8'>
|
||||
{/* Settings Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Import Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
|
||||
Export Settings
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
|
||||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 w-full max-w-48'>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
|
||||
Export Chats
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
|
||||
Reset Chats
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<ErrorBoundary>
|
||||
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
{/* AI Instructions section */}
|
||||
<div className='max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
System instructions to include with all AI requests.
|
||||
Alternatively, place a \`.voidrules\` file in the root of your workspace.
|
||||
`} chatMessageLocation={undefined} />
|
||||
</h4>
|
||||
<ErrorBoundary>
|
||||
<AIInstructionsBox />
|
||||
</ErrorBoundary>
|
||||
{/* --- Disable System Message Toggle --- */}
|
||||
<div className='my-4'>
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.disableSystemMessage}
|
||||
onChange={(newValue) => {
|
||||
voidSettingsService.setGlobalSetting('disableSystemMessage', newValue);
|
||||
}}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>
|
||||
{settingsState.globalSettings.disableSystemMessage ? 'Minimal system messages sent' : 'Full system messages sent'}
|
||||
</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
<div className='text-void-fg-3 text-xs mt-1'>
|
||||
{`When enabled, Void will not include anything in the system message except for content you specified in voidrules and AI Instructions.`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
<ErrorBoundary>
|
||||
<AIInstructionsBox />
|
||||
</ErrorBoundary>
|
||||
{/* --- Disable System Message Toggle --- */}
|
||||
<div className='my-4'>
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={!!settingsState.globalSettings.disableSystemMessage}
|
||||
onChange={(newValue) => {
|
||||
voidSettingsService.setGlobalSetting('disableSystemMessage', newValue);
|
||||
}}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>
|
||||
{'Disable system message'}
|
||||
</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
<div className='text-void-fg-3 text-xs mt-1'>
|
||||
{`When disabled, Void will not include anything in the system message except for content you specified above.`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className='text-3xl mb-2'>MCP</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* MCP section */}
|
||||
<div className={shouldShowTab('mcp') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2'>MCP</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
Use Model Context Protocol to provide Agent mode with more tools.
|
||||
`} chatMessageLocation={undefined} />
|
||||
</h4>
|
||||
<div>
|
||||
<VoidButtonBgDarken className='px-4 py-1 mb-2 w-full max-w-48' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
|
||||
Add MCP Server
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
<div className='my-2'>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full max-w-48' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
|
||||
Add MCP Server
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<MCPServersList />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<MCPServersList />
|
||||
</ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -297,11 +297,12 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
|
||||
|
||||
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.`)
|
||||
// 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 };
|
||||
|
||||
@@ -42,8 +42,29 @@ const validateStr = (argName: string, value: unknown) => {
|
||||
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)}.`)
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
|
||||
// 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 validateOptionalURI = (uriStr: unknown) => {
|
||||
@@ -133,7 +154,6 @@ export class ToolsService implements IToolsService {
|
||||
@IMarkerService private readonly markerService: IMarkerService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.validateParams = {
|
||||
|
||||
@@ -61,6 +61,9 @@ 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
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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)
|
||||
@@ -88,7 +88,7 @@ class MCPService extends Disposable implements IMCPService {
|
||||
|
||||
|
||||
const onEvent = (e: MCPServerEventResponse) => {
|
||||
console.log('GOT EVENT', e)
|
||||
// console.log('GOT EVENT', e)
|
||||
this._setMCPServerState(e.response.name, e.response.newServer)
|
||||
}
|
||||
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
|
||||
@@ -234,3 +234,9 @@ export interface MCPToolCallParams {
|
||||
toolName: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const removeMCPToolNamePrefix = (name: string) => {
|
||||
return name.split('_').slice(1).join('_')
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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>;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,11 @@ 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()
|
||||
|
||||
@@ -60,6 +60,12 @@ export const defaultProviderSettings = {
|
||||
apiKey: '',
|
||||
azureApiVersion: '2024-05-01-preview',
|
||||
},
|
||||
awsBedrock: {
|
||||
apiKey: '',
|
||||
region: 'us-east-1', // add region setting
|
||||
endpoint: '', // optionally allow overriding default
|
||||
},
|
||||
|
||||
} as const
|
||||
|
||||
|
||||
@@ -145,6 +151,7 @@ export const defaultModelsOfProvider = {
|
||||
openAICompatible: [], // fallback
|
||||
googleVertex: [],
|
||||
microsoftAzure: [],
|
||||
awsBedrock: [],
|
||||
liteLLM: [],
|
||||
|
||||
|
||||
@@ -978,7 +985,24 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
||||
'magistral-medium-latest': {
|
||||
contextWindow: 256_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0.30, output: 0.90 }, // TODO: check this
|
||||
supportsFIM: true,
|
||||
downloadable: { sizeGb: 13 },
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
'magistral-small-latest': {
|
||||
contextWindow: 40_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0.30, output: 0.90 }, // TODO: check this
|
||||
supportsFIM: true,
|
||||
downloadable: { sizeGb: 13 },
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
'devstral-small-latest': { //https://openrouter.ai/mistralai/devstral-small:free
|
||||
contextWindow: 131_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
@@ -988,7 +1012,6 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
||||
'ministral-8b-latest': { // ollama 'mistral'
|
||||
contextWindow: 131_000,
|
||||
reservedOutputTokenSpace: 4_096,
|
||||
@@ -1098,6 +1121,18 @@ const microsoftAzureSettings: VoidStaticProviderInfo = {
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------- AWS BEDROCK ----------------
|
||||
const awsBedrockModelOptions = {
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
|
||||
const awsBedrockSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: awsBedrockModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
||||
const ollamaModelOptions = {
|
||||
@@ -1214,6 +1249,7 @@ const openaiCompatible: VoidStaticProviderInfo = {
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1437,6 +1473,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
||||
|
||||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
awsBedrock: awsBedrockSettings,
|
||||
} as const
|
||||
|
||||
|
||||
|
||||
@@ -575,11 +575,24 @@ export const messageOfSelection = async (
|
||||
) => {
|
||||
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
|
||||
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
if (s.type === 'CodeSelection') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}`
|
||||
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
|
||||
}
|
||||
else if (s.type === 'File') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
|
||||
|
||||
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
|
||||
}
|
||||
else if (s.type === 'Folder') {
|
||||
@@ -980,3 +993,77 @@ 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()
|
||||
}
|
||||
|
||||
@@ -17,3 +17,7 @@ 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'
|
||||
|
||||
@@ -18,7 +18,7 @@ export type ShallowDirectoryItem = {
|
||||
}
|
||||
|
||||
|
||||
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'mcp-tools' }> = {
|
||||
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'MCP tools' }> = {
|
||||
'create_file_or_folder': 'edits',
|
||||
'delete_file_or_folder': 'edits',
|
||||
'rewrite_file': 'edits',
|
||||
@@ -35,7 +35,7 @@ export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>([
|
||||
...Object.values(approvalTypeOfBuiltinToolName),
|
||||
'mcp-tools',
|
||||
'MCP tools',
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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')
|
||||
@@ -116,6 +116,7 @@ export const modelFilterOfFeatureName: {
|
||||
'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, },
|
||||
}
|
||||
|
||||
|
||||
@@ -213,9 +214,9 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null, 'SCM': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {}, 'SCM': {} },
|
||||
overridesOfModel: deepClone(defaultOverridesOfModel),
|
||||
_modelOptions: [], // computed later
|
||||
mcpUserStateOfName: {},
|
||||
@@ -262,6 +263,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
}
|
||||
async resetState() {
|
||||
await this.dangerousSetState(defaultState())
|
||||
@@ -280,7 +282,16 @@ 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()
|
||||
@@ -392,7 +403,10 @@ 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) => {
|
||||
@@ -409,6 +423,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// hooks
|
||||
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
|
||||
if (this.state.globalSettings.syncSCMToChat) this._onUpdate_syncSCMToChat()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -428,7 +444,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// hooks
|
||||
if (featureName === 'Chat') {
|
||||
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
|
||||
// When Chat model changes, update synced features
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
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}"`)
|
||||
}
|
||||
@@ -120,6 +123,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
||||
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 === '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).'
|
||||
@@ -151,7 +155,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'mistral' ? 'api-key...' :
|
||||
providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
'',
|
||||
providerName === 'awsBedrock' ? 'key-...' :
|
||||
'',
|
||||
|
||||
isPasswordField: true,
|
||||
}
|
||||
@@ -165,14 +170,16 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
'(never)',
|
||||
providerName === 'awsBedrock' ? 'Endpoint' :
|
||||
'(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'
|
||||
: '(never)',
|
||||
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
|
||||
: '(never)',
|
||||
|
||||
|
||||
}
|
||||
@@ -185,7 +192,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
return {
|
||||
title: 'Region',
|
||||
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||
: ''
|
||||
: providerName === 'awsBedrock'
|
||||
? defaultProviderSettings.awsBedrock.region
|
||||
: ''
|
||||
}
|
||||
}
|
||||
else if (settingName === 'azureApiVersion') {
|
||||
@@ -222,12 +231,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
@@ -340,6 +346,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
awsBedrock: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.awsBedrock,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -350,7 +362,7 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
|
||||
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply', 'SCM'] as const
|
||||
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
|
||||
@@ -365,6 +377,9 @@ 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`)
|
||||
}
|
||||
@@ -428,6 +443,7 @@ export type GlobalSettings = {
|
||||
aiInstructions: string;
|
||||
enableAutocomplete: boolean;
|
||||
syncApplyToChat: boolean;
|
||||
syncSCMToChat: boolean;
|
||||
enableFastApply: boolean;
|
||||
chatMode: ChatMode;
|
||||
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
|
||||
@@ -435,6 +451,7 @@ export type GlobalSettings = {
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
disableSystemMessage: boolean;
|
||||
autoAcceptLLMChanges: boolean;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
@@ -442,6 +459,7 @@ export const defaultGlobalSettings: GlobalSettings = {
|
||||
aiInstructions: '',
|
||||
enableAutocomplete: false,
|
||||
syncApplyToChat: true,
|
||||
syncSCMToChat: true,
|
||||
enableFastApply: true,
|
||||
chatMode: 'agent',
|
||||
autoApprove: {},
|
||||
@@ -449,6 +467,7 @@ export const defaultGlobalSettings: GlobalSettings = {
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: false,
|
||||
disableSystemMessage: false,
|
||||
autoAcceptLLMChanges: false,
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
|
||||
@@ -122,6 +122,29 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
||||
const options = { endpoint, apiKey: thisConfig.apiKey, apiVersion };
|
||||
return new AzureOpenAI({ ...options, ...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]
|
||||
@@ -343,7 +366,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
|
||||
}
|
||||
@@ -490,7 +513,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
onText({
|
||||
fullText,
|
||||
fullReasoning,
|
||||
toolCall: { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
|
||||
toolCall: !fullToolName ? undefined : { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
|
||||
})
|
||||
}
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
@@ -540,8 +563,11 @@ 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 toolCallObj = toolCall ? { toolCall } : {}
|
||||
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||
})
|
||||
// on error
|
||||
@@ -785,7 +811,7 @@ const sendGeminiChat = async ({
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -906,6 +932,12 @@ export const sendLLMMessageToProviderImplementation = {
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
awsBedrock: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
|
||||
} satisfies CallFnOfProvider
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 } from '../common/mcpServiceTypes.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';
|
||||
@@ -195,7 +195,7 @@ export class MCPChannel implements IServerChannel {
|
||||
}
|
||||
}
|
||||
} else if (server.command) {
|
||||
console.log('ENV DATA: ', server.env)
|
||||
// console.log('ENV DATA: ', server.env)
|
||||
transport = new StdioClientTransport({
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
@@ -310,7 +310,7 @@ export class MCPChannel implements IServerChannel {
|
||||
|
||||
// Call the tool with the provided parameters
|
||||
const response = await client.callTool({
|
||||
name: toolName,
|
||||
name: removeMCPToolNamePrefix(toolName),
|
||||
arguments: params
|
||||
})
|
||||
const { content } = response as CallToolResult
|
||||
@@ -320,8 +320,7 @@ export class MCPChannel implements IServerChannel {
|
||||
// handle text response
|
||||
|
||||
if (response.isError) {
|
||||
throw new Error(`Tool call error: ${response.content}`)
|
||||
// handle error
|
||||
throw new Error(`Tool call error: ${returnValue.text}`)
|
||||
}
|
||||
|
||||
// handle success
|
||||
@@ -354,41 +353,26 @@ export class MCPChannel implements IServerChannel {
|
||||
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
|
||||
if (err && typeof err === 'object' && 'code' in err) {
|
||||
const errorCode = err.code;
|
||||
const errorName = err.name || 'Unknown Error';
|
||||
const errorMsg = err.message || '';
|
||||
|
||||
// Map common JSON-RPC error codes to user-friendly messages
|
||||
let codeDescription = '';
|
||||
switch (errorCode) {
|
||||
case -32700:
|
||||
codeDescription = 'Parse Error';
|
||||
break;
|
||||
case -32600:
|
||||
codeDescription = 'Invalid Request';
|
||||
break;
|
||||
case -32601:
|
||||
codeDescription = 'Method Not Found';
|
||||
break;
|
||||
case -32602:
|
||||
codeDescription = 'Invalid Parameters';
|
||||
break;
|
||||
case -32603:
|
||||
codeDescription = 'Internal Error';
|
||||
break;
|
||||
default:
|
||||
codeDescription = `Error Code ${errorCode}`;
|
||||
}
|
||||
|
||||
errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`;
|
||||
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||
// Standard error with message
|
||||
errorMessage = err.message;
|
||||
} else if (typeof err === 'string') {
|
||||
else if (typeof err === 'string') {
|
||||
// String error
|
||||
errorMessage = err;
|
||||
} else {
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -29,6 +30,8 @@ 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;
|
||||
|
||||
@@ -119,7 +122,18 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
distinctId: this.distinctId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
}
|
||||
@@ -127,10 +141,18 @@ 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:', capture)
|
||||
// console.log('full capture:', this.distinctId)
|
||||
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
|
||||
|
||||
@@ -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 { 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,6 +32,12 @@ 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)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 813 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 850 KiB |
Reference in New Issue
Block a user