add download/upload/reset

This commit is contained in:
Andrew Pareles 2025-04-20 06:43:49 -07:00
parent b02b2f0c89
commit 39ea63b207
10 changed files with 387 additions and 223 deletions

View file

@ -202,6 +202,10 @@ export interface IChatThreadService {
getCurrentFocusedMessageIdx(): number | undefined;
isCurrentlyFocusingMessage(): boolean;
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
dangerousSetState: (newState: ThreadsState) => void;
resetState: () => void;
// // current thread's staging selections
// closeCurrentStagingSelectionsInMessage(opts: { messageIdx: number }): void;
// closeCurrentStagingSelectionsInThread(): void;
@ -293,6 +297,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
dangerousSetState = (newState: ThreadsState) => {
this.state = newState
this._onDidChangeCurrentThread.fire()
}
resetState = () => {
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // see constructor
this.openNewThread()
this._onDidChangeCurrentThread.fire()
}
// !!! this is important for properly restoring URIs from storage
// should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
@ -499,7 +513,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (approvalType) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
@ -679,8 +693,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
if (interrupted) { return }
if (aborted) { return }
if (awaitingUserApproval) {
console.log('awaiting...')
isRunningWhenEnd = 'awaiting_user'
}
else {
@ -691,7 +707,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
} // end while (attempts)
} // end while (send message)
// if awaiting user approval, keep isRunning true, else end isRunning
this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge')

View file

