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").

Credit - https://github.com/microsoft/vscode/wiki/Source-Code-Organization

@@ -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 {