{
const model = modelOfEditorId[id] ?? modelService.createModel(
initValueRef.current, {
- languageId: languageRef.current ? languageRef.current : '',
+ languageId: languageRef.current ? languageRef.current : 'typescript',
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
})
modelRef.current = model
@@ -719,6 +722,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
if (parentNode) {
// const height = Math.min(, MAX_HEIGHT);
parentNode.style.height = `${height}px`;
+ parentNode.style.maxHeight = `${MAX_HEIGHT}px`;
editor.layout();
}
}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
index beec21dd..b15cd2a6 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
@@ -4,12 +4,13 @@
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect } from 'react'
-import { ThreadsState } from '../../../threadHistoryService.js'
+import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
+import { VoidUriState } from '../../../voidUriStateService.js';
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
@@ -28,9 +29,10 @@ import { ILLMMessageService } from '../../../../../../../platform/void/common/ll
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
+import { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
-import { IThreadHistoryService } from '../../../threadHistoryService.js';
+import { IChatThreadService } from '../../../chatThreadService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
@@ -47,18 +49,25 @@ import { IPathService } from '../../../../../../../workbench/services/path/commo
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
+
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
+let uriState: VoidUriState
+const uriStateListeners: Set<(s: VoidUriState) => void> = new Set()
+
let quickEditState: VoidQuickEditState
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
-let threadsState: ThreadsState
-const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
+let chatThreadsState: ThreadsState
+const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
+
+let chatThreadsStreamState: ThreadStreamState
+const chatThreadsStreamStateListeners: Set<(threadId: string) => void> = new Set()
let settingsState: VoidSettingsState
const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
@@ -87,15 +96,25 @@ export const _registerServices = (accessor: ServicesAccessor) => {
_registerAccessor(accessor)
const stateServices = {
+ uriStateService: accessor.get(IVoidUriStateService),
quickEditStateService: accessor.get(IQuickEditStateService),
sidebarStateService: accessor.get(ISidebarStateService),
- threadsStateService: accessor.get(IThreadHistoryService),
+ chatThreadsStateService: accessor.get(IChatThreadService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
themeService: accessor.get(IThemeService),
+ inlineDiffsService: accessor.get(IInlineDiffsService),
}
- const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
+ const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
+
+ uriState = uriStateService.state
+ disposables.push(
+ uriStateService.onDidChangeState(() => {
+ uriState = uriStateService.state
+ uriStateListeners.forEach(l => l(uriState))
+ })
+ )
quickEditState = quickEditStateService.state
disposables.push(
@@ -113,11 +132,20 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
- threadsState = threadsStateService.state
+ chatThreadsState = chatThreadsStateService.state
disposables.push(
- threadsStateService.onDidChangeCurrentThread(() => {
- threadsState = threadsStateService.state
- threadsStateListeners.forEach(l => l(threadsState))
+ chatThreadsStateService.onDidChangeCurrentThread(() => {
+ chatThreadsState = chatThreadsStateService.state
+ chatThreadsStateListeners.forEach(l => l(chatThreadsState))
+ })
+ )
+
+ // same service, different state
+ chatThreadsStreamState = chatThreadsStateService.streamState
+ disposables.push(
+ chatThreadsStateService.onDidChangeStreamState(({ threadId }) => {
+ chatThreadsStreamState = chatThreadsStateService.streamState
+ chatThreadsStreamStateListeners.forEach(l => l(threadId))
})
)
@@ -146,6 +174,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
+
return disposables
}
@@ -164,9 +193,10 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IInlineDiffsService: accessor.get(IInlineDiffsService),
+ IVoidUriStateService: accessor.get(IVoidUriStateService),
IQuickEditStateService: accessor.get(IQuickEditStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
- IThreadHistoryService: accessor.get(IThreadHistoryService),
+ IChatThreadService: accessor.get(IChatThreadService),
IInstantiationService: accessor.get(IInstantiationService),
ICodeEditorService: accessor.get(ICodeEditorService),
@@ -210,6 +240,16 @@ export const useAccessor = () => {
// -- state of services --
+export const useUriState = () => {
+ const [s, ss] = useState(uriState)
+ useEffect(() => {
+ ss(uriState)
+ uriStateListeners.add(ss)
+ return () => { uriStateListeners.delete(ss) }
+ }, [ss])
+ return s
+}
+
export const useQuickEditState = () => {
const [s, ss] = useState(quickEditState)
useEffect(() => {
@@ -240,17 +280,36 @@ export const useSettingsState = () => {
return s
}
-export const useThreadsState = () => {
- const [s, ss] = useState(threadsState)
+export const useChatThreadsState = () => {
+ const [s, ss] = useState(chatThreadsState)
useEffect(() => {
- ss(threadsState)
- threadsStateListeners.add(ss)
- return () => { threadsStateListeners.delete(ss) }
+ ss(chatThreadsState)
+ chatThreadsStateListeners.add(ss)
+ return () => { chatThreadsStateListeners.delete(ss) }
}, [ss])
return s
}
+
+
+export const useChatThreadsStreamState = (threadId: string) => {
+ const [s, ss] = useState
(chatThreadsStreamState[threadId])
+ useEffect(() => {
+ ss(chatThreadsStreamState[threadId])
+ const listener = (threadId_: string) => {
+ if (threadId_ !== threadId) return
+ ss(chatThreadsStreamState[threadId])
+ }
+ chatThreadsStreamStateListeners.add(listener)
+ return () => { chatThreadsStreamStateListeners.delete(listener) }
+ }, [ss, threadId])
+ return s
+}
+
+
+
+
export const useRefreshModelState = () => {
const [s, ss] = useState(refreshModelState)
useEffect(() => {
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx
index 450842fe..67a9a9c8 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx
@@ -37,7 +37,8 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
options={options}
selectedOption={selectedOption}
onChangeOption={onChangeOption}
- getOptionName={(option) => option.name}
+ getOptionDisplayName={(option) => option.selection.modelName}
+ getOptionDropdownName={(option) => option.name}
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
className={`text-xs text-void-fg-3 px-1`}
matchInputWidth={false}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
index 7636145b..5dd24c92 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
@@ -5,7 +5,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
-import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
+import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
@@ -38,32 +38,39 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
const refreshModelService = accessor.get('IRefreshModelService')
const metricsService = accessor.get('IMetricsService')
- const [justFinished, setJustFinished] = useState(false)
+ const [justFinished, setJustFinished] = useState(null)
useRefreshModelListener(
useCallback((providerName2, refreshModelState) => {
if (providerName2 !== providerName) return
const { state } = refreshModelState[providerName]
- if (state !== 'finished') return
+ if (!(state === 'finished' || state === 'error')) return
// now we know we just entered 'finished' state for this providerName
- setJustFinished(true)
- const tid = setTimeout(() => { setJustFinished(false) }, 2000)
+ setJustFinished(state)
+ const tid = setTimeout(() => { setJustFinished(null) }, 2000)
return () => clearTimeout(tid)
}, [providerName])
)
const { state } = refreshModelState[providerName]
- const isRefreshing = state === 'refreshing'
const { title: providerTitle } = displayInfoOfProviderName(providerName)
return {
- refreshModelService.refreshModels(providerName)
+ refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false })
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
}}
- text={justFinished ? `${providerTitle} Models are up-to-date!` : `Manually refresh models list for ${providerTitle}.`}
- icon={isRefreshing ? : (justFinished ? : )}
- disabled={isRefreshing || justFinished}
+ text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!`
+ : justFinished === 'error' ? `${providerTitle} not found!`
+ : `Manually refresh ${providerTitle} models.`
+ }
+ icon={justFinished === 'finished' ?
+ : justFinished === 'error' ?
+ : state === 'refreshing' ?
+ :
+ }
+
+ disabled={state === 'refreshing' || justFinished !== null}
/>
}
@@ -109,7 +116,8 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setProviderName(pn)}
- getOptionName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
+ getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
+ getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
getOptionsEqual={(a, b) => a === b}
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
py-[4px] px-[6px]
@@ -126,6 +134,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
@@ -353,31 +362,10 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide
>
}
-// export const VoidFeatureFlagSettings = () => {
-// const accessor = useAccessor()
-// const voidSettingsService = accessor.get('IVoidSettingsService')
-// const voidSettingsState = useSettingsState()
-
-// return <>
-// {featureFlagNames.map((flagName) => {
-// const value = voidSettingsState.featureFlagSettings[flagName]
-// const { description } = displayInfoOfFeatureFlag(flagName)
-// return
-//
-// { voidSettingsService.setFeatureFlag(flagName, !value) }}
-// />
-// {description}
-//
-//
-// })}
-// >
-// }
type TabName = 'models' | 'general'
-export const VoidFeatureFlagSettings = () => {
+export const AutoRefreshToggle = () => {
+ const settingName: GlobalSettingName = 'autoRefreshModels'
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
@@ -385,22 +373,33 @@ export const VoidFeatureFlagSettings = () => {
const voidSettingsState = useSettingsState()
- return featureFlagNames.map((flagName) => {
+ // right now this is just `enabled_autoRefreshModels`
+ const enabled = voidSettingsState.globalSettings[settingName]
- // right now this is just `enabled_autoRefreshModels`
- const enabled = voidSettingsState.featureFlagSettings[flagName]
- const { description } = displayInfoOfFeatureFlag(flagName)
+ return
{
+ voidSettingsService.setGlobalSetting(settingName, !enabled)
+ metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
+ }}
+ text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
+ icon={enabled ? : }
+ disabled={false}
+ />
+}
- return {
- voidSettingsService.setFeatureFlag(flagName, !enabled)
- metricsService.capture('Click', { action: 'Autorefresh Toggle', flagName, enabled: !enabled })
- }}
- text={description}
- icon={enabled ? : }
- disabled={false}
- />
- })
+export const AIInstructionsBox = () => {
+ const accessor = useAccessor()
+ const voidSettingsService = accessor.get('IVoidSettingsService')
+ const voidSettingsState = useSettingsState()
+ return {
+ voidSettingsService.setGlobalSetting('aiInstructions', newText)
+ }}
+ />
}
export const FeaturesTab = () => {
@@ -423,16 +422,16 @@ export const FeaturesTab = () => {
- Providers
+ Providers
{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}
{/* {`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}
*/}
- Models
+ Models
-
+
@@ -569,15 +568,8 @@ const GeneralTab = () => {
- {/*
-
Rules for AI
- {`placeholder: "Do not add ;'s. Do not change or delete spacing, formatting, or comments. Respond to queries in French when applicable. "`}
- */}
-
-
-
-
+
Built-in Settings
{`IDE settings, keyboard settings, and theme customization.`}
@@ -598,7 +590,13 @@ const GeneralTab = () => {
- {/* */}
+
+
+
AI Instructions
+
{`Instructions to include on all AI requests.`}
+
+
+
>
}
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
index 843f512d..5787fc4d 100644
--- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts
+++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
@@ -11,7 +11,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
-import { CodeStagingSelection, IThreadHistoryService } from './threadHistoryService.js';
+import { CodeStagingSelection, IChatThreadService } from './chatThreadService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
@@ -26,10 +26,9 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
-import { URI } from '../../../../base/common/uri.js';
import { localize2 } from '../../../../nls.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
-
+import { IVoidUriStateService } from './voidUriStateService.js';
// ---------- Register commands and keybindings ----------
@@ -126,8 +125,8 @@ registerAction2(class extends Action2 {
}
// add selection to staging
- const threadHistoryService = accessor.get(IThreadHistoryService)
- const currentStaging = threadHistoryService.state._currentStagingSelections
+ const chatThreadService = accessor.get(IChatThreadService)
+ const currentStaging = chatThreadService.state.currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
@@ -136,7 +135,7 @@ registerAction2(class extends Action2 {
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
- threadHistoryService.setStaging([
+ chatThreadService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
@@ -144,7 +143,7 @@ registerAction2(class extends Action2 {
}
// if no match, add
else {
- threadHistoryService.setStaging([...(currentStaging ?? []), selection])
+ chatThreadService.setStaging([...(currentStaging ?? []), selection])
}
}
@@ -153,7 +152,15 @@ registerAction2(class extends Action2 {
registerAction2(class extends Action2 {
constructor() {
- super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Press Ctrl+L', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
+ super({
+ id: VOID_CTRL_L_ACTION_ID,
+ f1: true,
+ title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
+ keybinding: {
+ primary: KeyMod.CtrlCmd | KeyCode.KeyL,
+ weight: KeybindingWeight.VoidExtension
+ }
+ });
}
async run(accessor: ServicesAccessor): Promise {
const commandService = accessor.get(ICommandService)
@@ -184,8 +191,8 @@ registerAction2(class extends Action2 {
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
- const historyService = accessor.get(IThreadHistoryService)
- historyService.startNewThread()
+ const chatThreadService = accessor.get(IChatThreadService)
+ chatThreadService.openNewThread()
}
})
@@ -233,7 +240,7 @@ registerAction2(class extends Action2 {
export class TabSwitchListener extends Disposable {
constructor(
- onSwitchTab: (uri: URI) => void,
+ onSwitchTab: () => void,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
@@ -241,9 +248,8 @@ export class TabSwitchListener extends Disposable {
// when editor switches tabs (models)
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(editor.onDidChangeModel(e => {
- if (e.newModelUrl && e.newModelUrl.scheme === 'file') {
- onSwitchTab(e.newModelUrl)
- }
+ if (e.newModelUrl?.scheme !== 'file') return
+ onSwitchTab()
}))
}
@@ -263,8 +269,10 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
- @ICommandService private readonly commandService: ICommandService,
@IViewsService private readonly viewsService: IViewsService,
+ @IVoidUriStateService private readonly uriStateService: IVoidUriStateService,
+ @ICodeEditorService private readonly codeEditorService: ICodeEditorService,
+ // @ICommandService private readonly commandService: ICommandService,
) {
super()
@@ -274,18 +282,22 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution
sidebarIsVisible = e.visible
}))
- const addCurrentFileIfVisible = () => {
- if (sidebarIsVisible)
- this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
+ const onSwitchTab = () => { // update state
+ if (sidebarIsVisible) {
+ const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri
+ if (!currentUri) return;
+ this.uriStateService.setState({ currentUri })
+ // this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
+ }
}
// when sidebar becomes visible, add current file
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
// run on current tab if it exists, and listen for tab switches and visibility changes
- addCurrentFileIfVisible()
- this._register(this.viewsService.onDidChangeViewVisibility(() => { addCurrentFileIfVisible() }))
- this._register(this.instantiationService.createInstance(TabSwitchListener, () => { addCurrentFileIfVisible() }))
+ onSwitchTab()
+ this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() }))
+ this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() }))
}
}
diff --git a/src/vs/workbench/contrib/void/browser/threadHistoryService.ts b/src/vs/workbench/contrib/void/browser/threadHistoryService.ts
deleted file mode 100644
index 58e471a6..00000000
--- a/src/vs/workbench/contrib/void/browser/threadHistoryService.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-/*--------------------------------------------------------------------------------------
- * Copyright 2025 Glass Devtools, Inc. All rights reserved.
- * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
- *--------------------------------------------------------------------------------------*/
-
-import { Disposable } from '../../../../base/common/lifecycle.js';
-import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
-import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
-import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
-
-import { URI } from '../../../../base/common/uri.js';
-import { Emitter, Event } from '../../../../base/common/event.js';
-import { IAutocompleteService } from './autocompleteService.js';
-import { IRange } from '../../../../editor/common/core/range.js';
-
-export type CodeSelection = {
- fileURI: URI;
- selectionStr: string | null;
- content: string; // TODO remove this (replace `selectionStr` with `content`)
- range: IRange;
-}
-
-// if selectionStr is null, it means to use the entire file at send time
-export type CodeStagingSelection = {
- type: 'Selection',
- fileURI: URI,
- selectionStr: string,
- range: IRange
-} | {
- type: 'File',
- fileURI: URI,
- selectionStr: null,
- range: null
-}
-
-
-// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
-export type ChatMessage =
- | {
- role: 'user';
- content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
- displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
- selections: CodeSelection[] | null; // the user's selection
- }
- | {
- role: 'assistant';
- content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
- displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
- }
- | {
- role: 'system';
- content: string;
- displayContent?: undefined;
- }
-
-// a 'thread' means a chat message history
-export type ChatThreads = {
- [id: string]: {
- id: string; // store the id here too
- createdAt: string; // ISO string
- lastModified: string; // ISO string
- messages: ChatMessage[];
-
- // editing state
- isBeingEdited: boolean;
- _currentStagingSelections: CodeStagingSelection[] | null;
- };
-}
-
-export type ThreadsState = {
- allThreads: ChatThreads;
- _currentThreadId: string | null; // intended for internal use only
- _currentStagingSelections: CodeStagingSelection[] | null;
-}
-
-
-const newThreadObject = () => {
- const now = new Date().toISOString()
- return {
- id: new Date().getTime().toString(),
- createdAt: now,
- lastModified: now,
- messages: [],
- isBeingEdited: false,
- _currentStagingSelections: null,
- }
-}
-
-const THREAD_STORAGE_KEY = 'void.threadHistory'
-
-export interface IThreadHistoryService {
- readonly _serviceBrand: undefined;
-
- readonly state: ThreadsState;
- onDidChangeCurrentThread: Event;
-
- getCurrentThread(state: ThreadsState): ChatThreads[string] | null;
- startNewThread(): void;
- switchToThread(threadId: string): void;
- addMessageToCurrentThread(message: ChatMessage): void;
-
- setStaging(stagingSelection: CodeStagingSelection[] | null): void;
-
-}
-
-export const IThreadHistoryService = createDecorator('voidThreadHistoryService');
-class ThreadHistoryService extends Disposable implements IThreadHistoryService {
- _serviceBrand: undefined;
-
- // this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
- private readonly _onDidChangeCurrentThread = new Emitter();
- readonly onDidChangeCurrentThread: Event = this._onDidChangeCurrentThread.event;
-
- state: ThreadsState // allThreads is persisted, currentThread is not
-
- constructor(
- @IStorageService private readonly _storageService: IStorageService,
- @IAutocompleteService private readonly _autocomplete: IAutocompleteService,
- ) {
- super()
- this._autocomplete
-
- this.state = {
- allThreads: this._readAllThreads(),
- _currentThreadId: null,
- _currentStagingSelections: null,
- }
- }
-
-
- private _readAllThreads(): ChatThreads {
- const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
- return threads ? JSON.parse(threads) : {}
- }
-
- private _storeAllThreads(threads: ChatThreads) {
- this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
- }
-
- // this should be the only place this.state = ... appears besides constructor
- private _setState(state: Partial, affectsCurrent: boolean) {
- this.state = {
- ...this.state,
- ...state
- }
- if (affectsCurrent)
- this._onDidChangeCurrentThread.fire()
- }
-
- // must "prove" that you have access to the current state by providing it
- getCurrentThread(state: ThreadsState): ChatThreads[string] | null {
- return state._currentThreadId ? state.allThreads[state._currentThreadId] ?? null : null;
- }
-
- switchToThread(threadId: string) {
- console.log('threadId', threadId)
- console.log('messages', this.state.allThreads[threadId].messages)
- this._setState({ _currentThreadId: threadId }, true)
- }
-
-
- startNewThread() {
- // if a thread with 0 messages already exists, switch to it
- const { allThreads: currentThreads } = this.state
- for (const threadId in currentThreads) {
- if (currentThreads[threadId].messages.length === 0) {
- this.switchToThread(threadId)
- return
- }
- }
- // otherwise, start a new thread
- const newThread = newThreadObject()
-
- // update state
- const newThreads = {
- ...currentThreads,
- [newThread.id]: newThread
- }
- this._storeAllThreads(newThreads)
- this._setState({ allThreads: newThreads, _currentThreadId: newThread.id }, true)
- }
-
-
- addMessageToCurrentThread(message: ChatMessage) {
- console.log('adding ', message.role, 'to chat')
- const { allThreads, _currentThreadId } = this.state
-
- // get the current thread, or create one
- let currentThread: ChatThreads[string]
- if (_currentThreadId && (_currentThreadId in allThreads)) {
- currentThread = allThreads[_currentThreadId]
- }
- else {
- currentThread = newThreadObject()
- this.state._currentThreadId = currentThread.id
- }
-
- // update state and store it
- const newThreads = {
- ...allThreads,
- [currentThread.id]: {
- ...currentThread,
- lastModified: new Date().toISOString(),
- messages: [...currentThread.messages, message],
- }
- }
- this._storeAllThreads(newThreads)
- this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
- }
-
-
- setStaging(stagingSelection: CodeStagingSelection[] | null): void {
- this._setState({ _currentStagingSelections: stagingSelection }, true) // this is a hack for now
- }
-
-}
-
-registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);
-
diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts
index 536b0ca1..ebb17358 100644
--- a/src/vs/workbench/contrib/void/browser/void.contribution.ts
+++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts
@@ -16,7 +16,7 @@ import './sidebarStateService.js'
import './quickEditActions.js'
// register Thread History
-import './threadHistoryService.js'
+import './chatThreadService.js'
// register Autocomplete
import './autocompleteService.js'
@@ -26,3 +26,6 @@ import './voidSettingsPane.js'
// register css
import './media/void.css'
+
+// update (frontend part, also see platform/)
+import './voidUpdateActions.js'
diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts
new file mode 100644
index 00000000..e58d26ad
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts
@@ -0,0 +1,87 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { Disposable } from '../../../../base/common/lifecycle.js';
+import Severity from '../../../../base/common/severity.js';
+import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
+import { localize2 } from '../../../../nls.js';
+import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
+import { INotificationService } from '../../../../platform/notification/common/notification.js';
+import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
+import { IVoidUpdateService } from '../../../../platform/void/common/voidUpdateService.js';
+import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
+
+
+
+
+const notifyYesUpdate = (notifService: INotificationService, msg?: string) => {
+ const message = msg || 'This is a very old version of void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
+ notifService.notify({
+ severity: Severity.Info,
+ message: message,
+ })
+}
+const notifyNoUpdate = (notifService: INotificationService) => {
+ notifService.notify({
+ severity: Severity.Info,
+ message: 'Void is up-to-date!',
+ })
+}
+const notifyErrChecking = (notifService: INotificationService) => {
+ const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
+ notifService.notify({
+ severity: Severity.Info,
+ message: message,
+ })
+}
+
+
+
+// Action
+registerAction2(class extends Action2 {
+ constructor() {
+ super({
+ f1: true,
+ id: 'void.voidCheckUpdate',
+ title: localize2('voidCheckUpdate', 'Void: Check for Updates'),
+ });
+ }
+ async run(accessor: ServicesAccessor): Promise {
+ const voidUpdateService = accessor.get(IVoidUpdateService)
+ const notifService = accessor.get(INotificationService)
+ const metricsService = accessor.get(IMetricsService)
+
+ const res = await voidUpdateService.check()
+ if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update: Error', {}) }
+ else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update: Yes', {}) }
+ else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update: No', {}) }
+ }
+})
+
+// on mount
+class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchContribution {
+ static readonly ID = 'workbench.contrib.void.voidUpdate'
+ constructor(
+ @IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService,
+ @INotificationService private readonly notifService: INotificationService,
+ @IMetricsService private readonly metricsService: IMetricsService,
+ ) {
+ super()
+
+ // on mount
+ setTimeout(async () => {
+ const res = await this.voidUpdateService.check()
+
+ const notifService = this.notifService
+ const metricsService = this.metricsService
+
+ if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Startup: Error', {}) }
+ else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); metricsService.capture('Void Update Startup: Yes', {}) }
+ else if (!res.hasUpdate) { metricsService.capture('Void Update Startup: No', {}) } // display nothing if up to date
+
+ }, 5 * 1000)
+ }
+}
+registerWorkbenchContribution2(VoidUpdateWorkbenchContribution.ID, VoidUpdateWorkbenchContribution, WorkbenchPhase.BlockRestore);
diff --git a/src/vs/workbench/contrib/void/browser/voidUriStateService.ts b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts
new file mode 100644
index 00000000..1a89c29b
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts
@@ -0,0 +1,57 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { Emitter, Event } from '../../../../base/common/event.js';
+import { Disposable } from '../../../../base/common/lifecycle.js';
+import { URI } from '../../../../base/common/uri.js';
+import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
+import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
+
+
+
+// service that manages state
+export type VoidUriState = {
+ currentUri?: URI
+}
+
+export interface IVoidUriStateService {
+ readonly _serviceBrand: undefined;
+
+ readonly state: VoidUriState; // readonly to the user
+ setState(newState: Partial): void;
+ onDidChangeState: Event;
+}
+
+export const IVoidUriStateService = createDecorator('voidUriStateService');
+class VoidUriStateService extends Disposable implements IVoidUriStateService {
+ _serviceBrand: undefined;
+
+ static readonly ID = 'voidUriStateService';
+
+ private readonly _onDidChangeState = new Emitter();
+ readonly onDidChangeState: Event = this._onDidChangeState.event;
+
+
+ // state
+ state: VoidUriState
+
+ constructor(
+ ) {
+ super()
+
+ // initial state
+ this.state = { currentUri: undefined }
+ }
+
+ setState(newState: Partial) {
+
+ this.state = { ...this.state, ...newState }
+ this._onDidChangeState.fire()
+ }
+
+
+}
+
+registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager);
diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css
index 485075f3..454875dd 100644
--- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css
+++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css
@@ -5,7 +5,7 @@
.file-icons-enabled .show-file-icons .vscode_getting_started_page-name-file-icon.file-icon::before {
content: ' ';
- background-image: url('../../../../browser/media/code-icon.svg');
+ background-image: url('../../../../browser/media/void-icon-sm.png');
}
.monaco-workbench .part.editor > .content .gettingStartedContainer {
diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css
index 7ab127ea..8084ecec 100644
--- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css
+++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css
@@ -113,7 +113,7 @@
.file-icons-enabled .show-file-icons .vs_code_editor_walkthrough\.md-name-file-icon.md-ext-file-icon.ext-file-icon.markdown-lang-file-icon.file-icon::before {
content: ' ';
- background-image: url('../../../../browser/media/code-icon.svg');
+ background-image: url('../../../../browser/media/void-icon-sm.png');
}
.monaco-workbench .part.editor > .content .walkThroughContent .mac-only,