diff --git a/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/Sidebar.tsx b/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/Sidebar.tsx index a834d1de..f0a6c647 100644 --- a/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/Sidebar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/Sidebar.tsx @@ -1,46 +1,18 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { mountFnGenerator } from '../util/mountFnGenerator' -import { VIEWPANE_FILTER_ACTION } from '../../../../../browser/parts/views/viewPane.js' -import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation' + +import { SidebarSettings } from './SidebarSettings.js'; +import { useServices } from '../util/contextForServices.js'; +import { IVoidSidebarStateService, VoidSidebarState } from '../../registerSidebar.js'; // import { SidebarThreadSelector } from './SidebarThreadSelector.js'; // import { SidebarChat } from './SidebarChat.js'; -// import { SidebarSettings } from './SidebarSettings.js'; -console.log('!!filteraction', VIEWPANE_FILTER_ACTION) -const Sidebar = ({ accessor }: { accessor: ServicesAccessor }) => { - // const chatInputRef = useRef(null) - - const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat') - - // // if they pressed the + to add a new chat - // useOnVSCodeMessage('startNewThread', (m) => { - // setTab('chat'); - // chatInputRef.current?.focus(); - // }) - - // // ctrl+l should switch back to chat - // useOnVSCodeMessage('ctrl+l', (m) => { - // setTab('chat'); - // chatInputRef.current?.focus(); - // }) - - // // if they toggled thread selector - // useOnVSCodeMessage('toggleThreadSelector', (m) => { - // if (tab === 'threadSelector') { - // setTab('chat') - // chatInputRef.current?.blur(); - // } else - // setTab('threadSelector') - // }) - - // // if they toggled settings - // useOnVSCodeMessage('toggleSettings', (m) => { - // if (tab === 'settings') { - // setTab('chat') - // chatInputRef.current?.blur(); - // } else - // setTab('settings') - // }) +const Sidebar = () => { + // state should come from sidebarStateService + const { sidebarStateService } = useServices() + const [sidebarState, setSideBarState] = useState(sidebarStateService.state) + const { isHistoryOpen, currentTab: tab } = sidebarState + useEffect(() => { sidebarStateService.onDidChangeState(() => setSideBarState(sidebarStateService.state)) }, [sidebarStateService]) return <>
@@ -48,20 +20,19 @@ const Sidebar = ({ accessor }: { accessor: ServicesAccessor }) => { { const tabs = ['chat', 'settings', 'threadSelector'] const index = tabs.indexOf(tab) - setTab(tabs[(index + 1) % tabs.length] as any) + sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any }) }}>clickme {tab} -
- hi +
{/* setTab('chat')} /> */}
-
+
{/* */}
-
- {/* */} +
+
diff --git a/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarChat.tsx index aa23e909..4558828b 100644 --- a/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarChat.tsx @@ -1,6 +1,11 @@ // import React, { FormEvent, useCallback, useRef, useState } from "react"; +// sidebarStateService.onDidFocusChat(() => {}) +// sidebarStateService.onDidBlurChat(() => {}) + + + // import MarkdownRender from "../../sidebar/markdown/!MarkdownRender"; // import BlockCode from "../../sidebar/markdown/!BlockCode"; // import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarSettings.tsx b/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarSettings.tsx index 9eb46683..acf7056a 100644 --- a/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarSettings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/sidebar-tsx/SidebarSettings.tsx @@ -1,105 +1,119 @@ -// import React, { useState } from "react"; -// import { configFields, useVoidConfig, VoidConfigField } from "../util/contextForConfig"; +import React, { useEffect, useState } from 'react'; +import { useServices } from '../util/contextForServices.js'; +import { IVoidConfigStateService, nonDefaultConfigFields, PartialVoidConfig, VoidConfig, VoidConfigField, VoidConfigInfo, SetFieldFnType, ConfigState } from '../../../browser/registerConfig.js'; -// const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => { -// const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig() -// const { enumArr, defaultVal, description } = voidConfigInfo[field][param] -// const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item +const SettingOfFieldAndParam = ({ field, param, configState, configStateService }: + { field: VoidConfigField; param: string; configState: NonNullable; configStateService: IVoidConfigStateService }) => { -// const updateState = (newValue: string) => { setConfigParam(field, param, newValue) } - -// const resetButton = - -// const inputElement = enumArr === undefined ? -// // string -// ( updateState(e.target.value)} -// />) -// : -// // enum -// () - -// return
-// -// {description} -//
-// {inputElement} -// {resetButton} -//
-//
-// } - -// export const SidebarSettings = () => { - -// const { voidConfig, voidConfigInfo } = useVoidConfig() - -// const current_field = voidConfig.default['whichApi'] as VoidConfigField + const { partialVoidConfig } = configState -// return ( -//
+ const { enumArr, defaultVal, description } = configStateService.voidConfigInfo[field][param] + const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item -// {/* choose the field */} -//
-// -// -//
+ const updateState = (newValue: string) => { configStateService.setField(field, param, newValue) } -//
+ const resetButton = -// {/* render all fields, but hide the ones not visible for fast tab switching */} -// {configFields.map(field => { -// return
-// {Object.keys(voidConfigInfo[field]).map((param) => ( -// -// ))} -//
-// })} + const inputElement = enumArr === undefined ? + // string + ( updateState(e.target.value)} + />) + : + // enum + () -// {/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */} -//
-// {`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `} -// {`type 'Open User Settings (JSON)',`} -// {` and look for 'void.'. `} -//
-//
-// ) -// } + return
+ + {description} +
+ {inputElement} + {resetButton} +
+
+} + + +export const SidebarSettings = () => { + // track the config state using React state so visual updates happen + const { configStateService } = useServices() + const [configState, setConfigState] = useState(configStateService.state) + const { voidConfig } = configState + useEffect(() => { configStateService.onDidChangeState(() => setConfigState(configStateService.state)) }, [configStateService]) + + const current_field = voidConfig.default['whichApi'] as VoidConfigField + + return ( +
+ + {/* choose the field */} +
+ + +
+ +
+ + {/* render all fields, but hide the ones not visible for fast tab switching */} + {nonDefaultConfigFields.map(field => { + return
+ {Object.keys(configStateService.voidConfigInfo[field]).map((param) => ( + + ))} +
+ })} + + {/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */} +
+ {`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `} + {`type 'Open User Settings (JSON)',`} + {` and look for 'void.'. `} +
+
+ ) +} diff --git a/src/vs/workbench/contrib/void/browser/react/util/contextForServices.tsx b/src/vs/workbench/contrib/void/browser/react/util/contextForServices.tsx new file mode 100644 index 00000000..32560210 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/util/contextForServices.tsx @@ -0,0 +1,20 @@ +import React, { createContext, useContext } from 'react' +import { ReactServicesType } from '../../registerSidebar.js'; + +const AccessorContext = createContext(undefined) + +export const AccessorProvider = ({ children, services }: { children: React.ReactNode; services: ReactServicesType }) => { + return + {children} + +} + +export const useServices = (): ReactServicesType => { + const context = useContext(AccessorContext) + if (context === undefined) { + throw new Error('useAccessor must be used within an AccessorProvider') + } + return context; +} + + diff --git a/src/vs/workbench/contrib/void/browser/react/util/mountFnGenerator.tsx b/src/vs/workbench/contrib/void/browser/react/util/mountFnGenerator.tsx index 3d12bb88..898f249d 100644 --- a/src/vs/workbench/contrib/void/browser/react/util/mountFnGenerator.tsx +++ b/src/vs/workbench/contrib/void/browser/react/util/mountFnGenerator.tsx @@ -1,43 +1,18 @@ import React from 'react'; import * as ReactDOM from 'react-dom/client' -import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation'; -// import { initPosthog, identifyUser } from './posthog'; - -// const ListenersAndTracking = () => { -// // initialize posthog -// useEffect(() => { -// initPosthog() -// }, []) - -// // // when we get the deviceid, identify the user -// // useEffect(() => { -// // getVSCodeAPI().postMessage({ type: 'getDeviceId' }); -// // awaitVSCodeResponse('deviceId').then((m => { -// // identifyUser(m.deviceId) -// // })) -// // }, []) - -// // // Receive messages from the VSCode extension -// // useEffect(() => { -// // const listener = (event: MessageEvent) => { -// // const m = event.data as MessageToSidebar; -// // onMessageFromVSCode(m) -// // } -// // window.addEventListener('message', listener); -// // return () => window.removeEventListener('message', listener) -// // }, []) - -// return null -// } +import { AccessorProvider } from './contextForServices'; +import { ReactServicesType } from '../../registerSidebar'; - - -export const mountFnGenerator = (Component: React.FC<{ accessor: ServicesAccessor }>) => (rootElement: HTMLElement, accessor: ServicesAccessor) => { +export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLElement, services: ReactServicesType) => { if (typeof document === 'undefined') { console.error('index.tsx error: document was undefined') return } const root = ReactDOM.createRoot(rootElement) - root.render(); + root.render( + + + + ); } diff --git a/src/vs/workbench/contrib/void/browser/registerSettings.ts b/src/vs/workbench/contrib/void/browser/registerConfig.ts similarity index 74% rename from src/vs/workbench/contrib/void/browser/registerSettings.ts rename to src/vs/workbench/contrib/void/browser/registerConfig.ts index 56961406..37fe0ab9 100644 --- a/src/vs/workbench/contrib/void/browser/registerSettings.ts +++ b/src/vs/workbench/contrib/void/browser/registerConfig.ts @@ -38,10 +38,10 @@ export const nonDefaultConfigFields = [ const voidConfigInfo: Record< typeof nonDefaultConfigFields[number] | 'default', { [prop: string]: { - description: string, - enumArr?: readonly string[] | undefined, - defaultVal: string, - }, + description: string; + enumArr?: readonly string[] | undefined; + defaultVal: string; + }; } > = { default: { @@ -186,7 +186,7 @@ const voidConfigInfo: Record< // this is the type that comes with metadata like desc, default val, etc -type VoidConfigInfo = typeof voidConfigInfo +export type VoidConfigInfo = typeof voidConfigInfo export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number] // this is the type that specifies the user's actual config @@ -203,27 +203,48 @@ export type VoidConfig = { } -const VOID_CONFIG_KEY = 'void.partialVoidConfig' - -type setFieldType = (field: K, param: keyof VoidConfigInfo[K], newVal: string) => Promise; - -export interface IVoidSettingsService { - readonly _serviceBrand: undefined; - onDidChange: Event; - getPartialVoidConfig(): Promise; - getVoidConfig(): Promise; - setField: setFieldType; +const getVoidConfig = (partialVoidConfig: PartialVoidConfig): VoidConfig => { + const config = {} as PartialVoidConfig + for (const field of [...nonDefaultConfigFields, 'default'] as const) { + config[field] = {} + for (const prop in voidConfigInfo[field]) { + config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal + } + } + return config as VoidConfig } -export const IVoidSettingsService = createDecorator('voidSettingsService'); -class VoidSettingsService extends Disposable implements IVoidSettingsService { + +const VOID_CONFIG_KEY = 'void.partialVoidConfig' + +export type SetFieldFnType = (field: K, param: keyof VoidConfigInfo[K], newVal: string) => Promise; + +export type ConfigState = { + partialVoidConfig: PartialVoidConfig; + voidConfig: VoidConfig; +} + +export interface IVoidConfigStateService { + readonly _serviceBrand: undefined; + readonly state: ConfigState; + readonly voidConfigInfo: VoidConfigInfo; + onDidChangeState: Event; + setField: SetFieldFnType; +} + +export const IVoidConfigStateService = createDecorator('VoidConfigStateService'); +class VoidConfigStateService extends Disposable implements IVoidConfigStateService { _serviceBrand: undefined; - private readonly _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; + + state: ConfigState; + + voidConfigInfo: VoidConfigInfo = voidConfigInfo; - async getPartialVoidConfig(): Promise { + private async _readPartialVoidConfig(): Promise { const encryptedPartialConfig = this._storageService.get(VOID_CONFIG_KEY, StorageScope.APPLICATION) if (!encryptedPartialConfig) @@ -234,28 +255,15 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } - async getVoidConfig(): Promise { - const partialVoidConfig = await this.getPartialVoidConfig() - const config = {} as PartialVoidConfig - for (let field of [...nonDefaultConfigFields, 'default'] as const) { - config[field] = {} - for (let prop in voidConfigInfo[field]) { - config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal - } - } - return config as VoidConfig - } - - - private async storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) { + private async _storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) { const encryptedPartialConfigStr = await this._encryptionService.encrypt(JSON.stringify(partialVoidConfig)) this._storageService.store(VOID_CONFIG_KEY, encryptedPartialConfigStr, StorageScope.APPLICATION, StorageTarget.USER) } // Set field on PartialVoidConfig - setField: setFieldType = async (field: K, param: keyof VoidConfigInfo[K], newVal: string) => { - const partialVoidConfig = await this.getPartialVoidConfig() + setField: SetFieldFnType = async (field: K, param: keyof VoidConfigInfo[K], newVal: string) => { + const partialVoidConfig = await this._readPartialVoidConfig() const newPartialConfig: PartialVoidConfig = { ...partialVoidConfig, @@ -264,19 +272,41 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { [param]: newVal } } - await this.storePartialVoidConfig(newPartialConfig) - this._onDidChange.fire() + await this._storePartialVoidConfig(newPartialConfig) + this._setState(newPartialConfig) + } + + // internal function to update state, should be called every time state changes + private async _setState(partialVoidConfig: PartialVoidConfig) { + this.state = { + partialVoidConfig: partialVoidConfig, + voidConfig: getVoidConfig(partialVoidConfig), + } + this._onDidChangeState.fire() } constructor( @IStorageService private readonly _storageService: IStorageService, @IEncryptionService private readonly _encryptionService: IEncryptionService, - // @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, // could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER) + // could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER) + // @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, ) { super() + + // at the start, we haven't read the partial config yet, but we need to set state to something, just treat partialVoidConfig like it's empty + this.state = { + partialVoidConfig: {}, + voidConfig: getVoidConfig({}), + } + + // read and update the actual state immediately + this._readPartialVoidConfig().then(partialVoidConfig => { + this._setState(partialVoidConfig) + }) + } } -registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager); +registerSingleton(IVoidConfigStateService, VoidConfigStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/registerSidebar.ts b/src/vs/workbench/contrib/void/browser/registerSidebar.ts index 50e5331f..b5bb44b8 100644 --- a/src/vs/workbench/contrib/void/browser/registerSidebar.ts +++ b/src/vs/workbench/contrib/void/browser/registerSidebar.ts @@ -36,9 +36,10 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -// import { IVoidSettingsService } from './registerSettings.js'; +// import { IVoidConfigService } from './registerSettings.js'; // import { IEditorService } from '../../../services/editor/common/editorService.js'; import mountFn from './react/out/Sidebar.js'; +import { IVoidConfigStateService } from './registerConfig.js'; // import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; // const mountFn = (...params: any) => { } @@ -47,11 +48,16 @@ import mountFn from './react/out/Sidebar.js'; // compare against search.contribution.ts and https://app.greptile.com/chat/w1nsmt3lauwzculipycpn?repo=github%3Amain%3Amicrosoft%2Fvscode // and debug.contribution.ts, scm.contribution.ts (source control) -type VoidSidebarState = { +export type VoidSidebarState = { isHistoryOpen: boolean; currentTab: 'chat' | 'settings'; } +export type ReactServicesType = { + sidebarStateService: IVoidSidebarStateService; + configStateService: IVoidConfigStateService; + threadHistoryService: IThreadHistoryService; +} // ---------- Define viewpane ---------- @@ -89,10 +95,13 @@ class VoidSidebarViewPane extends ViewPane { dom.append(parent, root); // gets set immediately - let accessor_: ServicesAccessor = null as unknown as ServicesAccessor - this.instantiationService.invokeFunction(accessor => { accessor_ = accessor }); - - mountFn(root, accessor_); + this.instantiationService.invokeFunction(accessor => { + mountFn(root, { + configStateService: accessor.get(IVoidConfigStateService), + sidebarStateService: accessor.get(IVoidSidebarStateService), + threadHistoryService: accessor.get(IThreadHistoryService), + }); + }); } @@ -184,37 +193,53 @@ viewsRegistry.registerViews([{ // ---------- Register service that manages sidebar's state ---------- -interface IVoidSidebarStateService { +export interface IVoidSidebarStateService { readonly _serviceBrand: undefined; - setState(newState: Partial): void; - state: VoidSidebarState; - focusChat(): void; - blurChat(): void; - onDidChange: Event; - onFocusChat: Event; - onBlurChat: Event; + state: VoidSidebarState; + setState(newState: Partial): void; + onDidChangeState: Event; + + onDidFocusChat: Event; + onDidBlurChat: Event; + fireFocusChat(): void; + fireBlurChat(): void; } -const IVoidSidebarStateService = createDecorator('voidSidebarStateService'); +export const IVoidSidebarStateService = createDecorator('voidSidebarStateService'); class VoidSidebarStateService extends Disposable implements IVoidSidebarStateService { _serviceBrand: undefined; - private readonly _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; private readonly _onFocusChat = new Emitter(); - readonly onFocusChat: Event = this._onFocusChat.event; + readonly onDidFocusChat: Event = this._onFocusChat.event; private readonly _onBlurChat = new Emitter(); - readonly onBlurChat: Event = this._onBlurChat.event; + readonly onDidBlurChat: Event = this._onBlurChat.event; // state - state: VoidSidebarState = { - isHistoryOpen: false, - currentTab: 'chat', + state: VoidSidebarState + + + setState(newState: Partial) { + // make sure view is open if the tab changes + if ('currentTab' in newState) + this._viewsService.openView(SIDEBAR_VIEW_ID); + + this.state = { ...this.state, ...newState } + this._onDidChangeState.fire() + } + + fireFocusChat() { + this._onFocusChat.fire() + } + + fireBlurChat() { + this._onBlurChat.fire() } constructor( @@ -223,23 +248,13 @@ class VoidSidebarStateService extends Disposable implements IVoidSidebarStateSer super() // auto open the view on mount (if it bothers you this is here, this is technically just initializing the state of the view) this._viewsService.openView(SIDEBAR_VIEW_ID); - } - setState(newState: Partial) { - // make sure view is open if the tab changes - if ('currentTab' in newState) - this._viewsService.openView(SIDEBAR_VIEW_ID); + // initial state + this.state = { + isHistoryOpen: false, + currentTab: 'chat', + } - this.state = { ...this.state, ...newState } - this._onDidChange.fire() - } - - focusChat() { - this._onFocusChat.fire() - } - - blurChat() { - this._onBlurChat.fire() } } @@ -258,7 +273,7 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { const stateService = accessor.get(IVoidSidebarStateService) stateService.setState({ isHistoryOpen: false, currentTab: 'chat' }) - stateService.focusChat() + stateService.fireFocusChat() // const selection = accessor.get(IEditorService).activeTextEditorControl?.getSelection() @@ -292,7 +307,7 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { const stateService = accessor.get(IVoidSidebarStateService) stateService.setState({ isHistoryOpen: false, currentTab: 'chat' }) - stateService.focusChat() + stateService.fireFocusChat() const historyService = accessor.get(IThreadHistoryService) historyService.startNewThread() @@ -312,7 +327,7 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { const stateService = accessor.get(IVoidSidebarStateService) stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen }) - stateService.blurChat() + stateService.fireBlurChat() } }) @@ -329,6 +344,6 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { const stateService = accessor.get(IVoidSidebarStateService) stateService.setState({ isHistoryOpen: false, currentTab: 'settings' }) - stateService.blurChat() + stateService.fireBlurChat() } }) diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 430f269d..68889f00 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -1,5 +1,5 @@ // register Settings -import './registerSettings.js' +import './registerConfig.js' // register Sidebar chat import './registerSidebar.js'