@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------*/
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
interface Props {

View file

@ -1228,9 +1228,9 @@ const titleOfToolName = {
'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) },
'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) },
'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') },
'run_terminal': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') },
'open_bg_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') },
'kill_bg_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') },
'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') },
'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') },
'kill_persistent_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') },
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') },
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
@ -1310,19 +1310,19 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'run_terminal': () => {
const toolParams = _toolParams as ToolCallParams['run_terminal']
'run_command': () => {
const toolParams = _toolParams as ToolCallParams['run_command']
return {
desc1: `"${toolParams.command}"`,
desc1Info: toolParams.bgTerminalId
}
},
'open_bg_terminal': () => {
const toolParams = _toolParams as ToolCallParams['open_bg_terminal']
'open_persistent_terminal': () => {
const toolParams = _toolParams as ToolCallParams['open_persistent_terminal']
return { desc1: '' }
},
'kill_bg_terminal': () => {
const toolParams = _toolParams as ToolCallParams['kill_bg_terminal']
'kill_persistent_terminal': () => {
const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal']
return { desc1: toolParams.terminalId }
},
'get_dir_tree': () => {
@ -2045,7 +2045,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
// ---
'run_terminal': {
'run_command': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
@ -2091,36 +2091,27 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
componentParams.desc2 = `(terminal ${params.bgTerminalId})`
}
else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
if (params) {
const { bgTerminalId, command } = params
if (bgTerminalId) {
componentParams.desc2 = '(persistent terminal)'
if (terminalToolsService.terminalExists(bgTerminalId))
componentParams.onClick = () => terminalToolsService.focusTerminal(bgTerminalId)
}
}
if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>{result}</ToolChildrenWrapper>
}
}
else if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
const { bgTerminalId } = toolMessage.params
else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error' || toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
const { bgTerminalId, command } = params
if (bgTerminalId) {
componentParams.desc2 = '(persistent terminal)'
if (terminalToolsService.terminalExists(bgTerminalId))
componentParams.onClick = () => terminalToolsService.focusTerminal(bgTerminalId)
}
if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>{result}</ToolChildrenWrapper>
}
}
return <ToolHeaderWrapper {...componentParams} />
}
},
'open_bg_terminal': {
'open_persistent_terminal': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const terminalToolsService = accessor.get('ITerminalToolService')
const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor)
const title = getTitle(toolMessage)
@ -2136,6 +2127,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
if (toolMessage.type === 'success') {
const { result } = toolMessage
const { terminalId } = result
if (terminalId) {
componentParams.desc2 = `(terminal ${terminalId})`
if (terminalToolsService.terminalExists(terminalId))
componentParams.onClick = () => terminalToolsService.focusTerminal(terminalId)
}
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
@ -2149,7 +2146,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <ToolHeaderWrapper {...componentParams} />
},
},
'kill_bg_terminal': {
'kill_persistent_terminal': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')

View file

@ -9,8 +9,9 @@ import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'l
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
const OVERRIDE_VALUE = false
@ -29,7 +30,9 @@ export const VoidOnboarding = () => {
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
>
<VoidOnboardingContent />
<ErrorBoundary>
<VoidOnboardingContent />
</ErrorBoundary>
</div>
</div>
)
@ -303,11 +306,11 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
// info used to show the table
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.type === 'default',
showAsDefault: m.type !== 'custom',
isDownloaded: true
}
})
@ -317,7 +320,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
for (const modelName of ollamaRecommendedModels) {
if (modelName in infoOfModelName) continue
infoOfModelName[modelName] = {
...infoOfModelName[modelName],
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
showAsDefault: true,
}
}
@ -339,7 +342,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
const capabilities = getModelCapabilities(providerName, modelName)
@ -380,7 +383,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
@ -388,10 +391,13 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true} />
<ErrorBoundary>
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true}
/>
</ErrorBoundary>
</td>
<td colSpan={4}></td>
</tr>
@ -672,19 +678,22 @@ const VoidOnboardingContent = () => {
{ id: 'cheap', label: 'Affordable' },
{ id: 'all', label: 'All' }
].map(option => (
<button
<ErrorBoundary
key={option.id}
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
<button
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
</ErrorBoundary>
))}
</div>
@ -693,108 +702,129 @@ const VoidOnboardingContent = () => {
{/* Provider Buttons - Modified to use separate components for each tab */}
<div className="mb-2 w-full">
{/* Intelligent tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Private tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Affordable tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* All tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
</div>
{/* Description */}
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
<ErrorBoundary>
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</ErrorBoundary>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
{/* Models Table */}
<TableOfModelsForProvider providerName={selectedProviderName} />
<ErrorBoundary>
<TableOfModelsForProvider providerName={selectedProviderName} />
</ErrorBoundary>
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8 mx-auto'>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<ErrorBoundary>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<div className='my-4'>
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
</div>
<div className='my-4'>
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
</div>
</ErrorBoundary>
</div>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
<ErrorBoundary>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
</ErrorBoundary>
{/* Button and status indicators */}
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
<ErrorBoundary>
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
</ErrorBoundary>
</div>
</div>}
@ -802,10 +832,11 @@ const VoidOnboardingContent = () => {
}
bottom={
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
<ErrorBoundary>
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
</ErrorBoundary>
}
/>,
@ -864,7 +895,9 @@ const VoidOnboardingContent = () => {
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
{contentOfIdx[pageIndex]}
<ErrorBoundary>
{contentOfIdx[pageIndex]}
</ErrorBoundary>
</div>
}

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
@ -17,7 +17,7 @@ import { WarningBox } from './WarningBox.js'
import { os } from '../../../../common/helpers/systemInfo.js'
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'
import Severity from '../../../../../../../base/common/severity.js'
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
@ -166,14 +166,14 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
const [showCheckmark, setShowCheckmark] = useState(false)
// const providerNameRef = useRef<ProviderName | null>(null)
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
const providerName = permanentProviderName ?? userChosenProviderName;
const [modelName, setModelName] = useState<string>('')
const [errorString, setErrorString] = useState('')
const numModels = settingsState.settingsOfProvider[providerName].models.length
const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length
if (showCheckmark) {
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white dark:text-black px-3 py-1 rounded-sm ${className}`} />
@ -199,59 +199,66 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
{/* provider input */}
{!permanentProviderName &&
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setUserChosenProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
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]`}
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
arrowTouchesText={false}
/>
}
<ErrorBoundary>
{!permanentProviderName &&
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setUserChosenProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
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]`}
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
arrowTouchesText={false}
/>
}
</ErrorBoundary>
{/* model input */}
<VoidSimpleInputBox
value={modelName}
onChangeValue={setModelName}
placeholder='Model Name'
compact={compact}
className={'max-w-32'}
/>
<ErrorBoundary>
<VoidSimpleInputBox
value={modelName}
onChangeValue={setModelName}
placeholder='Model Name'
compact={compact}
className={'max-w-32'}
/>
</ErrorBoundary>
{/* add button */}
<AddButton
type='submit'
disabled={!modelName}
onClick={(e) => {
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
// setErrorString(`This model already exists under ${providerName}.`)
setErrorString(`This model already exists.`)
return
}
<ErrorBoundary>
<AddButton
type='submit'
disabled={!modelName}
onClick={(e) => {
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
// setErrorString(`This model already exists under ${providerName}.`)
setErrorString(`This model already exists.`)
return
}
settingsStateService.addModel(providerName, modelName)
setShowCheckmark(true)
setTimeout(() => {
setShowCheckmark(false)
setIsOpen(false)
}, 1500)
setErrorString('')
setModelName('')
}}
/>
settingsStateService.addModel(providerName, modelName)
setShowCheckmark(true)
setTimeout(() => {
setShowCheckmark(false)
setIsOpen(false)
}, 1500)
setErrorString('')
setModelName('')
}}
/>
</ErrorBoundary>
</form>
@ -552,18 +559,20 @@ const FastApplyMethodDropdown = () => {
}
export const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div
className='pl-6 flex items-center w-fit'
data-tooltip-id='void-tooltip-ollama-settings'
>
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
export const OllamaSetupInstructions = () => {
return <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div
className='pl-6 flex items-center w-fit'
data-tooltip-id='void-tooltip-ollama-settings'
>
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
</div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
}
const RedoOnboardingButton = ({ className }: { className?: string }) => {
@ -823,6 +832,70 @@ export const Settings = () => {
const nativeHostService = accessor.get('INativeHostService')
const settingsState = useSettingsState()
const voidSettingsService = accessor.get('IVoidSettingsService')
const chatThreadsService = accessor.get('IChatThreadService')
const notificationService = accessor.get('INotificationService')
const onDownload = (t: 'Chats' | 'Settings') => {
let dataStr: string
let downloadName: string
if (t === 'Chats') {
dataStr = JSON.stringify(voidSettingsService.state, null, 2)
downloadName = 'void-chats.json'
}
else if (t === 'Settings') {
dataStr = JSON.stringify(chatThreadsService.state, null, 2)
downloadName = 'void-settings.json'
}
else {
dataStr = ''
downloadName = ''
}
const blob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = downloadName
a.click()
URL.revokeObjectURL(url)
}
// Add file input refs
const fileInputSettingsRef = useRef<HTMLInputElement>(null)
const fileInputChatsRef = useRef<HTMLInputElement>(null)
const [s, ss] = useState(0)
const handleUpload = (t: 'Chats' | 'Settings') => (e: React.ChangeEvent<HTMLInputElement>,) => {
const files = e.target.files
if (!files) return;
const file = files[0]
if (!file) return
const reader = new FileReader();
reader.onload = () => {
try {
const json = JSON.parse(reader.result as string);
if (t === 'Chats') {
chatThreadsService.dangerousSetState(json as any)
}
else if (t === 'Settings') {
voidSettingsService.dangerousSetState(json as any)
}
notificationService.info(`${t} imported successfully!`)
} catch (err) {
notificationService.notify({ message: `Failed to import ${t}`, source: err + '', severity: Severity.Error, })
}
};
reader.readAsText(file);
e.target.value = '';
ss(s => s + 1)
}
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
@ -834,6 +907,34 @@ export const Settings = () => {
{/* separator */}
<div className='w-full h-[1px] my-4' />
{/* Download & Upload Settings and Chats */}
<div className='flex gap-2 mb-6'>
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { fileInputSettingsRef.current?.click() }}>
Upload Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => onDownload('Settings')}>
Download Settings
</VoidButtonBgDarken>
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { fileInputChatsRef.current?.click() }}>
Upload Chats
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => onDownload('Chats')}>
Download Chats
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { voidSettingsService.resetState() }}>
Reset Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { chatThreadsService.resetState() }}>
Reset Chats
</VoidButtonBgDarken>
</div>
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Models</h2>
@ -849,7 +950,7 @@ export const Settings = () => {
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='opacity-80 mb-4'>
{ollamaSetupInstructions}
<OllamaSetupInstructions />
</div>
<ErrorBoundary>

View file

@ -15,7 +15,7 @@ export const WarningBox = ({ text, onClick, className }: { text: string; onClick
>
<IconWarning
size={14}
className='mr-1'
className='mr-1 flex-shrink-0'
/>
<span>{text}</span>
</div>

View file

@ -250,17 +250,17 @@ export class ToolsService implements IToolsService {
// ---
run_terminal: (params: RawToolParamsObj) => {
run_command: (params: RawToolParamsObj) => {
const { command: commandUnknown, terminal_id: terminalIdUnknown } = params;
const command = validateStr('command', commandUnknown);
const proposedTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null;
return { command, bgTerminalId: proposedTerminalId };
},
open_bg_terminal: (_params: RawToolParamsObj) => {
open_persistent_terminal: (_params: RawToolParamsObj) => {
// No parameters needed; will open a new background terminal
return {};
},
kill_bg_terminal: (params: RawToolParamsObj) => {
kill_persistent_terminal: (params: RawToolParamsObj) => {
const { terminal_id: terminalIdUnknown } = params;
const terminalId = validateProposedTerminalId(terminalIdUnknown);
return { terminalId };
@ -414,19 +414,19 @@ export class ToolsService implements IToolsService {
return { result: lintErrorsPromise, interruptTool }
},
// ---
run_terminal: async ({ command, bgTerminalId }) => {
run_command: async ({ command, bgTerminalId }) => {
const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, bgTerminalId)
const interruptTool = () => {
this.terminalToolService.killTerminal(terminalId)
}
return { result: resPromise, interruptTool }
},
open_bg_terminal: async () => {
open_persistent_terminal: async () => {
// Open a new background terminal without waiting for completion
const terminalId = await this.terminalToolService.createTerminal()
return { result: { terminalId } }
},
kill_bg_terminal: async ({ terminalId }) => {
kill_persistent_terminal: async ({ terminalId }) => {
// Close the background terminal by sending exit
await this.terminalToolService.killTerminal(terminalId)
return { result: {} }
@ -492,7 +492,7 @@ export class ToolsService implements IToolsService {
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
},
run_terminal: (params, result) => {
run_command: (params, result) => {
const {
resolveReason,
result: result_,
@ -520,11 +520,11 @@ export class ToolsService implements IToolsService {
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
open_bg_terminal: (_params, result) => {
open_persistent_terminal: (_params, result) => {
const { terminalId } = result;
return `Successfully created background terminal with ID ${terminalId}`;
},
kill_bg_terminal: (params, _result) => {
kill_persistent_terminal: (params, _result) => {
return `Successfully closed terminal ${params.terminalId}.`;
},

View file

@ -214,22 +214,22 @@ Here's an example of a good description:\n${editToolDescriptionExample}`
},
},
run_terminal: {
name: 'run_terminal',
run_command: {
name: 'run_command',
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor like vim, you might need to pipe to cat to get all results.`,
params: {
command: { description: 'The terminal command to run.' },
bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_bg_terminal. Runs the command in the terminal with the specified ID.' },
bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' },
},
},
open_bg_terminal: {
name: 'open_bg_terminal',
open_persistent_terminal: {
name: 'open_persistent_terminal',
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
params: {}
},
kill_bg_terminal: {
name: 'kill_bg_terminal',
kill_persistent_terminal: {
name: 'kill_persistent_terminal',
description: `Closes a BG terminal with the given ID.`,
params: { terminal_id: { description: `The terminal ID to interrupt and close.` } }
}
@ -518,9 +518,9 @@ ${DIVIDER}
${FINAL}
${tripleTick[1]}
1. Every single item written in \`CHANGE\` should show up in the final result, except for comments explicitly saying things like "// ... existing code". Make sure to include ALL other comments (even descriptive ones), code, whitespace, etc. in the final result.
1. Your SEARCH/REPLACE block(s) must implement the change EXACTLY.
2. Your SEARCH/REPLACE block(s) must implement the change EXACTLY. You should use comments like "// ... existing code" as reference points, and everything else in the change should be written verbatim.
2. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
3. You are allowed to output multiple SEARCH/REPLACE blocks.

View file

@ -20,7 +20,7 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
'create_file_or_folder': 'edits',
'delete_file_or_folder': 'edits',
'edit_file': 'edits',
'run_terminal': 'terminal',
'run_command': 'terminal',
}
@ -46,9 +46,9 @@ export type ToolCallParams = {
'create_file_or_folder': { uri: URI, isFolder: boolean },
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
// ---
'run_terminal': { command: string; bgTerminalId: string | null },
'open_bg_terminal': {},
'kill_bg_terminal': { terminalId: string },
'run_command': { command: string; bgTerminalId: string | null },
'open_persistent_terminal': {},
'kill_persistent_terminal': { terminalId: string },
}
// RESULT OF TOOL CALL
@ -65,8 +65,8 @@ export type ToolResultType = {
'create_file_or_folder': {},
'delete_file_or_folder': {},
// ---
'run_terminal': { result: string; resolveReason: TerminalResolveReason; },
'open_bg_terminal': { terminalId: string },
'kill_bg_terminal': {},
'run_command': { result: string; resolveReason: TerminalResolveReason; },
'open_persistent_terminal': { terminalId: string },
'kill_persistent_terminal': {},
}

View file

@ -62,6 +62,9 @@ export interface IVoidSettingsService {
setOptionsOfModelSelection: SetOptionsOfModelSelection;
setGlobalSetting: SetGlobalSettingFn;
dangerousSetState(newState: VoidSettingsState): Promise<void>;
resetState(): Promise<void>;
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
@ -231,6 +234,22 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this.readAndInitializeState()
}
dangerousSetState = async (newState: VoidSettingsState) => {
this.state = _validatedModelState(newState)
await this._storeState()
this._onDidChangeState.fire()
this._onUpdate_syncApplyToChat()
}
async resetState() {
await this.dangerousSetState(defaultState())
}
async readAndInitializeState() {
let readS: VoidSettingsState
try {