mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
commit
8233c59f2b
6 changed files with 105 additions and 19 deletions
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
|
|
@ -52,6 +52,8 @@ import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
|||
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
||||
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
|
||||
import { IMCPService } from '../../../../common/mcpService.js';
|
||||
import { IStorageService, StorageScope } from '../../../../../../../platform/storage/common/storage.js'
|
||||
import { OPT_OUT_KEY } from '../../../../common/storageKeys.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
|
@ -226,6 +228,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
||||
IMCPService: accessor.get(IMCPService),
|
||||
|
||||
IStorageService: accessor.get(IStorageService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
}
|
||||
|
|
@ -399,3 +403,26 @@ export const useMCPServiceState = () => {
|
|||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const useIsOptedOut = () => {
|
||||
const accessor = useAccessor()
|
||||
const storageService = accessor.get('IStorageService')
|
||||
|
||||
const getVal = useCallback(() => {
|
||||
return storageService.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
|
||||
}, [storageService])
|
||||
|
||||
const [s, ss] = useState(getVal())
|
||||
|
||||
useEffect(() => {
|
||||
const disposables = new DisposableStore();
|
||||
const d = storageService.onDidChangeValue(StorageScope.APPLICATION, OPT_OUT_KEY, disposables)(e => {
|
||||
ss(getVal())
|
||||
})
|
||||
disposables.add(d)
|
||||
return () => disposables.clear()
|
||||
}, [storageService, getVal])
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
|
|||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { useAccessor, useIsDark, useIsOptedOut, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check, Asterisk, Plus } from 'lucide-react'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
|
|
@ -21,6 +21,8 @@ import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../.
|
|||
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
|
||||
import { MCPServer } from '../../../../common/mcpServiceTypes.js';
|
||||
import { useMCPServiceState } from '../util/services.js';
|
||||
import { OPT_OUT_KEY } from '../../../../common/storageKeys.js';
|
||||
import { StorageScope, StorageTarget } from '../../../../../../../platform/storage/common/storage.js';
|
||||
|
||||
type Tab =
|
||||
| 'models'
|
||||
|
|
@ -215,10 +217,10 @@ const SimpleModelSettingsDialog = ({
|
|||
if (!isOpen || !modelInfo) return null;
|
||||
|
||||
const { modelName, providerName, type } = modelInfo;
|
||||
const accessor = useAccessor();
|
||||
const settingsState = useSettingsState();
|
||||
const accessor = useAccessor()
|
||||
const settingsState = useSettingsState()
|
||||
const mouseDownInsideModal = useRef(false); // Ref to track mousedown origin
|
||||
const settingsStateService = accessor.get('IVoidSettingsService');
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// current overrides and defaults
|
||||
const defaultModelCapabilities = getModelCapabilities(providerName, modelName, undefined);
|
||||
|
|
@ -1036,7 +1038,7 @@ export const Settings = () => {
|
|||
const navItems: { tab: Tab; label: string }[] = [
|
||||
{ tab: 'models', label: 'Models' },
|
||||
{ tab: 'localProviders', label: 'Local Providers' },
|
||||
{ tab: 'providers', label: 'Other Providers' },
|
||||
{ tab: 'providers', label: 'Main Providers' },
|
||||
{ tab: 'featureOptions', label: 'Feature Options' },
|
||||
{ tab: 'general', label: 'General' },
|
||||
{ tab: 'mcp', label: 'MCP' },
|
||||
|
|
@ -1052,6 +1054,9 @@ export const Settings = () => {
|
|||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
const storageService = accessor.get('IStorageService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const isOptedOut = useIsOptedOut()
|
||||
|
||||
const onDownload = (t: 'Chats' | 'Settings') => {
|
||||
let dataStr: string
|
||||
|
|
@ -1194,10 +1199,10 @@ export const Settings = () => {
|
|||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Other Providers section */}
|
||||
{/* Main Providers section */}
|
||||
<div className={shouldShowTab('providers') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Other Providers</h2>
|
||||
<h2 className={`text-3xl mb-2`}>Main Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
|
|
@ -1214,7 +1219,7 @@ export const Settings = () => {
|
|||
{/* FIM */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>
|
||||
<span>
|
||||
Experimental.{' '}
|
||||
</span>
|
||||
|
|
@ -1258,7 +1263,7 @@ export const Settings = () => {
|
|||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
|
|
@ -1294,7 +1299,7 @@ export const Settings = () => {
|
|||
{/* Tools Section */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>Tools</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
|
|
@ -1338,7 +1343,7 @@ export const Settings = () => {
|
|||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
|
|
@ -1360,7 +1365,7 @@ export const Settings = () => {
|
|||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('SCM')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the commit message generator.</div>
|
||||
<div className='text-sm text-void-fg-3 mt-1'>Settings that control the behavior of the commit message generator.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
|
|
@ -1419,8 +1424,9 @@ export const Settings = () => {
|
|||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 w-full max-w-48'>
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
|
|
@ -1461,6 +1467,29 @@ export const Settings = () => {
|
|||
</div>
|
||||
|
||||
|
||||
{/* Metrics section */}
|
||||
<div className='max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>Metrics</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>Very basic anonymous usage tracking helps us keep Void running smoothly. You may opt out below. Regardless of this setting, Void never sees your code, messages, or API keys.</h4>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Disable All Metrics Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={isOptedOut}
|
||||
onChange={(newVal) => {
|
||||
storageService.store(OPT_OUT_KEY, newVal, StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
metricsService.capture(`Set metrics opt-out to ${newVal}`, {}) // this only fires if it's enabled, so it's fine to have here
|
||||
}}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{'Opt-out (requires restart)'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Instructions section */}
|
||||
<div className='max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
|
|
@ -1494,6 +1523,7 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace.
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { INotificationService } from '../../../../platform/notification/common/n
|
|||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
setOptOut(val: boolean): void;
|
||||
getDebuggingProperties(): Promise<object>;
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +39,11 @@ export class MetricsService implements IMetricsService {
|
|||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
setOptOut(...params: Parameters<IMetricsService['setOptOut']>) {
|
||||
this.metricsService.setOptOut(...params);
|
||||
}
|
||||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
async getDebuggingProperties(): Promise<object> {
|
||||
return this.metricsService.getDebuggingProperties()
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ export const VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII'
|
|||
|
||||
// 1.0.3
|
||||
export const THREAD_STORAGE_KEY = 'void.chatThreadStorageII'
|
||||
|
||||
|
||||
|
||||
export const OPT_OUT_KEY = 'void.app.optOutAll'
|
||||
|
|
|
|||
|
|
@ -231,12 +231,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { IApplicationStorageMainService } from '../../../../platform/storage/ele
|
|||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
import { OPT_OUT_KEY } from '../common/storageKeys.js';
|
||||
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
|
|
@ -29,6 +30,8 @@ const osInfo = _getOSInfo()
|
|||
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
|
|
@ -119,7 +122,18 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
|||
distinctId: this.distinctId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
const didOptOut = this._appStorage.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
|
||||
|
||||
console.log('User is opted out of basic Void metrics?', didOptOut)
|
||||
if (didOptOut) {
|
||||
this.client.optOut()
|
||||
}
|
||||
else {
|
||||
this.client.optIn()
|
||||
this.client.identify(identifyMessage)
|
||||
}
|
||||
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
}
|
||||
|
|
@ -127,10 +141,18 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
|||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this.distinctId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
// console.log('full capture:', this.distinctId)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
setOptOut: IMetricsService['setOptOut'] = (newVal: boolean) => {
|
||||
if (newVal) {
|
||||
this._appStorage.store(OPT_OUT_KEY, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
}
|
||||
else {
|
||||
this._appStorage.remove(OPT_OUT_KEY, StorageScope.APPLICATION)
|
||||
}
|
||||
}
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
|
|
|
|||
Loading…
Reference in a new issue