diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
index 90455ee0..bc920307 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
@@ -220,7 +220,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
- INativeHostService: accessor.get(INativeHostService),
+ INativeHostService: (() => { try { return accessor.get(INativeHostService); } catch { return undefined; } })() as any,
IToolsService: accessor.get(IToolsService),
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
ITerminalService: accessor.get(ITerminalService),
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
index acc3c6d6..81ec4bd6 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
@@ -1459,7 +1459,7 @@ export const Settings = () => {
{ commandService.executeCommand('workbench.action.selectTheme') }}>
Theme Settings
- { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
+ { nativeHostService?.showItemInFolder(environmentService.logsHome.fsPath) }}>
Open Logs
diff --git a/src/vs/workbench/contrib/void/browser/void.web.services.ts b/src/vs/workbench/contrib/void/browser/void.web.services.ts
new file mode 100644
index 00000000..e647112c
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/void.web.services.ts
@@ -0,0 +1,294 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. 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 { Emitter, Event } from '../../../../base/common/event.js';
+import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
+import { generateUuid } from '../../../../base/common/uuid.js';
+
+import { ILLMMessageService } from '../common/sendLLMMessageService.js';
+import { ServiceSendLLMMessageParams, ServiceModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse } from '../common/sendLLMMessageTypes.js';
+import { IVoidSettingsService } from '../common/voidSettingsService.js';
+import { IMCPService } from '../common/mcpService.js';
+import { MCPToolCallParams, RawMCPToolCall } from '../common/mcpServiceTypes.js';
+import { InternalToolInfo } from '../common/prompt/prompts.js';
+import { IMetricsService } from '../common/metricsService.js';
+import { IVoidUpdateService } from '../common/voidUpdateService.js';
+import { IGenerateCommitMessageService } from './voidSCMService.js';
+import { ProviderName } from '../common/voidSettingsTypes.js';
+
+
+const OPENAI_COMPAT_BASE_URLS: Partial> = {
+ openRouter: 'https://openrouter.ai/api/v1',
+ openAI: 'https://api.openai.com/v1',
+ deepseek: 'https://api.deepseek.com',
+ 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 { onError, modelSelection } = params;
+
+ if (modelSelection === null) {
+ onError({ message: 'Please add a provider in Void\'s Settings.', fullError: null });
+ return null;
+ }
+
+ if (params.messagesType === 'chatMessages' && (params.messages?.length ?? 0) === 0) {
+ onError({ message: 'No messages detected.', fullError: null });
+ return null;
+ }
+
+ if (params.messagesType === 'FIMMessage') {
+ onError({ message: 'Autocomplete (FIM) is not supported in web mode.', fullError: null });
+ return null;
+ }
+
+ const requestId = generateUuid();
+ const abortController = new AbortController();
+ this._abortControllers.set(requestId, abortController);
+
+ this._doSendChat(params, requestId, abortController);
+
+ return requestId;
+ }
+
+ private async _doSendChat(
+ params: ServiceSendLLMMessageParams,
+ requestId: string,
+ abortController: AbortController
+ ) {
+ const { onText, onFinalMessage, onError, modelSelection } = params;
+
+ if (params.messagesType !== 'chatMessages' || !modelSelection) return;
+
+ try {
+ const { settingsOfProvider } = this.voidSettingsService.state;
+ const providerSettings = settingsOfProvider[modelSelection.providerName];
+ const apiKey = (providerSettings as Record).apiKey as string | undefined;
+
+ if (!apiKey) {
+ onError({
+ message: `API key not set for ${modelSelection.providerName}. Please configure it in Void Settings.`,
+ fullError: null
+ });
+ return;
+ }
+
+ const baseUrl = this._getBaseUrl(modelSelection.providerName, providerSettings);
+ if (!baseUrl) {
+ onError({
+ message: `Provider "${modelSelection.providerName}" requires the desktop app. Use OpenRouter instead.`,
+ fullError: null
+ });
+ return;
+ }
+
+ const messages = this._buildMessages(params.messages, params.separateSystemMessage);
+
+ const body: Record = {
+ model: modelSelection.modelName,
+ messages,
+ stream: true,
+ };
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${apiKey}`,
+ };
+
+ if (modelSelection.providerName === 'openRouter') {
+ headers['HTTP-Referer'] = 'https://ide.orcest.ai';
+ headers['X-Title'] = 'ide.orcest.ai';
+ }
+
+ const response = await fetch(`${baseUrl}/chat/completions`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(body),
+ signal: abortController.signal,
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ onError({
+ message: `API error (${response.status}): ${errorText}`,
+ fullError: new Error(errorText)
+ });
+ this._abortControllers.delete(requestId);
+ return;
+ }
+
+ const reader = response.body!.getReader();
+ const decoder = new TextDecoder();
+ let fullText = '';
+ let fullReasoning = '';
+ 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(6);
+ if (data === '[DONE]') {
+ onFinalMessage({ fullText, fullReasoning, anthropicReasoning: null });
+ this._abortControllers.delete(requestId);
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(data);
+ const delta = parsed.choices?.[0]?.delta;
+ if (delta?.content) {
+ fullText += delta.content;
+ onText({ fullText, fullReasoning });
+ }
+ if (delta?.reasoning_content || delta?.reasoning) {
+ fullReasoning += (delta.reasoning_content || delta.reasoning);
+ onText({ fullText, fullReasoning });
+ }
+ } catch {
+ // skip malformed SSE chunks
+ }
+ }
+ }
+
+ onFinalMessage({ fullText, fullReasoning, anthropicReasoning: null });
+ this._abortControllers.delete(requestId);
+ } catch (err: unknown) {
+ if (err instanceof Error && err.name === 'AbortError') return;
+ const message = err instanceof Error ? err.message : String(err);
+ onError({ message, fullError: err instanceof Error ? err : new Error(message) });
+ this._abortControllers.delete(requestId);
+ }
+ }
+
+ private _getBaseUrl(providerName: ProviderName, providerSettings: Record): string | null {
+ const known = OPENAI_COMPAT_BASE_URLS[providerName];
+ if (known) return known;
+
+ if (providerName === 'openAICompatible' || providerName === 'liteLLM' || providerName === 'awsBedrock') {
+ return (providerSettings.endpoint as string) || null;
+ }
+
+ return null;
+ }
+
+ private _buildMessages(
+ messages: unknown[],
+ separateSystemMessage: string | undefined
+ ): { role: string; content: string }[] {
+ const result: { role: string; content: string }[] = [];
+
+ if (separateSystemMessage) {
+ result.push({ role: 'system', content: separateSystemMessage });
+ }
+
+ for (const msg of messages) {
+ const m = msg as { role: string; content: unknown };
+ if (typeof m.content === 'string') {
+ result.push({ role: m.role === 'model' ? 'assistant' : m.role, content: m.content });
+ } else if (Array.isArray(m.content)) {
+ const textParts = m.content
+ .filter((p: Record) => p.type === 'text' || p.text)
+ .map((p: Record) => (p.text as string) || '')
+ .join('');
+ if (textParts) {
+ result.push({ role: m.role === 'model' ? 'assistant' : m.role, content: textParts });
+ }
+ }
+ }
+
+ return result;
+ }
+
+ abort(requestId: string) {
+ const controller = this._abortControllers.get(requestId);
+ if (controller) {
+ controller.abort();
+ this._abortControllers.delete(requestId);
+ }
+ }
+
+ ollamaList(params: ServiceModelListParams) {
+ params.onError({ error: 'Ollama model listing is not available in web mode.' });
+ }
+
+ openAICompatibleList(params: ServiceModelListParams) {
+ params.onError({ error: 'Model listing is not available in web mode.' });
+ }
+}
+
+
+class MCPServiceWeb extends Disposable implements IMCPService {
+ readonly _serviceBrand: undefined;
+
+ state: { mcpServerOfName: Record; error: string | undefined } = {
+ mcpServerOfName: {},
+ error: undefined,
+ };
+
+ private readonly _onDidChangeState = new Emitter();
+ readonly onDidChangeState: Event = this._onDidChangeState.event;
+
+ async revealMCPConfigFile(): Promise { }
+ async toggleServerIsOn(): Promise { }
+ getMCPTools(): InternalToolInfo[] | undefined { return undefined; }
+
+ async callMCPTool(_toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> {
+ throw new Error('MCP is not available in web mode.');
+ }
+
+ stringifyResult(result: RawMCPToolCall): string {
+ return JSON.stringify(result);
+ }
+}
+
+
+class MetricsServiceWeb implements IMetricsService {
+ readonly _serviceBrand: undefined;
+ capture(): void { }
+ setOptOut(): void { }
+ async getDebuggingProperties(): Promise