diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index b768f85e..5a60fffd 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -85,7 +85,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ for (const providerName of refreshableProviderNames) { const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] - this.refreshModels(providerName, !enabled, { isPolling: true, isInternal: true }) + this.refreshModels(providerName, !enabled, { isPolling: true, isInvisible: true }) // every time providerName.enabled changes, refresh models too, like a useEffect let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName]) @@ -101,7 +101,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // if it was just enabled, or there was a change and it wasn't to the enabled state, refresh if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) { // if user just clicked enable, refresh - this.refreshModels(providerName, !enabled, { isPolling: false, isInternal: true }) + this.refreshModels(providerName, !enabled, { isPolling: false, isInvisible: true }) } else { // else if user just clicked disable, don't refresh @@ -134,34 +134,46 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // start listening for models (and don't stop until success) - async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInternal?: boolean }) { + async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInvisible?: boolean }) { - const { isPolling, isInternal } = options ?? {} + const { isPolling, isInvisible } = options ?? {} - console.log(`refreshModels, isInternal ${isInternal} isPolling ${isPolling}`) + console.log(`refreshModels, isInvisible ${isInvisible} isPolling ${isPolling}`) this._clearProviderTimeout(providerName) // start loading models - if (!isInternal) this._setRefreshState(providerName, 'refreshing') + if (!isInvisible) this._setRefreshState(providerName, 'refreshing') - const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList + const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList : () => { } - fn({ + listFn({ onSuccess: ({ models }) => { - this.voidSettingsService.setDefaultModels(providerName, models.map(model => { - if (providerName === 'ollama') return (model as OllamaModelResponse).name - else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id - else throw new Error('refreshMode fn: unknown provider', providerName) - })) + // set the models to the detected models + this.voidSettingsService.setAutodetectedModels( + providerName, + models.map(model => { + if (providerName === 'ollama') return (model as OllamaModelResponse).name; + else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id; + else throw new Error('refreshMode fn: unknown provider', providerName); + }), + { enableProviderOnSuccess, isPolling, isInvisible } + ) + + // update state if (enableProviderOnSuccess) { this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true) } - if (!isInternal) this._setRefreshState(providerName, 'finished') + if (!isInvisible) { + this._setRefreshState(providerName, 'finished') + } else if (isInvisible) { + this._setRefreshState(providerName, 'finished_invisible') + } + }, onError: ({ error }) => { @@ -169,7 +181,6 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ } }) - if (isInternal) this._setRefreshState(providerName, 'finished_invisible') // check if we should poll // if it was originally called as `isPolling` and if the `autoRefreshModels` flag is enabled diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index 6e0d68fc..f3d43e6b 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -10,6 +10,7 @@ import { IEncryptionService } from '../../encryption/common/encryptionService.js import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; +import { IMetricsService } from './metricsService.js'; import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js'; @@ -55,7 +56,7 @@ export interface IVoidSettingsService { setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; setFeatureFlag: SetFeatureFlagFn; - setDefaultModels(providerName: ProviderName, modelNames: string[]): void; + setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }): void; toggleModelHidden(providerName: ProviderName, modelName: string): void; addModel(providerName: ProviderName, modelName: string): void; deleteModel(providerName: ProviderName, modelName: string): boolean; @@ -100,6 +101,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { constructor( @IStorageService private readonly _storageService: IStorageService, @IEncryptionService private readonly _encryptionService: IEncryptionService, + @IMetricsService private readonly _metricsService: IMetricsService, // could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER) // @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, ) { @@ -220,25 +222,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { - setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) { + setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }) { + const { models } = this.state.settingsOfProvider[providerName] + + const old_names = models.map(m => m.modelName) + const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models }) const newModels = [ ...newDefaultModels, ...models.filter(m => !m.isDefault), // keep any non-default models ] + + this.setSettingOfProvider(providerName, 'models', newModels) + + // if the models changed, log it + const new_names = newModels.map(m => m.modelName) + if (!(old_names.length === new_names.length + && old_names.every((_, i) => old_names[i] === new_names[i]) + )) { + this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging }) + } } toggleModelHidden(providerName: ProviderName, modelName: string) { + + const { models } = this.state.settingsOfProvider[providerName] const modelIdx = models.findIndex(m => m.modelName === modelName) if (modelIdx === -1) return + const newIsHidden = !models[modelIdx].isHidden const newModels: VoidModelInfo[] = [ ...models.slice(0, modelIdx), - { ...models[modelIdx], isHidden: !models[modelIdx].isHidden }, + { ...models[modelIdx], isHidden: newIsHidden }, ...models.slice(modelIdx + 1, Infinity) ] this.setSettingOfProvider(providerName, 'models', newModels) + + this._metricsService.capture('Toggle Model Hidden', { providerName, modelName, newIsHidden }) + } addModel(providerName: ProviderName, modelName: string) { const { models } = this.state.settingsOfProvider[providerName] @@ -249,6 +271,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { { modelName, isDefault: false, isHidden: false } ] this.setSettingOfProvider(providerName, 'models', newModels) + + this._metricsService.capture('Add Model', { providerName, modelName }) + } deleteModel(providerName: ProviderName, modelName: string): boolean { const { models } = this.state.settingsOfProvider[providerName] @@ -259,6 +284,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { ...models.slice(delIdx + 1, Infinity) ] this.setSettingOfProvider(providerName, 'models', newModels) + + this._metricsService.capture('Delete Model', { providerName, modelName }) + return true } diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 62becbfb..2b068780 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -14,26 +14,33 @@ export type VoidModelInfo = { isAutodetected?: boolean, // whether the model was autodetected by polling } -type ModelInfoOfDefaultNamesOptions = { isAutodetected: true, existingModels: VoidModelInfo[] } // | { isOtherOption: true, ...otherOptions } -export const modelInfoOfDefaultNames = (modelNames: string[], options?: ModelInfoOfDefaultNamesOptions): VoidModelInfo[] => { +// creates `modelInfo` from `modelNames` +export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => { const { isAutodetected, existingModels } = options ?? {} - const isDefault = true - const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually - if (!existingModels) { + if (!existingModels) { // default settings - return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden, })) + return modelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: isAutodetected, + isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually + })) - } else { - // keep existing `isHidden` property + } else { // settings if there are existing models (keep existing `isHidden` property) const existingModelsMap: Record = {} - for (const em of existingModels) { - existingModelsMap[em.modelName] = em + for (const existingModel of existingModels) { + existingModelsMap[existingModel.modelName] = existingModel } - return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden: !!existingModelsMap[modelName]?.isHidden, })) + return modelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: isAutodetected, + isHidden: !!existingModelsMap[modelName]?.isHidden, + })) } diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index 96d51e34..b3970614 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -34,6 +34,7 @@ export const sendLLMMessage = ({ const captureChatEvent = (eventId: string, extras?: object) => { metricsService.capture(eventId, { providerName, + modelName, numMessages: messages?.length, messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })), version: '2024-11-14', diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 557d3fe2..00fe8b50 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -37,6 +37,7 @@ import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; import { LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -198,6 +199,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { @IConsistentItemService private readonly _consistentItemService: IConsistentItemService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService, + @IMetricsService private readonly _metricsService: IMetricsService, ) { super(); @@ -480,8 +482,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { fn: (editor) => { const buttonsWidget = new AcceptRejectWidget({ editor, - onAccept: () => { this.acceptDiff({ diffid }) }, - onReject: () => { this.rejectDiff({ diffid }) }, + onAccept: () => { + this.acceptDiff({ diffid }) + this._metricsService.capture('Accept Diff', { batch: false }) + }, + onReject: () => { + this.rejectDiff({ diffid }) + this._metricsService.capture('Reject Diff', { batch: false }) + }, diffid: diffid.toString(), startLine: diff.startLine, }) diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 12da5324..82f3526c 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -49,7 +49,7 @@ registerAction2(class extends Action2 { const editorService = accessor.get(ICodeEditorService) const metricsService = accessor.get(IMetricsService) - metricsService.capture('User Action', { type: 'Open Ctrl+K' }) + metricsService.capture('Ctrl+K', {}) const editor = editorService.getActiveCodeEditor() if (!editor) return; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index a44bd626..b95d6b5b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -20,9 +20,12 @@ const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' const CodeButtonsOnHover = ({ text }: { text: string }) => { const accessor = useAccessor() + const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) const inlineDiffService = accessor.get('IInlineDiffsService') const clipboardService = accessor.get('IClipboardService') + const metricsService = accessor.get('IMetricsService') + useEffect(() => { if (copyButtonState !== CopyButtonState.Copy) { @@ -36,6 +39,8 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => { clipboardService.writeText(text) .then(() => { setCopyButtonState(CopyButtonState.Copied) }) .catch(() => { setCopyButtonState(CopyButtonState.Error) }) + metricsService.capture('Copy Code', { length: text.length }) // capture the length only + }, [text, clipboardService]) const onApply = useCallback(() => { @@ -43,6 +48,7 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => { featureName: 'Ctrl+L', userMessage: text, }) + metricsService.capture('Apply Code', { length: text.length }) // capture the length only }, [inlineDiffService]) const isSingleLine = !text.includes('\n') diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index c6fb062c..68d06c38 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -604,7 +604,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars } -export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?:boolean; onClick: () => void }) => { +export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => { return