diff --git a/.voidrules b/.voidrules
index 55ab5af6..19702f73 100644
--- a/.voidrules
+++ b/.voidrules
@@ -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.
diff --git a/VOID_CODEBASE_GUIDE.md b/VOID_CODEBASE_GUIDE.md
index 5d22bd88..1f17ef55 100644
--- a/VOID_CODEBASE_GUIDE.md
+++ b/VOID_CODEBASE_GUIDE.md
@@ -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").
@@ -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:

@@ -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.
@@ -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).
diff --git a/package.json b/package.json
index e6341c09..862ffc2f 100644
--- a/package.json
+++ b/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"
diff --git a/product.json b/product.json
index b43c6c65..1788d482 100644
--- a/product.json
+++ b/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"
diff --git a/src/vs/workbench/contrib/void/browser/extensionTransferService.ts b/src/vs/workbench/contrib/void/browser/extensionTransferService.ts
index fba3a233..13656cb5 100644
--- a/src/vs/workbench/contrib/void/browser/extensionTransferService.ts
+++ b/src/vs/workbench/contrib/void/browser/extensionTransferService.ts
@@ -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'),
diff --git a/src/vs/workbench/contrib/void/browser/fileService.ts b/src/vs/workbench/contrib/void/browser/fileService.ts
index 93da1b1e..7e456802 100644
--- a/src/vs/workbench/contrib/void/browser/fileService.ts
+++ b/src/vs/workbench/contrib/void/browser/fileService.ts
@@ -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',
diff --git a/src/vs/workbench/contrib/void/browser/orcideSSOBrowserService.ts b/src/vs/workbench/contrib/void/browser/orcideSSOBrowserService.ts
new file mode 100644
index 00000000..1bdb4450
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/orcideSSOBrowserService.ts
@@ -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 | null = null;
+ private _popupTimeoutTimer: ReturnType | 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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}`,
+ });
+ }
+ }
+});
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx
index 0f998ebf..176db641 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx
@@ -547,7 +547,7 @@ const VoidOnboardingContent = () => {
voidMetricsService.capture('Completed Onboarding', { selectedProviderName, wantToUseOption })
}}
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
- >Enter the Void
+ >Enter Orcide
@@ -596,7 +596,7 @@ const VoidOnboardingContent = () => {
0:
- Welcome to Void
+ Welcome to Orcide
{/* Slice of Void image */}
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
index ed2b97c9..d4f45a28 100644
--- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts
+++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
@@ -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
{
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), }]
});
diff --git a/src/vs/workbench/contrib/void/browser/sidebarPane.ts b/src/vs/workbench/contrib/void/browser/sidebarPane.ts
index 803b2b8b..877f7265 100644
--- a/src/vs/workbench/contrib/void/browser/sidebarPane.ts
+++ b/src/vs/workbench/contrib/void/browser/sidebarPane.ts
@@ -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 {
diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts
index 35c89184..c3e7a951 100644
--- a/src/vs/workbench/contrib/void/browser/void.contribution.ts
+++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts
@@ -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'
diff --git a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts
index b70aef91..a87c9972 100644
--- a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts
+++ b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts
@@ -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(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
});
diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts
index 2d66e43e..232a33ff 100644
--- a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts
+++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts
@@ -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 {
diff --git a/src/vs/workbench/contrib/void/common/metricsService.ts b/src/vs/workbench/contrib/void/common/metricsService.ts
index 21ae1c3d..642e953a 100644
--- a/src/vs/workbench/contrib/void/common/metricsService.ts
+++ b/src/vs/workbench/contrib/void/common/metricsService.ts
@@ -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 {
@@ -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)}`)
}
})
diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts
index d784ab93..aee4544d 100644
--- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts
+++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts
@@ -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
@@ -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
diff --git a/src/vs/workbench/contrib/void/common/orcideCollaborationService.ts b/src/vs/workbench/contrib/void/common/orcideCollaborationService.ts
new file mode 100644
index 00000000..919c85e3
--- /dev/null
+++ b/src/vs/workbench/contrib/void/common/orcideCollaborationService.ts
@@ -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;
+}
+
+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;
+ onDidShareResource: Event;
+ onDidReceiveInvitation: Event;
+
+ // Resource sharing
+ shareResource(resource: Omit): Promise;
+ unshareResource(resourceId: string): Promise;
+ updateResourcePermission(resourceId: string, userId: string, permission: SharePermission): Promise;
+ removeUserFromResource(resourceId: string, userId: string): Promise;
+ getSharedResources(): SharedResource[];
+ getResourcesSharedWithMe(myUserId: string): SharedResource[];
+ getResourcesSharedByMe(myUserId: string): SharedResource[];
+
+ // Team management
+ inviteTeamMember(email: string, role: TeamMember['role']): Promise;
+ removeTeamMember(userId: string): Promise;
+ updateTeamMemberRole(userId: string, role: TeamMember['role']): Promise;
+ getTeamMembers(): TeamMember[];
+
+ // Invitations
+ acceptInvitation(invitationId: string): Promise;
+ declineInvitation(invitationId: string): Promise;
+ getPendingInvitations(): Invitation[];
+ revokeInvitation(invitationId: string): Promise;
+
+ // Workspace sharing
+ shareWorkspace(workspaceName: string, userIds: string[], permission: SharePermission): Promise;
+ shareChatThread(threadId: string, threadName: string, userIds: string[]): Promise;
+ shareModelConfig(configName: string, providerSettings: Record, userIds: string[]): Promise;
+}
+
+export const IOrcideCollaborationService = createDecorator('orcideCollaborationService');
+
+
+class OrcideCollaborationService extends Disposable implements IOrcideCollaborationService {
+ readonly _serviceBrand: undefined;
+
+ private _state: CollaborationState;
+
+ private readonly _onDidChangeState = this._register(new Emitter());
+ readonly onDidChangeState: Event = this._onDidChangeState.event;
+
+ private readonly _onDidShareResource = this._register(new Emitter());
+ readonly onDidShareResource: Event = this._onDidShareResource.event;
+
+ private readonly _onDidReceiveInvitation = this._register(new Emitter());
+ readonly onDidReceiveInvitation: Event = 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): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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, userIds: string[]): Promise {
+ 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);
diff --git a/src/vs/workbench/contrib/void/common/orcideSSOService.ts b/src/vs/workbench/contrib/void/common/orcideSSOService.ts
new file mode 100644
index 00000000..2d56aeab
--- /dev/null
+++ b/src/vs/workbench/contrib/void/common/orcideSSOService.ts
@@ -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;
+
+ onDidChangeState: Event;
+
+ login(): Promise;
+ logout(): Promise;
+ getAccessToken(): Promise;
+ getUserProfile(): SSOUserProfile | null;
+ isAuthenticated(): boolean;
+ refreshToken(): Promise;
+
+ /**
+ * 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;
+
+ /**
+ * 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 {
+ 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 | 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('OrcideSSOService');
+
+
+// ─── Service Implementation ─────────────────────────────────────────────────────
+
+class OrcideSSOService extends Disposable implements IOrcideSSOService {
+ _serviceBrand: undefined;
+
+ private readonly _onDidChangeState = new Emitter();
+ readonly onDidChangeState: Event = this._onDidChangeState.event;
+
+ state: SSOState;
+
+ private readonly _resolver: () => void;
+ waitForInitState: Promise;
+
+ private _refreshTimer: ReturnType | 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ // 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 {
+ // 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 {
+ 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;
+ }
+
+ private async _fetchUserProfile(accessToken: string): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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);
diff --git a/src/vs/workbench/contrib/void/common/orcideUserProfileService.ts b/src/vs/workbench/contrib/void/common/orcideUserProfileService.ts
new file mode 100644
index 00000000..d49cf568
--- /dev/null
+++ b/src/vs/workbench/contrib/void/common/orcideUserProfileService.ts
@@ -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;
+ onDidChangeProfile: Event;
+
+ setProfile(profile: OrcideUserProfile): Promise;
+ clearProfile(): Promise;
+ getProfile(): OrcideUserProfile | null;
+
+ setPreference(key: K, value: OrcideUserPreferences[K]): Promise;
+ getPreferences(): OrcideUserPreferences;
+ resetPreferences(): Promise;
+
+ addRepository(repo: OrcideRepository): Promise;
+ removeRepository(repoId: string): Promise;
+ getRepositories(): OrcideRepository[];
+ shareRepository(repoId: string, userId: string): Promise;
+ unshareRepository(repoId: string, userId: string): Promise;
+
+ addSession(session: OrcideUserSession): void;
+ removeSession(sessionId: string): void;
+ getActiveSessions(): OrcideUserSession[];
+}
+
+export const IOrcideUserProfileService = createDecorator('orcideUserProfileService');
+
+
+class OrcideUserProfileService extends Disposable implements IOrcideUserProfileService {
+ readonly _serviceBrand: undefined;
+
+ private _state: UserProfileState;
+
+ private readonly _onDidChangeState = this._register(new Emitter());
+ readonly onDidChangeState: Event = this._onDidChangeState.event;
+
+ private readonly _onDidChangeProfile = this._register(new Emitter());
+ readonly onDidChangeProfile: Event = 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 {
+ this._state = { ...this._state, profile };
+ this._saveProfile();
+ this._onDidChangeProfile.fire(profile);
+ this._onDidChangeState.fire();
+ }
+
+ async clearProfile(): Promise {
+ this._state = {
+ ...this._state,
+ profile: null,
+ repositories: [],
+ activeSessions: [],
+ };
+ this._saveProfile();
+ this._saveRepositories();
+ this._onDidChangeState.fire();
+ }
+
+ getProfile(): OrcideUserProfile | null {
+ return this._state.profile;
+ }
+
+ async setPreference(key: K, value: OrcideUserPreferences[K]): Promise {
+ this._state = {
+ ...this._state,
+ preferences: { ...this._state.preferences, [key]: value },
+ };
+ this._savePreferences();
+ this._onDidChangeState.fire();
+ }
+
+ getPreferences(): OrcideUserPreferences {
+ return this._state.preferences;
+ }
+
+ async resetPreferences(): Promise {
+ this._state = {
+ ...this._state,
+ preferences: { ...defaultPreferences },
+ };
+ this._savePreferences();
+ this._onDidChangeState.fire();
+ }
+
+ async addRepository(repo: OrcideRepository): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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);
diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts
index fd13cac2..3132934a 100644
--- a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts
+++ b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts
@@ -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 {
diff --git a/src/vs/workbench/contrib/void/common/storageKeys.ts b/src/vs/workbench/contrib/void/common/storageKeys.ts
index b23d7ffb..17871623 100644
--- a/src/vs/workbench/contrib/void/common/storageKeys.ts
+++ b/src/vs/workbench/contrib/void/common/storageKeys.ts
@@ -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'
diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts
index 38497c60..a093517a 100644
--- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts
+++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts
@@ -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,
+ },
}
diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts
index 1b8c8617..f3ae0b99 100644
--- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts
+++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts
@@ -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
diff --git a/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts b/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts
index b6553c47..efcc903f 100644
--- a/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts
+++ b/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts
@@ -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))
}
diff --git a/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts b/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts
index 35bca7d1..bf39e972 100644
--- a/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts
+++ b/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts
@@ -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 {