reactify Widget

This commit is contained in:
Andrew Pareles 2024-12-10 02:09:06 -08:00
parent 8b90d7827d
commit adb504f2e0
8 changed files with 150 additions and 108 deletions

View file

@ -27,16 +27,28 @@ export interface ISendLLMMessageService {
export class SendLLMMessageService implements ISendLLMMessageService { export class SendLLMMessageService implements ISendLLMMessageService {
readonly _serviceBrand: undefined; readonly _serviceBrand: undefined;
private readonly channel: IChannel; private readonly channel: IChannel // LLMMessageChannel
private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {} private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {}
private readonly onTextEvent: Event<ProxyOnTextPayload>
private readonly onFinalMessageEvent: Event<ProxyOnFinalMessagePayload>
private readonly onErrorEvent: Event<ProxyOnErrorPayload>
constructor( constructor(
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side) @IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
) { ) {
this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage') this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage')
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service, not needed here
console.log('setting up IPC')
// this sets up an IPC channel and takes a few ms, so should happen immediately
this.onTextEvent = this.channel.listen('onText')
this.onFinalMessageEvent = this.channel.listen('onFinalMessage')
this.onErrorEvent = this.channel.listen('onError')
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
} }
_addDisposable(requestId: string, disposable: IDisposable) { _addDisposable(requestId: string, disposable: IDisposable) {
@ -54,28 +66,26 @@ export class SendLLMMessageService implements ISendLLMMessageService {
// listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it // listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it
const onTextEvent: Event<ProxyOnTextPayload> = this.channel.listen('onText')
this._addDisposable(requestId_, this._addDisposable(requestId_,
onTextEvent(e => { this.onTextEvent(e => {
if (requestId_ !== e.requestId) return; if (requestId_ !== e.requestId) return;
onText(e) onText(e)
}) })
) )
const onFinalMessageEvent: Event<ProxyOnFinalMessagePayload> = this.channel.listen('onFinalMessage')
this._addDisposable(requestId_, this._addDisposable(requestId_,
onFinalMessageEvent(e => { this.onFinalMessageEvent(e => {
if (requestId_ !== e.requestId) return; if (requestId_ !== e.requestId) return;
onFinalMessage(e) onFinalMessage(e)
this._dispose(requestId_) this._dispose(requestId_)
}) })
) )
const onErrorEvent: Event<ProxyOnErrorPayload> = this.channel.listen('onError')
this._addDisposable(requestId_, this._addDisposable(requestId_,
onErrorEvent(e => { this.onErrorEvent(e => {
console.log('sendLLMMessageService - error event received (havent checked req)')
if (requestId_ !== e.requestId) return; if (requestId_ !== e.requestId) return;
console.log('event onError', JSON.stringify(e)) console.log('sendLLMMessageService - error event received', JSON.stringify(e))
onError(e) onError(e)
this._dispose(requestId_) this._dispose(requestId_)
}) })

View file

@ -228,7 +228,7 @@ export type VoidProviderState = {
type UnionOfKeys<T> = T extends T ? keyof T : never; type UnionOfKeys<T> = T extends T ? keyof T : never;
type ProviderSettingName = UnionOfKeys<VoidProviderState[ProviderName]> export type ProviderSettingName = UnionOfKeys<VoidProviderState[ProviderName]>
@ -361,5 +361,5 @@ type VoidFeatureState = {
} | null, } | null,
} }
export type FeatureName = keyof VoidFeatureState export type FeatureName = keyof VoidFeatureState
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const

View file

@ -68,7 +68,7 @@ export class LLMMessageChannel implements IServerChannel {
} }
// the only place sendLLMMessage is actually called // the only place sendLLMMessage is actually called
private _callSendLLMMessage(params: ProxyLLMMessageParams) { private async _callSendLLMMessage(params: ProxyLLMMessageParams) {
const { requestId } = params; const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId)) if (!(requestId in this._abortRefOfRequestId))
@ -76,9 +76,9 @@ export class LLMMessageChannel implements IServerChannel {
const mainThreadParams: SendLLMMMessageParams = { const mainThreadParams: SendLLMMMessageParams = {
...params, ...params,
onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); }, onText: ({ newText, fullText }) => { console.log('sendLLM: firing onText'); this._onText.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); }, onFinalMessage: ({ fullText }) => { console.log('sendLLM: firing finalMsg'); this._onFinalMessage.fire({ requestId, fullText }); },
onError: ({ error }) => { this._onError.fire({ requestId, error }); }, onError: ({ error }) => { console.log('sendLLM: firing err'); this._onError.fire({ requestId, error }); },
abortRef: this._abortRefOfRequestId[requestId], abortRef: this._abortRefOfRequestId[requestId],
} }
sendLLMMessage(mainThreadParams, this.metricsService); sendLLMMessage(mainThreadParams, this.metricsService);

View file

@ -10,18 +10,12 @@ import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node' import { PostHog } from 'posthog-node'
// posthog-js (old):
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', }) // posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
// const buildEnv = 'development'; // const buildEnv = 'development';
// const buildNumber = '1.0.0'; // const buildNumber = '1.0.0';
// const isMac = process.platform === 'darwin'; // const isMac = process.platform === 'darwin';
// // TODO use commandKey
// const commandKey = isMac ? '⌘' : 'Ctrl';
// const systemInfo = {
// buildEnv,
// buildNumber,
// isMac,
// }
export class MetricsMainService extends Disposable implements IMetricsService { export class MetricsMainService extends Disposable implements IMetricsService {
_serviceBrand: undefined; _serviceBrand: undefined;
@ -43,9 +37,9 @@ export class MetricsMainService extends Disposable implements IMetricsService {
} }
capture: IMetricsService['capture'] = (event, params) => { capture: IMetricsService['capture'] = (event, params) => {
console.log('Capturing', { event, params }) const capture = { distinctId: this._distinctId, event, properties: params } as const
console.log('full capture:', { distinctId: this._distinctId, event, properties: params }) // console.log('full capture:', capture)
this.client.capture({ distinctId: this._distinctId, event, properties: params }) this.client.capture(capture)
} }
} }

