mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
Merge f9647e877b into 17e7a5b152
This commit is contained in:
commit
418e9aebc9
48 changed files with 2854 additions and 241 deletions
31
.dockerignore
Normal file
31
.dockerignore
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Git
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Node (rebuilt in Docker)
|
||||
node_modules
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
HOW_TO_CONTRIBUTE.md
|
||||
VOID_CODEBASE_GUIDE.md
|
||||
LICENSE*.txt
|
||||
ThirdPartyNotices.txt
|
||||
|
||||
# CI/CD
|
||||
CodeQL.yml
|
||||
|
||||
# Build output (rebuilt in Docker)
|
||||
out
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Playwright browsers (not needed for server)
|
||||
.playwright
|
||||
3
.github/CODEOWNERS
vendored
Normal file
3
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Default code owners for all files
|
||||
* @danialsamiei
|
||||
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
This is a fork of the VSCode repo called Void.
|
||||
This is the Orcide IDE repository, a fork of VSCode.
|
||||
|
||||
Most code we care about lives in src/vs/workbench/contrib/void.
|
||||
|
||||
|
|
|
|||
61
Dockerfile
Normal file
61
Dockerfile
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Dockerfile for ide.orcest.ai (VS Code fork) - Render.com deploy
|
||||
# Requires X11 libs for native-keymap, node-pty, etc.
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
|
||||
# Install build deps for native modules (native-keymap, node-pty, etc.)
|
||||
# ripgrep: @vscode/ripgrep postinstall downloads from GitHub and gets 403 in cloud builds
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
pkg-config \
|
||||
libxkbfile-dev \
|
||||
libx11-dev \
|
||||
libxrandr-dev \
|
||||
libxi-dev \
|
||||
libxtst-dev \
|
||||
libxrender-dev \
|
||||
libxfixes-dev \
|
||||
libxext-dev \
|
||||
libxkbcommon-dev \
|
||||
libsecret-1-dev \
|
||||
libkrb5-dev \
|
||||
git \
|
||||
ripgrep \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy source (postinstall needs full tree for build/, remote/, etc.)
|
||||
COPY . .
|
||||
|
||||
# Install deps - requires X11 libs above for native-keymap, node-pty
|
||||
# Use --ignore-scripts to skip @vscode/ripgrep postinstall (403 from GitHub in cloud builds),
|
||||
# supply system ripgrep, run postinstall (with install not rebuild for subdirs), then rebuild native modules
|
||||
RUN npm i --ignore-scripts \
|
||||
&& mkdir -p node_modules/@vscode/ripgrep/bin \
|
||||
&& cp /usr/bin/rg node_modules/@vscode/ripgrep/bin/rg \
|
||||
&& VSCODE_USE_SYSTEM_RIPGREP=1 npm rebuild \
|
||||
&& mkdir -p build/node_modules/@vscode/ripgrep/bin \
|
||||
&& cp /usr/bin/rg build/node_modules/@vscode/ripgrep/bin/rg \
|
||||
&& (cd build && npm rebuild) \
|
||||
&& mkdir -p remote/node_modules/@vscode/ripgrep/bin \
|
||||
&& cp /usr/bin/rg remote/node_modules/@vscode/ripgrep/bin/rg \
|
||||
&& (cd remote && npm rebuild)
|
||||
|
||||
# Build: React components first, then compile produces out/ (server + workbench), compile-web adds extension web bundles
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
RUN npm run buildreact \
|
||||
&& npm run compile \
|
||||
&& npm run compile-web
|
||||
|
||||
# VSCODE_DEV=1 tells the server to use the dev workbench template which loads CSS
|
||||
# dynamically via import maps. Without this, the server expects a bundled workbench.css
|
||||
# that is only produced by the full packaging pipeline (gulp vscode-web-min), not by
|
||||
# npm run compile.
|
||||
ENV VSCODE_DEV=1
|
||||
|
||||
# Render sets PORT; use code-server (production) not code-web (test harness)
|
||||
EXPOSE 10000
|
||||
CMD ["sh", "-c", "node out/server-main.js --host 0.0.0.0 --port ${PORT:-10000} --without-connection-token --accept-server-license-terms"]
|
||||
|
|
@ -4,15 +4,15 @@ This is the official guide on how to contribute to Void. We want to make it as e
|
|||
|
||||
There are a few ways to contribute:
|
||||
|
||||
- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor/projects/2).
|
||||
- 💫 Complete items on the [Roadmap](https://github.com/orgs/orcide/projects/2).
|
||||
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
- 🪴 Start new Issues - see [Issues](https://github.com/voideditor/void/issues).
|
||||
- 🪴 Start new Issues - see [Issues](https://github.com/orcest-ai/Orcide/issues).
|
||||
|
||||
|
||||
|
||||
### Codebase Guide
|
||||
|
||||
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to add new features.
|
||||
We [highly recommend reading this](https://github.com/orcest-ai/Orcide/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to add new features.
|
||||
|
||||
The repo is not as intimidating as it first seems if you read the guide!
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ First, run `npm install -g node-gyp`. Then:
|
|||
|
||||
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.
|
||||
1. `git clone https://github.com/orcest-ai/Orcide` to clone the repo.
|
||||
2. `npm install` to install all dependencies.
|
||||
3. Open Void or VSCode, and initialize Developer Mode (this can take ~5 min to finish, it's done when 2 of the 3 spinners turn to check marks):
|
||||
- Windows: Press <kbd>Ctrl+Shift+B</kbd>.
|
||||
|
|
@ -85,7 +85,7 @@ If you get any errors, scroll down for common fixes.
|
|||
- 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.
|
||||
- If you have any other questions, feel free to [submit an issue](https://github.com/orcest-ai/Orcide/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
|
||||
|
||||
|
|
@ -103,9 +103,9 @@ To build Void from the terminal instead of from inside VSCode, follow the steps
|
|||
|
||||
|
||||
### 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).
|
||||
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/orcest-ai/Orcide-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.
|
||||
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/orcest-ai/Orcide-builder) repo which builds Void and contains a few important notes about auto-updating and rebasing.
|
||||
|
||||
|
||||
#### Building a Local Executible
|
||||
|
|
|
|||
71
README.md
71
README.md
|
|
@ -1,41 +1,70 @@
|
|||
# Welcome to Void.
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<div align="center">
|
||||
<img
|
||||
src="./src/vs/workbench/browser/parts/editor/media/slice_of_void.png"
|
||||
alt="Void Welcome"
|
||||
width="300"
|
||||
height="300"
|
||||
/>
|
||||
<h1 align="center" style="border-bottom: none">Orcide: Cloud IDE</h1>
|
||||
<p align="center"><b>Part of the Orcest AI Ecosystem</b></p>
|
||||
</div>
|
||||
|
||||
Void is the open-source Cursor alternative.
|
||||
<div align="center">
|
||||
<a href="https://github.com/orcest-ai/Orcide/blob/main/LICENSE"><img src="https://img.shields.io/badge/LICENSE-MIT-20B2AA?style=for-the-badge" alt="MIT License"></a>
|
||||
</div>
|
||||
|
||||
Use AI agents on your codebase, checkpoint and visualize changes, and bring any model or host locally. Void sends messages directly to providers without retaining your data.
|
||||
<hr>
|
||||
|
||||
This repo contains the full sourcecode for Void. If you're new, welcome!
|
||||
Orcide is an AI-powered cloud IDE that provides intelligent code editing, autocomplete, and refactoring capabilities. It is a core component of the **Orcest AI** ecosystem, integrated with **RainyModel** (rm.orcest.ai) for intelligent LLM routing.
|
||||
|
||||
- 🧭 [Website](https://voideditor.com)
|
||||
### Orcest AI Ecosystem
|
||||
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
| Service | Domain | Role |
|
||||
|---------|--------|------|
|
||||
| **Lamino** | llm.orcest.ai | LLM Workspace |
|
||||
| **RainyModel** | rm.orcest.ai | LLM Routing Proxy |
|
||||
| **Maestrist** | agent.orcest.ai | AI Agent Platform |
|
||||
| **Orcide** | ide.orcest.ai | Cloud IDE |
|
||||
| **Login** | login.orcest.ai | SSO Authentication |
|
||||
|
||||
- 🚙 [Project Board](https://github.com/orgs/voideditor/projects/2)
|
||||
## Features
|
||||
|
||||
- **AI-Powered Code Editing**: Intelligent code suggestions and completions
|
||||
- **Chat Interface**: Built-in AI chat for code assistance
|
||||
- **Autocomplete**: Context-aware code completion powered by RainyModel
|
||||
- **Code Refactoring**: AI-assisted code improvements
|
||||
- **RainyModel Integration**: Smart LLM routing with automatic fallback (Free → Internal → Premium)
|
||||
- **SSO Authentication**: Enterprise-grade access control via login.orcest.ai
|
||||
- **VS Code Compatible**: Full VS Code extension ecosystem support
|
||||
|
||||
## Note
|
||||
## RainyModel Configuration
|
||||
|
||||
We've paused work on the Void IDE (this repo) to explore a few novel coding ideas. We want to focus on innovation over feature-parity. Void will continue running, but without maintenance some existing features might stop working over time. Depending on the direction of our new work, we might not resume Void as an IDE.
|
||||
Configure Orcide to use RainyModel as its AI backend:
|
||||
|
||||
We won't be actively reviewing Issues and PRs, but we will respond to all [email](mailto:hello@voideditor.com) inquiries on building and maintaining your own version of Void while we're paused.
|
||||
1. Open Settings (Ctrl+,)
|
||||
2. Navigate to AI / LLM settings
|
||||
3. Set:
|
||||
- **API Provider**: OpenAI-Compatible
|
||||
- **Base URL**: `https://rm.orcest.ai/v1`
|
||||
- **API Key**: Your RainyModel API key
|
||||
- **Chat Model**: `rainymodel/chat`
|
||||
- **Autocomplete Model**: `rainymodel/code`
|
||||
|
||||
## Reference
|
||||
## Development
|
||||
|
||||
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).
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
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).
|
||||
# Build
|
||||
yarn build
|
||||
|
||||
# Run in development mode
|
||||
yarn watch
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
See [HOW_TO_CONTRIBUTE.md](HOW_TO_CONTRIBUTE.md) for contribution guidelines.
|
||||
|
||||
## Support
|
||||
You can always reach us in our Discord server or contact us via email: hello@voideditor.com.
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
|
||||
Part of the [Orcest AI](https://orcest.ai) ecosystem.
|
||||
|
|
|
|||
27
SECURITY.md
Normal file
27
SECURITY.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| Latest | Yes |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, please report it responsibly:
|
||||
|
||||
1. **Do NOT** open a public GitHub issue
|
||||
2. Use [GitHub Security Advisories](https://github.com/orcest-ai/Orcide/security/advisories/new) to report privately
|
||||
3. Or email: support@orcest.ai
|
||||
|
||||
We will acknowledge receipt within 48 hours and provide a timeline for resolution.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- All secrets must be stored in environment variables, never in code
|
||||
- All services require SSO authentication via login.orcest.ai
|
||||
- API keys must be rotated regularly
|
||||
- All traffic must use HTTPS/TLS
|
||||
|
||||
Part of the [Orcest AI](https://orcest.ai) ecosystem.
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# Void Codebase Guide
|
||||
# Orcide Codebase Guide
|
||||
|
||||
The Void codebase is not as intimidating as it seems!
|
||||
The Orcide codebase is not as intimidating as it seems!
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
Most of Orcide's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
The purpose of this document is to explain how Void's codebase works. If you want build instructions instead, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
The purpose of this document is to explain how Orcide's codebase works. If you want build instructions instead, see [Contributing](https://github.com/orcest-ai/Orcide/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
|
||||
|
||||
|
|
@ -14,10 +14,10 @@ The purpose of this document is to explain how Void's codebase works. If you wan
|
|||
|
||||
|
||||
|
||||
## Void Codebase Guide
|
||||
## Orcide Codebase Guide
|
||||
|
||||
### VSCode Rundown
|
||||
Here's a VSCode rundown if you're just getting started with Void. You can also see Microsoft's [wiki](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for some pictures. VSCode is an Electron app. Electron runs two processes: a **main** process (for internals) and a **browser** process (browser means HTML in general, not just "web browser").
|
||||
Here's a VSCode rundown if you're just getting started with Orcide. You can also see Microsoft's [wiki](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for some pictures. VSCode is an Electron app. Electron runs two processes: a **main** process (for internals) and a **browser** process (browser means HTML in general, not just "web browser").
|
||||
<p align="center" >
|
||||
<img src="https://github.com/user-attachments/assets/eef80306-2bfe-4cac-ba15-6156f65ab3bb" alt="Credit - https://github.com/microsoft/vscode/wiki/Source-Code-Organization" width="700px">
|
||||
</p>
|
||||
|
|
@ -54,7 +54,7 @@ Here's some terminology you might want to know about when working inside VSCode:
|
|||
|
||||
### Internal LLM Message Pipeline
|
||||
|
||||
Here's a picture of all the dependencies that are relevent between the time you first send a message through Void's sidebar, and the time a request is sent to your provider.
|
||||
Here's a picture of all the dependencies that are relevent between the time you first send a message through Orcide'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.
|
||||
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Sending LLM messages from the main process avoids CSP issues with local provider
|
|||
|
||||
### Apply
|
||||
|
||||
Void has two types of Apply: **Fast Apply** (uses Search/Replace, see below), and **Slow Apply** (rewrites whole file).
|
||||
Orcide has two types of Apply: **Fast Apply** (uses Search/Replace, see below), and **Slow Apply** (rewrites whole file).
|
||||
|
||||
When you click Apply and Fast Apply is enabled, we prompt the LLM to output Search/Replace block(s) like this:
|
||||
```
|
||||
|
|
@ -79,7 +79,7 @@ When you click Apply and Fast Apply is enabled, we prompt the LLM to output Sear
|
|||
// replaced code goes here
|
||||
>>>>>>> UPDATED
|
||||
```
|
||||
This is what allows Void to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query.
|
||||
This is what allows Orcide to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query.
|
||||
|
||||
### Apply Inner Workings
|
||||
|
||||
|
|
@ -97,10 +97,10 @@ How Apply works:
|
|||
|
||||
|
||||
### Writing Files Inner Workings
|
||||
When Void wants to change your code, it just writes to a text model. This means all you need to know to write to a file is its URI - you don't have to load it, save it, etc. There are some annoying background URI/model things to think about to get this to work, but we handled them all in `voidModelService`.
|
||||
When Orcide wants to change your code, it just writes to a text model. This means all you need to know to write to a file is its URI - you don't have to load it, save it, etc. There are some annoying background URI/model things to think about to get this to work, but we handled them all in `voidModelService`.
|
||||
|
||||
### Void Settings Inner Workings
|
||||
We have a service `voidSettingsService` that stores all your Void settings (providers, models, global Void settings, etc). Imagine this as an implicit dependency for any of the core Void services:
|
||||
### Orcide Settings Inner Workings
|
||||
We have a service `voidSettingsService` that stores all your Orcide settings (providers, models, global Orcide settings, etc). Imagine this as an implicit dependency for any of the core Orcide services:
|
||||
|
||||
<div align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/9f3cb68c-a61b-4810-8429-bb90b992b3fa">
|
||||
|
|
@ -126,13 +126,13 @@ Here's a guide to some of the terminology we're using:
|
|||
|
||||
|
||||
### Build process
|
||||
If you want to know how our build pipeline works, see our build repo [here](https://github.com/voideditor/void-builder).
|
||||
If you want to know how our build pipeline works, see our build repo [here](https://github.com/orcest-ai/Orcide-builder).
|
||||
|
||||
|
||||
|
||||
## VSCode Codebase Guide
|
||||
|
||||
For additional references, the Void team put together this list of links to get up and running with VSCode.
|
||||
For additional references, the Orcide team put together this list of links to get up and running with VSCode.
|
||||
<details>
|
||||
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ For additional references, the Void team put together this list of links to get
|
|||
|
||||
#### VSCode's Extension API
|
||||
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
Orcide is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
|
|
|||
|
|
@ -46,7 +46,12 @@ function npmInstall(dir, opts) {
|
|||
shell: true
|
||||
};
|
||||
|
||||
const command = process.env['npm_command'] || 'install';
|
||||
// When parent runs "npm rebuild", npm_command=rebuild causes subdirs to run "npm rebuild"
|
||||
// which doesn't install packages. Subdirs need "npm install" to populate node_modules.
|
||||
const rawCommand = opts.npmCommandOverride != null
|
||||
? opts.npmCommandOverride
|
||||
: (process.env['npm_command'] || 'install');
|
||||
const command = (rawCommand === 'rebuild' ? 'install' : rawCommand);
|
||||
|
||||
if (process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'] && /^(.build\/distro\/npm\/)?remote$/.test(dir)) {
|
||||
const userinfo = os.userInfo();
|
||||
|
|
@ -128,6 +133,10 @@ for (let dir of dirs) {
|
|||
...process.env
|
||||
},
|
||||
}
|
||||
// When set, use --ignore-scripts for build to skip @vscode/ripgrep postinstall (403 from GitHub).
|
||||
if (process.env['VSCODE_USE_SYSTEM_RIPGREP']) {
|
||||
opts.npmCommandOverride = 'install --ignore-scripts';
|
||||
}
|
||||
if (process.env['CC']) { opts.env['CC'] = 'gcc'; }
|
||||
if (process.env['CXX']) { opts.env['CXX'] = 'g++'; }
|
||||
if (process.env['CXXFLAGS']) { opts.env['CXXFLAGS'] = ''; }
|
||||
|
|
@ -145,6 +154,11 @@ for (let dir of dirs) {
|
|||
...process.env
|
||||
},
|
||||
}
|
||||
// When set, use --ignore-scripts for remote to skip @vscode/ripgrep postinstall (403 from GitHub).
|
||||
// Caller must then copy system ripgrep into remote/node_modules/@vscode/ripgrep/bin and run npm rebuild in remote.
|
||||
if (process.env['VSCODE_USE_SYSTEM_RIPGREP']) {
|
||||
opts.npmCommandOverride = 'install --ignore-scripts';
|
||||
}
|
||||
if (process.env['VSCODE_REMOTE_CC']) {
|
||||
opts.env['CC'] = process.env['VSCODE_REMOTE_CC'];
|
||||
} else {
|
||||
|
|
@ -188,5 +202,8 @@ for (let dir of dirs) {
|
|||
npmInstall(dir, opts);
|
||||
}
|
||||
|
||||
cp.execSync('git config pull.rebase merges');
|
||||
cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');
|
||||
// Skip git config if not in a git repo (e.g. Docker build where .git is not copied)
|
||||
if (fs.existsSync(path.join(root, '.git'))) {
|
||||
cp.execSync('git config pull.rebase merges');
|
||||
cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "open-remote-ssh",
|
||||
"displayName": "Open Remote - SSH",
|
||||
"publisher": "voideditor",
|
||||
"publisher": "orcide",
|
||||
"description": "Use any remote machine with a SSH server as your development environment.",
|
||||
"version": "0.0.48",
|
||||
"icon": "resources/icon.png",
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
|
||||
"default": "https://github.com/orcest-ai/Orcide-binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
|
||||
},
|
||||
"remote.SSH.remotePlatform": {
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/orcest-ai/Orcide-binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
|
||||
|
||||
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
|
||||
let shell = 'powershell';
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
|
||||
"default": "https://github.com/orcest-ai/Orcide-binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/orcest-ai/Orcide-binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
|
||||
|
||||
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.99.3",
|
||||
"name": "orcide",
|
||||
"version": "2.0.0",
|
||||
"distro": "21c8d8ea1e46d97c5639a7cabda6c0e063cc8dd5",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
"name": "Orcest AI"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "./out/main.js",
|
||||
|
|
@ -263,10 +263,10 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/vscode.git"
|
||||
"url": "https://github.com/orcest-ai/Orcide.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/microsoft/vscode/issues"
|
||||
"url": "https://github.com/orcest-ai/Orcide/issues"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"windows-foreground-love": "0.5.0"
|
||||
|
|
|
|||
80
product.json
80
product.json
|
|
@ -1,46 +1,74 @@
|
|||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.4.9",
|
||||
"voidRelease": "0044",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
"nameShort": "Orcide",
|
||||
"nameLong": "Orcide",
|
||||
"orcideVersion": "2.0.0",
|
||||
"orcideRelease": "0001",
|
||||
"applicationName": "orcide",
|
||||
"dataFolderName": ".orcide",
|
||||
"win32MutexName": "orcide",
|
||||
"licenseName": "MIT",
|
||||
"licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
|
||||
"serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
|
||||
"licenseUrl": "https://github.com/orcest-ai/Orcide/blob/main/LICENSE.txt",
|
||||
"serverLicenseUrl": "https://github.com/orcest-ai/Orcide/blob/main/LICENSE.txt",
|
||||
"serverGreeting": [],
|
||||
"serverLicense": [],
|
||||
"serverLicensePrompt": "",
|
||||
"serverApplicationName": "void-server",
|
||||
"serverDataFolderName": ".void-server",
|
||||
"tunnelApplicationName": "void-tunnel",
|
||||
"win32DirName": "Void",
|
||||
"win32NameVersion": "Void",
|
||||
"win32RegValueName": "VoidEditor",
|
||||
"serverApplicationName": "orcide-server",
|
||||
"serverDataFolderName": ".orcide-server",
|
||||
"tunnelApplicationName": "orcide-tunnel",
|
||||
"win32DirName": "Orcide",
|
||||
"win32NameVersion": "Orcide",
|
||||
"win32RegValueName": "Orcide",
|
||||
"win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}",
|
||||
"win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}",
|
||||
"win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}",
|
||||
"win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}",
|
||||
"win32AppUserModelId": "Void.Editor",
|
||||
"win32ShellNameShort": "V&oid",
|
||||
"win32TunnelServiceMutex": "void-tunnelservice",
|
||||
"win32TunnelMutex": "void-tunnel",
|
||||
"darwinBundleIdentifier": "com.voideditor.code",
|
||||
"linuxIconName": "void-editor",
|
||||
"win32AppUserModelId": "Orcide.Editor",
|
||||
"win32ShellNameShort": "O&rcide",
|
||||
"win32TunnelServiceMutex": "orcide-tunnelservice",
|
||||
"win32TunnelMutex": "orcide-tunnel",
|
||||
"darwinBundleIdentifier": "com.orcide.code",
|
||||
"linuxIconName": "orcide",
|
||||
"licenseFileName": "LICENSE.txt",
|
||||
"reportIssueUrl": "https://github.com/voideditor/void/issues/new",
|
||||
"reportIssueUrl": "https://github.com/orcest-ai/Orcide/issues/new",
|
||||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "void",
|
||||
"urlProtocol": "orcide",
|
||||
"ssoProvider": {
|
||||
"issuer": "https://login.orcest.ai",
|
||||
"clientId": "orcide",
|
||||
"redirectUri": "https://ide.orcest.ai/auth/callback",
|
||||
"scopes": ["openid", "profile", "email"],
|
||||
"authorizationEndpoint": "https://login.orcest.ai/oauth2/authorize",
|
||||
"tokenEndpoint": "https://login.orcest.ai/oauth2/token",
|
||||
"userInfoEndpoint": "https://login.orcest.ai/oauth2/userinfo",
|
||||
"jwksUri": "https://login.orcest.ai/oauth2/jwks",
|
||||
"logoutUrl": "https://login.orcest.ai/logout"
|
||||
},
|
||||
"defaultApiProvider": {
|
||||
"name": "rainymodel",
|
||||
"endpoint": "https://rm.orcest.ai/v1",
|
||||
"displayName": "RainyModel"
|
||||
},
|
||||
"orcestApis": {
|
||||
"rainymodel": "https://rm.orcest.ai",
|
||||
"lamino": "https://llm.orcest.ai",
|
||||
"maestrist": "https://agent.orcest.ai",
|
||||
"ollamafreeapi": "https://ollamafreeapi.orcest.ai"
|
||||
},
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
|
||||
"itemUrl": "https://marketplace.visualstudio.com/items"
|
||||
},
|
||||
"builtInExtensions": [],
|
||||
"linkProtectionTrustedDomains": [
|
||||
"https://voideditor.com",
|
||||
"https://voideditor.dev",
|
||||
"https://github.com/voideditor/void",
|
||||
"https://orcest.ai",
|
||||
"https://login.orcest.ai",
|
||||
"https://ide.orcest.ai",
|
||||
"https://rm.orcest.ai",
|
||||
"https://llm.orcest.ai",
|
||||
"https://agent.orcest.ai",
|
||||
"https://ollamafreeapi.orcest.ai",
|
||||
"https://orcide.dev",
|
||||
"https://github.com/orcest-ai/Orcide",
|
||||
"https://ollama.com"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,6 +298,9 @@ export class WebClientServer {
|
|||
remoteAuthority = replacePort(remoteAuthority, forwardedPort);
|
||||
}
|
||||
|
||||
const forwardedProto = getFirstHeader('x-forwarded-proto');
|
||||
const remoteScheme: 'http' | 'https' = forwardedProto === 'https' ? 'https' : 'http';
|
||||
|
||||
function asJSON(value: unknown): string {
|
||||
return JSON.stringify(value).replace(/"/g, '"');
|
||||
}
|
||||
|
|
@ -338,7 +341,7 @@ export class WebClientServer {
|
|||
extensionsGallery: this._webExtensionResourceUrlTemplate && this._productService.extensionsGallery ? {
|
||||
...this._productService.extensionsGallery,
|
||||
resourceUrlTemplate: this._webExtensionResourceUrlTemplate.with({
|
||||
scheme: 'http',
|
||||
scheme: remoteScheme,
|
||||
authority: remoteAuthority,
|
||||
path: `${webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}`
|
||||
}).toString(true)
|
||||
|
|
@ -415,10 +418,11 @@ export class WebClientServer {
|
|||
const webWorkerExtensionHostIframeScriptSHA = 'sha256-2Q+j4hfT09+1+imS46J2YlkCtHWQt0/BE79PXjJ0ZJ8=';
|
||||
|
||||
const cspDirectives = [
|
||||
...(remoteScheme === 'https' ? ['upgrade-insecure-requests;'] : []),
|
||||
'default-src \'self\';',
|
||||
'img-src \'self\' https: data: blob:;',
|
||||
'media-src \'self\';',
|
||||
`script-src 'self' 'unsafe-eval' ${WORKBENCH_NLS_BASE_URL ?? ''} blob: 'nonce-1nline-m4p' ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' 'sha256-/r7rqQ+yrxt57sxLuQ6AMYcy/lUpvAIzHjIJt/OeLWU=' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
|
||||
`script-src 'self' 'unsafe-eval' ${WORKBENCH_NLS_BASE_URL ?? ''} blob: 'nonce-1nline-m4p' ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' 'sha256-/r7rqQ+yrxt57sxLuQ6AMYcy/lUpvAIzHjIJt/OeLWU=' ${useTestResolver ? '' : `${remoteScheme}://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
|
||||
'child-src \'self\';',
|
||||
`frame-src 'self' https://*.vscode-cdn.net data:;`,
|
||||
'worker-src \'self\' data: blob:;',
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
background-image: url('./orcide_logo.svg'); /* Orcide */
|
||||
background-size: contain;
|
||||
background-position-x: center;
|
||||
background-repeat: no-repeat;
|
||||
|
|
@ -63,17 +63,17 @@
|
|||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
background-image: url('./orcide_logo.svg'); /* Orcide */
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
background-image: url('./orcide_logo.svg'); /* Orcide */
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
background-image: url('./orcide_logo.svg'); /* Orcide */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts,
|
||||
|
|
|
|||
19
src/vs/workbench/browser/parts/editor/media/orcide_logo.svg
Normal file
19
src/vs/workbench/browser/parts/editor/media/orcide_logo.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Outer hexagonal frame -->
|
||||
<path d="M100 10 L180 50 L180 140 L100 180 L20 140 L20 50 Z" stroke="#888888" stroke-width="2" fill="none" opacity="0.25"/>
|
||||
<!-- Inner hexagonal frame -->
|
||||
<path d="M100 30 L165 60 L165 130 L100 160 L35 130 L35 60 Z" stroke="#888888" stroke-width="1.5" fill="none" opacity="0.15"/>
|
||||
<!-- Central O circle -->
|
||||
<circle cx="100" cy="95" r="36" stroke="#60a5fa" stroke-width="2.5" fill="none"/>
|
||||
<circle cx="100" cy="95" r="26" stroke="#60a5fa" stroke-width="1.2" fill="none" opacity="0.4"/>
|
||||
<!-- Connection nodes -->
|
||||
<circle cx="100" cy="59" r="2.5" fill="#60a5fa"/>
|
||||
<circle cx="136" cy="95" r="2.5" fill="#60a5fa"/>
|
||||
<circle cx="100" cy="131" r="2.5" fill="#60a5fa"/>
|
||||
<circle cx="64" cy="95" r="2.5" fill="#60a5fa"/>
|
||||
<!-- Connecting lines to hexagon -->
|
||||
<line x1="100" y1="59" x2="100" y2="30" stroke="#60a5fa" stroke-width="0.8" opacity="0.35"/>
|
||||
<line x1="136" y1="95" x2="165" y2="95" stroke="#60a5fa" stroke-width="0.8" opacity="0.35"/>
|
||||
<line x1="100" y1="131" x2="100" y2="160" stroke="#60a5fa" stroke-width="0.8" opacity="0.35"/>
|
||||
<line x1="64" y1="95" x2="35" y2="95" stroke="#60a5fa" stroke-width="0.8" opacity="0.35"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -28,7 +28,7 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
|||
const allLinebreakSymbols = ['\r\n', '\n']
|
||||
const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
// The extension this was called from is here - https://github.com/orcest-ai/Orcide/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
// run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
// }]
|
||||
// },
|
||||
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
|
||||
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/orcest-ai/Orcide/issues/new) it.` : undefined
|
||||
// })
|
||||
// }
|
||||
|
||||
|
|
|
|||
|
|
@ -195,37 +195,37 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
}
|
||||
|
|
@ -238,37 +238,37 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
}
|
||||
|
|
@ -283,37 +283,37 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Orcide', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Orcide', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.orcide', 'extensions'),
|
||||
isExtensions: true,
|
||||
}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class FilePromptActionService extends Action2 {
|
|||
constructor() {
|
||||
super({
|
||||
id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID,
|
||||
title: localize2('voidCopyPrompt', 'Void: Copy Prompt'),
|
||||
title: localize2('voidCopyPrompt', 'Orcide: Copy Prompt'),
|
||||
menu: [{
|
||||
id: MenuId.ExplorerContext,
|
||||
group: '8_void',
|
||||
|
|
|
|||
459
src/vs/workbench/contrib/void/browser/orcideSSOBrowserService.ts
Normal file
459
src/vs/workbench/contrib/void/browser/orcideSSOBrowserService.ts
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Orcest. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
import { IOrcideSSOService, ORCIDE_SSO_CONFIG } from '../common/orcideSSOService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
|
||||
|
||||
// ─── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Popup window dimensions
|
||||
const POPUP_WIDTH = 500;
|
||||
const POPUP_HEIGHT = 700;
|
||||
|
||||
// Maximum time to wait for the popup to complete (10 minutes)
|
||||
const POPUP_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
// Interval for polling the popup window state
|
||||
const POPUP_POLL_INTERVAL_MS = 500;
|
||||
|
||||
|
||||
// ─── Browser SSO Contribution ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Workbench contribution that handles browser-specific SSO behavior:
|
||||
* - Listens for OAuth2 callback messages from the popup window
|
||||
* - Handles the authorization code exchange
|
||||
* - Manages the popup window lifecycle
|
||||
*/
|
||||
export class OrcideSSOBrowserContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.orcideSSO';
|
||||
|
||||
private _popupWindow: Window | null = null;
|
||||
private _popupPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _popupTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(
|
||||
@IOrcideSSOService private readonly _ssoService: IOrcideSSOService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
) {
|
||||
super();
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
const targetWindow = getActiveWindow();
|
||||
|
||||
// Listen for postMessage from the OAuth2 callback popup.
|
||||
// The callback page at /auth/callback posts a message with the authorization
|
||||
// code and state back to the opener window.
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
this._handleOAuthMessage(event);
|
||||
};
|
||||
targetWindow.addEventListener('message', messageHandler);
|
||||
this._register({
|
||||
dispose: () => targetWindow.removeEventListener('message', messageHandler),
|
||||
});
|
||||
|
||||
// Also check if the current URL itself is a callback (for redirect-based flow
|
||||
// where the entire IDE is redirected to the callback URL)
|
||||
this._handleRedirectCallback(targetWindow);
|
||||
}
|
||||
|
||||
|
||||
// ── Redirect Flow Handling ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* If the IDE is loaded at the callback URL itself (redirect-based OAuth flow),
|
||||
* extract the code and state from the URL parameters and process the callback.
|
||||
*/
|
||||
private _handleRedirectCallback(targetWindow: Window): void {
|
||||
try {
|
||||
const url = new URL(targetWindow.location.href);
|
||||
const callbackPath = new URL(ORCIDE_SSO_CONFIG.redirectUri).pathname;
|
||||
|
||||
if (url.pathname !== callbackPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const error = url.searchParams.get('error');
|
||||
const errorDescription = url.searchParams.get('error_description');
|
||||
|
||||
// Clean the callback parameters from the URL so they don't persist
|
||||
// in the address bar or browser history
|
||||
url.searchParams.delete('code');
|
||||
url.searchParams.delete('state');
|
||||
url.searchParams.delete('error');
|
||||
url.searchParams.delete('error_description');
|
||||
url.searchParams.delete('session_state');
|
||||
targetWindow.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
||||
|
||||
if (error) {
|
||||
const message = errorDescription ?? error;
|
||||
console.error(`[OrcideSSOBrowser] OAuth error in redirect: ${message}`);
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `SSO login failed: ${message}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (code && state) {
|
||||
this._processAuthorizationCode(code, state);
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a callback URL, or parsing failed. This is expected in the
|
||||
// common case where the IDE is loaded normally.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Popup Flow Handling ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Opens the SSO login page in a centered popup window.
|
||||
* Called when the login() method triggers _openAuthorizationUrl.
|
||||
*/
|
||||
openLoginPopup(authUrl: string): void {
|
||||
// Close any existing popup
|
||||
this._closePopup();
|
||||
|
||||
const targetWindow = getActiveWindow();
|
||||
|
||||
// Calculate center position for the popup
|
||||
const left = Math.max(0, Math.round(targetWindow.screenX + (targetWindow.outerWidth - POPUP_WIDTH) / 2));
|
||||
const top = Math.max(0, Math.round(targetWindow.screenY + (targetWindow.outerHeight - POPUP_HEIGHT) / 2));
|
||||
|
||||
const features = [
|
||||
`width=${POPUP_WIDTH}`,
|
||||
`height=${POPUP_HEIGHT}`,
|
||||
`left=${left}`,
|
||||
`top=${top}`,
|
||||
'menubar=no',
|
||||
'toolbar=no',
|
||||
'location=yes',
|
||||
'status=yes',
|
||||
'resizable=yes',
|
||||
'scrollbars=yes',
|
||||
].join(',');
|
||||
|
||||
this._popupWindow = targetWindow.open(authUrl, 'orcide-sso-login', features);
|
||||
|
||||
if (!this._popupWindow) {
|
||||
// Popup was blocked by the browser. Fall back to redirect flow.
|
||||
console.warn('[OrcideSSOBrowser] Popup blocked, falling back to redirect flow');
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: 'Popup was blocked by the browser. Redirecting to SSO login page...',
|
||||
});
|
||||
targetWindow.location.href = authUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus the popup
|
||||
this._popupWindow.focus();
|
||||
|
||||
// Poll the popup to detect if the user closes it manually
|
||||
this._popupPollTimer = setInterval(() => {
|
||||
if (this._popupWindow && this._popupWindow.closed) {
|
||||
this._cleanupPopup();
|
||||
}
|
||||
}, POPUP_POLL_INTERVAL_MS);
|
||||
|
||||
// Set a timeout to auto-close the popup if it takes too long
|
||||
this._popupTimeoutTimer = setTimeout(() => {
|
||||
if (this._popupWindow && !this._popupWindow.closed) {
|
||||
console.warn('[OrcideSSOBrowser] Login popup timed out');
|
||||
this._closePopup();
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: 'SSO login timed out. Please try again.',
|
||||
});
|
||||
}
|
||||
}, POPUP_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
|
||||
// ── Message Handling ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Handles postMessage events from the OAuth callback page.
|
||||
* The callback page at the redirect URI should post a message with:
|
||||
* { type: 'orcide-sso-callback', code: string, state: string }
|
||||
* or
|
||||
* { type: 'orcide-sso-callback', error: string, errorDescription?: string }
|
||||
*/
|
||||
private _handleOAuthMessage(event: MessageEvent): void {
|
||||
// Validate the origin - only accept messages from our SSO issuer or
|
||||
// from the IDE itself (for same-origin callback pages)
|
||||
const allowedOrigins = [
|
||||
ORCIDE_SSO_CONFIG.issuer,
|
||||
new URL(ORCIDE_SSO_CONFIG.redirectUri).origin,
|
||||
];
|
||||
|
||||
if (!allowedOrigins.includes(event.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== 'object' || data.type !== 'orcide-sso-callback') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the popup since we got our response
|
||||
this._closePopup();
|
||||
|
||||
if (data.error) {
|
||||
const message = data.errorDescription ?? data.error;
|
||||
console.error(`[OrcideSSOBrowser] OAuth error from callback: ${message}`);
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `SSO login failed: ${message}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.code && data.state) {
|
||||
this._processAuthorizationCode(data.code, data.state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Authorization Code Processing ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Processes the received authorization code by delegating to the SSO service
|
||||
* to exchange it for tokens and set up the session.
|
||||
*/
|
||||
private async _processAuthorizationCode(code: string, state: string): Promise<void> {
|
||||
try {
|
||||
await this._ssoService.handleAuthorizationCallback(code, state);
|
||||
|
||||
const user = this._ssoService.getUserProfile();
|
||||
const displayName = user?.name || user?.email || 'User';
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: `Welcome, ${displayName}! You are now signed in.`,
|
||||
});
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
console.error('[OrcideSSOBrowser] Failed to process authorization code:', e);
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `SSO login failed: ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Popup Lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
private _closePopup(): void {
|
||||
if (this._popupWindow && !this._popupWindow.closed) {
|
||||
this._popupWindow.close();
|
||||
}
|
||||
this._cleanupPopup();
|
||||
}
|
||||
|
||||
private _cleanupPopup(): void {
|
||||
this._popupWindow = null;
|
||||
|
||||
if (this._popupPollTimer !== null) {
|
||||
clearInterval(this._popupPollTimer);
|
||||
this._popupPollTimer = null;
|
||||
}
|
||||
|
||||
if (this._popupTimeoutTimer !== null) {
|
||||
clearTimeout(this._popupTimeoutTimer);
|
||||
this._popupTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Cleanup ────────────────────────────────────────────────────────────────
|
||||
|
||||
override dispose(): void {
|
||||
this._closePopup();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Register the browser contribution ──────────────────────────────────────────
|
||||
|
||||
registerWorkbenchContribution2(
|
||||
OrcideSSOBrowserContribution.ID,
|
||||
OrcideSSOBrowserContribution,
|
||||
WorkbenchPhase.AfterRestored
|
||||
);
|
||||
|
||||
|
||||
// ─── Command Palette Actions ────────────────────────────────────────────────────
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'orcide.sso.login',
|
||||
f1: true,
|
||||
title: localize2('orcideSSOLogin', 'Orcide: Sign In with SSO'),
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const ssoService = accessor.get(IOrcideSSOService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
|
||||
if (ssoService.isAuthenticated()) {
|
||||
const user = ssoService.getUserProfile();
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: `Already signed in as ${user?.name ?? user?.email ?? 'unknown user'}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ssoService.login();
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `SSO login failed: ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'orcide.sso.logout',
|
||||
f1: true,
|
||||
title: localize2('orcideSSOLogout', 'Orcide: Sign Out'),
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const ssoService = accessor.get(IOrcideSSOService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
|
||||
if (!ssoService.isAuthenticated()) {
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: 'You are not currently signed in.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = ssoService.getUserProfile();
|
||||
try {
|
||||
await ssoService.logout();
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: `Signed out${user?.name ? ` (${user.name})` : ''}. See you next time!`,
|
||||
});
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `Sign out failed: ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'orcide.sso.status',
|
||||
f1: true,
|
||||
title: localize2('orcideSSOStatus', 'Orcide: SSO Status'),
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const ssoService = accessor.get(IOrcideSSOService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
|
||||
if (!ssoService.isAuthenticated()) {
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: 'Not signed in. Use "Orcide: Sign In with SSO" to authenticate.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = ssoService.getUserProfile();
|
||||
const { expiresAt } = ssoService.state;
|
||||
const expiresIn = expiresAt ? Math.max(0, Math.round((expiresAt - Date.now()) / 1000 / 60)) : 'unknown';
|
||||
|
||||
const lines = [
|
||||
`Signed in as: ${user?.name ?? 'Unknown'}`,
|
||||
`Email: ${user?.email ?? 'N/A'}`,
|
||||
`Role: ${user?.role ?? 'N/A'}`,
|
||||
`Token expires in: ${expiresIn} minutes`,
|
||||
];
|
||||
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: lines.join('\n'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'orcide.sso.refreshToken',
|
||||
f1: true,
|
||||
title: localize2('orcideSSORefresh', 'Orcide: Refresh SSO Token'),
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const ssoService = accessor.get(IOrcideSSOService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
|
||||
if (!ssoService.isAuthenticated()) {
|
||||
notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: 'Cannot refresh token: not signed in.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await ssoService.refreshToken();
|
||||
if (success) {
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: 'SSO token refreshed successfully.',
|
||||
});
|
||||
} else {
|
||||
notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: 'Failed to refresh SSO token. You may need to sign in again.',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `Token refresh failed: ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -220,7 +220,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
|
||||
|
||||
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
INativeHostService: accessor.get(INativeHostService),
|
||||
INativeHostService: (() => { try { return accessor.get(INativeHostService); } catch { return undefined; } })() as any,
|
||||
IToolsService: accessor.get(IToolsService),
|
||||
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
|
||||
ITerminalService: accessor.get(ITerminalService),
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
import { isLinux } from '../../../../../../../base/common/platform.js';
|
||||
|
||||
const OVERRIDE_VALUE = false
|
||||
|
||||
|
|
@ -39,29 +37,32 @@ export const VoidOnboarding = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const VoidIcon = () => {
|
||||
const accessor = useAccessor()
|
||||
const themeService = accessor.get('IThemeService')
|
||||
const OrcideIcon = () => {
|
||||
const isDark = useIsDark()
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// void icon style
|
||||
const updateTheme = () => {
|
||||
const theme = themeService.getColorTheme().type
|
||||
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
|
||||
if (divRef.current) {
|
||||
divRef.current.style.maxWidth = '220px'
|
||||
divRef.current.style.opacity = '50%'
|
||||
divRef.current.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
|
||||
}
|
||||
}
|
||||
updateTheme()
|
||||
const d = themeService.onDidColorThemeChange(updateTheme)
|
||||
return () => d.dispose()
|
||||
}, [])
|
||||
|
||||
return <div ref={divRef} className='@@void-void-icon' />
|
||||
return (
|
||||
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ opacity: 0.7 }}>
|
||||
{/* Outer hexagonal frame */}
|
||||
<path d="M110 10 L195 55 L195 145 L110 190 L25 145 L25 55 Z" stroke={isDark ? '#ffffff' : '#1a1a2e'} strokeWidth="2" fill="none" opacity="0.3" />
|
||||
{/* Inner hexagonal frame */}
|
||||
<path d="M110 35 L175 68 L175 132 L110 165 L45 132 L45 68 Z" stroke={isDark ? '#ffffff' : '#1a1a2e'} strokeWidth="1.5" fill="none" opacity="0.2" />
|
||||
{/* Central "O" letterform with circuit-like details */}
|
||||
<circle cx="110" cy="100" r="38" stroke={isDark ? '#60a5fa' : '#2563eb'} strokeWidth="3" fill="none" />
|
||||
<circle cx="110" cy="100" r="28" stroke={isDark ? '#60a5fa' : '#2563eb'} strokeWidth="1.5" fill="none" opacity="0.5" />
|
||||
{/* Connection nodes */}
|
||||
<circle cx="110" cy="62" r="3" fill={isDark ? '#60a5fa' : '#2563eb'} />
|
||||
<circle cx="148" cy="100" r="3" fill={isDark ? '#60a5fa' : '#2563eb'} />
|
||||
<circle cx="110" cy="138" r="3" fill={isDark ? '#60a5fa' : '#2563eb'} />
|
||||
<circle cx="72" cy="100" r="3" fill={isDark ? '#60a5fa' : '#2563eb'} />
|
||||
{/* Connecting lines to hexagon */}
|
||||
<line x1="110" y1="62" x2="110" y2="35" stroke={isDark ? '#60a5fa' : '#2563eb'} strokeWidth="1" opacity="0.4" />
|
||||
<line x1="148" y1="100" x2="175" y2="100" stroke={isDark ? '#60a5fa' : '#2563eb'} strokeWidth="1" opacity="0.4" />
|
||||
<line x1="110" y1="138" x2="110" y2="165" stroke={isDark ? '#60a5fa' : '#2563eb'} strokeWidth="1" opacity="0.4" />
|
||||
<line x1="72" y1="100" x2="45" y2="100" stroke={isDark ? '#60a5fa' : '#2563eb'} strokeWidth="1" opacity="0.4" />
|
||||
{/* Brand text */}
|
||||
<text x="110" y="200" textAnchor="middle" fill={isDark ? '#ffffff' : '#1a1a2e'} fontSize="18" fontWeight="300" fontFamily="system-ui, -apple-system, sans-serif" letterSpacing="4" opacity="0.6">ORCIDE</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const FADE_DURATION_MS = 2000
|
||||
|
|
@ -104,14 +105,14 @@ const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzu
|
|||
|
||||
// Data structures for provider tabs
|
||||
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
|
||||
Free: ['gemini', 'openRouter'],
|
||||
Free: ['orcestAI', 'gemini', 'openRouter'],
|
||||
Local: localProviderNames,
|
||||
Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[],
|
||||
Paid: providerNames.filter(pn => !(['orcestAI', 'gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[],
|
||||
'Cloud/Other': cloudProviders,
|
||||
};
|
||||
|
||||
const descriptionOfTab: Record<TabName, string> = {
|
||||
Free: `Providers with a 100% free tier. Add as many as you'd like!`,
|
||||
Free: `Free providers — Orcest AI works out of the box with no API key!`,
|
||||
Paid: `Connect directly with any provider (bring your own key).`,
|
||||
Local: `Active providers should appear automatically. Add as many as you'd like! `,
|
||||
'Cloud/Other': `Add as many as you'd like! Reach out for custom configuration requests.`,
|
||||
|
|
@ -204,6 +205,14 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
|
|||
<div key={providerName} className="w-full max-w-xl mb-10">
|
||||
<div className="text-xl mb-2">
|
||||
Add {displayInfoOfProviderName(providerName).title}
|
||||
{providerName === 'orcestAI' && (
|
||||
<span
|
||||
data-tooltip-id="void-tooltip-provider-info"
|
||||
data-tooltip-content="Orcest AI provides 650+ free models via OllamaFreeAPI. No API key required — just works out of the box."
|
||||
data-tooltip-place="right"
|
||||
className="ml-1 text-xs align-top text-blue-400"
|
||||
>*</span>
|
||||
)}
|
||||
{providerName === 'gemini' && (
|
||||
<span
|
||||
data-tooltip-id="void-tooltip-provider-info"
|
||||
|
|
@ -484,10 +493,10 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
// Replace the single selectedProviderName with four separate states
|
||||
// page 2 state - each tab gets its own state
|
||||
const [selectedIntelligentProvider, setSelectedIntelligentProvider] = useState<ProviderName>('anthropic');
|
||||
const [selectedIntelligentProvider, setSelectedIntelligentProvider] = useState<ProviderName>('orcestAI');
|
||||
const [selectedPrivateProvider, setSelectedPrivateProvider] = useState<ProviderName>('ollama');
|
||||
const [selectedAffordableProvider, setSelectedAffordableProvider] = useState<ProviderName>('gemini');
|
||||
const [selectedAllProvider, setSelectedAllProvider] = useState<ProviderName>('anthropic');
|
||||
const [selectedAffordableProvider, setSelectedAffordableProvider] = useState<ProviderName>('orcestAI');
|
||||
const [selectedAllProvider, setSelectedAllProvider] = useState<ProviderName>('orcestAI');
|
||||
|
||||
// Helper function to get the current selected provider based on active tab
|
||||
const getSelectedProvider = (): ProviderName => {
|
||||
|
|
@ -510,9 +519,9 @@ const VoidOnboardingContent = () => {
|
|||
}
|
||||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
smart: ['orcestAI', 'anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
cheap: ['orcestAI', 'gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
all: providerNames,
|
||||
}
|
||||
|
||||
|
|
@ -547,7 +556,7 @@ const VoidOnboardingContent = () => {
|
|||
voidMetricsService.capture('Completed Onboarding', { selectedProviderName, wantToUseOption })
|
||||
}}
|
||||
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
>Enter the Void</PrimaryActionButton>
|
||||
>Enter Orcide</PrimaryActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -563,7 +572,7 @@ const VoidOnboardingContent = () => {
|
|||
// can be md
|
||||
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Most intelligent and best for agent mode.",
|
||||
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
|
||||
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:support@orcest.ai) for help setting up at your company.",
|
||||
cheap: "Use great deals like Gemini 2.5 Pro, or self-host a model with Ollama or vLLM for free.",
|
||||
all: "",
|
||||
}
|
||||
|
|
@ -596,11 +605,11 @@ const VoidOnboardingContent = () => {
|
|||
0: <OnboardingPageShell
|
||||
content={
|
||||
<div className='flex flex-col items-center gap-8'>
|
||||
<div className="text-5xl font-light text-center">Welcome to Void</div>
|
||||
<div className="text-5xl font-light text-center">Welcome to Orcide</div>
|
||||
|
||||
{/* Slice of Void image */}
|
||||
{/* Orcide logo */}
|
||||
<div className='max-w-md w-full h-[30vh] mx-auto flex items-center justify-center'>
|
||||
{!isLinux && <VoidIcon />}
|
||||
<OrcideIcon />
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ const SimpleModelSettingsDialog = ({
|
|||
onClose();
|
||||
};
|
||||
|
||||
const sourcecodeOverridesLink = `https://github.com/voideditor/void/blob/2e5ecb291d33afbe4565921664fb7e183189c1c5/src/vs/workbench/contrib/void/common/modelCapabilities.ts#L146-L172`
|
||||
const sourcecodeOverridesLink = `https://github.com/orcest-ai/Orcide/blob/2e5ecb291d33afbe4565921664fb7e183189c1c5/src/vs/workbench/contrib/void/common/modelCapabilities.ts#L146-L172`
|
||||
|
||||
return (
|
||||
<div // Backdrop
|
||||
|
|
@ -1459,7 +1459,7 @@ export const Settings = () => {
|
|||
<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) }}>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService?.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
|
|||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
|
||||
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Orcide: Open Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
|
|
@ -81,7 +81,7 @@ registerAction2(class extends Action2 {
|
|||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCmdL', 'Void: Add Selection to Chat'),
|
||||
title: localize2('voidCmdL', 'Orcide: Add Selection to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
|
|
@ -240,7 +240,7 @@ registerAction2(class extends Action2 {
|
|||
constructor() {
|
||||
super({
|
||||
id: 'void.settingsAction',
|
||||
title: `Void's Settings`,
|
||||
title: `Orcide Settings`,
|
||||
icon: { id: 'settings-gear' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ registerAction2(class extends Action2 {
|
|||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SIDEBAR_ACTION_ID,
|
||||
title: 'Open Void Sidebar',
|
||||
title: 'Open Orcide Sidebar',
|
||||
})
|
||||
}
|
||||
run(accessor: ServicesAccessor): void {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
* Copyright 2025 Orcest AI. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
|
@ -64,12 +64,17 @@ 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) ----------
|
||||
// ---------- Orcide SSO & Profile services ----------
|
||||
|
||||
// SSO authentication service (browser-side)
|
||||
import './orcideSSOBrowserService.js'
|
||||
|
||||
// ---------- common ----------
|
||||
|
||||
// llmMessage
|
||||
import '../common/sendLLMMessageService.js'
|
||||
|
||||
// voidSettings
|
||||
// orcideSettings (previously voidSettings)
|
||||
import '../common/voidSettingsService.js'
|
||||
|
||||
// refreshModel
|
||||
|
|
@ -83,3 +88,12 @@ import '../common/voidUpdateService.js'
|
|||
|
||||
// model service
|
||||
import '../common/voidModelService.js'
|
||||
|
||||
// Orcide SSO service
|
||||
import '../common/orcideSSOService.js'
|
||||
|
||||
// Orcide user profile service
|
||||
import '../common/orcideUserProfileService.js'
|
||||
|
||||
// Orcide collaboration service
|
||||
import '../common/orcideCollaborationService.js'
|
||||
|
|
|
|||
294
src/vs/workbench/contrib/void/browser/void.web.services.ts
Normal file
294
src/vs/workbench/contrib/void/browser/void.web.services.ts
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { ServiceSendLLMMessageParams, ServiceModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { IMCPService } from '../common/mcpService.js';
|
||||
import { MCPToolCallParams, RawMCPToolCall } from '../common/mcpServiceTypes.js';
|
||||
import { InternalToolInfo } from '../common/prompt/prompts.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { IGenerateCommitMessageService } from './voidSCMService.js';
|
||||
import { ProviderName } from '../common/voidSettingsTypes.js';
|
||||
|
||||
|
||||
const OPENAI_COMPAT_BASE_URLS: Partial<Record<ProviderName, string>> = {
|
||||
openRouter: 'https://openrouter.ai/api/v1',
|
||||
openAI: 'https://api.openai.com/v1',
|
||||
deepseek: 'https://api.deepseek.com',
|
||||
groq: 'https://api.groq.com/openai/v1',
|
||||
xAI: 'https://api.x.ai/v1',
|
||||
mistral: 'https://api.mistral.ai/v1',
|
||||
};
|
||||
|
||||
class LLMMessageServiceWeb extends Disposable implements ILLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly _abortControllers = new Map<string, AbortController>();
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
sendLLMMessage(params: ServiceSendLLMMessageParams): string | null {
|
||||
const { onError, modelSelection } = params;
|
||||
|
||||
if (modelSelection === null) {
|
||||
onError({ message: 'Please add a provider in Void\'s Settings.', fullError: null });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params.messagesType === 'chatMessages' && (params.messages?.length ?? 0) === 0) {
|
||||
onError({ message: 'No messages detected.', fullError: null });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params.messagesType === 'FIMMessage') {
|
||||
onError({ message: 'Autocomplete (FIM) is not supported in web mode.', fullError: null });
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestId = generateUuid();
|
||||
const abortController = new AbortController();
|
||||
this._abortControllers.set(requestId, abortController);
|
||||
|
||||
this._doSendChat(params, requestId, abortController);
|
||||
|
||||
return requestId;
|
||||
}
|
||||
|
||||
private async _doSendChat(
|
||||
params: ServiceSendLLMMessageParams,
|
||||
requestId: string,
|
||||
abortController: AbortController
|
||||
) {
|
||||
const { onText, onFinalMessage, onError, modelSelection } = params;
|
||||
|
||||
if (params.messagesType !== 'chatMessages' || !modelSelection) return;
|
||||
|
||||
try {
|
||||
const { settingsOfProvider } = this.voidSettingsService.state;
|
||||
const providerSettings = settingsOfProvider[modelSelection.providerName];
|
||||
const apiKey = (providerSettings as Record<string, unknown>).apiKey as string | undefined;
|
||||
|
||||
if (!apiKey) {
|
||||
onError({
|
||||
message: `API key not set for ${modelSelection.providerName}. Please configure it in Void Settings.`,
|
||||
fullError: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = this._getBaseUrl(modelSelection.providerName, providerSettings);
|
||||
if (!baseUrl) {
|
||||
onError({
|
||||
message: `Provider "${modelSelection.providerName}" requires the desktop app. Use OpenRouter instead.`,
|
||||
fullError: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = this._buildMessages(params.messages, params.separateSystemMessage);
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model: modelSelection.modelName,
|
||||
messages,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
};
|
||||
|
||||
if (modelSelection.providerName === 'openRouter') {
|
||||
headers['HTTP-Referer'] = 'https://ide.orcest.ai';
|
||||
headers['X-Title'] = 'ide.orcest.ai';
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
onError({
|
||||
message: `API error (${response.status}): ${errorText}`,
|
||||
fullError: new Error(errorText)
|
||||
});
|
||||
this._abortControllers.delete(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullText = '';
|
||||
let fullReasoning = '';
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('data: ')) continue;
|
||||
|
||||
const data = trimmed.slice(6);
|
||||
if (data === '[DONE]') {
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning: null });
|
||||
this._abortControllers.delete(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const delta = parsed.choices?.[0]?.delta;
|
||||
if (delta?.content) {
|
||||
fullText += delta.content;
|
||||
onText({ fullText, fullReasoning });
|
||||
}
|
||||
if (delta?.reasoning_content || delta?.reasoning) {
|
||||
fullReasoning += (delta.reasoning_content || delta.reasoning);
|
||||
onText({ fullText, fullReasoning });
|
||||
}
|
||||
} catch {
|
||||
// skip malformed SSE chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning: null });
|
||||
this._abortControllers.delete(requestId);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'AbortError') return;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
onError({ message, fullError: err instanceof Error ? err : new Error(message) });
|
||||
this._abortControllers.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
private _getBaseUrl(providerName: ProviderName, providerSettings: Record<string, unknown>): string | null {
|
||||
const known = OPENAI_COMPAT_BASE_URLS[providerName];
|
||||
if (known) return known;
|
||||
|
||||
if (providerName === 'openAICompatible' || providerName === 'liteLLM' || providerName === 'awsBedrock') {
|
||||
return (providerSettings.endpoint as string) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private _buildMessages(
|
||||
messages: unknown[],
|
||||
separateSystemMessage: string | undefined
|
||||
): { role: string; content: string }[] {
|
||||
const result: { role: string; content: string }[] = [];
|
||||
|
||||
if (separateSystemMessage) {
|
||||
result.push({ role: 'system', content: separateSystemMessage });
|
||||
}
|
||||
|
||||
for (const msg of messages) {
|
||||
const m = msg as { role: string; content: unknown };
|
||||
if (typeof m.content === 'string') {
|
||||
result.push({ role: m.role === 'model' ? 'assistant' : m.role, content: m.content });
|
||||
} else if (Array.isArray(m.content)) {
|
||||
const textParts = m.content
|
||||
.filter((p: Record<string, unknown>) => p.type === 'text' || p.text)
|
||||
.map((p: Record<string, unknown>) => (p.text as string) || '')
|
||||
.join('');
|
||||
if (textParts) {
|
||||
result.push({ role: m.role === 'model' ? 'assistant' : m.role, content: textParts });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
abort(requestId: string) {
|
||||
const controller = this._abortControllers.get(requestId);
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
this._abortControllers.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
ollamaList(params: ServiceModelListParams<OllamaModelResponse>) {
|
||||
params.onError({ error: 'Ollama model listing is not available in web mode.' });
|
||||
}
|
||||
|
||||
openAICompatibleList(params: ServiceModelListParams<OpenaiCompatibleModelResponse>) {
|
||||
params.onError({ error: 'Model listing is not available in web mode.' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MCPServiceWeb extends Disposable implements IMCPService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
state: { mcpServerOfName: Record<string, never>; error: string | undefined } = {
|
||||
mcpServerOfName: {},
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
async revealMCPConfigFile(): Promise<void> { }
|
||||
async toggleServerIsOn(): Promise<void> { }
|
||||
getMCPTools(): InternalToolInfo[] | undefined { return undefined; }
|
||||
|
||||
async callMCPTool(_toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> {
|
||||
throw new Error('MCP is not available in web mode.');
|
||||
}
|
||||
|
||||
stringifyResult(result: RawMCPToolCall): string {
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MetricsServiceWeb implements IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(): void { }
|
||||
setOptOut(): void { }
|
||||
async getDebuggingProperties(): Promise<object> { return { mode: 'web' }; }
|
||||
}
|
||||
|
||||
|
||||
class VoidUpdateServiceWeb implements IVoidUpdateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
check: IVoidUpdateService['check'] = async () => null;
|
||||
}
|
||||
|
||||
|
||||
class GenerateCommitMessageServiceWeb implements IGenerateCommitMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
async generateCommitMessage(): Promise<void> { }
|
||||
abort(): void { }
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(ILLMMessageService, LLMMessageServiceWeb, InstantiationType.Eager);
|
||||
registerSingleton(IMCPService, MCPServiceWeb, InstantiationType.Eager);
|
||||
registerSingleton(IMetricsService, MetricsServiceWeb, InstantiationType.Eager);
|
||||
registerSingleton(IVoidUpdateService, VoidUpdateServiceWeb, InstantiationType.Eager);
|
||||
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageServiceWeb, InstantiationType.Delayed);
|
||||
|
|
@ -21,6 +21,7 @@ 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 { isWeb } from '../../../../base/common/platform.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'
|
||||
|
|
@ -227,4 +228,13 @@ class LoadingGenerateCommitMessageAction extends Action2 {
|
|||
|
||||
registerAction2(GenerateCommitMessageAction)
|
||||
registerAction2(LoadingGenerateCommitMessageAction)
|
||||
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed)
|
||||
if (!isWeb) {
|
||||
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed)
|
||||
} else {
|
||||
class GenerateCommitMessageServiceWeb implements IGenerateCommitMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
async generateCommitMessage() { }
|
||||
abort() { }
|
||||
}
|
||||
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageServiceWeb, InstantiationType.Delayed)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class VoidSettingsInput extends EditorInput {
|
|||
}
|
||||
|
||||
override getName(): string {
|
||||
return nls.localize('voidSettingsInputsName', 'Void\'s Settings');
|
||||
return nls.localize('voidSettingsInputsName', 'Orcide Settings');
|
||||
}
|
||||
|
||||
override getIcon() {
|
||||
|
|
@ -112,7 +112,7 @@ class VoidSettingsPane extends EditorPane {
|
|||
|
||||
// register Settings pane
|
||||
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
|
||||
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void\'s Settings Pane")),
|
||||
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Orcide Settings Pane")),
|
||||
[new SyncDescriptor(VoidSettingsInput)]
|
||||
);
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ registerAction2(class extends Action2 {
|
|||
constructor() {
|
||||
super({
|
||||
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettings', "Void: Toggle Settings"),
|
||||
title: nls.localize2('voidSettings', "Orcide: Toggle Settings"),
|
||||
icon: Codicon.settingsGear,
|
||||
menu: [
|
||||
{
|
||||
|
|
@ -172,7 +172,7 @@ registerAction2(class extends Action2 {
|
|||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettingsAction2', "Void: Open Settings"),
|
||||
title: nls.localize2('voidSettingsAction2', "Orcide: Open Settings"),
|
||||
f1: true,
|
||||
icon: Codicon.settingsGear,
|
||||
});
|
||||
|
|
@ -202,7 +202,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
|||
group: '0_command',
|
||||
command: {
|
||||
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
|
||||
title: nls.localize('voidSettingsActionGear', "Void\'s Settings")
|
||||
title: nls.localize('voidSettingsActionGear', "Orcide Settings")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { IAction } from '../../../../base/common/actions.js';
|
|||
|
||||
|
||||
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService): INotificationHandle => {
|
||||
const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
|
||||
const message = res?.message || 'This is a very old version of Orcide. Please download the latest version! [Orcide](https://orcest.ai/download-beta)!'
|
||||
|
||||
let actions: INotificationActions | undefined
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
|
|||
class: undefined,
|
||||
run: () => {
|
||||
const { window } = dom.getActiveWindow()
|
||||
window.open('https://voideditor.com/download-beta')
|
||||
window.open('https://orcest.ai/download-beta')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -85,12 +85,12 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
|
|||
primary.push({
|
||||
id: 'void.updater.site',
|
||||
enabled: true,
|
||||
label: `Void Site`,
|
||||
label: `Orcide Site`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
const { window } = dom.getActiveWindow()
|
||||
window.open('https://voideditor.com/')
|
||||
window.open('https://orcest.ai/')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
|
|||
// })
|
||||
}
|
||||
const notifyErrChecking = (notifService: INotificationService): INotificationHandle => {
|
||||
const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
|
||||
const message = `Orcide Error: There was an error checking for updates. If this persists, please get in touch or reinstall Orcide [here](https://orcest.ai/download-beta)!`
|
||||
const notifController = notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: message,
|
||||
|
|
@ -147,21 +147,21 @@ const performVoidCheck = async (
|
|||
|
||||
const metricsTag = explicit ? 'Manual' : 'Auto'
|
||||
|
||||
metricsService.capture(`Void Update ${metricsTag}: Checking...`, {})
|
||||
metricsService.capture(`Orcide Update ${metricsTag}: Checking...`, {})
|
||||
const res = await voidUpdateService.check(explicit)
|
||||
if (!res) {
|
||||
const notifController = notifyErrChecking(notifService);
|
||||
metricsService.capture(`Void Update ${metricsTag}: Error`, { res })
|
||||
metricsService.capture(`Orcide Update ${metricsTag}: Error`, { res })
|
||||
return notifController
|
||||
}
|
||||
else {
|
||||
if (res.message) {
|
||||
const notifController = notifyUpdate(res, notifService, updateService)
|
||||
metricsService.capture(`Void Update ${metricsTag}: Yes`, { res })
|
||||
metricsService.capture(`Orcide Update ${metricsTag}: Yes`, { res })
|
||||
return notifController
|
||||
}
|
||||
else {
|
||||
metricsService.capture(`Void Update ${metricsTag}: No`, { res })
|
||||
metricsService.capture(`Orcide Update ${metricsTag}: No`, { res })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -177,7 +177,7 @@ registerAction2(class extends Action2 {
|
|||
super({
|
||||
f1: true,
|
||||
id: 'void.voidCheckUpdate',
|
||||
title: localize2('voidCheckUpdate', 'Void: Check for Updates'),
|
||||
title: localize2('voidCheckUpdate', 'Orcide: Check for Updates'),
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { isWeb } from '../../../../base/common/platform.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IPathService } from '../../../services/path/common/pathService.js';
|
||||
|
|
@ -357,4 +358,19 @@ class MCPService extends Disposable implements IMCPService {
|
|||
// }
|
||||
}
|
||||
|
||||
registerSingleton(IMCPService, MCPService, InstantiationType.Eager);
|
||||
if (!isWeb) {
|
||||
registerSingleton(IMCPService, MCPService, InstantiationType.Eager);
|
||||
} else {
|
||||
class MCPServiceWeb extends Disposable implements IMCPService {
|
||||
_serviceBrand: undefined;
|
||||
state: { mcpServerOfName: MCPServerOfName; error: string | undefined } = { mcpServerOfName: {}, error: undefined };
|
||||
private readonly _onDidChangeState = this._register(new Emitter<void>());
|
||||
onDidChangeState = this._onDidChangeState.event;
|
||||
async revealMCPConfigFile() { }
|
||||
async toggleServerIsOn() { }
|
||||
getMCPTools() { return undefined; }
|
||||
async callMCPTool(): Promise<{ result: RawMCPToolCall }> { return { result: { type: 'text', text: 'MCP not available in web mode' } as any }; }
|
||||
stringifyResult() { return ''; }
|
||||
}
|
||||
registerSingleton(IMCPService, MCPServiceWeb, InstantiationType.Eager);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { isWeb } from '../../../../base/common/platform.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
|
||||
|
|
@ -50,7 +51,17 @@ export class MetricsService implements IMetricsService {
|
|||
}
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
if (!isWeb) {
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
} else {
|
||||
class MetricsServiceWeb implements IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture() { }
|
||||
setOptOut() { }
|
||||
async getDebuggingProperties() { return {} }
|
||||
}
|
||||
registerSingleton(IMetricsService, MetricsServiceWeb, InstantiationType.Eager);
|
||||
}
|
||||
|
||||
|
||||
// debugging action
|
||||
|
|
@ -59,7 +70,7 @@ registerAction2(class extends Action2 {
|
|||
super({
|
||||
id: 'voidDebugInfo',
|
||||
f1: true,
|
||||
title: localize2('voidMetricsDebug', 'Void: Log Debug Info'),
|
||||
title: localize2('voidMetricsDebug', 'Orcide: Log Debug Info'),
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
|
@ -68,6 +79,6 @@ registerAction2(class extends Action2 {
|
|||
|
||||
const debugProperties = await metricsService.getDebuggingProperties()
|
||||
console.log('Metrics:', debugProperties)
|
||||
notifService.info(`Void Debug info:\n${JSON.stringify(debugProperties, null, 2)}`)
|
||||
notifService.info(`Orcide Debug info:\n${JSON.stringify(debugProperties, null, 2)}`)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ export const defaultProviderSettings = {
|
|||
region: 'us-east-1', // add region setting
|
||||
endpoint: '', // optionally allow overriding default
|
||||
},
|
||||
orcestAI: {
|
||||
endpoint: 'https://ollamafreeapi.orcest.ai/v1',
|
||||
},
|
||||
|
||||
} as const
|
||||
|
||||
|
|
@ -115,24 +118,15 @@ export const defaultModelsOfProvider = {
|
|||
],
|
||||
lmStudio: [], // autodetected
|
||||
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
// 'anthropic/claude-3.7-sonnet:thinking',
|
||||
'anthropic/claude-opus-4',
|
||||
openRouter: [ // https://openrouter.ai/models (keep <=9 to avoid all-hidden default)
|
||||
'anthropic/claude-sonnet-4',
|
||||
'qwen/qwen3-235b-a22b',
|
||||
'anthropic/claude-3.7-sonnet',
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'anthropic/claude-opus-4',
|
||||
'deepseek/deepseek-r1',
|
||||
'deepseek/deepseek-r1-zero:free',
|
||||
'mistralai/devstral-small:free'
|
||||
// 'openrouter/quasar-alpha',
|
||||
// 'google/gemini-2.5-pro-preview-03-25',
|
||||
// 'mistralai/codestral-2501',
|
||||
// 'qwen/qwen-2.5-coder-32b-instruct',
|
||||
// 'mistralai/mistral-small-3.1-24b-instruct:free',
|
||||
// 'google/gemini-2.0-flash-lite-preview-02-05:free',
|
||||
// 'google/gemini-2.0-pro-exp-02-05:free',
|
||||
// 'google/gemini-2.0-flash-exp:free',
|
||||
'mistralai/codestral-2501',
|
||||
'qwen/qwen3-235b-a22b',
|
||||
'mistralai/devstral-small:free',
|
||||
'google/gemini-2.0-flash-exp:free',
|
||||
'microsoft/phi-4-reasoning-plus:free',
|
||||
],
|
||||
groq: [ // https://console.groq.com/docs/models
|
||||
'qwen-qwq-32b',
|
||||
|
|
@ -153,6 +147,15 @@ export const defaultModelsOfProvider = {
|
|||
microsoftAzure: [],
|
||||
awsBedrock: [],
|
||||
liteLLM: [],
|
||||
orcestAI: [
|
||||
'llama3.2:latest',
|
||||
'llama3.1:latest',
|
||||
'llama3.3:latest',
|
||||
'mistral:latest',
|
||||
'deepseek-r1:latest',
|
||||
'qwen2.5-coder:7b',
|
||||
'gemma:latest',
|
||||
],
|
||||
|
||||
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
|
@ -1449,6 +1452,23 @@ const openRouterSettings: VoidStaticProviderInfo = {
|
|||
|
||||
|
||||
|
||||
// ---------------- ORCEST AI (OllamaFreeAPI) ----------------
|
||||
const orcestAIModelOptions = {
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const orcestAISettings: VoidStaticProviderInfo = {
|
||||
modelOptions: orcestAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
// OllamaFreeAPI serves 650+ open-source models, use extensive fallback for capability detection
|
||||
return extensiveModelOptionsFallback(modelName, { cost: { input: 0, output: 0 } })
|
||||
},
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { needsManualParse: true },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// ---------------- model settings of everything above ----------------
|
||||
|
||||
const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProviderInfo } = {
|
||||
|
|
@ -1474,6 +1494,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
|||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
awsBedrock: awsBedrockSettings,
|
||||
orcestAI: orcestAISettings,
|
||||
} as const
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,391 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Orcest AI. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
|
||||
const ORCIDE_SHARES_KEY = 'orcide.sharedResources'
|
||||
const ORCIDE_TEAM_KEY = 'orcide.teamMembers'
|
||||
const ORCIDE_INVITATIONS_KEY = 'orcide.invitations'
|
||||
|
||||
export type SharePermission = 'view' | 'edit' | 'admin';
|
||||
|
||||
export type SharedResource = {
|
||||
id: string;
|
||||
type: 'project' | 'repository' | 'workspace' | 'file' | 'snippet' | 'chat-thread' | 'model-config' | 'mcp-server';
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
ownerEmail: string;
|
||||
sharedWith: SharedUser[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
resourceUri?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type SharedUser = {
|
||||
userId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
permission: SharePermission;
|
||||
addedAt: number;
|
||||
addedBy: string;
|
||||
}
|
||||
|
||||
export type TeamMember = {
|
||||
userId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'owner' | 'admin' | 'member' | 'viewer';
|
||||
joinedAt: number;
|
||||
lastActive: number;
|
||||
status: 'active' | 'invited' | 'suspended';
|
||||
}
|
||||
|
||||
export type Invitation = {
|
||||
id: string;
|
||||
email: string;
|
||||
role: 'admin' | 'member' | 'viewer';
|
||||
invitedBy: string;
|
||||
invitedByEmail: string;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
status: 'pending' | 'accepted' | 'declined' | 'expired';
|
||||
resourceId?: string;
|
||||
permission?: SharePermission;
|
||||
}
|
||||
|
||||
export type CollaborationState = {
|
||||
sharedResources: SharedResource[];
|
||||
teamMembers: TeamMember[];
|
||||
pendingInvitations: Invitation[];
|
||||
isTeamOwner: boolean;
|
||||
}
|
||||
|
||||
export interface IOrcideCollaborationService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: CollaborationState;
|
||||
onDidChangeState: Event<void>;
|
||||
onDidShareResource: Event<SharedResource>;
|
||||
onDidReceiveInvitation: Event<Invitation>;
|
||||
|
||||
// Resource sharing
|
||||
shareResource(resource: Omit<SharedResource, 'id' | 'createdAt' | 'updatedAt'>): Promise<SharedResource>;
|
||||
unshareResource(resourceId: string): Promise<void>;
|
||||
updateResourcePermission(resourceId: string, userId: string, permission: SharePermission): Promise<void>;
|
||||
removeUserFromResource(resourceId: string, userId: string): Promise<void>;
|
||||
getSharedResources(): SharedResource[];
|
||||
getResourcesSharedWithMe(myUserId: string): SharedResource[];
|
||||
getResourcesSharedByMe(myUserId: string): SharedResource[];
|
||||
|
||||
// Team management
|
||||
inviteTeamMember(email: string, role: TeamMember['role']): Promise<Invitation>;
|
||||
removeTeamMember(userId: string): Promise<void>;
|
||||
updateTeamMemberRole(userId: string, role: TeamMember['role']): Promise<void>;
|
||||
getTeamMembers(): TeamMember[];
|
||||
|
||||
// Invitations
|
||||
acceptInvitation(invitationId: string): Promise<void>;
|
||||
declineInvitation(invitationId: string): Promise<void>;
|
||||
getPendingInvitations(): Invitation[];
|
||||
revokeInvitation(invitationId: string): Promise<void>;
|
||||
|
||||
// Workspace sharing
|
||||
shareWorkspace(workspaceName: string, userIds: string[], permission: SharePermission): Promise<SharedResource>;
|
||||
shareChatThread(threadId: string, threadName: string, userIds: string[]): Promise<SharedResource>;
|
||||
shareModelConfig(configName: string, providerSettings: Record<string, unknown>, userIds: string[]): Promise<SharedResource>;
|
||||
}
|
||||
|
||||
export const IOrcideCollaborationService = createDecorator<IOrcideCollaborationService>('orcideCollaborationService');
|
||||
|
||||
|
||||
class OrcideCollaborationService extends Disposable implements IOrcideCollaborationService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private _state: CollaborationState;
|
||||
|
||||
private readonly _onDidChangeState = this._register(new Emitter<void>());
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onDidShareResource = this._register(new Emitter<SharedResource>());
|
||||
readonly onDidShareResource: Event<SharedResource> = this._onDidShareResource.event;
|
||||
|
||||
private readonly _onDidReceiveInvitation = this._register(new Emitter<Invitation>());
|
||||
readonly onDidReceiveInvitation: Event<Invitation> = this._onDidReceiveInvitation.event;
|
||||
|
||||
get state(): CollaborationState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this._state = {
|
||||
sharedResources: [],
|
||||
teamMembers: [],
|
||||
pendingInvitations: [],
|
||||
isTeamOwner: false,
|
||||
};
|
||||
this._loadFromStorage();
|
||||
}
|
||||
|
||||
private _loadFromStorage(): void {
|
||||
const sharesStr = this.storageService.get(ORCIDE_SHARES_KEY, StorageScope.APPLICATION);
|
||||
if (sharesStr) {
|
||||
try { this._state.sharedResources = JSON.parse(sharesStr); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const teamStr = this.storageService.get(ORCIDE_TEAM_KEY, StorageScope.APPLICATION);
|
||||
if (teamStr) {
|
||||
try { this._state.teamMembers = JSON.parse(teamStr); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const invitesStr = this.storageService.get(ORCIDE_INVITATIONS_KEY, StorageScope.APPLICATION);
|
||||
if (invitesStr) {
|
||||
try { this._state.pendingInvitations = JSON.parse(invitesStr); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
private _saveSharedResources(): void {
|
||||
this.storageService.store(ORCIDE_SHARES_KEY, JSON.stringify(this._state.sharedResources), StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
private _saveTeamMembers(): void {
|
||||
this.storageService.store(ORCIDE_TEAM_KEY, JSON.stringify(this._state.teamMembers), StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
private _saveInvitations(): void {
|
||||
this.storageService.store(ORCIDE_INVITATIONS_KEY, JSON.stringify(this._state.pendingInvitations), StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
// Resource Sharing
|
||||
|
||||
async shareResource(resource: Omit<SharedResource, 'id' | 'createdAt' | 'updatedAt'>): Promise<SharedResource> {
|
||||
const now = Date.now();
|
||||
const newResource: SharedResource = {
|
||||
...resource,
|
||||
id: generateUuid(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
this._state = {
|
||||
...this._state,
|
||||
sharedResources: [...this._state.sharedResources, newResource],
|
||||
};
|
||||
this._saveSharedResources();
|
||||
this._onDidShareResource.fire(newResource);
|
||||
this._onDidChangeState.fire();
|
||||
return newResource;
|
||||
}
|
||||
|
||||
async unshareResource(resourceId: string): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
sharedResources: this._state.sharedResources.filter(r => r.id !== resourceId),
|
||||
};
|
||||
this._saveSharedResources();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
async updateResourcePermission(resourceId: string, userId: string, permission: SharePermission): Promise<void> {
|
||||
const resources = this._state.sharedResources.map(r => {
|
||||
if (r.id !== resourceId) return r;
|
||||
return {
|
||||
...r,
|
||||
updatedAt: Date.now(),
|
||||
sharedWith: r.sharedWith.map(u =>
|
||||
u.userId === userId ? { ...u, permission } : u
|
||||
),
|
||||
};
|
||||
});
|
||||
this._state = { ...this._state, sharedResources: resources };
|
||||
this._saveSharedResources();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
async removeUserFromResource(resourceId: string, userId: string): Promise<void> {
|
||||
const resources = this._state.sharedResources.map(r => {
|
||||
if (r.id !== resourceId) return r;
|
||||
return {
|
||||
...r,
|
||||
updatedAt: Date.now(),
|
||||
sharedWith: r.sharedWith.filter(u => u.userId !== userId),
|
||||
};
|
||||
});
|
||||
this._state = { ...this._state, sharedResources: resources };
|
||||
this._saveSharedResources();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
getSharedResources(): SharedResource[] {
|
||||
return this._state.sharedResources;
|
||||
}
|
||||
|
||||
getResourcesSharedWithMe(myUserId: string): SharedResource[] {
|
||||
return this._state.sharedResources.filter(r =>
|
||||
r.ownerId !== myUserId && r.sharedWith.some(u => u.userId === myUserId)
|
||||
);
|
||||
}
|
||||
|
||||
getResourcesSharedByMe(myUserId: string): SharedResource[] {
|
||||
return this._state.sharedResources.filter(r =>
|
||||
r.ownerId === myUserId && r.sharedWith.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
// Team Management
|
||||
|
||||
async inviteTeamMember(email: string, role: TeamMember['role']): Promise<Invitation> {
|
||||
const now = Date.now();
|
||||
const invitation: Invitation = {
|
||||
id: generateUuid(),
|
||||
email,
|
||||
role: role === 'owner' ? 'admin' : role as 'admin' | 'member' | 'viewer',
|
||||
invitedBy: '', // populated by caller
|
||||
invitedByEmail: '',
|
||||
createdAt: now,
|
||||
expiresAt: now + (7 * 24 * 60 * 60 * 1000), // 7 days
|
||||
status: 'pending',
|
||||
};
|
||||
this._state = {
|
||||
...this._state,
|
||||
pendingInvitations: [...this._state.pendingInvitations, invitation],
|
||||
};
|
||||
this._saveInvitations();
|
||||
this._onDidReceiveInvitation.fire(invitation);
|
||||
this._onDidChangeState.fire();
|
||||
return invitation;
|
||||
}
|
||||
|
||||
async removeTeamMember(userId: string): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
teamMembers: this._state.teamMembers.filter(m => m.userId !== userId),
|
||||
};
|
||||
this._saveTeamMembers();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
async updateTeamMemberRole(userId: string, role: TeamMember['role']): Promise<void> {
|
||||
const members = this._state.teamMembers.map(m =>
|
||||
m.userId === userId ? { ...m, role } : m
|
||||
);
|
||||
this._state = { ...this._state, teamMembers: members };
|
||||
this._saveTeamMembers();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
getTeamMembers(): TeamMember[] {
|
||||
return this._state.teamMembers;
|
||||
}
|
||||
|
||||
// Invitations
|
||||
|
||||
async acceptInvitation(invitationId: string): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
pendingInvitations: this._state.pendingInvitations.map(inv =>
|
||||
inv.id === invitationId ? { ...inv, status: 'accepted' as const } : inv
|
||||
),
|
||||
};
|
||||
this._saveInvitations();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
async declineInvitation(invitationId: string): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
pendingInvitations: this._state.pendingInvitations.map(inv =>
|
||||
inv.id === invitationId ? { ...inv, status: 'declined' as const } : inv
|
||||
),
|
||||
};
|
||||
this._saveInvitations();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
getPendingInvitations(): Invitation[] {
|
||||
const now = Date.now();
|
||||
return this._state.pendingInvitations.filter(inv =>
|
||||
inv.status === 'pending' && inv.expiresAt > now
|
||||
);
|
||||
}
|
||||
|
||||
async revokeInvitation(invitationId: string): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
pendingInvitations: this._state.pendingInvitations.filter(inv => inv.id !== invitationId),
|
||||
};
|
||||
this._saveInvitations();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
// Convenience methods for sharing specific resource types
|
||||
|
||||
async shareWorkspace(workspaceName: string, userIds: string[], permission: SharePermission): Promise<SharedResource> {
|
||||
const sharedUsers: SharedUser[] = userIds.map(userId => ({
|
||||
userId,
|
||||
email: '',
|
||||
name: '',
|
||||
permission,
|
||||
addedAt: Date.now(),
|
||||
addedBy: '',
|
||||
}));
|
||||
return this.shareResource({
|
||||
type: 'workspace',
|
||||
name: workspaceName,
|
||||
ownerId: '',
|
||||
ownerEmail: '',
|
||||
sharedWith: sharedUsers,
|
||||
});
|
||||
}
|
||||
|
||||
async shareChatThread(threadId: string, threadName: string, userIds: string[]): Promise<SharedResource> {
|
||||
const sharedUsers: SharedUser[] = userIds.map(userId => ({
|
||||
userId,
|
||||
email: '',
|
||||
name: '',
|
||||
permission: 'view' as SharePermission,
|
||||
addedAt: Date.now(),
|
||||
addedBy: '',
|
||||
}));
|
||||
return this.shareResource({
|
||||
type: 'chat-thread',
|
||||
name: threadName,
|
||||
ownerId: '',
|
||||
ownerEmail: '',
|
||||
sharedWith: sharedUsers,
|
||||
resourceUri: threadId,
|
||||
});
|
||||
}
|
||||
|
||||
async shareModelConfig(configName: string, providerSettings: Record<string, unknown>, userIds: string[]): Promise<SharedResource> {
|
||||
const sharedUsers: SharedUser[] = userIds.map(userId => ({
|
||||
userId,
|
||||
email: '',
|
||||
name: '',
|
||||
permission: 'view' as SharePermission,
|
||||
addedAt: Date.now(),
|
||||
addedBy: '',
|
||||
}));
|
||||
return this.shareResource({
|
||||
type: 'model-config',
|
||||
name: configName,
|
||||
ownerId: '',
|
||||
ownerEmail: '',
|
||||
sharedWith: sharedUsers,
|
||||
metadata: providerSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IOrcideCollaborationService, OrcideCollaborationService, InstantiationType.Eager);
|
||||
698
src/vs/workbench/contrib/void/common/orcideSSOService.ts
Normal file
698
src/vs/workbench/contrib/void/common/orcideSSOService.ts
Normal file
|
|
@ -0,0 +1,698 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Orcest. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
|
||||
// ─── SSO Configuration ──────────────────────────────────────────────────────────
|
||||
|
||||
export const ORCIDE_SSO_CONFIG = {
|
||||
issuer: 'https://login.orcest.ai',
|
||||
clientId: 'orcide',
|
||||
authorizationEndpoint: 'https://login.orcest.ai/oauth2/authorize',
|
||||
tokenEndpoint: 'https://login.orcest.ai/oauth2/token',
|
||||
userInfoEndpoint: 'https://login.orcest.ai/oauth2/userinfo',
|
||||
jwksUri: 'https://login.orcest.ai/oauth2/jwks',
|
||||
redirectUri: 'https://ide.orcest.ai/auth/callback',
|
||||
scopes: 'openid profile email',
|
||||
logoutEndpoint: 'https://login.orcest.ai/oauth2/logout',
|
||||
endSessionEndpoint: 'https://login.orcest.ai/oauth2/logout',
|
||||
} as const;
|
||||
|
||||
export const ORCIDE_SSO_STORAGE_KEY = 'orcide.ssoSessionState';
|
||||
export const ORCIDE_SSO_PKCE_STORAGE_KEY = 'orcide.ssoPKCEState';
|
||||
|
||||
// Refresh tokens 5 minutes before they expire
|
||||
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
||||
|
||||
// Minimum interval between refresh attempts to avoid hammering the server
|
||||
const MIN_REFRESH_INTERVAL_MS = 30 * 1000;
|
||||
|
||||
// Maximum number of consecutive refresh failures before forcing logout
|
||||
const MAX_REFRESH_FAILURES = 3;
|
||||
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type SSOUserProfile = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
export type SSOState = {
|
||||
isAuthenticated: boolean;
|
||||
user: SSOUserProfile | null;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
idToken: string | null;
|
||||
expiresAt: number | null;
|
||||
};
|
||||
|
||||
export type SSOTokenResponse = {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
token_type: string;
|
||||
id_token?: string;
|
||||
scope?: string;
|
||||
};
|
||||
|
||||
export type SSOUserInfoResponse = {
|
||||
sub: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
name?: string;
|
||||
preferred_username?: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
picture?: string;
|
||||
role?: string;
|
||||
roles?: string[];
|
||||
groups?: string[];
|
||||
};
|
||||
|
||||
export type PKCEState = {
|
||||
codeVerifier: string;
|
||||
state: string;
|
||||
nonce: string;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
|
||||
// ─── Service Interface ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface IOrcideSSOService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: SSOState;
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
login(): Promise<void>;
|
||||
logout(): Promise<void>;
|
||||
getAccessToken(): Promise<string | null>;
|
||||
getUserProfile(): SSOUserProfile | null;
|
||||
isAuthenticated(): boolean;
|
||||
refreshToken(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Called by the browser-side service when the authorization callback is received.
|
||||
* Exchanges the authorization code for tokens and updates the session.
|
||||
*/
|
||||
handleAuthorizationCallback(code: string, returnedState: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Retrieves the stored PKCE state for the current login flow.
|
||||
* Used by the browser-side service to validate callbacks.
|
||||
*/
|
||||
getPendingPKCEState(): PKCEState | null;
|
||||
}
|
||||
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generates a cryptographically random string for use as PKCE code verifier,
|
||||
* state parameter, or nonce.
|
||||
*/
|
||||
function generateRandomString(length: number): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[array[i] % chars.length];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SHA-256 hash of the input string and returns it as a base64url-encoded string.
|
||||
* Used for PKCE code_challenge.
|
||||
*/
|
||||
async function sha256Base64Url(input: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(input);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = new Uint8Array(hashBuffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < hashArray.length; i++) {
|
||||
binary += String.fromCharCode(hashArray[i]);
|
||||
}
|
||||
return btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JWT token and returns the payload. Does NOT verify the signature;
|
||||
* signature verification should be done server-side or using the JWKS endpoint.
|
||||
*/
|
||||
function parseJwtPayload(token: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
const payload = parts[1];
|
||||
const padded = payload + '='.repeat((4 - payload.length % 4) % 4);
|
||||
const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
return JSON.parse(decoded);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Default State ──────────────────────────────────────────────────────────────
|
||||
|
||||
const defaultSSOState = (): SSOState => ({
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
idToken: null,
|
||||
expiresAt: null,
|
||||
});
|
||||
|
||||
|
||||
// ─── Service Decorator ──────────────────────────────────────────────────────────
|
||||
|
||||
export const IOrcideSSOService = createDecorator<IOrcideSSOService>('OrcideSSOService');
|
||||
|
||||
|
||||
// ─── Service Implementation ─────────────────────────────────────────────────────
|
||||
|
||||
class OrcideSSOService extends Disposable implements IOrcideSSOService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
state: SSOState;
|
||||
|
||||
private readonly _resolver: () => void;
|
||||
waitForInitState: Promise<void>;
|
||||
|
||||
private _refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private _lastRefreshAttempt: number = 0;
|
||||
private _consecutiveRefreshFailures: number = 0;
|
||||
|
||||
// PKCE state stored transiently during login flow
|
||||
private _pendingPKCEState: PKCEState | null = null;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IEncryptionService private readonly _encryptionService: IEncryptionService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.state = defaultSSOState();
|
||||
let resolver: () => void = () => { };
|
||||
this.waitForInitState = new Promise((res) => resolver = res);
|
||||
this._resolver = resolver;
|
||||
|
||||
this._readAndInitializeState();
|
||||
}
|
||||
|
||||
|
||||
// ── Initialization ─────────────────────────────────────────────────────────
|
||||
|
||||
private async _readAndInitializeState(): Promise<void> {
|
||||
try {
|
||||
const stored = await this._readState();
|
||||
if (stored && stored.accessToken) {
|
||||
this.state = stored;
|
||||
|
||||
// Ensure idToken field exists for sessions stored before this field was added
|
||||
if (this.state.idToken === undefined) {
|
||||
this.state = { ...this.state, idToken: null };
|
||||
}
|
||||
|
||||
// If the token is expired or about to expire, attempt a refresh
|
||||
if (this._isTokenExpiredOrExpiring()) {
|
||||
const refreshed = await this.refreshToken();
|
||||
if (!refreshed) {
|
||||
// Token refresh failed, clear the session
|
||||
this.state = defaultSSOState();
|
||||
}
|
||||
} else {
|
||||
this._scheduleTokenRefresh();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[OrcideSSOService] Failed to read stored state:', e);
|
||||
this.state = defaultSSOState();
|
||||
}
|
||||
|
||||
// Also try to restore any pending PKCE state (e.g., if the user was in the
|
||||
// middle of a login flow when the window was refreshed)
|
||||
try {
|
||||
const pkceStr = this._storageService.get(ORCIDE_SSO_PKCE_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
if (pkceStr) {
|
||||
const pkce = JSON.parse(pkceStr) as PKCEState;
|
||||
// Only restore if PKCE state is less than 10 minutes old
|
||||
const PKCE_MAX_AGE_MS = 10 * 60 * 1000;
|
||||
if (Date.now() - pkce.createdAt < PKCE_MAX_AGE_MS) {
|
||||
this._pendingPKCEState = pkce;
|
||||
} else {
|
||||
// Stale PKCE state; clean it up
|
||||
this._storageService.remove(ORCIDE_SSO_PKCE_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[OrcideSSOService] Failed to restore PKCE state:', e);
|
||||
}
|
||||
|
||||
this._resolver();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
private async _readState(): Promise<SSOState | null> {
|
||||
const encryptedState = this._storageService.get(ORCIDE_SSO_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
if (!encryptedState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const stateStr = await this._encryptionService.decrypt(encryptedState);
|
||||
return JSON.parse(stateStr) as SSOState;
|
||||
} catch (e) {
|
||||
console.error('[OrcideSSOService] Failed to decrypt stored state:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _storeState(): Promise<void> {
|
||||
try {
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(this.state));
|
||||
this._storageService.store(ORCIDE_SSO_STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
} catch (e) {
|
||||
console.error('[OrcideSSOService] Failed to store state:', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async _clearStoredState(): Promise<void> {
|
||||
this._storageService.remove(ORCIDE_SSO_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
this._storageService.remove(ORCIDE_SSO_PKCE_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
}
|
||||
|
||||
private _storePKCEState(pkce: PKCEState): void {
|
||||
this._storageService.store(
|
||||
ORCIDE_SSO_PKCE_STORAGE_KEY,
|
||||
JSON.stringify(pkce),
|
||||
StorageScope.APPLICATION,
|
||||
StorageTarget.USER
|
||||
);
|
||||
}
|
||||
|
||||
private _clearPKCEState(): void {
|
||||
this._pendingPKCEState = null;
|
||||
this._storageService.remove(ORCIDE_SSO_PKCE_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
}
|
||||
|
||||
|
||||
// ── Login Flow ─────────────────────────────────────────────────────────────
|
||||
|
||||
async login(): Promise<void> {
|
||||
// Generate PKCE parameters
|
||||
const codeVerifier = generateRandomString(64);
|
||||
const codeChallenge = await sha256Base64Url(codeVerifier);
|
||||
const stateParam = generateRandomString(32);
|
||||
const nonce = generateRandomString(32);
|
||||
|
||||
// Store PKCE state for the callback (both in-memory and persisted for
|
||||
// surviving page reloads during the redirect-based login flow)
|
||||
const pkceState: PKCEState = {
|
||||
codeVerifier,
|
||||
state: stateParam,
|
||||
nonce,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
this._pendingPKCEState = pkceState;
|
||||
this._storePKCEState(pkceState);
|
||||
|
||||
// Build the authorization URL
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: ORCIDE_SSO_CONFIG.clientId,
|
||||
redirect_uri: ORCIDE_SSO_CONFIG.redirectUri,
|
||||
scope: ORCIDE_SSO_CONFIG.scopes,
|
||||
state: stateParam,
|
||||
nonce: nonce,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
});
|
||||
|
||||
const authUrl = `${ORCIDE_SSO_CONFIG.authorizationEndpoint}?${params.toString()}`;
|
||||
|
||||
// Open the authorization URL; browser-side service handles the actual window/redirect
|
||||
this._openAuthorizationUrl(authUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the authorization URL. In the common layer this is a no-op;
|
||||
* the browser-side service overrides this to open a popup or redirect.
|
||||
*/
|
||||
protected _openAuthorizationUrl(_url: string): void {
|
||||
// No-op in common; overridden in browser service
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pending PKCE state for the browser-side service to use
|
||||
* when handling the authorization callback.
|
||||
*/
|
||||
getPendingPKCEState(): PKCEState | null {
|
||||
return this._pendingPKCEState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the browser-side service when the authorization callback is received.
|
||||
* Exchanges the authorization code for tokens.
|
||||
*/
|
||||
async handleAuthorizationCallback(code: string, returnedState: string): Promise<void> {
|
||||
// Validate the state parameter
|
||||
if (!this._pendingPKCEState || returnedState !== this._pendingPKCEState.state) {
|
||||
console.error('[OrcideSSOService] State mismatch in authorization callback');
|
||||
this._clearPKCEState();
|
||||
throw new Error('Invalid state parameter. Possible CSRF attack.');
|
||||
}
|
||||
|
||||
const codeVerifier = this._pendingPKCEState.codeVerifier;
|
||||
const nonce = this._pendingPKCEState.nonce;
|
||||
this._clearPKCEState();
|
||||
|
||||
// Exchange the authorization code for tokens
|
||||
const tokenResponse = await this._exchangeCodeForTokens(code, codeVerifier);
|
||||
|
||||
// Validate the id_token nonce if present
|
||||
if (tokenResponse.id_token) {
|
||||
const idPayload = parseJwtPayload(tokenResponse.id_token);
|
||||
if (idPayload && idPayload['nonce'] !== nonce) {
|
||||
throw new Error('ID token nonce mismatch. Possible replay attack.');
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch user profile from the UserInfo endpoint
|
||||
const userProfile = await this._fetchUserProfile(tokenResponse.access_token);
|
||||
|
||||
// Update state
|
||||
this.state = {
|
||||
isAuthenticated: true,
|
||||
user: userProfile,
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token ?? null,
|
||||
idToken: tokenResponse.id_token ?? null,
|
||||
expiresAt: Date.now() + (tokenResponse.expires_in * 1000),
|
||||
};
|
||||
|
||||
this._consecutiveRefreshFailures = 0;
|
||||
await this._storeState();
|
||||
this._scheduleTokenRefresh();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
private async _exchangeCodeForTokens(code: string, codeVerifier: string): Promise<SSOTokenResponse> {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: ORCIDE_SSO_CONFIG.clientId,
|
||||
code: code,
|
||||
redirect_uri: ORCIDE_SSO_CONFIG.redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
});
|
||||
|
||||
const response = await fetch(ORCIDE_SSO_CONFIG.tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Token exchange failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<SSOTokenResponse>;
|
||||
}
|
||||
|
||||
private async _fetchUserProfile(accessToken: string): Promise<SSOUserProfile> {
|
||||
const response = await fetch(ORCIDE_SSO_CONFIG.userInfoEndpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`UserInfo request failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as SSOUserInfoResponse;
|
||||
|
||||
// Build a display name from available fields
|
||||
let displayName = data.name ?? '';
|
||||
if (!displayName && (data.given_name || data.family_name)) {
|
||||
displayName = [data.given_name, data.family_name].filter(Boolean).join(' ');
|
||||
}
|
||||
if (!displayName) {
|
||||
displayName = data.preferred_username ?? data.email ?? data.sub;
|
||||
}
|
||||
|
||||
// Determine the user's primary role from the various possible fields
|
||||
let role = 'user';
|
||||
if (data.role) {
|
||||
role = data.role;
|
||||
} else if (data.roles && data.roles.length > 0) {
|
||||
role = data.roles[0];
|
||||
} else if (data.groups && data.groups.length > 0) {
|
||||
// Some OIDC providers use groups instead of roles
|
||||
const adminGroups = ['admin', 'admins', 'administrator'];
|
||||
if (data.groups.some(g => adminGroups.includes(g.toLowerCase()))) {
|
||||
role = 'admin';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: data.sub,
|
||||
email: data.email ?? '',
|
||||
name: displayName,
|
||||
role: role,
|
||||
avatar: data.picture,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ── Logout ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this._cancelRefreshTimer();
|
||||
this._consecutiveRefreshFailures = 0;
|
||||
|
||||
const idToken = this.state.idToken;
|
||||
const accessToken = this.state.accessToken;
|
||||
|
||||
// Clear local state first so the UI updates immediately
|
||||
this.state = defaultSSOState();
|
||||
await this._clearStoredState();
|
||||
this._onDidChangeState.fire();
|
||||
|
||||
// Then notify the OIDC provider about the logout (best-effort)
|
||||
if (accessToken || idToken) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
client_id: ORCIDE_SSO_CONFIG.clientId,
|
||||
});
|
||||
if (idToken) {
|
||||
params.set('id_token_hint', idToken);
|
||||
}
|
||||
if (accessToken) {
|
||||
params.set('token', accessToken);
|
||||
}
|
||||
await fetch(`${ORCIDE_SSO_CONFIG.logoutEndpoint}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
mode: 'no-cors',
|
||||
});
|
||||
} catch (e) {
|
||||
// Best-effort logout notification; do not block on failure
|
||||
console.warn('[OrcideSSOService] Failed to notify OIDC provider about logout:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Token Management ───────────────────────────────────────────────────────
|
||||
|
||||
async getAccessToken(): Promise<string | null> {
|
||||
if (!this.state.isAuthenticated || !this.state.accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If token is expired or about to expire, refresh it first
|
||||
if (this._isTokenExpiredOrExpiring()) {
|
||||
const refreshed = await this.refreshToken();
|
||||
if (!refreshed) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.state.accessToken;
|
||||
}
|
||||
|
||||
async refreshToken(): Promise<boolean> {
|
||||
if (!this.state.refreshToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Throttle refresh attempts
|
||||
const now = Date.now();
|
||||
if (now - this._lastRefreshAttempt < MIN_REFRESH_INTERVAL_MS) {
|
||||
return this.state.isAuthenticated;
|
||||
}
|
||||
this._lastRefreshAttempt = now;
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: ORCIDE_SSO_CONFIG.clientId,
|
||||
refresh_token: this.state.refreshToken,
|
||||
});
|
||||
|
||||
const response = await fetch(ORCIDE_SSO_CONFIG.tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this._consecutiveRefreshFailures++;
|
||||
console.error(`[OrcideSSOService] Token refresh failed (${response.status}), attempt ${this._consecutiveRefreshFailures}/${MAX_REFRESH_FAILURES}`);
|
||||
|
||||
// If refresh fails with 401/403, or we've exceeded max retries, session is invalid
|
||||
if (response.status === 401 || response.status === 403 || this._consecutiveRefreshFailures >= MAX_REFRESH_FAILURES) {
|
||||
console.error('[OrcideSSOService] Session invalidated after refresh failure');
|
||||
await this.logout();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokenResponse = await response.json() as SSOTokenResponse;
|
||||
|
||||
// Re-fetch user profile in case it changed (roles, name, etc.)
|
||||
let userProfile = this.state.user;
|
||||
try {
|
||||
userProfile = await this._fetchUserProfile(tokenResponse.access_token);
|
||||
} catch (e) {
|
||||
// Keep existing user profile if re-fetch fails
|
||||
console.warn('[OrcideSSOService] Failed to refresh user profile:', e);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isAuthenticated: true,
|
||||
user: userProfile,
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token ?? this.state.refreshToken,
|
||||
idToken: tokenResponse.id_token ?? this.state.idToken,
|
||||
expiresAt: Date.now() + (tokenResponse.expires_in * 1000),
|
||||
};
|
||||
|
||||
this._consecutiveRefreshFailures = 0;
|
||||
await this._storeState();
|
||||
this._scheduleTokenRefresh();
|
||||
this._onDidChangeState.fire();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
this._consecutiveRefreshFailures++;
|
||||
console.error(`[OrcideSSOService] Token refresh error (attempt ${this._consecutiveRefreshFailures}/${MAX_REFRESH_FAILURES}):`, e);
|
||||
|
||||
if (this._consecutiveRefreshFailures >= MAX_REFRESH_FAILURES) {
|
||||
console.error('[OrcideSSOService] Max refresh retries exceeded, logging out');
|
||||
await this.logout();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getUserProfile(): SSOUserProfile | null {
|
||||
return this.state.user;
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
if (!this.state.isAuthenticated || !this.state.accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the token has fully expired (past the refresh buffer)
|
||||
if (this.state.expiresAt !== null && Date.now() > this.state.expiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ── Auto-Refresh Scheduling ────────────────────────────────────────────────
|
||||
|
||||
private _isTokenExpiredOrExpiring(): boolean {
|
||||
if (this.state.expiresAt === null) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() >= (this.state.expiresAt - TOKEN_REFRESH_BUFFER_MS);
|
||||
}
|
||||
|
||||
private _scheduleTokenRefresh(): void {
|
||||
this._cancelRefreshTimer();
|
||||
|
||||
if (!this.state.expiresAt || !this.state.refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeUntilRefresh = this.state.expiresAt - Date.now() - TOKEN_REFRESH_BUFFER_MS;
|
||||
const delay = Math.max(timeUntilRefresh, MIN_REFRESH_INTERVAL_MS);
|
||||
|
||||
this._refreshTimer = setTimeout(async () => {
|
||||
const success = await this.refreshToken();
|
||||
if (!success) {
|
||||
console.warn('[OrcideSSOService] Scheduled token refresh failed');
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private _cancelRefreshTimer(): void {
|
||||
if (this._refreshTimer !== null) {
|
||||
clearTimeout(this._refreshTimer);
|
||||
this._refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Cleanup ────────────────────────────────────────────────────────────────
|
||||
|
||||
override dispose(): void {
|
||||
this._cancelRefreshTimer();
|
||||
this._onDidChangeState.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Registration ───────────────────────────────────────────────────────────────
|
||||
|
||||
registerSingleton(IOrcideSSOService, OrcideSSOService, InstantiationType.Eager);
|
||||
296
src/vs/workbench/contrib/void/common/orcideUserProfileService.ts
Normal file
296
src/vs/workbench/contrib/void/common/orcideUserProfileService.ts
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Orcest AI. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
|
||||
const ORCIDE_USER_PROFILE_KEY = 'orcide.userProfile'
|
||||
const ORCIDE_USER_PREFERENCES_KEY = 'orcide.userPreferences'
|
||||
const ORCIDE_USER_REPOS_KEY = 'orcide.userRepositories'
|
||||
|
||||
export type OrcideUserProfile = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'admin' | 'developer' | 'researcher' | 'viewer';
|
||||
avatar?: string;
|
||||
organization?: string;
|
||||
lastLogin: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export type OrcideUserPreferences = {
|
||||
theme: string;
|
||||
language: string;
|
||||
fontSize: number;
|
||||
defaultModel: string | null;
|
||||
autoSave: boolean;
|
||||
showWelcome: boolean;
|
||||
sidebarPosition: 'left' | 'right';
|
||||
terminalFont: string;
|
||||
enableTelemetry: boolean;
|
||||
collaborationEnabled: boolean;
|
||||
}
|
||||
|
||||
export type OrcideRepository = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
isPrivate: boolean;
|
||||
createdAt: number;
|
||||
lastAccessed: number;
|
||||
sharedWith: string[]; // user IDs
|
||||
owner: string; // user ID
|
||||
}
|
||||
|
||||
export type OrcideUserSession = {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
startedAt: number;
|
||||
lastActivity: number;
|
||||
deviceInfo: string;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
export type UserProfileState = {
|
||||
profile: OrcideUserProfile | null;
|
||||
preferences: OrcideUserPreferences;
|
||||
repositories: OrcideRepository[];
|
||||
activeSessions: OrcideUserSession[];
|
||||
isLoaded: boolean;
|
||||
}
|
||||
|
||||
const defaultPreferences: OrcideUserPreferences = {
|
||||
theme: 'dark',
|
||||
language: 'en',
|
||||
fontSize: 14,
|
||||
defaultModel: 'rainymodel-pro',
|
||||
autoSave: true,
|
||||
showWelcome: true,
|
||||
sidebarPosition: 'right',
|
||||
terminalFont: 'monospace',
|
||||
enableTelemetry: true,
|
||||
collaborationEnabled: true,
|
||||
}
|
||||
|
||||
export interface IOrcideUserProfileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: UserProfileState;
|
||||
onDidChangeState: Event<void>;
|
||||
onDidChangeProfile: Event<OrcideUserProfile>;
|
||||
|
||||
setProfile(profile: OrcideUserProfile): Promise<void>;
|
||||
clearProfile(): Promise<void>;
|
||||
getProfile(): OrcideUserProfile | null;
|
||||
|
||||
setPreference<K extends keyof OrcideUserPreferences>(key: K, value: OrcideUserPreferences[K]): Promise<void>;
|
||||
getPreferences(): OrcideUserPreferences;
|
||||
resetPreferences(): Promise<void>;
|
||||
|
||||
addRepository(repo: OrcideRepository): Promise<void>;
|
||||
removeRepository(repoId: string): Promise<void>;
|
||||
getRepositories(): OrcideRepository[];
|
||||
shareRepository(repoId: string, userId: string): Promise<void>;
|
||||
unshareRepository(repoId: string, userId: string): Promise<void>;
|
||||
|
||||
addSession(session: OrcideUserSession): void;
|
||||
removeSession(sessionId: string): void;
|
||||
getActiveSessions(): OrcideUserSession[];
|
||||
}
|
||||
|
||||
export const IOrcideUserProfileService = createDecorator<IOrcideUserProfileService>('orcideUserProfileService');
|
||||
|
||||
|
||||
class OrcideUserProfileService extends Disposable implements IOrcideUserProfileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private _state: UserProfileState;
|
||||
|
||||
private readonly _onDidChangeState = this._register(new Emitter<void>());
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onDidChangeProfile = this._register(new Emitter<OrcideUserProfile>());
|
||||
readonly onDidChangeProfile: Event<OrcideUserProfile> = this._onDidChangeProfile.event;
|
||||
|
||||
get state(): UserProfileState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this._state = {
|
||||
profile: null,
|
||||
preferences: { ...defaultPreferences },
|
||||
repositories: [],
|
||||
activeSessions: [],
|
||||
isLoaded: false,
|
||||
};
|
||||
this._loadFromStorage();
|
||||
}
|
||||
|
||||
private _loadFromStorage(): void {
|
||||
// Load profile
|
||||
const profileStr = this.storageService.get(ORCIDE_USER_PROFILE_KEY, StorageScope.APPLICATION);
|
||||
if (profileStr) {
|
||||
try {
|
||||
this._state.profile = JSON.parse(profileStr);
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
// Load preferences
|
||||
const prefsStr = this.storageService.get(ORCIDE_USER_PREFERENCES_KEY, StorageScope.APPLICATION);
|
||||
if (prefsStr) {
|
||||
try {
|
||||
const stored = JSON.parse(prefsStr);
|
||||
this._state.preferences = { ...defaultPreferences, ...stored };
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
// Load repositories
|
||||
const reposStr = this.storageService.get(ORCIDE_USER_REPOS_KEY, StorageScope.APPLICATION);
|
||||
if (reposStr) {
|
||||
try {
|
||||
this._state.repositories = JSON.parse(reposStr);
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
this._state.isLoaded = true;
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
private _saveProfile(): void {
|
||||
if (this._state.profile) {
|
||||
this.storageService.store(ORCIDE_USER_PROFILE_KEY, JSON.stringify(this._state.profile), StorageScope.APPLICATION, StorageTarget.USER);
|
||||
} else {
|
||||
this.storageService.remove(ORCIDE_USER_PROFILE_KEY, StorageScope.APPLICATION);
|
||||
}
|
||||
}
|
||||
|
||||
private _savePreferences(): void {
|
||||
this.storageService.store(ORCIDE_USER_PREFERENCES_KEY, JSON.stringify(this._state.preferences), StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
private _saveRepositories(): void {
|
||||
this.storageService.store(ORCIDE_USER_REPOS_KEY, JSON.stringify(this._state.repositories), StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
async setProfile(profile: OrcideUserProfile): Promise<void> {
|
||||
this._state = { ...this._state, profile };
|
||||
this._saveProfile();
|
||||
this._onDidChangeProfile.fire(profile);
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
async clearProfile(): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
profile: null,
|
||||
repositories: [],
|
||||
activeSessions: [],
|
||||
};
|
||||
this._saveProfile();
|
||||
this._saveRepositories();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
getProfile(): OrcideUserProfile | null {
|
||||
return this._state.profile;
|
||||
}
|
||||
|
||||
async setPreference<K extends keyof OrcideUserPreferences>(key: K, value: OrcideUserPreferences[K]): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
preferences: { ...this._state.preferences, [key]: value },
|
||||
};
|
||||
this._savePreferences();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
getPreferences(): OrcideUserPreferences {
|
||||
return this._state.preferences;
|
||||
}
|
||||
|
||||
async resetPreferences(): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
preferences: { ...defaultPreferences },
|
||||
};
|
||||
this._savePreferences();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
async addRepository(repo: OrcideRepository): Promise<void> {
|
||||
const existingIdx = this._state.repositories.findIndex(r => r.id === repo.id);
|
||||
const newRepos = [...this._state.repositories];
|
||||
if (existingIdx >= 0) {
|
||||
newRepos[existingIdx] = repo;
|
||||
} else {
|
||||
newRepos.push(repo);
|
||||
}
|
||||
this._state = { ...this._state, repositories: newRepos };
|
||||
this._saveRepositories();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
async removeRepository(repoId: string): Promise<void> {
|
||||
this._state = {
|
||||
...this._state,
|
||||
repositories: this._state.repositories.filter(r => r.id !== repoId),
|
||||
};
|
||||
this._saveRepositories();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
getRepositories(): OrcideRepository[] {
|
||||
return this._state.repositories;
|
||||
}
|
||||
|
||||
async shareRepository(repoId: string, userId: string): Promise<void> {
|
||||
const repo = this._state.repositories.find(r => r.id === repoId);
|
||||
if (!repo) return;
|
||||
if (repo.sharedWith.includes(userId)) return;
|
||||
const updatedRepo: OrcideRepository = {
|
||||
...repo,
|
||||
sharedWith: [...repo.sharedWith, userId],
|
||||
};
|
||||
await this.addRepository(updatedRepo);
|
||||
}
|
||||
|
||||
async unshareRepository(repoId: string, userId: string): Promise<void> {
|
||||
const repo = this._state.repositories.find(r => r.id === repoId);
|
||||
if (!repo) return;
|
||||
const updatedRepo: OrcideRepository = {
|
||||
...repo,
|
||||
sharedWith: repo.sharedWith.filter(id => id !== userId),
|
||||
};
|
||||
await this.addRepository(updatedRepo);
|
||||
}
|
||||
|
||||
addSession(session: OrcideUserSession): void {
|
||||
const newSessions = [...this._state.activeSessions, session];
|
||||
this._state = { ...this._state, activeSessions: newSessions };
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
removeSession(sessionId: string): void {
|
||||
this._state = {
|
||||
...this._state,
|
||||
activeSessions: this._state.activeSessions.filter(s => s.sessionId !== sessionId),
|
||||
};
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
getActiveSessions(): OrcideUserSession[] {
|
||||
return this._state.activeSessions;
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IOrcideUserProfileService, OrcideUserProfileService, InstantiationType.Eager);
|
||||
|
|
@ -7,6 +7,7 @@ import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMess
|
|||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { isWeb } from '../../../../base/common/platform.js';
|
||||
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
|
|
@ -195,5 +196,130 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
}
|
||||
}
|
||||
|
||||
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
|
||||
if (!isWeb) {
|
||||
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
|
||||
} else {
|
||||
const _baseUrls: Partial<Record<string, string>> = {
|
||||
openRouter: 'https://openrouter.ai/api/v1',
|
||||
openAI: 'https://api.openai.com/v1',
|
||||
deepseek: 'https://api.deepseek.com/v1',
|
||||
groq: 'https://api.groq.com/openai/v1',
|
||||
xAI: 'https://api.x.ai/v1',
|
||||
mistral: 'https://api.mistral.ai/v1',
|
||||
orcestAI: 'https://ollamafreeapi.orcest.ai/v1',
|
||||
};
|
||||
|
||||
class LLMMessageServiceWeb extends Disposable implements ILLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly _abortControllers = new Map<string, AbortController>();
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) { super(); }
|
||||
|
||||
sendLLMMessage = (params: ServiceSendLLMMessageParams): string | null => {
|
||||
const { onText, onFinalMessage, onError, modelSelection } = params;
|
||||
if (modelSelection === null) {
|
||||
onError({ message: `Please add a provider in Orcide's Settings.`, fullError: null });
|
||||
return null;
|
||||
}
|
||||
const requestId = generateUuid();
|
||||
const abort = new AbortController();
|
||||
this._abortControllers.set(requestId, abort);
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state;
|
||||
const providerSettings = settingsOfProvider[modelSelection.providerName];
|
||||
const apiKey = (providerSettings as any).apiKey as string | undefined;
|
||||
const endpoint = (providerSettings as any).endpoint as string | undefined;
|
||||
const baseUrl = endpoint || _baseUrls[modelSelection.providerName] || 'https://openrouter.ai/api/v1';
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; }
|
||||
if (modelSelection.providerName === 'openRouter') {
|
||||
headers['HTTP-Referer'] = 'https://ide.orcest.ai';
|
||||
headers['X-Title'] = 'ide.orcest.ai';
|
||||
}
|
||||
|
||||
let messages: any[];
|
||||
let systemMessage: string | undefined;
|
||||
if (params.messagesType === 'chatMessages') {
|
||||
messages = params.messages.map((m: any) => ({ role: m.role === 'model' ? 'assistant' : m.role, content: typeof m.content === 'string' ? m.content : (m.parts ? m.parts.map((p: any) => p.text).join('') : JSON.stringify(m.content)) }));
|
||||
systemMessage = params.separateSystemMessage;
|
||||
} else {
|
||||
messages = [{ role: 'user', content: params.messages.prefix }];
|
||||
systemMessage = undefined;
|
||||
}
|
||||
|
||||
const body: any = { model: modelSelection.modelName, messages: systemMessage ? [{ role: 'system', content: systemMessage }, ...messages] : messages, stream: true };
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body), signal: abort.signal });
|
||||
if (!res.ok) {
|
||||
const errText = await res.text().catch(() => res.statusText);
|
||||
onError({ message: `${res.status}: ${errText}`, fullError: null });
|
||||
return;
|
||||
}
|
||||
const reader = res.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullText = '';
|
||||
let buffer = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop()!;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('data:')) continue;
|
||||
const data = trimmed.slice(5).trim();
|
||||
if (data === '[DONE]') continue;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const delta = json.choices?.[0]?.delta;
|
||||
if (delta?.content) {
|
||||
fullText += delta.content;
|
||||
onText({ fullText, fullReasoning: '' });
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null });
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
onError({ message: e?.message || String(e), fullError: null });
|
||||
}
|
||||
} finally {
|
||||
this._abortControllers.delete(requestId);
|
||||
}
|
||||
})();
|
||||
return requestId;
|
||||
}
|
||||
|
||||
abort = (requestId: string) => {
|
||||
this._abortControllers.get(requestId)?.abort();
|
||||
this._abortControllers.delete(requestId);
|
||||
}
|
||||
|
||||
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
|
||||
params.onError({ error: 'Ollama not available in web mode' });
|
||||
}
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { settingsOfProvider } = this.voidSettingsService.state;
|
||||
const providerSettings = settingsOfProvider[params.providerName];
|
||||
const apiKey = (providerSettings as any).apiKey as string | undefined;
|
||||
const endpoint = (providerSettings as any).endpoint as string | undefined;
|
||||
const baseUrl = endpoint || _baseUrls[params.providerName] || '';
|
||||
if (!baseUrl) { params.onError({ error: 'No endpoint configured' }); return; }
|
||||
const headers: Record<string, string> = {};
|
||||
if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; }
|
||||
fetch(`${baseUrl}/models`, { headers }).then(r => r.json()).then(json => {
|
||||
params.onSuccess({ models: json.data || [] });
|
||||
}).catch(e => { params.onError({ error: e?.message || String(e) }); });
|
||||
}
|
||||
}
|
||||
registerSingleton(ILLMMessageService, LLMMessageServiceWeb, InstantiationType.Eager);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
* Copyright 2025 Orcest AI. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// past values:
|
||||
// 'void.settingsServiceStorage'
|
||||
// 'void.settingsServiceStorageI' // 1.0.2
|
||||
// 'void.settingsServiceStorageII' // 1.0.3
|
||||
|
||||
// 1.0.3
|
||||
export const VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII'
|
||||
// 2.0.0 - Orcide rebrand
|
||||
export const VOID_SETTINGS_STORAGE_KEY = 'orcide.settingsServiceStorage'
|
||||
|
||||
|
||||
// past values:
|
||||
// 'void.chatThreadStorage'
|
||||
// 'void.chatThreadStorageI' // 1.0.2
|
||||
// 'void.chatThreadStorageII' // 1.0.3
|
||||
|
||||
// 1.0.3
|
||||
export const THREAD_STORAGE_KEY = 'void.chatThreadStorageII'
|
||||
// 2.0.0 - Orcide rebrand
|
||||
export const THREAD_STORAGE_KEY = 'orcide.chatThreadStorage'
|
||||
|
||||
|
||||
|
||||
export const OPT_OUT_KEY = 'void.app.optOutAll'
|
||||
export const OPT_OUT_KEY = 'orcide.app.optOutAll'
|
||||
|
|
|
|||
|
|
@ -128,7 +128,11 @@ const _stateWithMergedDefaultModels = (state: VoidSettingsState): VoidSettingsSt
|
|||
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
|
||||
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
|
||||
const defaultModelNames = defaultModels.map(m => m.modelName)
|
||||
const newModels = _modelsWithSwappedInNewModels({ existingModels: currentModels, models: defaultModelNames, type: 'default' })
|
||||
let newModels = _modelsWithSwappedInNewModels({ existingModels: currentModels, models: defaultModelNames, type: 'default' })
|
||||
const defaultsInNew = newModels.filter(m => m.type === 'default')
|
||||
if (defaultsInNew.length > 0 && defaultsInNew.length < 10 && defaultsInNew.every(m => m.isHidden)) {
|
||||
newModels = newModels.map(m => m.type === 'default' ? { ...m, isHidden: false } : m)
|
||||
}
|
||||
newSettingsOfProvider = {
|
||||
...newSettingsOfProvider,
|
||||
[providerName]: {
|
||||
|
|
@ -260,10 +264,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
dangerousSetState = async (newState: VoidSettingsState) => {
|
||||
this.state = _validatedModelState(newState)
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
await this._storeState()
|
||||
}
|
||||
async resetState() {
|
||||
await this.dangerousSetState(defaultState())
|
||||
|
|
@ -360,9 +364,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
|
||||
private async _storeState() {
|
||||
const state = this.state
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
|
||||
this._storageService.store(VOID_SETTINGS_STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
try {
|
||||
const state = this.state
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
|
||||
this._storageService.store(VOID_SETTINGS_STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
} catch (e) {
|
||||
console.error('[VoidSettingsService] Failed to store state:', e);
|
||||
}
|
||||
}
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
|
||||
|
|
@ -393,14 +401,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
|
||||
this.state = _validatedModelState(newState)
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
await this._storeState()
|
||||
|
||||
}
|
||||
|
||||
|
||||
private _onUpdate_syncApplyToChat() {
|
||||
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']))
|
||||
}
|
||||
|
|
@ -418,10 +425,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
}
|
||||
this.state = _validatedModelState(newState)
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
|
||||
// hooks
|
||||
await this._storeState()
|
||||
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
|
||||
if (this.state.globalSettings.syncSCMToChat) this._onUpdate_syncSCMToChat()
|
||||
|
||||
|
|
@ -438,10 +445,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
|
||||
this.state = _validatedModelState(newState)
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
|
||||
await this._storeState()
|
||||
// hooks
|
||||
if (featureName === 'Chat') {
|
||||
// When Chat model changes, update synced features
|
||||
|
|
@ -469,9 +475,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
}
|
||||
this.state = _validatedModelState(newState)
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
await this._storeState()
|
||||
}
|
||||
|
||||
setOverridesOfModel = async (providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined) => {
|
||||
|
|
@ -490,8 +495,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
};
|
||||
|
||||
this.state = _validatedModelState(newState);
|
||||
await this._storeState();
|
||||
this._onDidChangeState.fire();
|
||||
await this._storeState();
|
||||
|
||||
this._metricsService.capture('Update Model Overrides', { providerName, modelName, overrides });
|
||||
}
|
||||
|
|
@ -570,8 +575,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
};
|
||||
this.state = _validatedModelState(newState);
|
||||
await this._storeState();
|
||||
this._onDidChangeState.fire();
|
||||
await this._storeState();
|
||||
this._metricsService.capture('Set MCP Server States', { newStates });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
else if (providerName === 'awsBedrock') {
|
||||
return { title: 'AWS Bedrock', }
|
||||
}
|
||||
else if (providerName === 'orcestAI') {
|
||||
return { title: 'Orcest AI (Free)', }
|
||||
}
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
|
@ -120,14 +123,15 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
|||
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
|
||||
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
|
||||
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
|
||||
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).`
|
||||
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
if (providerName === 'openAICompatible') return `OpenAI-compatible provider. Orcide supports llama.cpp and more.`
|
||||
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Orcide. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
|
||||
if (providerName === 'awsBedrock') return 'Connect via a LiteLLM proxy or the AWS [Bedrock-Access-Gateway](https://github.com/aws-samples/bedrock-access-gateway). LiteLLM Bedrock setup docs are [here](https://docs.litellm.ai/docs/providers/bedrock).'
|
||||
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
|
||||
if (providerName === 'orcestAI') return 'Orcest AI free API. No API key required. Powered by [OllamaFreeAPI](https://ollamafreeapi.orcest.ai) with 650+ open-source models.'
|
||||
|
||||
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
|
@ -156,7 +160,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
providerName === 'awsBedrock' ? 'key-...' :
|
||||
'',
|
||||
'',
|
||||
|
||||
isPasswordField: true,
|
||||
}
|
||||
|
|
@ -171,7 +175,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
providerName === 'awsBedrock' ? 'Endpoint' :
|
||||
'(never)',
|
||||
providerName === 'orcestAI' ? 'Endpoint' :
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
|
|
@ -179,7 +184,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
|
||||
: '(never)',
|
||||
: providerName === 'orcestAI' ? defaultProviderSettings.orcestAI.endpoint
|
||||
: '(never)'
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -352,6 +358,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
|||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
orcestAI: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.orcestAI,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.orcestAI),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { isWeb } from '../../../../base/common/platform.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { VoidCheckUpdateRespose } from './voidUpdateServiceTypes.js';
|
||||
|
|
@ -41,6 +42,14 @@ export class VoidUpdateService implements IVoidUpdateService {
|
|||
}
|
||||
}
|
||||
|
||||
registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager);
|
||||
if (!isWeb) {
|
||||
registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager);
|
||||
} else {
|
||||
class VoidUpdateServiceWeb implements IVoidUpdateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
check = async () => ({ type: 'noUpdate' }) as any;
|
||||
}
|
||||
registerSingleton(IVoidUpdateService, VoidUpdateServiceWeb, InstantiationType.Eager);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -100,8 +100,8 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
|||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: thisConfig.apiKey,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void', // Optional. Shows in rankings on openrouter.ai.
|
||||
'HTTP-Referer': 'https://ide.orcest.ai',
|
||||
'X-Title': 'ide.orcest.ai',
|
||||
},
|
||||
...commonPayloadOpts,
|
||||
})
|
||||
|
|
@ -167,6 +167,10 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
|||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'orcestAI') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey || 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
|
||||
else throw new Error(`Void providerName was invalid: ${providerName}.`)
|
||||
}
|
||||
|
|
@ -937,6 +941,11 @@ export const sendLLMMessageToProviderImplementation = {
|
|||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
orcestAI: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
|
||||
} satisfies CallFnOfProvider
|
||||
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
|||
}
|
||||
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
console.log('Orcide posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
|||
|
||||
if (this._updateService.state.type === StateType.Ready) {
|
||||
// Update is ready
|
||||
return { message: 'Restart Void to update!', action: 'restart' } as const
|
||||
return { message: 'Restart Orcide to update!', action: 'restart' } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Disabled) {
|
||||
|
|
@ -95,7 +95,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
|||
|
||||
private async _manualCheckGHTagIfDisabled(explicit: boolean): Promise<VoidCheckUpdateRespose> {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/voideditor/binaries/releases/latest');
|
||||
const response = await fetch('https://api.github.com/repos/orcide/binaries/releases/latest');
|
||||
|
||||
const data = await response.json();
|
||||
const version = data.tag_name;
|
||||
|
|
@ -112,11 +112,11 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
|||
if (explicit) {
|
||||
if (response.ok) {
|
||||
if (!isUpToDate) {
|
||||
message = 'A new version of Void is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
|
||||
message = 'A new version of Orcide is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
|
||||
action = 'reinstall'
|
||||
}
|
||||
else {
|
||||
message = 'Void is up-to-date!'
|
||||
message = 'Orcide is up-to-date!'
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -127,7 +127,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
|||
// not explicit
|
||||
else {
|
||||
if (response.ok && !isUpToDate) {
|
||||
message = 'A new version of Void is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
|
||||
message = 'A new version of Orcide is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
|
||||
action = 'reinstall'
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue