mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
feat: Complete Orcide rebrand with SSO, user profiles, collaboration, and Orcest AI API integration
- Complete rebrand from Void to Orcide across all user-facing strings, configs, and messages - Add SSO authentication service (OIDC/OAuth2) with login.orcest.ai integration - PKCE-based authorization flow with popup and redirect support - Automatic token refresh, secure storage, and session management - Add SSO browser service with popup window management and command palette actions - Sign In, Sign Out, SSO Status, and Refresh Token commands - Add user profile service with SSO-based identity isolation - Per-user preferences, repositories, and active session tracking - Add collaboration service for resource sharing and team management - Share workspaces, chat threads, model configs, and MCP servers - Team member invitation and role-based access control - Add orcestAI as new API provider with RainyModel as default - Pre-configured endpoint: https://rm.orcest.ai/v1 - Default models: rainymodel-pro, gpt-4o, claude-3.5-sonnet, etc. - Update product.json with SSO config, default API, and Orcest ecosystem URLs - Update storage keys, metrics, file transfer paths, and build configs https://claude.ai/code/session_01CjDqzV3ECQxE1g4jFn7PBu
This commit is contained in:
parent
9294b95911
commit
38966a08da
24 changed files with 2069 additions and 96 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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/orcest-ai/Orcide/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">
|
||||
|
|
@ -132,7 +132,7 @@ If you want to know how our build pipeline works, see our build repo [here](http
|
|||
|
||||
## VSCode Codebase Guide
|
||||
|
||||
For additional references, the Void team put together this list of links to get up and running with VSCode.
|
||||
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).
|
||||
|
|
|
|||
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"
|
||||
|
|
|
|||
58
product.json
58
product.json
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.4.9",
|
||||
"voidRelease": "0044",
|
||||
"applicationName": "void",
|
||||
"nameShort": "Orcide",
|
||||
"nameLong": "Orcide",
|
||||
"orcideVersion": "2.0.0",
|
||||
"orcideRelease": "0001",
|
||||
"applicationName": "orcide",
|
||||
"dataFolderName": ".orcide",
|
||||
"win32MutexName": "orcide",
|
||||
"licenseName": "MIT",
|
||||
|
|
@ -12,26 +12,48 @@
|
|||
"serverGreeting": [],
|
||||
"serverLicense": [],
|
||||
"serverLicensePrompt": "",
|
||||
"serverApplicationName": "void-server",
|
||||
"serverDataFolderName": ".void-server",
|
||||
"tunnelApplicationName": "void-tunnel",
|
||||
"win32DirName": "Void",
|
||||
"win32NameVersion": "Void",
|
||||
"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",
|
||||
"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/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"
|
||||
|
|
@ -39,6 +61,12 @@
|
|||
"builtInExtensions": [],
|
||||
"linkProtectionTrustedDomains": [
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -195,10 +195,10 @@ 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, '.orcide', 'extensions'),
|
||||
|
|
@ -207,10 +207,10 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
} 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, '.orcide', 'extensions'),
|
||||
|
|
@ -219,10 +219,10 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
} 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, '.orcide', 'extensions'),
|
||||
|
|
@ -238,10 +238,10 @@ 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, '.orcide', 'extensions'),
|
||||
|
|
@ -250,10 +250,10 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
} 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, '.orcide', 'extensions'),
|
||||
|
|
@ -262,10 +262,10 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
} 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, '.orcide', 'extensions'),
|
||||
|
|
@ -283,10 +283,10 @@ 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, '.orcide', 'extensions'),
|
||||
|
|
@ -295,10 +295,10 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
} 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, '.orcide', 'extensions'),
|
||||
|
|
@ -307,10 +307,10 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
} 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, '.orcide', 'extensions'),
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -547,7 +547,7 @@ const VoidOnboardingContent = () => {
|
|||
voidMetricsService.capture('Completed Onboarding', { selectedProviderName, wantToUseOption })
|
||||
}}
|
||||
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
>Enter the Void</PrimaryActionButton>
|
||||
>Enter Orcide</PrimaryActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -596,7 +596,7 @@ 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 */}
|
||||
<div className='max-w-md w-full h-[30vh] mx-auto flex items-center justify-center'>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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://orcest.ai/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
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ 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: () => {
|
||||
|
|
@ -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://orcest.ai/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> {
|
||||
|
|
|
|||
|
|
@ -70,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> {
|
||||
|
|
@ -79,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,10 @@ export const defaultProviderSettings = {
|
|||
region: 'us-east-1', // add region setting
|
||||
endpoint: '', // optionally allow overriding default
|
||||
},
|
||||
orcestAI: {
|
||||
endpoint: 'https://rm.orcest.ai/v1',
|
||||
apiKey: '', // Will be populated from environment/SSO
|
||||
},
|
||||
|
||||
} as const
|
||||
|
||||
|
|
@ -144,6 +148,17 @@ export const defaultModelsOfProvider = {
|
|||
microsoftAzure: [],
|
||||
awsBedrock: [],
|
||||
liteLLM: [],
|
||||
orcestAI: [
|
||||
'rainymodel-pro',
|
||||
'rainymodel-standard',
|
||||
'rainymodel-lite',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'claude-3.5-sonnet',
|
||||
'gemini-1.5-pro',
|
||||
'llama-3.1-70b',
|
||||
'mixtral-8x7b',
|
||||
],
|
||||
|
||||
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
|
@ -1440,6 +1455,52 @@ const openRouterSettings: VoidStaticProviderInfo = {
|
|||
|
||||
|
||||
|
||||
// ---------------- ORCEST AI (RainyModel) ----------------
|
||||
const orcestAIModelOptions = {
|
||||
'rainymodel-pro': {
|
||||
contextWindow: 128_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'rainymodel-standard': {
|
||||
contextWindow: 128_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'rainymodel-lite': {
|
||||
contextWindow: 64_000,
|
||||
reservedOutputTokenSpace: 4_096,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const orcestAISettings: VoidStaticProviderInfo = {
|
||||
modelOptions: orcestAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
// For non-RainyModel models served through the aggregator, use the extensive fallback
|
||||
return extensiveModelOptionsFallback(modelName)
|
||||
},
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// ---------------- model settings of everything above ----------------
|
||||
|
||||
const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProviderInfo } = {
|
||||
|
|
@ -1465,6 +1526,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';
|
||||
import { isWeb } from '../../../../base/common/platform.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);
|
||||
|
|
@ -206,6 +206,7 @@ if (!isWeb) {
|
|||
groq: 'https://api.groq.com/openai/v1',
|
||||
xAI: 'https://api.x.ai/v1',
|
||||
mistral: 'https://api.mistral.ai/v1',
|
||||
orcestAI: 'https://rm.orcest.ai/v1',
|
||||
};
|
||||
|
||||
class LLMMessageServiceWeb extends Disposable implements ILLMMessageService {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 (RainyModel)', }
|
||||
}
|
||||
|
||||
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 integrated API. Models are available automatically through your SSO login. Powered by [RainyModel](https://rm.orcest.ai).'
|
||||
|
||||
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
|
@ -156,7 +160,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
providerName === 'awsBedrock' ? 'key-...' :
|
||||
'',
|
||||
providerName === 'orcestAI' ? 'sk-orcest-key...' :
|
||||
'',
|
||||
|
||||
isPasswordField: true,
|
||||
}
|
||||
|
|
@ -171,7 +176,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 +185,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 +359,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
|||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
orcestAI: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.orcestAI,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.orcestAI),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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