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 {
readonly _serviceBrand: undefined;
private readonly channel: IChannel;
private readonly channel: IChannel // LLMMessageChannel
private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {}
private readonly onTextEvent: Event<ProxyOnTextPayload>
private readonly onFinalMessageEvent: Event<ProxyOnFinalMessagePayload>
private readonly onErrorEvent: Event<ProxyOnErrorPayload>
constructor(
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
) {
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) {
@ -54,28 +66,26 @@ export class SendLLMMessageService implements ISendLLMMessageService {
// listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it
const onTextEvent: Event<ProxyOnTextPayload> = this.channel.listen('onText')
this._addDisposable(requestId_,
onTextEvent(e => {
this.onTextEvent(e => {
if (requestId_ !== e.requestId) return;
onText(e)
})
)
const onFinalMessageEvent: Event<ProxyOnFinalMessagePayload> = this.channel.listen('onFinalMessage')
this._addDisposable(requestId_,
onFinalMessageEvent(e => {
this.onFinalMessageEvent(e => {
if (requestId_ !== e.requestId) return;
onFinalMessage(e)
this._dispose(requestId_)
})
)
const onErrorEvent: Event<ProxyOnErrorPayload> = this.channel.listen('onError')
this._addDisposable(requestId_,
onErrorEvent(e => {
this.onErrorEvent(e => {
console.log('sendLLMMessageService - error event received (havent checked req)')
if (requestId_ !== e.requestId) return;
console.log('event onError', JSON.stringify(e))
console.log('sendLLMMessageService - error event received', JSON.stringify(e))
onError(e)
this._dispose(requestId_)
})

View file

@ -228,7 +228,7 @@ export type VoidProviderState = {
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,
}
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
private _callSendLLMMessage(params: ProxyLLMMessageParams) {
private async _callSendLLMMessage(params: ProxyLLMMessageParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId))
@ -76,9 +76,9 @@ export class LLMMessageChannel implements IServerChannel {
const mainThreadParams: SendLLMMMessageParams = {
...params,
onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); },
onError: ({ error }) => { this._onError.fire({ requestId, error }); },
onText: ({ newText, fullText }) => { console.log('sendLLM: firing onText'); this._onText.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { console.log('sendLLM: firing finalMsg'); this._onFinalMessage.fire({ requestId, fullText }); },
onError: ({ error }) => { console.log('sendLLM: firing err'); this._onError.fire({ requestId, error }); },
abortRef: this._abortRefOfRequestId[requestId],
}
sendLLMMessage(mainThreadParams, this.metricsService);

View file

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

View file

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

View file

@ -3,44 +3,45 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { Fragment } from 'react'
import { displayInfoOfSettingName, ProviderName, providerNames } from '../../../../../../../platform/void/common/configTypes.js'
import React, { Fragment, useCallback, useRef } from 'react'
import { displayInfoOfSettingName, ProviderName, providerNames, ProviderSettingName, VoidProviderState } from '../../../../../../../platform/void/common/configTypes.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox } from './inputs.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 voidConfigState = useConfigState()
const voidConfigService = useService('configStateService')
const { models, model, ...others } = voidConfigState[providerName]
const voidConfigService = useService('configStateService')
return <>
<h1>{providerName}</h1>
{/* other settings (e.g. api key) */}
{Object.entries(others).map(([settingName, defaultVal], i) => {
const sName = settingName as keyof typeof others
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>
{Object.entries(others).map(([settingName, val], i) => {
return <Setting key={settingName} val={val} providerName={providerName} settingName={settingName} />
})}
<h2>{'Models'}</h2>
@ -49,12 +50,10 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
: <VoidSelectBox
initVal={models[0]}
options={models}
onChangeSelection={(newVal) => { () => { } }}
onChangeSelection={(newVal) => { voidConfigService.setState(providerName, 'model', newVal) }}
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 { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultToggleStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox, unthemedSelectBoxStyles } from '../../../../../../../base/browser/ui/selectBox/selectBox.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
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;
onCreateInstance?: { current: InputBox | null } | ((instance: InputBox) => void | IDisposable[]);
placeholder: string;
inputBoxRef: React.MutableRefObject<InputBox | null>;
multiline: boolean;
initVal: string;
}) => {
const contextViewProvider = useService('contextViewService');
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
// create and mount the HistoryInputBox
inputBoxRef.current = new InputBox(
containerRef.current,
return <WidgetComponent
ctor={InputBox}
propsFn={useCallback((container) => [
container,
contextViewProvider,
{
inputBoxStyles: {
@ -34,32 +59,29 @@ export const VoidInputBox = ({ onChangeText, initVal, placeholder, inputBoxRef,
flexibleHeight: multiline,
flexibleMaxHeight: 500,
flexibleWidth: false,
}
);
inputBoxRef.current.value = initVal;
inputBoxRef.current.onDidChange((newStr) => {
console.log('CHANGE TEXT on inputbox', newStr)
onChangeText(newStr)
})
// cleanup
return () => {
if (inputBoxRef.current) {
inputBoxRef.current.dispose();
if (containerRef.current) {
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
}
inputBoxRef.current = null;
] as const, [contextViewProvider, placeholder, multiline])}
dispose={useCallback((instance: InputBox) => {
instance.dispose()
instance.element.remove()
}, [])}
onCreateInstance={useCallback((instance: InputBox) => {
instance.value = initVal
const disposables: IDisposable[] = []
disposables.push(
instance.onDidChange((newText) => onChangeText(newText))
)
if (typeof onCreateInstance === 'function') {
const ds = onCreateInstance(instance) ?? []
disposables.push(...ds)
}
};
}, [inputBoxRef, contextViewProvider, placeholder, multiline, initVal, onChangeText]);
return <div ref={containerRef} className="w-full" />;
if (typeof onCreateInstance === 'object') {
onCreateInstance.current = instance
}
return disposables
}, [initVal, onChangeText, onCreateInstance])
}
/>
};
@ -113,27 +135,39 @@ export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, option
export const VoidCheckBox = ({ onChangeChecked, initVal, label, checkboxRef, }: {
onChangeChecked: (checked: boolean) => void;
initVal: boolean;
checkboxRef: React.MutableRefObject<Toggle | null>;
checkboxRef: React.MutableRefObject<ObjectSettingCheckboxWidget | null>;
label: string;
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const themeService = useService('themeService');
const contextViewService = useService('contextViewService');
const hoverService = useService('hoverService');
useEffect(() => {
if (!containerRef.current) return;
// 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
return () => {

View file

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