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 <danial.samiei@gmail.com>
This commit is contained in:
Devin AI 2026-02-16 19:31:06 +00:00
parent 4d6b992739
commit 8d7599c65b
6 changed files with 156 additions and 3 deletions

View file

@ -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)
}

View file

@ -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<void>());
onDidChangeState = this._onDidChangeState.event;
async revealMCPConfigFile() { }
async toggleServerIsOn() { }
getMCPTools() { return undefined; }
async callMCPTool(): Promise<{ result: RawMCPToolCall }> { return { result: { type: 'text', text: 'MCP not available in web mode' } as any }; }
stringifyResult() { return ''; }
}
registerSingleton(IMCPService, MCPServiceWeb, InstantiationType.Eager);
}

View file

@ -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);
}

View file

@ -198,5 +198,127 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
if (!isWeb) {
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
} else {
const _baseUrls: Partial<Record<string, string>> = {
openRouter: 'https://openrouter.ai/api/v1',
openAI: 'https://api.openai.com/v1',
deepseek: 'https://api.deepseek.com/v1',
groq: 'https://api.groq.com/openai/v1',
xAI: 'https://api.x.ai/v1',
mistral: 'https://api.mistral.ai/v1',
};
class LLMMessageServiceWeb extends Disposable implements ILLMMessageService {
readonly _serviceBrand: undefined;
private readonly _abortControllers = new Map<string, AbortController>();
constructor(
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
) { super(); }
sendLLMMessage(params: ServiceSendLLMMessageParams): string | null {
const { onText, onFinalMessage, onError, 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<string, string> = { 'Content-Type': 'application/json' };
if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; }
if (modelSelection.providerName === 'openRouter') {
headers['HTTP-Referer'] = 'https://ide.orcest.ai';
headers['X-Title'] = 'ide.orcest.ai';
}
let messages: any[];
let systemMessage: string | undefined;
if (params.messagesType === 'chatMessages') {
messages = params.messages.map((m: any) => ({ role: m.role === 'model' ? 'assistant' : m.role, content: typeof m.content === 'string' ? m.content : (m.parts ? m.parts.map((p: any) => p.text).join('') : JSON.stringify(m.content)) }));
systemMessage = params.separateSystemMessage;
} else {
messages = [{ role: 'user', content: params.messages.prefix }];
systemMessage = undefined;
}
const body: any = { model: modelSelection.modelName, messages: systemMessage ? [{ role: 'system', content: systemMessage }, ...messages] : messages, stream: true };
(async () => {
try {
const res = await fetch(`${baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body), signal: abort.signal });
if (!res.ok) {
const errText = await res.text().catch(() => res.statusText);
onError({ message: `${res.status}: ${errText}`, fullError: null });
return;
}
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let fullText = '';
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop()!;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('data:')) continue;
const data = trimmed.slice(5).trim();
if (data === '[DONE]') continue;
try {
const json = JSON.parse(data);
const delta = json.choices?.[0]?.delta;
if (delta?.content) {
fullText += delta.content;
onText({ fullText, fullReasoning: '' });
}
} catch { }
}
}
onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null });
} catch (e: any) {
if (e?.name !== 'AbortError') {
onError({ message: e?.message || String(e), fullError: null });
}
} finally {
this._abortControllers.delete(requestId);
}
})();
return requestId;
}
abort(requestId: string) {
this._abortControllers.get(requestId)?.abort();
this._abortControllers.delete(requestId);
}
ollamaList(params: ServiceModelListParams<OllamaModelResponse>) {
params.onError({ error: 'Ollama not available in web mode' });
}
openAICompatibleList(params: ServiceModelListParams<OpenaiCompatibleModelResponse>) {
const { settingsOfProvider } = this.voidSettingsService.state;
const providerSettings = settingsOfProvider[params.providerName];
const apiKey = (providerSettings as any).apiKey as string | undefined;
const endpoint = (providerSettings as any).endpoint as string | undefined;
const baseUrl = endpoint || _baseUrls[params.providerName] || '';
if (!baseUrl) { params.onError({ error: 'No endpoint configured' }); return; }
const headers: Record<string, string> = {};
if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; }
fetch(`${baseUrl}/models`, { headers }).then(r => r.json()).then(json => {
params.onSuccess({ models: json.data || [] });
}).catch(e => { params.onError({ error: e?.message || String(e) }); });
}
}
registerSingleton(ILLMMessageService, LLMMessageServiceWeb, InstantiationType.Eager);
}

View file

@ -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);
}

View file

@ -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';