View file

@ -19,7 +19,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js'; import { ErrorDisplay } from './ErrorDisplay.js';
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js'; import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { getCmdKey } from '../../../getCmdKey.js' import { getCmdKey } from '../../../getCmdKey.js'
import { HistoryInputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox } from './inputs.js'; import { VoidInputBox } from './inputs.js';
// read files from VSCode // read files from VSCode
@ -124,7 +124,7 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
export const SidebarChat = () => { export const SidebarChat = () => {
const chatInputRef = useRef<HTMLTextAreaElement | null>(null) const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
const modelService = useService('modelService') const modelService = useService('modelService')
@ -134,11 +134,11 @@ export const SidebarChat = () => {
useEffect(() => { useEffect(() => {
const disposables: IDisposable[] = [] const disposables: IDisposable[] = []
disposables.push( disposables.push(
sidebarStateService.onDidFocusChat(() => { chatInputRef.current?.focus() }), sidebarStateService.onDidFocusChat(() => { inputBoxRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { chatInputRef.current?.blur() }) sidebarStateService.onDidBlurChat(() => { inputBoxRef.current?.blur() })
) )
return () => disposables.forEach(d => d.dispose()) return () => disposables.forEach(d => d.dispose())
}, [sidebarStateService, chatInputRef]) }, [sidebarStateService, inputBoxRef])
// config state // config state
const voidConfigState = useConfigState() const voidConfigState = useConfigState()
@ -164,7 +164,7 @@ export const SidebarChat = () => {
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions]) const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
const isDisabled = !instructions const isDisabled = !instructions
const formRef = useRef<HTMLFormElement | null>(null) const formRef = useRef<HTMLFormElement | null>(null)
const inputBoxRef: React.MutableRefObject<HistoryInputBox | null> = useRef(null);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => { const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
@ -298,7 +298,7 @@ export const SidebarChat = () => {
<VoidInputBox <VoidInputBox
placeholder={`${getCmdKey()}+L to select`} placeholder={`${getCmdKey()}+L to select`}
onChangeText={onChangeText} onChangeText={onChangeText}
inputBoxRef={inputBoxRef} onCreateInstance={inputBoxRef}
multiline={true} multiline={true}
initVal='' initVal=''
/> />

View file

@ -3,44 +3,45 @@
* Void Editor additions licensed under the AGPLv3 License. * Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import React, { Fragment } from 'react' import React, { Fragment, useCallback, useRef } from 'react'
import { displayInfoOfSettingName, ProviderName, providerNames } from '../../../../../../../platform/void/common/configTypes.js' import { displayInfoOfSettingName, ProviderName, providerNames, ProviderSettingName, VoidProviderState } from '../../../../../../../platform/void/common/configTypes.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox } from './inputs.js' import { VoidCheckBox, VoidInputBox, VoidSelectBox } from './inputs.js'
import { useConfigState, useService } from '../util/services.js' import { useConfigState, useService } from '../util/services.js'
const Setting = ({ val, providerName, settingName }: { val: string, providerName: ProviderName, settingName: any }) => {
const { title, type, placeholder } = displayInfoOfSettingName(providerName, settingName)
const voidConfigService = useService('configStateService')
const initValRef = useRef(val)
return <>
<h2>{title}</h2>
{<VoidInputBox
initVal={initValRef.current}
placeholder={placeholder}
onChangeText={useCallback((newVal) => {
voidConfigService.setState(providerName, settingName, newVal)
}, [voidConfigService, providerName, settingName])
}
multiline={false}
/>}
</>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => { const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidConfigState = useConfigState() const voidConfigState = useConfigState()
const voidConfigService = useService('configStateService')
const { models, model, ...others } = voidConfigState[providerName] const { models, model, ...others } = voidConfigState[providerName]
const voidConfigService = useService('configStateService')
return <> return <>
<h1>{providerName}</h1> <h1>{providerName}</h1>
{/* other settings (e.g. api key) */} {/* other settings (e.g. api key) */}
{Object.entries(others).map(([settingName, defaultVal], i) => { {Object.entries(others).map(([settingName, val], i) => {
const sName = settingName as keyof typeof others return <Setting key={settingName} val={val} providerName={providerName} settingName={settingName} />
const { title, type, placeholder } = displayInfoOfSettingName(providerName, sName)
return <Fragment key={i}>
<h2>{title}</h2>
{
type === 'boolean' ?
<VoidCheckBox
initVal={defaultVal === 'true'}
onChangeChecked={(newVal) => { voidConfigService.setState(providerName, sName, newVal ? 'true' : 'false') }}
label={settingName}
checkboxRef={{ current: null }}
/>
:
<VoidInputBox
initVal={defaultVal}
placeholder={placeholder}
onChangeText={(newVal) => { () => { voidConfigService.setState(providerName, sName, newVal) } }}
multiline={false}
inputBoxRef={{ current: null }}
/>}
</Fragment>
})} })}
<h2>{'Models'}</h2> <h2>{'Models'}</h2>
@ -49,12 +50,10 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
: <VoidSelectBox : <VoidSelectBox
initVal={models[0]} initVal={models[0]}
options={models} options={models}
onChangeSelection={(newVal) => { () => { } }} onChangeSelection={(newVal) => { voidConfigService.setState(providerName, 'model', newVal) }}
selectBoxRef={{ current: null }} selectBoxRef={{ current: null }}
/>} />}
</> </>
} }

View file

@ -1,29 +1,54 @@
import React, { useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { useService } from '../util/services.js'; import { useService } from '../util/services.js';
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultToggleStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; import { defaultCheckboxStyles, defaultInputBoxStyles, defaultToggleStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox, unthemedSelectBoxStyles } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'; import { SelectBox, unthemedSelectBoxStyles } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
import { Checkbox, Toggle } from '../../../../../../../base/browser/ui/toggle/toggle.js'; import { Checkbox, Toggle } from '../../../../../../../base/browser/ui/toggle/toggle.js';
import { ObjectSettingCheckboxWidget } from '../../../../../preferences/browser/settingsWidgets.js'
import { Widget } from '../../../../../../../base/browser/ui/widget.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
// settingitem // settingitem
export const VoidInputBox = ({ onChangeText, initVal, placeholder, inputBoxRef, multiline }: {
export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, propsFn, dispose, onCreateInstance }
: {
ctor: { new(...params: CtorParams): Instance },
propsFn: (container: HTMLDivElement) => CtorParams,
onCreateInstance: (instance: Instance) => IDisposable[],
dispose: (instance: Instance) => void,
}
) => {
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const instance = new ctor(...propsFn(containerRef.current!));
const disposables = onCreateInstance(instance);
return () => {
disposables.forEach(d => d.dispose());
dispose(instance)
}
}, [ctor, propsFn, dispose, onCreateInstance, containerRef])
return <div ref={containerRef} className='w-full' />
}
export const VoidInputBox = ({ onChangeText, onCreateInstance, initVal, placeholder, multiline }: {
onChangeText: (value: string) => void; onChangeText: (value: string) => void;
onCreateInstance?: { current: InputBox | null } | ((instance: InputBox) => void | IDisposable[]);
placeholder: string; placeholder: string;
inputBoxRef: React.MutableRefObject<InputBox | null>;
multiline: boolean; multiline: boolean;
initVal: string; initVal: string;
}) => { }) => {
const contextViewProvider = useService('contextViewService'); const contextViewProvider = useService('contextViewService');
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => { return <WidgetComponent
if (!containerRef.current) return; ctor={InputBox}
propsFn={useCallback((container) => [
// create and mount the HistoryInputBox container,
inputBoxRef.current = new InputBox(
containerRef.current,
contextViewProvider, contextViewProvider,
{ {
inputBoxStyles: { inputBoxStyles: {
@ -34,32 +59,29 @@ export const VoidInputBox = ({ onChangeText, initVal, placeholder, inputBoxRef,
flexibleHeight: multiline, flexibleHeight: multiline,
flexibleMaxHeight: 500, flexibleMaxHeight: 500,
flexibleWidth: false, flexibleWidth: false,
} }
); ] as const, [contextViewProvider, placeholder, multiline])}
inputBoxRef.current.value = initVal; dispose={useCallback((instance: InputBox) => {
instance.dispose()
instance.element.remove()
inputBoxRef.current.onDidChange((newStr) => { }, [])}
console.log('CHANGE TEXT on inputbox', newStr) onCreateInstance={useCallback((instance: InputBox) => {
onChangeText(newStr) instance.value = initVal
}) const disposables: IDisposable[] = []
disposables.push(
// cleanup instance.onDidChange((newText) => onChangeText(newText))
return () => { )
if (inputBoxRef.current) { if (typeof onCreateInstance === 'function') {
inputBoxRef.current.dispose(); const ds = onCreateInstance(instance) ?? []
if (containerRef.current) { disposables.push(...ds)
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
}
inputBoxRef.current = null;
} }
}; if (typeof onCreateInstance === 'object') {
}, [inputBoxRef, contextViewProvider, placeholder, multiline, initVal, onChangeText]); onCreateInstance.current = instance
}
return <div ref={containerRef} className="w-full" />; return disposables
}, [initVal, onChangeText, onCreateInstance])
}
/>
}; };
@ -113,27 +135,39 @@ export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, option
export const VoidCheckBox = ({ onChangeChecked, initVal, label, checkboxRef, }: { export const VoidCheckBox = ({ onChangeChecked, initVal, label, checkboxRef, }: {
onChangeChecked: (checked: boolean) => void; onChangeChecked: (checked: boolean) => void;
initVal: boolean; initVal: boolean;
checkboxRef: React.MutableRefObject<Toggle | null>; checkboxRef: React.MutableRefObject<ObjectSettingCheckboxWidget | null>;
label: string; label: string;
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const themeService = useService('themeService');
const contextViewService = useService('contextViewService');
const hoverService = useService('hoverService');
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
// Create and mount the Checkbox using VSCode's implementation // Create and mount the Checkbox using VSCode's implementation
checkboxRef.current = new Toggle({
title: label,
isChecked: initVal,
...defaultToggleStyles
});
containerRef.current.appendChild(checkboxRef.current.domNode); checkboxRef.current = new ObjectSettingCheckboxWidget(
containerRef.current,
themeService,
contextViewService,
hoverService,
);
checkboxRef.current.setValue([{
key: { type: 'string', data: label },
value: { type: 'boolean', data: initVal },
removable: false,
resetable: true,
}])
checkboxRef.current.onDidChangeList((list) => {
onChangeChecked(!!list);
})
checkboxRef.current.onChange(checked => {
console.log('CHANGE checked state on checkbox', checked);
onChangeChecked(checked);
});
// cleanup // cleanup
return () => { return () => {

View file

@ -70,6 +70,9 @@ export type ReactServicesType = {
sendLLMMessageService: ISendLLMMessageService; sendLLMMessageService: ISendLLMMessageService;
clipboardService: IClipboardService; clipboardService: IClipboardService;
themeService: IThemeService,
hoverService: IHoverService,
contextViewService: IContextViewService; contextViewService: IContextViewService;
contextMenuService: IContextMenuService; contextMenuService: IContextMenuService;
} }
@ -113,6 +116,8 @@ class VoidSidebarViewPane extends ViewPane {
inlineDiffService: accessor.get(IInlineDiffsService), inlineDiffService: accessor.get(IInlineDiffsService),
sendLLMMessageService: accessor.get(ISendLLMMessageService), sendLLMMessageService: accessor.get(ISendLLMMessageService),
clipboardService: accessor.get(IClipboardService), clipboardService: accessor.get(IClipboardService),
themeService: accessor.get(IThemeService),
hoverService: accessor.get(IHoverService),
contextViewService: accessor.get(IContextViewService), contextViewService: accessor.get(IContextViewService),
contextMenuService: accessor.get(IContextMenuService), contextMenuService: accessor.get(IContextMenuService),
} }