From 8d7599c65b33a36510b0152cc95b3b55f0b7360b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:31:06 +0000 Subject: [PATCH] Inline web service stubs into existing service files The separate void.web.services.ts file was not being compiled/loaded, so all 5 services were UNKNOWN in web mode. This commit adds web stub implementations directly into each service file's else branch of the existing isWeb guard, guaranteeing they compile and register. - LLMMessageServiceWeb: real fetch+SSE streaming for OpenRouter/OpenAI - MetricsServiceWeb: no-op stub - VoidUpdateServiceWeb: no-op stub - MCPServiceWeb: minimal stub with empty state - GenerateCommitMessageServiceWeb: no-op stub Also removes the now-unnecessary void.web.services.js import from workbench.web.main.internal.ts. Co-Authored-By: Danial Piterson --- .../contrib/void/browser/voidSCMService.ts | 7 + .../contrib/void/common/mcpService.ts | 13 ++ .../contrib/void/common/metricsService.ts | 8 ++ .../void/common/sendLLMMessageService.ts | 122 ++++++++++++++++++ .../contrib/void/common/voidUpdateService.ts | 6 + .../workbench/workbench.web.main.internal.ts | 3 - 6 files changed, 156 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCMService.ts b/src/vs/workbench/contrib/void/browser/voidSCMService.ts index 02342513..c06ec149 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCMService.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCMService.ts @@ -230,4 +230,11 @@ registerAction2(GenerateCommitMessageAction) registerAction2(LoadingGenerateCommitMessageAction) if (!isWeb) { registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed) +} else { + class GenerateCommitMessageServiceWeb implements IGenerateCommitMessageService { + readonly _serviceBrand: undefined; + async generateCommitMessage() { } + abort() { } + } + registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageServiceWeb, InstantiationType.Delayed) } diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 3c70309a..d44438e6 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -360,4 +360,17 @@ class MCPService extends Disposable implements IMCPService { if (!isWeb) { registerSingleton(IMCPService, MCPService, InstantiationType.Eager); +} else { + class MCPServiceWeb extends Disposable implements IMCPService { + _serviceBrand: undefined; + state: { mcpServerOfName: MCPServerOfName; error: string | undefined } = { mcpServerOfName: {}, error: undefined }; + private readonly _onDidChangeState = this._register(new Emitter()); + onDidChangeState = this._onDidChangeState.event; + async revealMCPConfigFile() { } + async toggleServerIsOn() { } + getMCPTools() { return undefined; } + async callMCPTool(): Promise<{ result: RawMCPToolCall }> { return { result: { type: 'text', text: 'MCP not available in web mode' } as any }; } + stringifyResult() { return ''; } + } + registerSingleton(IMCPService, MCPServiceWeb, InstantiationType.Eager); } diff --git a/src/vs/workbench/contrib/void/common/metricsService.ts b/src/vs/workbench/contrib/void/common/metricsService.ts index 389ed43d..21ae1c3d 100644 --- a/src/vs/workbench/contrib/void/common/metricsService.ts +++ b/src/vs/workbench/contrib/void/common/metricsService.ts @@ -53,6 +53,14 @@ export class MetricsService implements IMetricsService { if (!isWeb) { registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager); +} else { + class MetricsServiceWeb implements IMetricsService { + readonly _serviceBrand: undefined; + capture() { } + setOptOut() { } + async getDebuggingProperties() { return {} } + } + registerSingleton(IMetricsService, MetricsServiceWeb, InstantiationType.Eager); } diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts index 5f74d0c1..82c7b75a 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts @@ -198,5 +198,127 @@ export class LLMMessageService extends Disposable implements ILLMMessageService if (!isWeb) { registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager); +} else { + const _baseUrls: Partial> = { + openRouter: 'https://openrouter.ai/api/v1', + openAI: 'https://api.openai.com/v1', + deepseek: 'https://api.deepseek.com/v1', + groq: 'https://api.groq.com/openai/v1', + xAI: 'https://api.x.ai/v1', + mistral: 'https://api.mistral.ai/v1', + }; + + class LLMMessageServiceWeb extends Disposable implements ILLMMessageService { + readonly _serviceBrand: undefined; + private readonly _abortControllers = new Map(); + + constructor( + @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, + ) { super(); } + + sendLLMMessage(params: ServiceSendLLMMessageParams): string | null { + const { onText, onFinalMessage, onError, onAbort, modelSelection } = params; + if (modelSelection === null) { + onError({ message: `Please add a provider in Void's Settings.`, fullError: null }); + return null; + } + const requestId = generateUuid(); + const abort = new AbortController(); + this._abortControllers.set(requestId, abort); + + const { settingsOfProvider } = this.voidSettingsService.state; + const providerSettings = settingsOfProvider[modelSelection.providerName]; + const apiKey = (providerSettings as any).apiKey as string | undefined; + const endpoint = (providerSettings as any).endpoint as string | undefined; + const baseUrl = endpoint || _baseUrls[modelSelection.providerName] || 'https://openrouter.ai/api/v1'; + + const headers: Record = { 'Content-Type': 'application/json' }; + if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; } + if (modelSelection.providerName === 'openRouter') { + headers['HTTP-Referer'] = 'https://ide.orcest.ai'; + headers['X-Title'] = 'ide.orcest.ai'; + } + + let messages: any[]; + let systemMessage: string | undefined; + if (params.messagesType === 'chatMessages') { + messages = params.messages.map((m: any) => ({ role: m.role === 'model' ? 'assistant' : m.role, content: typeof m.content === 'string' ? m.content : (m.parts ? m.parts.map((p: any) => p.text).join('') : JSON.stringify(m.content)) })); + systemMessage = params.separateSystemMessage; + } else { + messages = [{ role: 'user', content: params.messages.prefix }]; + systemMessage = undefined; + } + + const body: any = { model: modelSelection.modelName, messages: systemMessage ? [{ role: 'system', content: systemMessage }, ...messages] : messages, stream: true }; + + (async () => { + try { + const res = await fetch(`${baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body), signal: abort.signal }); + if (!res.ok) { + const errText = await res.text().catch(() => res.statusText); + onError({ message: `${res.status}: ${errText}`, fullError: null }); + return; + } + const reader = res.body!.getReader(); + const decoder = new TextDecoder(); + let fullText = ''; + let buffer = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop()!; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith('data:')) continue; + const data = trimmed.slice(5).trim(); + if (data === '[DONE]') continue; + try { + const json = JSON.parse(data); + const delta = json.choices?.[0]?.delta; + if (delta?.content) { + fullText += delta.content; + onText({ fullText, fullReasoning: '' }); + } + } catch { } + } + } + onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); + } catch (e: any) { + if (e?.name !== 'AbortError') { + onError({ message: e?.message || String(e), fullError: null }); + } + } finally { + this._abortControllers.delete(requestId); + } + })(); + return requestId; + } + + abort(requestId: string) { + this._abortControllers.get(requestId)?.abort(); + this._abortControllers.delete(requestId); + } + + ollamaList(params: ServiceModelListParams) { + params.onError({ error: 'Ollama not available in web mode' }); + } + + openAICompatibleList(params: ServiceModelListParams) { + const { settingsOfProvider } = this.voidSettingsService.state; + const providerSettings = settingsOfProvider[params.providerName]; + const apiKey = (providerSettings as any).apiKey as string | undefined; + const endpoint = (providerSettings as any).endpoint as string | undefined; + const baseUrl = endpoint || _baseUrls[params.providerName] || ''; + if (!baseUrl) { params.onError({ error: 'No endpoint configured' }); return; } + const headers: Record = {}; + if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; } + fetch(`${baseUrl}/models`, { headers }).then(r => r.json()).then(json => { + params.onSuccess({ models: json.data || [] }); + }).catch(e => { params.onError({ error: e?.message || String(e) }); }); + } + } + registerSingleton(ILLMMessageService, LLMMessageServiceWeb, InstantiationType.Eager); } diff --git a/src/vs/workbench/contrib/void/common/voidUpdateService.ts b/src/vs/workbench/contrib/void/common/voidUpdateService.ts index 28006c52..e8f1f0f8 100644 --- a/src/vs/workbench/contrib/void/common/voidUpdateService.ts +++ b/src/vs/workbench/contrib/void/common/voidUpdateService.ts @@ -44,6 +44,12 @@ export class VoidUpdateService implements IVoidUpdateService { if (!isWeb) { registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager); +} else { + class VoidUpdateServiceWeb implements IVoidUpdateService { + readonly _serviceBrand: undefined; + check = async () => ({ type: 'noUpdate' }) as any; + } + registerSingleton(IVoidUpdateService, VoidUpdateServiceWeb, InstantiationType.Eager); } diff --git a/src/vs/workbench/workbench.web.main.internal.ts b/src/vs/workbench/workbench.web.main.internal.ts index dfa8a5c0..59a19ef5 100644 --- a/src/vs/workbench/workbench.web.main.internal.ts +++ b/src/vs/workbench/workbench.web.main.internal.ts @@ -121,9 +121,6 @@ registerSingleton(IDefaultAccountService, NullDefaultAccountService, Instantiati //#region --- workbench contributions -// Void: web-compatible service overrides (must come after common to override Electron-only registrations) -import './contrib/void/browser/void.web.services.js'; - // Logs import './contrib/logs/browser/logs.contribution.js';