From e0156803ff54026d5bb661513752700ee458f8d3 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 5 May 2025 21:36:20 -0700 Subject: [PATCH 01/14] better commandbar ui --- .../react/src/sidebar-tsx/SidebarChat.tsx | 47 --- .../VoidCommandBar.tsx | 357 +++++++++--------- 2 files changed, 187 insertions(+), 217 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index dbb539f0..6c6d1b91 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -2420,53 +2420,6 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me } - - - -export const AcceptAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( - -) - -export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( - -) - - - const CommandBarInChat = () => { const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = useCommandBarState() const numFilesChanged = sortedCommandBarURIs.length diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index dcf97f8e..405c147e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -9,9 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js' import '../styles.css' import { useCallback, useEffect, useState, useRef } from 'react'; import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; -import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; +import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; import { VoidCommandBarProps } from '../../../voidCommandBarService.js'; -import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js'; +import { Check, EllipsisVertical, Menu, MoveDown, MoveLeft, MoveRight, MoveUp, X } from 'lucide-react'; export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() @@ -23,8 +23,6 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { } - - const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { if (len === 0) return null return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset @@ -32,7 +30,55 @@ const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { -const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { + + +export const AcceptAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( + +) + + +export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( + +) + + +export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const editorService = accessor.get('ICodeEditorService') @@ -41,11 +87,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const commandBarService = accessor.get('IVoidCommandBarService') const voidModelService = accessor.get('IVoidModelService') const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() - - - // useEffect(() => { - // console.log('MOUNTING!!!') - // }, []) + const [showAcceptRejectAllButtons, setShowAcceptRejectAllButtons] = useState(false) // latestUriIdx is used to remember place in leftRight const _latestValidUriIdxRef = useRef(null) @@ -111,7 +153,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const { model } = await voidModelService.getModelSafe(nextURI) if (model) { // switch to the URI - editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor) + editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor) } } @@ -119,7 +161,6 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : [] const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : [] - const isADiffInThisFile = sortedDiffIds.length !== 0 const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0 const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0 @@ -133,185 +174,161 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const prevURIIdx = getNextUriIdx(-1) const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null - const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile) - - const upButton = - - const downButton = - - const leftButton = - - const rightButton = - - + const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // accept/reject if current URI has changes - const onAcceptAll = () => { + const onAcceptFile = () => { if (!uri) return editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) metricsService.capture('Accept All', {}) } - const onRejectAll = () => { + const onRejectFile = () => { if (!uri) return editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true }) metricsService.capture('Reject All', {}) } - if (!isADiffZoneInAnyFile) return null - // const acceptAllButton = + return ( +
- // const rejectAllButton = - const acceptAllButton = + {/* Accept All / Reject All buttons that appear when the vertical ellipsis is clicked */} + {showAcceptRejectAllButtons && showAcceptRejectAll && ( +
+
+
+ { + onAcceptFile(); + setShowAcceptRejectAllButtons(false); + }} + /> + { + onRejectFile(); + setShowAcceptRejectAllButtons(false); + }} + /> +
+
+
+ )} - const rejectAllButton = +
- const acceptRejectAllButtons =
- {acceptAllButton} - {rejectAllButton} -
+ {/* Diff Navigation Group */} +
+ + + {isADiffInThisFile + ? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` + : streamState === 'streaming' + ? 'No changes yet' + : 'No changes' + } - // const closeCommandBar = useCallback(() => { - // commandService.executeCommand('void.hideCommandBar'); - // }, [commandService]); + + +
- // const hideButton = - const leftRightUpDownButtons =
-
- {/* Changes in file */} -
- {upButton} - {downButton} - - {isADiffInThisFile ? - `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` - : streamState === 'streaming' ? - 'No changes yet' - : `No changes` - } - -
- {/* Files */} -
- {leftButton} - {/*
*/} - {rightButton} - {/*
*/} - - {currFileIdx !== null ? - `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` - : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` - } - + {/* File Navigation Group */} +
+ + + {currFileIdx !== null + ? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` + : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}` + } + + +
+ + + {/* Accept/Reject buttons - only shown when appropriate */} + {showAcceptRejectAll && ( +
+ + +
+ )} + {/* Triple colon menu button */} + {showAcceptRejectAll &&
+ + setShowAcceptRejectAllButtons(!showAcceptRejectAllButtons)} + /> + +
}
-
- - return
- {showAcceptRejectAll && acceptRejectAllButtons} - {leftRightUpDownButtons} - -
+ ) } + + + + From a5b43ce1465c87958d6db82335997f67a8752603 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 5 May 2025 21:44:40 -0700 Subject: [PATCH 02/14] Cmd+Shift+L transfers old text+selections, delete sidebarState --- .../contrib/void/browser/chatThreadService.ts | 108 ++++++-- .../react/src/markdown/ChatMarkdownRender.tsx | 1 - .../react/src/quick-edit-tsx/QuickEdit.tsx | 2 +- .../browser/react/src/sidebar-tsx/Sidebar.tsx | 28 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 43 ++- .../src/sidebar-tsx/SidebarThreadSelector.tsx | 134 ---------- .../void/browser/react/src/util/inputs.tsx | 17 +- .../void/browser/react/src/util/services.tsx | 28 +- .../contrib/void/browser/sidebarActions.ts | 248 ++++++++---------- .../void/browser/sidebarStateService.ts | 82 ------ .../contrib/void/browser/void.contribution.ts | 1 - 11 files changed, 245 insertions(+), 447 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/browser/sidebarStateService.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7f8cc7af..83063dae 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -100,6 +100,13 @@ const defaultMessageState: UserMessageState = { // a 'thread' means a chat message history +type WhenMounted = { + textAreaRef: { current: HTMLTextAreaElement | null }; // the textarea that this thread has, gets set in SidebarChat + scrollToBottom: () => void; +} + + + export type ThreadType = { id: string; // store the id here too createdAt: string; // ISO string @@ -120,6 +127,15 @@ export type ThreadType = { [codespanName: string]: CodespanLocationLink } } + + + mountedInfo?: { + whenMounted: Promise + _whenMountedResolver: (res: WhenMounted) => void + mountedIsResolvedRef: { current: boolean }; + } + + }; } @@ -267,6 +283,9 @@ export interface IChatThreadService { // jump to history jumpToCheckpointBeforeMessageIdx(opts: { threadId: string, messageIdx: number, jumpToUserModified: boolean }): void; + + focusCurrentChat: () => Promise + blurCurrentChat: () => Promise } export const IChatThreadService = createDecorator('voidChatThreadService'); @@ -333,6 +352,29 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + async focusCurrentChat() { + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return + console.log('awaiting') + const s = await thread.state.mountedInfo?.whenMounted + console.log('got!', s) + if (!this.isCurrentlyFocusingMessage()) { + console.log('running focus!') + s?.textAreaRef.current?.focus() + } + } + async blurCurrentChat() { + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return + const s = await thread.state.mountedInfo?.whenMounted + if (!this.isCurrentlyFocusingMessage()) { + s?.textAreaRef.current?.blur() + } + } + + dangerousSetState = (newState: ThreadsState) => { this.state = newState @@ -377,7 +419,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // this should be the only place this.state = ... appears besides constructor - private _setState(state: Partial, affectsCurrent: boolean) { + private _setState(state: Partial, doNotRefreshMountInfo?: boolean) { const newState = { ...this.state, ...state @@ -385,8 +427,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this.state = newState - if (affectsCurrent) - this._onDidChangeCurrentThread.fire() + this._onDidChangeCurrentThread.fire() // if we just switched to a thread, update its current stream state if it's not streaming to possibly streaming @@ -408,6 +449,27 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + + // if we did not just set the state to true, set mount info + if (doNotRefreshMountInfo) return + + let whenMountedResolver: (w: WhenMounted) => void + const whenMountedPromise = new Promise((res) => whenMountedResolver = res) + + this._setThreadState(threadId, { + mountedInfo: { + whenMounted: whenMountedPromise, + mountedIsResolvedRef: { current: false }, + _whenMountedResolver: (w: WhenMounted) => { + whenMountedResolver(w) + const mountInfo = this.state.allThreads[threadId]?.state.mountedInfo + if (mountInfo) mountInfo.mountedIsResolvedRef.current = true + }, + } + }, true) // do not trigger an update + + + } @@ -724,8 +786,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) }) const llmRes = await messageIsDonePromise // wait for message to complete + + // if something else started running in the meantime if (this.streamState[threadId]?.isRunning !== 'LLM') { - console.log('Unexpected chat agent state when', this.streamState[threadId]?.isRunning) + // console.log('Chat thread interrupted by a newer chat thread', this.streamState[threadId]?.isRunning) return } @@ -823,7 +887,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } this._storeAllThreads(newThreads) - this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) + this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it) } @@ -1091,7 +1155,10 @@ We only need to do it for files that were edited since `from`, ie files between class: undefined, run: () => { this.switchToThread(threadId) - // TODO!!! scroll to bottom + // scroll to bottom + this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => { + m.scrollToBottom() + }) } }] }, @@ -1142,6 +1209,11 @@ We only need to do it for files that were edited since `from`, ie files between this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }), threadId, ) + + // scroll to bottom + this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => { + m.scrollToBottom() + }) } @@ -1164,7 +1236,7 @@ We only need to do it for files that were edited since `from`, ie files between } }; this._storeAllThreads(newThreads); - this._setState({ allThreads: newThreads }, true); + this._setState({ allThreads: newThreads }); } // Now call the original method to add the user message and stream the response @@ -1194,7 +1266,7 @@ We only need to do it for files that were edited since `from`, ie files between messages: slicedMessages } } - }, true) + }) // re-add the message and stream it this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId }) @@ -1467,7 +1539,7 @@ We only need to do it for files that were edited since `from`, ie files between } } - }, true) + }) } @@ -1498,7 +1570,7 @@ We only need to do it for files that were edited since `from`, ie files between } switchToThread(threadId: string) { - this._setState({ currentThreadId: threadId }, true) + this._setState({ currentThreadId: threadId }) } @@ -1521,7 +1593,7 @@ We only need to do it for files that were edited since `from`, ie files between [newThread.id]: newThread } this._storeAllThreads(newThreads) - this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true) + this._setState({ allThreads: newThreads, currentThreadId: newThread.id }) } @@ -1534,7 +1606,7 @@ We only need to do it for files that were edited since `from`, ie files between // store the updated threads this._storeAllThreads(newThreads); - this._setState({ ...this.state, allThreads: newThreads }, true) + this._setState({ ...this.state, allThreads: newThreads }) } duplicateThread(threadId: string) { @@ -1550,7 +1622,7 @@ We only need to do it for files that were edited since `from`, ie files between [newThread.id]: newThread, } this._storeAllThreads(newThreads) - this._setState({ allThreads: newThreads }, true) + this._setState({ allThreads: newThreads }) } @@ -1571,7 +1643,7 @@ We only need to do it for files that were edited since `from`, ie files between } } this._storeAllThreads(newThreads) - this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) + this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it) } // sets the currently selected message (must be undefined if no message is selected) @@ -1592,7 +1664,7 @@ We only need to do it for files that were edited since `from`, ie files between } } } - }, true) + }) // // when change focused message idx, jump - do not jump back when click edit, too confusing. // if (messageIdx !== undefined) @@ -1680,12 +1752,12 @@ We only need to do it for files that were edited since `from`, ie files between ) } } - }, true) + }) } // set thread.state - private _setThreadState(threadId: string, state: Partial): void { + private _setThreadState(threadId: string, state: Partial, doNotRefreshMountInfo?: boolean): void { const thread = this.state.allThreads[threadId] if (!thread) return @@ -1700,7 +1772,7 @@ We only need to do it for files that were edited since `from`, ie files between } } } - }, true) + }, doNotRefreshMountInfo) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 96067725..3f6a9882 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -158,7 +158,6 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string } return { - const sidebarState = useSidebarState() - const { currentTab: tab } = sidebarState const isDark = useIsDark() return
{ `} > - {/* { - const tabs = ['chat', 'settings', 'threadSelector'] - const index = tabs.indexOf(tab) - sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any }) - }}>clickme {tab} */} - - {/*
- - - -
*/} - -
+
- {/* - - */}
- - {/*
- - - -
*/} -
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 6c6d1b91..f015d33f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -6,7 +6,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js'; +import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js'; import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js'; import { URI } from '../../../../../../../base/common/uri.js'; @@ -14,7 +14,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js'; import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; -import { OldSidebarThreadSelector, PastThreadsList } from './SidebarThreadSelector.js'; +import { PastThreadsList } from './SidebarThreadSelector.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; @@ -950,7 +950,6 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - const sidebarStateService = accessor.get('ISidebarStateService') // global state let isBeingEdited = false @@ -1046,7 +1045,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr } catch (e) { console.error('Error while editing message:', e) } - sidebarStateService.fireFocusChat() + await chatThreadsService.focusCurrentChat() requestAnimationFrame(() => _scrollToBottom?.()) } @@ -2724,18 +2723,6 @@ export const SidebarChat = () => { const settingsState = useSettingsState() // ----- HIGHER STATE ----- - // sidebar state - const sidebarStateService = accessor.get('ISidebarStateService') - useEffect(() => { - const disposables: IDisposable[] = [] - disposables.push( - sidebarStateService.onDidFocusChat(() => { !chatThreadsService.isCurrentlyFocusingMessage() && textAreaRef.current?.focus() }), - sidebarStateService.onDidBlurChat(() => { !chatThreadsService.isCurrentlyFocusingMessage() && textAreaRef.current?.blur() }) - ) - return () => disposables.forEach(d => d.dispose()) - }, [sidebarStateService, textAreaRef]) - - const { isHistoryOpen } = useSidebarState() // threads state const chatThreadsState = useChatThreadsState() @@ -2794,16 +2781,24 @@ export const SidebarChat = () => { const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel() - // scroll to top on thread switch - useEffect(() => { - if (isHistoryOpen) - scrollContainerRef.current?.scrollTo({ top: 0, left: 0 }) - }, [isHistoryOpen, currentThread.id]) - - const threadId = currentThread.id const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity) + + + // resolve mount info + const isResolved = chatThreadsState.allThreads[threadId]?.state.mountedInfo?.mountedIsResolvedRef.current + useEffect(() => { + if (isResolved) return + chatThreadsState.allThreads[threadId]?.state.mountedInfo?._whenMountedResolver?.({ + textAreaRef: textAreaRef, + scrollToBottom: () => scrollToBottom(scrollContainerRef), + }) + }, [chatThreadsState, threadId, textAreaRef, scrollContainerRef, isResolved]) + + + + const previousMessagesHTML = useMemo(() => { // const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') // tool request shows up as Editing... if in progress @@ -2973,7 +2968,7 @@ export const SidebarChat = () => { {landingPageInput} - {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads + {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads
Previous Threads
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index e69c67ab..d2f44a6b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -11,138 +11,6 @@ import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserChe import { IsRunningType, ThreadType } from '../../../chatThreadService.js'; -export const OldSidebarThreadSelector = () => { - - - const accessor = useAccessor() - const sidebarStateService = accessor.get('ISidebarStateService') - - return ( -
- -
- {/* title */} -

{`History`}

- {/* X button at top right */} - -
- - {/* a list of all the past threads */} - {/* */} - -
- ) -} - - - - - - -const truncate = (s: string) => { - let len = s.length - const TRUNC_AFTER = 16 - if (len >= TRUNC_AFTER) - s = s.substring(0, TRUNC_AFTER) + '...' - return s -} - - - -const OldPastThreadsList = () => { - - const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') - const sidebarStateService = accessor.get('ISidebarStateService') - - const threadsState = useChatThreadsState() - const { allThreads } = threadsState - - // sorted by most recent to least recent - const sortedThreadIds = Object.keys(allThreads ?? {}) - .sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1) - .filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0) - - - return
-
    - - {sortedThreadIds.length === 0 - - ?
    {`There are no chat threads yet.`}
    - - : sortedThreadIds.map((threadId) => { - if (!allThreads) { - return
  • {`Error accessing chat history.`}
  • ; - } - const pastThread = allThreads[threadId]; - if (!pastThread) { - return
  • {`Error accessing chat history.`}
  • ; - } - - - let firstMsg = null; - // let secondMsg = null; - - const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user'); - - if (firstUserMsgIdx !== -1) { - // firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? ''); - const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx] - firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || ''; - } else { - firstMsg = '""'; - } - - // const secondMsgIdx = pastThread.messages.findIndex( - // (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx - // ); - - // if (secondMsgIdx !== -1) { - // secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? ''); - // } - - const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length; - - return ( -
  • - -
  • - ); - }) - } -
-
-} - - const numInitialThreads = 3 export const PastThreadsList = ({ className = '' }: { className?: string }) => { @@ -313,7 +181,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - const sidebarStateService = accessor.get('ISidebarStateService') // const settingsState = useSettingsState() // const convertService = accessor.get('IConvertToLLMMessageService') @@ -369,7 +236,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni `} onClick={() => { chatThreadsService.switchToThread(pastThread.id); - sidebarStateService.setState({ isHistoryOpen: false }); }} onMouseEnter={() => setHoveredIdx(idx)} onMouseLeave={() => setHoveredIdx(null)} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 4530e78c..8f98bdac 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -383,8 +383,21 @@ export const VoidInputBox2 = forwardRef(fun // Focus the textarea first textarea.focus(); - // Insert the @ to mention text in the editor (we decided not to do this for now) - // document.execCommand('insertText', false, text + ' '); // add space after too + // delete the @ and set the cursor position + // Get cursor position + const startPos = textarea.selectionStart; + const endPos = textarea.selectionEnd; + + // Get the text before the cursor, excluding the @ symbol that triggered the menu + const textBeforeCursor = textarea.value.substring(0, startPos - 1); + const textAfterCursor = textarea.value.substring(endPos); + + // Replace the text including the @ symbol with the selected option + textarea.value = textBeforeCursor + textAfterCursor; + + // Set cursor position after the inserted text + const newCursorPos = textBeforeCursor.length; + textarea.setSelectionRange(newCursorPos, newCursorPos); // React's onChange relies on a SyntheticEvent system // The best way to ensure it runs is to call callbacks directly 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 fdeddf76..8b9ada0e 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 @@ -6,7 +6,6 @@ import React, { useState, useEffect, useCallback } from 'react' import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' -import { VoidSidebarState } from '../../../sidebarStateService.js' import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js' import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js' import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js' @@ -23,7 +22,6 @@ import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js' import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'; -import { ISidebarStateService } from '../../../sidebarStateService.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' @@ -58,9 +56,6 @@ import { ISearchService } from '../../../../../../services/search/common/search. // 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 sidebarState: VoidSidebarState -const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set() - let chatThreadsState: ThreadsState const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set() @@ -91,7 +86,6 @@ export const _registerServices = (accessor: ServicesAccessor) => { _registerAccessor(accessor) const stateServices = { - sidebarStateService: accessor.get(ISidebarStateService), chatThreadsStateService: accessor.get(IChatThreadService), settingsStateService: accessor.get(IVoidSettingsService), refreshModelService: accessor.get(IRefreshModelService), @@ -101,15 +95,10 @@ export const _registerServices = (accessor: ServicesAccessor) => { modelService: accessor.get(IModelService), } - const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices + const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices + + - sidebarState = sidebarStateService.state - disposables.push( - sidebarStateService.onDidChangeState(() => { - sidebarState = sidebarStateService.state - sidebarStateListeners.forEach(l => l(sidebarState)) - }) - ) chatThreadsState = chatThreadsStateService.state disposables.push( @@ -193,7 +182,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => { IRefreshModelService: accessor.get(IRefreshModelService), IVoidSettingsService: accessor.get(IVoidSettingsService), IEditCodeService: accessor.get(IEditCodeService), - ISidebarStateService: accessor.get(ISidebarStateService), IChatThreadService: accessor.get(IChatThreadService), IInstantiationService: accessor.get(IInstantiationService), @@ -250,16 +238,6 @@ export const useAccessor = () => { // -- state of services -- -export const useSidebarState = () => { - const [s, ss] = useState(sidebarState) - useEffect(() => { - ss(sidebarState) - sidebarStateListeners.add(ss) - return () => { sidebarStateListeners.delete(ss) } - }, [ss]) - return s -} - export const useSettingsState = () => { const [s, ss] = useState(settingsState) useEffect(() => { diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index c4e5aef8..a06a344b 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -14,23 +14,18 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { VOID_VIEW_ID } from './sidebarPane.js'; +import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js'; import { IMetricsService } from '../common/metricsService.js'; -import { ISidebarStateService } from './sidebarStateService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize2 } from '../../../../nls.js'; -import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; import { IChatThreadService } from './chatThreadService.js'; -import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; // ---------- Register commands and keybindings ---------- - export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => { if (!range) return null @@ -72,68 +67,21 @@ registerAction2(class extends Action2 { super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true }); } async run(accessor: ServicesAccessor): Promise { - const stateService = accessor.get(ISidebarStateService) - stateService.setState({ isHistoryOpen: false, currentTab: 'chat' }) - stateService.fireFocusChat() + const viewsService = accessor.get(IViewsService) + const chatThreadsService = accessor.get(IChatThreadService) + viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID) + await chatThreadsService.focusCurrentChat() } }) -// Action: when press ctrl+L, show the sidebar chat and add to the selection -const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select' -registerAction2(class extends Action2 { - constructor() { - super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true }); - } - async run(accessor: ServicesAccessor): Promise { - - const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel() - if (!model) - return - - const metricsService = accessor.get(IMetricsService) - const editorService = accessor.get(ICodeEditorService) - - metricsService.capture('Ctrl+L', {}) - - const editor = editorService.getActiveCodeEditor() - // accessor.get(IEditorService).activeTextEditorControl?.getSelection() - const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' }) - - - // select whole lines - if (selectionRange) { - editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }) - } - - - const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { - type: 'File', - uri: model.uri, - language: model.getLanguageId(), - state: { wasAddedAsCurrentFile: false } - } : { - type: 'CodeSelection', - uri: model.uri, - language: model.getLanguageId(), - range: [selectionRange.startLineNumber, selectionRange.endLineNumber], - state: { wasAddedAsCurrentFile: false } - } - - const chatThreadService = accessor.get(IChatThreadService) - - chatThreadService.addNewStagingSelection(newSelection) - - } -}); - - +// cmd L registerAction2(class extends Action2 { constructor() { super({ id: VOID_CTRL_L_ACTION_ID, f1: true, - title: localize2('voidCtrlL', 'Void: Add Selection to Chat'), + title: localize2('voidCmdL', 'Void: Add Selection to Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.VoidExtension @@ -141,72 +89,112 @@ registerAction2(class extends Action2 { }); } async run(accessor: ServicesAccessor): Promise { + + + // Get the views service to check if the sidebar is open + // const viewsService = accessor.get(IViewsService) const commandService = accessor.get(ICommandService) - await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID) - // await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID) - } -}) - - -const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => { - - const stateService = accessor.get(ISidebarStateService) - stateService.setState({ isHistoryOpen: false, currentTab: 'chat' }) - const chatThreadService = accessor.get(IChatThreadService) - chatThreadService.openNewThread() - - // focus - stateService.fireFocusChat() - const window = getActiveWindow() - window.requestAnimationFrame(() => stateService.fireFocusChat()) - -} - - -// New chat menu button -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'void.newChatAction', - title: 'New Chat', - icon: { id: 'add' }, - menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }], - - }); - } - async run(accessor: ServicesAccessor): Promise { - + const viewsService = accessor.get(IViewsService) const metricsService = accessor.get(IMetricsService) - metricsService.capture('Chat Navigation', { type: 'New Chat' }) + const editorService = accessor.get(ICodeEditorService) + const chatThreadService = accessor.get(IChatThreadService) - openNewThreadAndFireFocus(accessor) + metricsService.capture('Ctrl+L', {}) + + const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID) + if (!wasAlreadyOpen) { + await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID) + return + } + + + // if was already open + + const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel() + if (!model) return + + const editor = editorService.getActiveCodeEditor() + const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' }) + + // if has no selection, close + return + if (!selectionRange) { + viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID); + return; + } + + // if has selection, add it + editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }) + chatThreadService.addNewStagingSelection({ + type: 'CodeSelection', + uri: model.uri, + language: model.getLanguageId(), + range: [selectionRange.startLineNumber, selectionRange.endLineNumber], + state: { wasAddedAsCurrentFile: false } + }) } }) -// New chat keybind + +// New chat keybind + menu button +const VOID_CMD_SHIFT_L_ACTION_ID = 'void.cmdShiftL' registerAction2(class extends Action2 { constructor() { super({ - id: 'void.newChatKeybindAction', - title: 'New Chat Keybind', + id: VOID_CMD_SHIFT_L_ACTION_ID, + title: 'New Chat', keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, weight: KeybindingWeight.VoidExtension, }, + icon: { id: 'add' }, + menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }], }); } async run(accessor: ServicesAccessor): Promise { const metricsService = accessor.get(IMetricsService) - const commandService = accessor.get(ICommandService) - metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' }) + const chatThreadsService = accessor.get(IChatThreadService) + const editorService = accessor.get(ICodeEditorService) + metricsService.capture('Chat Navigation', { type: 'Start New Chat' }) - openNewThreadAndFireFocus(accessor) + // get current selections and value to transfer + const oldThreadId = chatThreadsService.state.currentThreadId + const oldThread = chatThreadsService.state.allThreads[oldThreadId] - // add user's selection to chat - await commandService.executeCommand(VOID_CTRL_L_ACTION_ID) + const oldUI = await oldThread?.state.mountedInfo?.whenMounted + const oldSelns = oldThread?.state.stagingSelections + const oldVal = oldUI?.textAreaRef.current?.value + + // open and focus new thread + chatThreadsService.openNewThread() + await chatThreadsService.focusCurrentChat() + + + // set new thread values + const newThreadId = chatThreadsService.state.currentThreadId + const newThread = chatThreadsService.state.allThreads[newThreadId] + + const newUI = await newThread?.state.mountedInfo?.whenMounted + chatThreadsService.setCurrentThreadState({ stagingSelections: oldSelns, }) + if (newUI?.textAreaRef?.current && oldVal) newUI.textAreaRef.current.value = oldVal + + + // if has selection, add it + const editor = editorService.getActiveCodeEditor() + const model = editor?.getModel() + if (!model) return + const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' }) + if (!selectionRange) return + editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }) + chatThreadsService.addNewStagingSelection({ + type: 'CodeSelection', + uri: model.uri, + language: model.getLanguageId(), + range: [selectionRange.startLineNumber, selectionRange.endLineNumber], + state: { wasAddedAsCurrentFile: false } + }) } }) @@ -229,18 +217,12 @@ registerAction2(class extends Action2 { return; } - const stateService = accessor.get(ISidebarStateService) const metricsService = accessor.get(IMetricsService) + const commandService = accessor.get(ICommandService) metricsService.capture('Chat Navigation', { type: 'History' }) - - openNewThreadAndFireFocus(accessor) - - // doesnt do anything right now - stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' }) - stateService.fireBlurChat() - + commandService.executeCommand(VOID_CMD_SHIFT_L_ACTION_ID) } }) @@ -265,28 +247,28 @@ registerAction2(class extends Action2 { -export class TabSwitchListener extends Disposable { +// export class TabSwitchListener extends Disposable { - constructor( - onSwitchTab: () => void, - @ICodeEditorService private readonly _editorService: ICodeEditorService, - ) { - super() +// constructor( +// onSwitchTab: () => void, +// @ICodeEditorService private readonly _editorService: ICodeEditorService, +// ) { +// super() - // when editor switches tabs (models) - const addTabSwitchListeners = (editor: ICodeEditor) => { - this._register(editor.onDidChangeModel(e => { - if (e.newModelUrl?.scheme !== 'file') return - onSwitchTab() - })) - } +// // when editor switches tabs (models) +// const addTabSwitchListeners = (editor: ICodeEditor) => { +// this._register(editor.onDidChangeModel(e => { +// if (e.newModelUrl?.scheme !== 'file') return +// onSwitchTab() +// })) +// } - const initializeEditor = (editor: ICodeEditor) => { - addTabSwitchListeners(editor) - } +// const initializeEditor = (editor: ICodeEditor) => { +// addTabSwitchListeners(editor) +// } - // initialize current editors + any new editors - for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor) - this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) - } -} +// // initialize current editors + any new editors +// for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor) +// this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) +// } +// } diff --git a/src/vs/workbench/contrib/void/browser/sidebarStateService.ts b/src/vs/workbench/contrib/void/browser/sidebarStateService.ts deleted file mode 100644 index bd56657e..00000000 --- a/src/vs/workbench/contrib/void/browser/sidebarStateService.ts +++ /dev/null @@ -1,82 +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 { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js'; - - -// service that manages sidebar's state -export type VoidSidebarState = { - isHistoryOpen: boolean; // this isn't doing anything right now - currentTab: 'chat'; -} - -export interface ISidebarStateService { - readonly _serviceBrand: undefined; - - readonly state: VoidSidebarState; // readonly to the user - setState(newState: Partial): void; - onDidChangeState: Event; - - onDidFocusChat: Event; - onDidBlurChat: Event; - fireFocusChat(): void; - fireBlurChat(): void; -} - -export const ISidebarStateService = createDecorator('voidSidebarStateService'); -class VoidSidebarStateService extends Disposable implements ISidebarStateService { - _serviceBrand: undefined; - - static readonly ID = 'voidSidebarStateService'; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - private readonly _onFocusChat = new Emitter(); - readonly onDidFocusChat: Event = this._onFocusChat.event; - - private readonly _onBlurChat = new Emitter(); - readonly onDidBlurChat: Event = this._onBlurChat.event; - - - // state - state: VoidSidebarState - - constructor( - @ICommandService private readonly commandService: ICommandService, - ) { - super() - - // initial state - this.state = { isHistoryOpen: false, currentTab: 'chat', } - } - - - setState(newState: Partial) { - // make sure view is open if the tab changes - if ('currentTab' in newState) { - this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID) - } - - this.state = { ...this.state, ...newState } - this._onDidChangeState.fire() - } - - fireFocusChat() { - this._onFocusChat.fire() - } - - fireBlurChat() { - this._onBlurChat.fire() - } - -} - -registerSingleton(ISidebarStateService, VoidSidebarStateService, 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 cd7b6f60..6d46b6eb 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -10,7 +10,6 @@ import './editCodeService.js' // register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L) import './sidebarActions.js' import './sidebarPane.js' -import './sidebarStateService.js' // register quick edit (Ctrl+K) import './quickEditActions.js' From 170ccb9298bb4343951df20126ddc98dee9710ae Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 5 May 2025 21:49:04 -0700 Subject: [PATCH 03/14] ui --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1ea48e14..9bef52b3 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 @@ -720,8 +720,8 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi {showProviderSuggestions && needsModel ? providerName === 'ollama' ? - - : + + : : null}
From ae179b811199edc4e89b24d881e350cd72f2f3d5 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 5 May 2025 22:05:01 -0700 Subject: [PATCH 04/14] fix gemini OR --- .../browser/convertToLLMMessageService.ts | 12 ++++------ .../react/src/sidebar-tsx/SidebarChat.tsx | 10 ++++----- .../contrib/void/common/modelCapabilities.ts | 22 +++++++++---------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 27c054ce..c4ffa286 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -498,19 +498,15 @@ const prepareMessages = (params: { providerName: ProviderName }): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => { - const specialFormat = params.specialToolFormat // this is just for ts idiocy - if (params.providerName === 'gemini') { - // treat as anthropic style, then convert to gemini style + const specialFormat = params.specialToolFormat // this is just for ts stupidness + + // if need to convert to gemini style of messaes, do that (treat as anthropic style, then convert to gemini style) + if (params.providerName === 'gemini' || specialFormat === 'gemini-style') { const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined }) const messages = res.messages as AnthropicLLMChatMessage[] const messages2 = prepareGeminiMessages(messages) return { messages: messages2, separateSystemMessage: res.separateSystemMessage } } - else { - if (specialFormat === 'gemini-style') { - throw new Error(`Tried preparing messages with tool format ${params.specialToolFormat} but the provider was ${params.providerName}, not Gemini.`) - } - } return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat }) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index f015d33f..47ea83c0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1552,8 +1552,8 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R } -export const CodeChildren = ({ children }: { children: React.ReactNode }) => { - return
+export const CodeChildren = ({ children, className }: { children: React.ReactNode, className?: string }) => { + return
{children}
@@ -1642,7 +1642,7 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin const componentParams: ToolHeaderParams = { title, desc1, isError, icon } componentParams.children = - + {message} @@ -2042,7 +2042,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.numResults = result.lines.length; componentParams.children = result.lines.length === 0 ? undefined : - +
 								{toolsService.stringOfResult['search_in_file'](params, result)}
 							
@@ -2176,7 +2176,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, else if (toolMessage.type === 'tool_error') { const { result } = toolMessage if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - componentParams.children = componentParams.bottomChildren = + componentParams.bottomChildren = {result} diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index c6589e10..eeeb6bf3 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -352,7 +352,7 @@ const openSourceModelOptions_assumingOAICompat = { // keep modelName, but use the fallback's defaults -const extensiveOAICompatModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (modelName, fallbackKnownValues) => { +const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (modelName, fallbackKnownValues) => { const lower = modelName.toLowerCase() @@ -1018,32 +1018,32 @@ export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', const vLLMSettings: VoidStaticProviderInfo = { // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, }, - modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), + modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), modelOptions: {}, // TODO } const lmStudioSettings: VoidStaticProviderInfo = { providerReasoningIOSettings: { output: { needsManualParse: true }, }, - modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' }, contextWindow: 4_096 }), + modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' }, contextWindow: 4_096 }), modelOptions: {}, // TODO } const ollamaSettings: VoidStaticProviderInfo = { // reasoning: we need to filter out reasoning tags manually providerReasoningIOSettings: { output: { needsManualParse: true }, }, - modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), + modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), modelOptions: ollamaModelOptions, } const openaiCompatible: VoidStaticProviderInfo = { // reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning - modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName), + modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName), modelOptions: {}, } const liteLLMSettings: VoidStaticProviderInfo = { // https://docs.litellm.ai/docs/reasoning_content providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' } }, - modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), + modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), modelOptions: {}, // TODO } @@ -1197,11 +1197,11 @@ const openRouterSettings: VoidStaticProviderInfo = { modelOptions: openRouterModelOptions_assumingOpenAICompat, // TODO!!! send a query to openrouter to get the price, etc. modelOptionsFallback: (modelName) => { - const res = extensiveOAICompatModelOptionsFallback(modelName) - // openRouter does not support gemini-style, use openai-style instead - if (res?.specialToolFormat === 'gemini-style') { - res.specialToolFormat = 'openai-style' - } + const res = extensiveModelOptionsFallback(modelName) + // // openRouter does not support gemini-style, use openai-style instead + // if (res?.specialToolFormat === 'gemini-style') { + // res.specialToolFormat = 'openai-style' + // } return res }, } From 71d703852bcea8b8ee047244ec7e58a6d35b9b73 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 6 May 2025 02:08:38 -0700 Subject: [PATCH 05/14] improve onboarding --- product.json | 3 +- .../contrib/void/browser/chatThreadService.ts | 3 - .../src/void-onboarding/VoidOnboarding.tsx | 589 ++++-------------- .../react/src/void-settings-tsx/Settings.tsx | 254 ++++---- .../react/src/void-tooltip/VoidTooltip.tsx | 9 +- 5 files changed, 265 insertions(+), 593 deletions(-) diff --git a/product.json b/product.json index a2f96552..1cc09ea6 100644 --- a/product.json +++ b/product.json @@ -39,6 +39,7 @@ "linkProtectionTrustedDomains": [ "https://voideditor.com", "https://voideditor.dev", - "https://github.com/voideditor/void" + "https://github.com/voideditor/void", + "https://ollama.com" ] } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 83063dae..49a6dd09 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -356,11 +356,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { const threadId = this.state.currentThreadId const thread = this.state.allThreads[threadId] if (!thread) return - console.log('awaiting') const s = await thread.state.mountedInfo?.whenMounted - console.log('got!', s) if (!this.isCurrentlyFocusingMessage()) { - console.log('running focus!') s?.textAreaRef.current?.focus() } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 8c968c62..7bf94b47 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -6,10 +6,9 @@ import { useEffect, useRef, useState } from 'react'; import { useAccessor, useIsDark, useSettingsState } from '../util/services.js'; import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react'; -import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js'; -import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js'; +import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName } from '../../../../common/voidSettingsTypes.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; -import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js'; +import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js'; import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'; import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'; import { isLinux } from '../../../../../../../base/common/platform.js'; @@ -27,9 +26,10 @@ export const VoidOnboarding = () => {
@@ -90,6 +90,126 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch } // Onboarding + +// ============================================= +// New AddProvidersPage Component and helpers +// ============================================= + +const tabNames = ['Free', 'Paid', 'Local'] as const; + +type TabName = typeof tabNames[number] | 'Cloud/Other'; + +// Data for cloud providers tab +const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'openAICompatible']; + +// Data structures for provider tabs +const providerNamesOfTab: Record = { + Free: ['gemini', 'openRouter'], + Local: localProviderNames, + Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[], + 'Cloud/Other': cloudProviders, +}; + +const descriptionOfTab: Record = { + Free: `Providers with a 100% free tier. Add as many as you'd like!`, + Paid: `Connect directly with any provider (bring your own key).`, + Local: `Add as many local providers as you'd like! Running providers should appear automatically.`, + 'Cloud/Other': `Reach out for custom configuration requests.`, +}; + +const subtextMdOfTab: Record = { + Free: ` +Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500. +We recommend using models down the line as you run out of free credits. More information [here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits). + +OpenRouter offers 50 free messages a day, and that increases to 1000 if you deposit $10. Only applies to models labeled \`:free\`. More information [here](https://openrouter.ai/docs/api-reference/limits). +`, + Paid: null, + Local: null, + 'Cloud/Other': null, +}; + +const featureNameMap: { display: string, featureName: FeatureName }[] = [ + { display: 'Chat', featureName: 'Chat' }, + { display: 'Quick Edit', featureName: 'Ctrl+K' }, + { display: 'Autocomplete', featureName: 'Autocomplete' }, + { display: 'Fast Apply', featureName: 'Apply' }, +]; + +const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => { + const [currentTab, setCurrentTab] = useState('Free'); + const settingsState = useSettingsState(); + + return ( +
+ {/* Left Column - Fixed */} +
+ {/* Tab Selector */} +
+ {[...tabNames, 'Cloud/Other'].map(tab => ( + + ))} +
+ + {/* Feature Checklist */} +
+ {featureNameMap.map(({ display, featureName }) => { + const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null; + return ( +
+ {hasModel ? : } + {display} +
+ ); + })} +
+
+ + {/* Right Column */} +
+
{currentTab}
+
{descriptionOfTab[currentTab]}
+ {subtextMdOfTab[currentTab] ?
+ +
: null} + + {providerNamesOfTab[currentTab].map((providerName) => ( +
+
Add {displayInfoOfProviderName(providerName).title}
+ + {providerName === 'ollama' && } +
+ ))} + + {(currentTab === 'Local' || currentTab === 'Cloud/Other') && ( +
+
Models
+ {currentTab === 'Local' && } + {currentTab === 'Cloud/Other' && } +
+ )} + + {/* Navigation buttons in right column */} +
+
+ setPageIndex(pageIndex - 1)} /> + setPageIndex(pageIndex + 1)} /> +
+
+
+
+ ); +}; +// ============================================= // OnboardingPage // title: // div @@ -179,7 +299,7 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa className?: string, }) => { return ( -
+
{top && {top}} {content && {content}} {bottom &&
{bottom}
} @@ -188,8 +308,6 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa } const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => { - - // for now just link to the ollama download page return - // if (isModelInstalled) { - // return
- - // Uninstall - - // { - - // setIsModelInstalling(false); - // }} - // /> - - //
- // } - - - - // else if (isModelInstalling) { - // return
- - // {`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`} - - // { - // // abort() - - // // TODO!!!!!!!!!!! don't do this - // setIsModelInstalling(false); - // }} - // /> - - //
- // } - - - // else if (!isModelInstalled) { - - // return
- - // Download ({sizeGb} Gb) - - // { - // // this is a check for whether the model was installed: - - // if (isModelInstalling) return - - - // // TODO!!!!!! don't do this - - - // // install(modelname), callback = setIsModelInstalling(false); - - // setIsModelInstalling(true); - // }} - // /> - - //
- - // } - - // return <> - - } @@ -306,114 +354,7 @@ const abbreviateNumber = (num: number): string => { } } -const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => { - const accessor = useAccessor() - const voidSettingsService = accessor.get('IVoidSettingsService') - const voidSettingsState = useSettingsState() - const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName) - // const providerCapabilities = getProviderCapabilities(providerName) - - - // info used to show the table - const infoOfModelName: Record = {} - - voidSettingsState.settingsOfProvider[providerName].models.forEach(m => { - infoOfModelName[m.modelName] = { - showAsDefault: m.type !== 'custom', - isDownloaded: true - } - }) - - // special case columns for ollama; show recommended models as default - if (providerName === 'ollama') { - for (const modelName of ollamaRecommendedModels) { - if (modelName in infoOfModelName) continue - infoOfModelName[modelName] = { - isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false, - showAsDefault: true, - } - } - } - - return - - - - - - - - - {/* */} - {isDetectableLocally && } - {providerName === 'ollama' && } - - - - {Object.keys(infoOfModelName).map(modelName => { - const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {} - - - const capabilities = getModelCapabilities(providerName, modelName, undefined) - const { - downloadable, - cost, - supportsFIM, - reasoningCapabilities, - contextWindow, - - isUnrecognizedModel, - reservedOutputTokenSpace, - supportsSystemMessage, - } = capabilities - - // TODO update this when tools work - - const removeModelButton = - - - - return ( - - - - - - - - {/* */} - {isDetectableLocally && } - {providerName === 'ollama' && } - - - ) - })} - - - - - -
Models OfferedCost/MContextChatAgentAutotabReasoningDetectedDownload
- {!showAsDefault && removeModelButton} - {modelName} - ${cost.output ?? ''}{contextWindow ? abbreviateNumber(contextWindow) : ''}{!!isDownloaded ? : <>} - -
- - - -
-} @@ -431,22 +372,16 @@ const PrimaryActionButton = ({ children, className, ringSize, ...props }: { chil ${ringSize === 'xl' ? ` gap-2 px-16 py-8 - hover:ring-8 active:ring-8 transition-all duration-300 ease-in-out ` : ringSize === 'screen' ? ` gap-2 px-16 py-8 - ring-[3000px] transition-all duration-1000 ease-in-out `: ringSize === undefined ? ` gap-1 px-4 py-2 - hover:ring-2 active:ring-2 transition-all duration-300 ease-in-out `: ''} - hover:ring-black/90 dark:hover:ring-white/90 - active:ring-black/90 dark:active:ring-white/90 - rounded-lg group ${className} @@ -534,7 +469,6 @@ const VoidOnboardingContent = () => { /> { setPageIndex(pageIndex + 1) }} - disabled={pageIndex === 2 && !didFillInSelectedProviderSettings} />
@@ -612,7 +546,7 @@ const VoidOnboardingContent = () => { delayMs={1000} > { setPageIndex(pageIndex + 1) }} + onClick={() => { setPageIndex(1) }} > Get Started @@ -621,255 +555,13 @@ const VoidOnboardingContent = () => {
} />, - 1: } - content={
- {/*
AI Preferences
*/} - -
Model Preferences
- - -
- - - - - - -
- - -
} - bottom={ -
- { setPageIndex(pageIndex - 1) }} /> -
+ 1: } />, 2: - {/* Title */} - -
Choose a Provider
- - {/* Preference Selector */} - -
- {[ - { id: 'smart', label: 'Intelligent' }, - { id: 'private', label: 'Private' }, - { id: 'cheap', label: 'Affordable' }, - { id: 'all', label: 'All' } - ].map(option => ( - - - - ))} -
- - - - {/* Provider Buttons - Modified to use separate components for each tab */} -
- {/* Intelligent tab */} - -
- {providerNamesOfWantToUseOption['smart'].map((providerName) => { - const isSelected = selectedIntelligentProvider === providerName; - return ( - - ); - })} -
-
- - - {/* Private tab */} - -
- {providerNamesOfWantToUseOption['private'].map((providerName) => { - const isSelected = selectedPrivateProvider === providerName; - return ( - - ); - })} -
-
- - - {/* Affordable tab */} - - -
- {providerNamesOfWantToUseOption['cheap'].map((providerName) => { - const isSelected = selectedAffordableProvider === providerName; - return ( - - ); - })} -
-
- - - {/* All tab */} - -
- {providerNames.map((providerName) => { - const isSelected = selectedAllProvider === providerName; - return ( - - ); - })} -
-
-
- - {/* Description */} - -
- -
-
- - - {/* ModelsTable and ProviderFields */} - {selectedProviderName &&
- {/* Models Table */} - - - - - - {/* Add provider section - simplified styling */} - -
- -
- Add {displayInfoOfProviderName(selectedProviderName).title} - -
- {selectedProviderName === 'ollama' ? : ''} -
- -
-
- - - {selectedProviderName && - - } - - - {/* Button and status indicators */} - - {!didFillInProviderSettings ?

Please fill in all fields to continue

- : !isAtLeastOneModel ?

Please add a model to continue

- : !isApiKeyLongEnoughIfApiKeyExists ?

Please enter a valid API key

- : } -
-
- -
} - - } - - bottom={ - - - {prevAndNextButtons} - - - } - - />, - - // 2.5:
- // - //
Autocomplete
- - //
- //

Void offers free autocomplete with locally hosted models

- //

[have buttons for Ollama install Qwen2.5coder3b and memory requirements]

- - //
- //
- - // {prevAndNextButtons} - //
, - 3: @@ -884,32 +576,11 @@ const VoidOnboardingContent = () => {
} bottom={lastPagePrevAndNextButtons} - // bottom={prevAndNextButtons} />, - // 4: - //
- // { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }} - // ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined} - // className='text-4xl' - // >Enter the Void - //
- // - // } - // bottom={ - // { setPageIndex(pageIndex - 1) }} - // /> - // } - // />, } - return
+ return
{contentOfIdx[pageIndex]} 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 9bef52b3..641d7370 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 @@ -361,127 +361,9 @@ const SimpleModelSettingsDialog = ({ }; -// shows a providerName dropdown if no `providerName` is given -export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => { - - const accessor = useAccessor() - const settingsStateService = accessor.get('IVoidSettingsService') - - const settingsState = useSettingsState() - - const [isOpen, setIsOpen] = useState(false) - const [showCheckmark, setShowCheckmark] = useState(false) - - // const providerNameRef = useRef(null) - const [userChosenProviderName, setUserChosenProviderName] = useState(null) - - const providerName = permanentProviderName ?? userChosenProviderName; - - const [modelName, setModelName] = useState('') - const [errorString, setErrorString] = useState('') - - const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length - - if (showCheckmark) { - return - } - - if (!isOpen) { - return
setIsOpen(true)} - - > -
- {numModels > 0 ? `Add a different model?` : `Add a model`} -
-
- } - return <> -
- - {/* X button - */} - - {/* provider input */} - - {!permanentProviderName && - 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} - /> - } - - - - {/* model input */} - - - - - {/* add button */} - - { - 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('') - }} - /> - - - -
- - {!errorString ? null :
- {errorString} -
} - - - -} - - - - -export const ModelDump = () => { +export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderName[] }) => { const accessor = useAccessor() const settingsStateService = accessor.get('IVoidSettingsService') const settingsState = useSettingsState() @@ -493,9 +375,20 @@ export const ModelDump = () => { type: 'autodetected' | 'custom' | 'default' } | null>(null); + // States for add model functionality + const [isAddModelOpen, setIsAddModelOpen] = useState(false); + const [showCheckmark, setShowCheckmark] = useState(false); + const [userChosenProviderName, setUserChosenProviderName] = useState(null); + const [modelName, setModelName] = useState(''); + const [errorString, setErrorString] = useState(''); + // a dump of all the enabled providers' models const modelDump: (VoidStatefulModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] - for (let providerName of providerNames) { + + // Use either filtered providers or all providers + const providersToShow = filteredProviders || providerNames; + + for (let providerName of providersToShow) { const providerSettings = settingsState.settingsOfProvider[providerName] // if (!providerSettings.enabled) continue modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings }))) @@ -506,6 +399,34 @@ export const ModelDump = () => { return Number(b.providerEnabled) - Number(a.providerEnabled) }) + // Add model handler + const handleAddModel = () => { + if (!userChosenProviderName) { + setErrorString('Please select a provider.'); + return; + } + if (!modelName) { + setErrorString('Please enter a model name.'); + return; + } + + // Check if model already exists + if (settingsState.settingsOfProvider[userChosenProviderName].models.find(m => m.modelName === modelName)) { + setErrorString(`This model already exists.`); + return; + } + + settingsStateService.addModel(userChosenProviderName, modelName); + setShowCheckmark(true); + setTimeout(() => { + setShowCheckmark(false); + setIsAddModelOpen(false); + setUserChosenProviderName(null); + setModelName(''); + }, 1500); + setErrorString(''); + }; + return
{modelDump.map((m, i) => { const { isHidden, type, modelName, providerName, providerEnabled } = m @@ -584,6 +505,82 @@ export const ModelDump = () => {
})} + {/* Add Model Section */} + {showCheckmark ? ( +
+ +
+ ) : isAddModelOpen ? ( +
+
+ + {/* Provider dropdown */} + + 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-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} + /> + + + {/* Model name input */} + + + + + {/* Add button */} + + + + + {/* X button to cancel */} + +
+ + {errorString && ( +
+ {errorString} +
+ )} +
+ ) : ( +
setIsAddModelOpen(true)} + > +
+ + Add a model +
+
+ )} + {/* Model Settings Dialog */} { } -export const OllamaSetupInstructions = () => { +export const OllamaSetupInstructions = ({ sayWeAutoDetect }: { sayWeAutoDetect?: boolean }) => { return
@@ -815,7 +812,7 @@ export const OllamaSetupInstructions = () => { >
-
+ {sayWeAutoDetect &&
}
} @@ -1176,17 +1173,20 @@ export const Settings = () => {

{`Void's Settings`}

- {/* separator */} -
+
{/* Models section (formerly FeaturesTab) */} + + + + +
{/* Models section (formerly FeaturesTab) */}

Models

- - +
@@ -1196,7 +1196,7 @@ export const Settings = () => {

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

- +
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx index 9c2eec45..c4106432 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx @@ -112,11 +112,14 @@ export const VoidTooltip = () => {
For chat:{` `} - llama3.1 + gemma3
-
+
For autocomplete:{` `} - qwen2.5-coder:1.5b + qwen2.5-coder +
+
+ Use the largest version of these you can!
From 786f5707e31613421e2648f38207263668a4cc8c Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 6 May 2025 03:47:18 -0700 Subject: [PATCH 06/14] add keybinds --- .../contrib/void/browser/actionIDs.ts | 16 + .../contrib/void/browser/editCodeService.ts | 1 - .../VoidCommandBar.tsx | 107 +++--- .../contrib/void/browser/sidebarActions.ts | 3 + .../void/browser/voidCommandBarService.ts | 323 +++++++++++++++++- 5 files changed, 380 insertions(+), 70 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/actionIDs.ts b/src/vs/workbench/contrib/void/browser/actionIDs.ts index 5efe6b25..f935d42e 100644 --- a/src/vs/workbench/contrib/void/browser/actionIDs.ts +++ b/src/vs/workbench/contrib/void/browser/actionIDs.ts @@ -8,3 +8,19 @@ export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' export const VOID_ACCEPT_DIFF_ACTION_ID = 'void.acceptDiff' export const VOID_REJECT_DIFF_ACTION_ID = 'void.rejectDiff' + +export const VOID_GOTO_NEXT_DIFF_ACTION_ID = 'void.goToNextDiff' + +export const VOID_GOTO_PREV_DIFF_ACTION_ID = 'void.goToPrevDiff' + +export const VOID_GOTO_NEXT_URI_ACTION_ID = 'void.goToNextUri' + +export const VOID_GOTO_PREV_URI_ACTION_ID = 'void.goToPrevUri' + +export const VOID_ACCEPT_FILE_ACTION_ID = 'void.acceptFile' + +export const VOID_REJECT_FILE_ACTION_ID = 'void.rejectFile' + +export const VOID_ACCEPT_ALL_DIFFS_ACTION_ID = 'void.acceptAllDiffs' + +export const VOID_REJECT_ALL_DIFFS_ACTION_ID = 'void.rejectAllDiffs' diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 00c664a2..56e89cf5 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -2259,7 +2259,6 @@ const processRawKeybindingText = (keybindingStr: string) => { return keybindingStr .replace(/Enter/g, '↵') // ⏎ .replace(/Backspace/g, '⌫') - } class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index 405c147e..1115ef8a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -112,50 +112,13 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const s = commandBarService.stateOfURI[uri.fsPath] if (!s) return const { diffIdx } = s - goToDiffIdx(diffIdx ?? 0) + commandBarService.goToDiffIdx(diffIdx ?? 0) }, 50) }, [uri, commandBarService]) if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files - const getNextDiffIdx = (step: 1 | -1) => { - // check undefined - if (!uri) return null - const s = commandBarState[uri.fsPath] - if (!s) return null - const { diffIdx, sortedDiffIds } = s - // get next idx - const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step) - return nextDiffIdx - } - const goToDiffIdx = (idx: number | null) => { - if (idx === null) return - // check undefined - if (!uri) return - const s = commandBarState[uri.fsPath] - if (!s) return - const { sortedDiffIds } = s - // reveal - const diffid = sortedDiffIds[idx] - if (diffid === undefined) return - const diff = editCodeService.diffOfId[diffid] - if (!diff) return - editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate) - commandBarService.setDiffIdx(uri, idx) - } - const getNextUriIdx = (step: 1 | -1) => { - return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step) - } - const goToURIIdx = async (idx: number | null) => { - if (idx === null) return - const nextURI = sortedCommandBarURIs[idx] - editCodeService.diffAreasOfURI - const { model } = await voidModelService.getModelSafe(nextURI) - if (model) { - // switch to the URI - editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor) - } - } + // Using service methods directly const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : [] @@ -168,10 +131,10 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const streamState = uri ? commandBarService.getStreamState(uri) : null const showAcceptRejectAll = streamState === 'idle-has-changes' - const nextDiffIdx = getNextDiffIdx(1) - const prevDiffIdx = getNextDiffIdx(-1) - const nextURIIdx = getNextUriIdx(1) - const prevURIIdx = getNextUriIdx(-1) + const nextDiffIdx = commandBarService.getNextDiffIdx(1) + const prevDiffIdx = commandBarService.getNextDiffIdx(-1) + const nextURIIdx = commandBarService.getNextUriIdx(1) + const prevURIIdx = commandBarService.getNextUriIdx(-1) const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null const leftRightDisabled = prevURIIdx === null || nextURIIdx === null @@ -188,6 +151,18 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { metricsService.capture('Reject All', {}) } + const onAcceptAll = () => { + commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' }); + metricsService.capture('Accept File', {}) + setShowAcceptRejectAllButtons(false); + } + + const onRejectAll = () => { + commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' }); + metricsService.capture('Reject File', {}) + setShowAcceptRejectAllButtons(false); + } + if (!isADiffZoneInAnyFile) return null return ( @@ -201,17 +176,11 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
{ - onAcceptFile(); - setShowAcceptRejectAllButtons(false); - }} + onClick={onAcceptAll} /> { - onRejectFile(); - setShowAcceptRejectAllButtons(false); - }} + onClick={onRejectAll} />
@@ -221,22 +190,22 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
{/* Diff Navigation Group */} -
+
- + {isADiffInThisFile ? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` : streamState === 'streaming' @@ -248,11 +217,11 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
)} {/* Triple colon menu button */} - {showAcceptRejectAll &&
- - +
setShowAcceptRejectAllButtons(!showAcceptRejectAllButtons)} - /> - + > + +
}
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index a06a344b..ac5513fe 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -132,6 +132,9 @@ registerAction2(class extends Action2 { state: { wasAddedAsCurrentFile: false } }) + await chatThreadService.focusCurrentChat() + + } }) diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index a8b5191f..5d50a62d 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -19,13 +19,15 @@ import { ITextModel } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID } from './actionIDs.js'; +import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID, VOID_GOTO_NEXT_DIFF_ACTION_ID, VOID_GOTO_PREV_DIFF_ACTION_ID, VOID_GOTO_NEXT_URI_ACTION_ID, VOID_GOTO_PREV_URI_ACTION_ID, VOID_ACCEPT_FILE_ACTION_ID, VOID_REJECT_FILE_ACTION_ID, VOID_ACCEPT_ALL_DIFFS_ACTION_ID, VOID_REJECT_ALL_DIFFS_ACTION_ID } from './actionIDs.js'; import { localize2 } from '../../../../nls.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { IMetricsService } from '../common/metricsService.js'; import { KeyMod } from '../../../../editor/common/services/editorBaseApi.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { ScrollType } from '../../../../editor/common/editorCommon.js'; +import { IVoidModelService } from '../common/voidModelService.js'; @@ -41,6 +43,11 @@ export interface IVoidCommandBarService { getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes'; setDiffIdx(uri: URI, newIdx: number | null): void; + getNextDiffIdx(step: 1 | -1): number | null; + getNextUriIdx(step: 1 | -1): number | null; + goToDiffIdx(idx: number | null): void; + goToURIIdx(idx: number | null): Promise; + acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void; anyFileIsStreaming(): boolean; @@ -93,6 +100,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IModelService private readonly _modelService: IModelService, @IEditCodeService private readonly _editCodeService: IEditCodeService, + @IVoidModelService private readonly _voidModelService: IVoidModelService, ) { super(); @@ -374,6 +382,99 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming') } + getNextDiffIdx(step: 1 | -1): number | null { + // If no active URI, return null + if (!this.activeURI) return null; + + const state = this.stateOfURI[this.activeURI.fsPath]; + if (!state) return null; + + const { diffIdx, sortedDiffIds } = state; + + // If no diffs, return null + if (sortedDiffIds.length === 0) return null; + + // Calculate next index with wrapping + const nextIdx = ((diffIdx ?? 0) + step + sortedDiffIds.length) % sortedDiffIds.length; + return nextIdx; + } + + getNextUriIdx(step: 1 | -1): number | null { + // If no URIs with changes, return null + if (this.sortedURIs.length === 0) return null; + + // If no active URI, return first or last based on step + if (!this.activeURI) { + return step === 1 ? 0 : this.sortedURIs.length - 1; + } + + // Find current index + const currentIdx = this.sortedURIs.findIndex(uri => uri.fsPath === this.activeURI?.fsPath); + + // If not found, return first or last based on step + if (currentIdx === -1) { + return step === 1 ? 0 : this.sortedURIs.length - 1; + } + + // Calculate next index with wrapping + const nextIdx = (currentIdx + step + this.sortedURIs.length) % this.sortedURIs.length; + return nextIdx; + } + + goToDiffIdx(idx: number | null): void { + // If null or no active URI, return + if (idx === null || !this.activeURI) return; + + // Get state for the current URI + const state = this.stateOfURI[this.activeURI.fsPath]; + if (!state) return; + + const { sortedDiffIds } = state; + + // Find the diff at the specified index + const diffid = sortedDiffIds[idx]; + if (diffid === undefined) return; + + // Get the diff object + const diff = this._editCodeService.diffOfId[diffid]; + if (!diff) return; + + // Find an active editor to focus + const editor = this._codeEditorService.getFocusedCodeEditor() || + this._codeEditorService.getActiveCodeEditor(); + if (!editor) return; + + // Reveal the line in the editor + editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate); + + // Update the current diff index + this.setDiffIdx(this.activeURI, idx); + } + + async goToURIIdx(idx: number | null): Promise { + // If null or no URIs, return + if (idx === null || this.sortedURIs.length === 0) return; + + // Get the URI at the specified index + const nextURI = this.sortedURIs[idx]; + if (!nextURI) return; + + // Get the model for this URI + const { model } = await this._voidModelService.getModelSafe(nextURI); + if (!model) return; + + // Find an editor to use + const editor = this._codeEditorService.getFocusedCodeEditor() || + this._codeEditorService.getActiveCodeEditor(); + if (!editor) return; + + // Open the URI in the editor + await this._codeEditorService.openCodeEditor( + { resource: model.uri, options: { revealIfVisible: true } }, + editor + ); + } + acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) { const { behavior } = opts // if anything is streaming, do nothing @@ -537,3 +638,223 @@ registerAction2(class extends Action2 { editCodeService.rejectDiff({ diffid: parseInt(diffid) }) } }); + +// Go to next diff action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_GOTO_NEXT_DIFF_ACTION_ID, + f1: true, + title: localize2('voidGoToNextDiffAction', 'Void: Go to Next Diff'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.DownArrow }, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const metricsService = accessor.get(IMetricsService); + + const nextDiffIdx = commandBarService.getNextDiffIdx(1); + if (nextDiffIdx === null) return; + + metricsService.capture('Navigate Diff', { direction: 'next', keyboard: true }); + commandBarService.goToDiffIdx(nextDiffIdx); + } +}); + +// Go to previous diff action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_GOTO_PREV_DIFF_ACTION_ID, + f1: true, + title: localize2('voidGoToPrevDiffAction', 'Void: Go to Previous Diff'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.UpArrow }, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const metricsService = accessor.get(IMetricsService); + + const prevDiffIdx = commandBarService.getNextDiffIdx(-1); + if (prevDiffIdx === null) return; + + metricsService.capture('Navigate Diff', { direction: 'previous', keyboard: true }); + commandBarService.goToDiffIdx(prevDiffIdx); + } +}); + +// Go to next URI action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_GOTO_NEXT_URI_ACTION_ID, + f1: true, + title: localize2('voidGoToNextUriAction', 'Void: Go to Next File with Diffs'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.RightArrow }, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const metricsService = accessor.get(IMetricsService); + + const nextUriIdx = commandBarService.getNextUriIdx(1); + if (nextUriIdx === null) return; + + metricsService.capture('Navigate URI', { direction: 'next', keyboard: true }); + await commandBarService.goToURIIdx(nextUriIdx); + } +}); + +// Go to previous URI action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_GOTO_PREV_URI_ACTION_ID, + f1: true, + title: localize2('voidGoToPrevUriAction', 'Void: Go to Previous File with Diffs'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.LeftArrow }, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const metricsService = accessor.get(IMetricsService); + + const prevUriIdx = commandBarService.getNextUriIdx(-1); + if (prevUriIdx === null) return; + + metricsService.capture('Navigate URI', { direction: 'previous', keyboard: true }); + await commandBarService.goToURIIdx(prevUriIdx); + } +}); + +// Accept current file action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_ACCEPT_FILE_ACTION_ID, + f1: true, + title: localize2('voidAcceptFileAction', 'Void: Accept All Diffs in Current File'), + keybinding: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const editCodeService = accessor.get(IEditCodeService); + const metricsService = accessor.get(IMetricsService); + + const activeURI = commandBarService.activeURI; + if (!activeURI) return; + + metricsService.capture('Accept File', { keyboard: true }); + editCodeService.acceptOrRejectAllDiffAreas({ + uri: activeURI, + behavior: 'accept', + removeCtrlKs: true + }); + } +}); + +// Reject current file action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_REJECT_FILE_ACTION_ID, + f1: true, + title: localize2('voidRejectFileAction', 'Void: Reject All Diffs in Current File'), + keybinding: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const editCodeService = accessor.get(IEditCodeService); + const metricsService = accessor.get(IMetricsService); + + const activeURI = commandBarService.activeURI; + if (!activeURI) return; + + metricsService.capture('Reject File', { keyboard: true }); + editCodeService.acceptOrRejectAllDiffAreas({ + uri: activeURI, + behavior: 'reject', + removeCtrlKs: true + }); + } +}); + +// Accept all diffs in all files action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_ACCEPT_ALL_DIFFS_ACTION_ID, + f1: true, + title: localize2('voidAcceptAllDiffsAction', 'Void: Accept All Diffs in All Files'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const metricsService = accessor.get(IMetricsService); + + if (commandBarService.anyFileIsStreaming()) return; + + metricsService.capture('Accept All Files', { keyboard: true }); + commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' }); + } +}); + +// Reject all diffs in all files action +registerAction2(class extends Action2 { + constructor() { + super({ + id: VOID_REJECT_ALL_DIFFS_ACTION_ID, + f1: true, + title: localize2('voidRejectAllDiffsAction', 'Void: Reject All Diffs in All Files'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace, + weight: KeybindingWeight.VoidExtension, + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandBarService = accessor.get(IVoidCommandBarService); + const metricsService = accessor.get(IMetricsService); + + if (commandBarService.anyFileIsStreaming()) return; + + metricsService.capture('Reject All Files', { keyboard: true }); + commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' }); + } +}); From 02115f5b6cb32201772a43aeadc55067b89331a4 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 6 May 2025 03:52:28 -0700 Subject: [PATCH 07/14] bug in names --- .../react/src/void-editor-widgets-tsx/VoidCommandBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index 1115ef8a..a890a081 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -143,23 +143,23 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const onAcceptFile = () => { if (!uri) return editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) - metricsService.capture('Accept All', {}) + metricsService.capture('Accept File', {}) } const onRejectFile = () => { if (!uri) return editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true }) - metricsService.capture('Reject All', {}) + metricsService.capture('Reject File', {}) } const onAcceptAll = () => { commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' }); - metricsService.capture('Accept File', {}) + metricsService.capture('Accept All', {}) setShowAcceptRejectAllButtons(false); } const onRejectAll = () => { commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' }); - metricsService.capture('Reject File', {}) + metricsService.capture('Reject All', {}) setShowAcceptRejectAllButtons(false); } From 4b40ac1b47bcd6f2a9c914a2699afb9dd655698f Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 6 May 2025 04:33:48 -0700 Subject: [PATCH 08/14] commandbar decent draft --- .../contrib/void/browser/editCodeService.ts | 21 +++---- .../void/browser/editCodeServiceInterface.ts | 2 + .../VoidCommandBar.tsx | 56 ++++++++++++++++--- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 56e89cf5..6bb52d7e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -269,7 +269,11 @@ class EditCodeService extends Disposable implements IEditCodeService { } - + public processRawKeybindingText(keybindingStr: string): string { + return keybindingStr + .replace(/Enter/g, '↵') // ⏎ + .replace(/Backspace/g, '⌫'); + } // private _notifyError = (e: Parameters[0]) => { // const details = errorDetails(e.fullError) @@ -2255,12 +2259,6 @@ registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager); -const processRawKeybindingText = (keybindingStr: string) => { - return keybindingStr - .replace(/Enter/g, '↵') // ⏎ - .replace(/Backspace/g, '⌫') -} - class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { public getId(): string { @@ -2288,7 +2286,8 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { offsetLines: number }, @IVoidCommandBarService private readonly _voidCommandBarService: IVoidCommandBarService, - @IKeybindingService private readonly _keybindingService: IKeybindingService + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IEditCodeService private readonly _editCodeService: IEditCodeService, ) { super(); @@ -2312,8 +2311,10 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { const acceptKeybinding = this._keybindingService.lookupKeybinding(VOID_ACCEPT_DIFF_ACTION_ID); const rejectKeybinding = this._keybindingService.lookupKeybinding(VOID_REJECT_DIFF_ACTION_ID); - const acceptKeybindLabel = processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || ''); - const rejectKeybindLabel = processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '') + // Use the standalone function directly since we're in a nested class that + // can't access EditCodeService's methods + const acceptKeybindLabel = this._editCodeService.processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || ''); + const rejectKeybindLabel = this._editCodeService.processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || ''); const commandBarStateAtUri = this._voidCommandBarService.stateOfURI[uri.fsPath]; const selectedDiffIdx = commandBarStateAtUri?.diffIdx ?? 0; // 0th item is selected by default diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 4d7fafd6..f0712349 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -42,6 +42,8 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; + processRawKeybindingText(keybindingStr: string): string; + callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; startApplying(opts: StartApplyingOpts): [URI, Promise] | null; instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void; diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index a890a081..5604749e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -12,6 +12,16 @@ import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; import { VoidCommandBarProps } from '../../../voidCommandBarService.js'; import { Check, EllipsisVertical, Menu, MoveDown, MoveLeft, MoveRight, MoveUp, X } from 'lucide-react'; +import { + VOID_GOTO_NEXT_DIFF_ACTION_ID, + VOID_GOTO_PREV_DIFF_ACTION_ID, + VOID_GOTO_NEXT_URI_ACTION_ID, + VOID_GOTO_PREV_URI_ACTION_ID, + VOID_ACCEPT_FILE_ACTION_ID, + VOID_REJECT_FILE_ACTION_ID, + VOID_ACCEPT_ALL_DIFFS_ACTION_ID, + VOID_REJECT_ALL_DIFFS_ACTION_ID +} from '../../../actionIDs.js'; export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() @@ -86,6 +96,7 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const commandService = accessor.get('ICommandService') const commandBarService = accessor.get('IVoidCommandBarService') const voidModelService = accessor.get('IVoidModelService') + const keybindingService = accessor.get('IKeybindingService') const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() const [showAcceptRejectAllButtons, setShowAcceptRejectAllButtons] = useState(false) @@ -163,6 +174,27 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { setShowAcceptRejectAllButtons(false); } + + + const _upKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_DIFF_ACTION_ID); + const _downKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_DIFF_ACTION_ID); + const _leftKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_URI_ACTION_ID); + const _rightKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_URI_ACTION_ID); + const _acceptFileKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_FILE_ACTION_ID); + const _rejectFileKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_FILE_ACTION_ID); + const _acceptAllKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_ALL_DIFFS_ACTION_ID); + const _rejectAllKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_ALL_DIFFS_ACTION_ID); + + const upKeybindLabel = editCodeService.processRawKeybindingText(_upKeybinding?.getLabel() || ''); + const downKeybindLabel = editCodeService.processRawKeybindingText(_downKeybinding?.getLabel() || ''); + const leftKeybindLabel = editCodeService.processRawKeybindingText(_leftKeybinding?.getLabel() || ''); + const rightKeybindLabel = editCodeService.processRawKeybindingText(_rightKeybinding?.getLabel() || ''); + const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getLabel() || ''); + const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getLabel() || ''); + const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getLabel() || ''); + const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getLabel() || ''); + + if (!isADiffZoneInAnyFile) return null return ( @@ -175,11 +207,11 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
@@ -201,7 +233,9 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { commandBarService.goToDiffIdx(prevDiffIdx); } }} - title="Previous diff" + data-tooltip-id="void-tooltip" + data-tooltip-content={`Previous diff ${upKeybindLabel ? `${upKeybindLabel}` : ''}`} + data-tooltip-delay-show={500} > @@ -224,7 +258,9 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { commandBarService.goToDiffIdx(nextDiffIdx); } }} - title="Next diff" + data-tooltip-id="void-tooltip" + data-tooltip-content={`Next diff ${downKeybindLabel ? `${downKeybindLabel}` : ''}`} + data-tooltip-delay-show={500} > @@ -244,7 +280,9 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { commandBarService.goToURIIdx(prevURIIdx); } }} - title="Previous file" + data-tooltip-id="void-tooltip" + data-tooltip-content={`Previous file ${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`} + data-tooltip-delay-show={500} > @@ -264,7 +302,9 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { commandBarService.goToURIIdx(nextURIIdx); } }} - title="Next file" + data-tooltip-id="void-tooltip" + data-tooltip-content={`Next file ${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`} + data-tooltip-delay-show={500} > @@ -275,11 +315,11 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { {showAcceptRejectAll && (
From 8816c7194dc7212e84748eef3ee51b9c54d4639c Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 6 May 2025 05:09:44 -0700 Subject: [PATCH 09/14] better tooltips --- .../VoidCommandBar.tsx | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index 5604749e..a4e961e3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -33,16 +33,9 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
} -const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { - if (len === 0) return null - return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset -} - - - -export const AcceptAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( +export const AcceptAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes) => ( ) - -export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( +export const RejectAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes) => ( ) + export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') @@ -189,14 +184,41 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const downKeybindLabel = editCodeService.processRawKeybindingText(_downKeybinding?.getLabel() || ''); const leftKeybindLabel = editCodeService.processRawKeybindingText(_leftKeybinding?.getLabel() || ''); const rightKeybindLabel = editCodeService.processRawKeybindingText(_rightKeybinding?.getLabel() || ''); - const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getLabel() || ''); - const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getLabel() || ''); - const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getLabel() || ''); - const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getLabel() || ''); + const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getAriaLabel() || ''); + const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getAriaLabel() || ''); + const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getAriaLabel() || ''); + const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getAriaLabel() || ''); if (!isADiffZoneInAnyFile) return null + // For pages without a current file index, show a simplified command bar + if (currFileIdx === null) { + return ( +
+
+
+ + {`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`} + +
+ +
+
+ ); + } + return (
@@ -207,11 +229,19 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
+ />
@@ -234,7 +264,7 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } }} data-tooltip-id="void-tooltip" - data-tooltip-content={`Previous diff ${upKeybindLabel ? `${upKeybindLabel}` : ''}`} + data-tooltip-content={`${upKeybindLabel ? `${upKeybindLabel}` : ''}`} data-tooltip-delay-show={500} > @@ -259,7 +289,7 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } }} data-tooltip-id="void-tooltip" - data-tooltip-content={`Next diff ${downKeybindLabel ? `${downKeybindLabel}` : ''}`} + data-tooltip-content={`${downKeybindLabel ? `${downKeybindLabel}` : ''}`} data-tooltip-delay-show={500} > @@ -281,7 +311,7 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } }} data-tooltip-id="void-tooltip" - data-tooltip-content={`Previous file ${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`} + data-tooltip-content={`${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`} data-tooltip-delay-show={500} > @@ -303,7 +333,7 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } }} data-tooltip-id="void-tooltip" - data-tooltip-content={`Next file ${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`} + data-tooltip-content={`${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`} data-tooltip-delay-show={500} > @@ -315,11 +345,19 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { {showAcceptRejectAll && (
From c5862188cb108ec56a4e0f7dd47db7c2f4fd6775 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 6 May 2025 05:19:14 -0700 Subject: [PATCH 10/14] goto next diff after accept/reject --- .../contrib/void/browser/voidCommandBarService.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 5d50a62d..6c0c17a9 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -598,8 +598,13 @@ registerAction2(class extends Action2 { if (!diffid) return; metricsService.capture('Accept Diff', { diffid, keyboard: true }); - editCodeService.acceptDiff({ diffid: parseInt(diffid) }) + editCodeService.acceptDiff({ diffid: parseInt(diffid) }); + // After accepting the diff, navigate to the next diff + const nextDiffIdx = commandBarService.getNextDiffIdx(1); + if (nextDiffIdx !== null) { + commandBarService.goToDiffIdx(nextDiffIdx); + } } }); @@ -635,7 +640,13 @@ registerAction2(class extends Action2 { if (!diffid) return; metricsService.capture('Reject Diff', { diffid, keyboard: true }); - editCodeService.rejectDiff({ diffid: parseInt(diffid) }) + editCodeService.rejectDiff({ diffid: parseInt(diffid) }); + + // After rejecting the diff, navigate to the next diff + const nextDiffIdx = commandBarService.getNextDiffIdx(1); + if (nextDiffIdx !== null) { + commandBarService.goToDiffIdx(nextDiffIdx); + } } }); From fa83dfc5298be16d7c600acc6c13e4654008e45f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 6 May 2025 03:07:42 -0700 Subject: [PATCH 11/14] onboarding improvements --- .../src/void-onboarding/VoidOnboarding.tsx | 82 +++++++++++++------ .../contrib/void/browser/sidebarActions.ts | 4 +- .../contrib/void/common/voidSettingsTypes.ts | 8 +- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 7bf94b47..d03556a1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from 'react'; import { useAccessor, useIsDark, useSettingsState } from '../util/services.js'; import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react'; -import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName } from '../../../../common/voidSettingsTypes.js'; +import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js'; import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'; @@ -113,21 +113,10 @@ const providerNamesOfTab: Record = { const descriptionOfTab: Record = { Free: `Providers with a 100% free tier. Add as many as you'd like!`, Paid: `Connect directly with any provider (bring your own key).`, - Local: `Add as many local providers as you'd like! Running providers should appear automatically.`, + Local: `Add as many local providers as you'd like! Active providers should appear automatically.`, 'Cloud/Other': `Reach out for custom configuration requests.`, }; -const subtextMdOfTab: Record = { - Free: ` -Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500. -We recommend using models down the line as you run out of free credits. More information [here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits). - -OpenRouter offers 50 free messages a day, and that increases to 1000 if you deposit $10. Only applies to models labeled \`:free\`. More information [here](https://openrouter.ai/docs/api-reference/limits). -`, - Paid: null, - Local: null, - 'Cloud/Other': null, -}; const featureNameMap: { display: string, featureName: FeatureName }[] = [ { display: 'Chat', featureName: 'Chat' }, @@ -139,6 +128,7 @@ const featureNameMap: { display: string, featureName: FeatureName }[] = [ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => { const [currentTab, setCurrentTab] = useState('Free'); const settingsState = useSettingsState(); + const [errorMessage, setErrorMessage] = useState(null); return (
@@ -153,7 +143,10 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP ? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm' : 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1' } transition-all duration-200`} - onClick={() => setCurrentTab(tab as TabName)} + onClick={() => { + setCurrentTab(tab as TabName); + setErrorMessage(null); // Reset error message when changing tabs + }} > {tab} @@ -161,12 +154,18 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
{/* Feature Checklist */} -
+
{featureNameMap.map(({ display, featureName }) => { const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null; return (
- {hasModel ? : } + {hasModel ? ( + + ) : ( +
+
+
+ )} {display}
); @@ -178,9 +177,6 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
{currentTab}
{descriptionOfTab[currentTab]}
- {subtextMdOfTab[currentTab] ?
- -
: null} {providerNamesOfTab[currentTab].map((providerName) => (
@@ -191,18 +187,58 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP ))} {(currentTab === 'Local' || currentTab === 'Cloud/Other') && ( -
-
Models
+
+
+
Models
+
+
+ + {currentTab === 'Local' && ( +
+ Local models should be detected automatically. You can add custom models below. +
+ )} + {currentTab === 'Local' && } {currentTab === 'Cloud/Other' && }
)} + + {currentTab === 'Free' &&
+ +
+
+
+
+
+ } {/* Navigation buttons in right column */} -
+
+ {errorMessage && ( +
{errorMessage}
+ )}
setPageIndex(pageIndex - 1)} /> - setPageIndex(pageIndex + 1)} /> + { + const isDisabled = isFeatureNameDisabled('Chat', settingsState) + + if (!isDisabled) { + setPageIndex(pageIndex + 1); + setErrorMessage(null); + } else { + // Show error message + setErrorMessage("Please set up at least one Chat model first."); + } + }} + />
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index ac5513fe..72e5bf17 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -129,7 +129,7 @@ registerAction2(class extends Action2 { uri: model.uri, language: model.getLanguageId(), range: [selectionRange.startLineNumber, selectionRange.endLineNumber], - state: { wasAddedAsCurrentFile: false } + state: { wasAddedAsCurrentFile: false }, }) await chatThreadService.focusCurrentChat() @@ -196,7 +196,7 @@ registerAction2(class extends Action2 { uri: model.uri, language: model.getLanguageId(), range: [selectionRange.startLineNumber, selectionRange.endLineNumber], - state: { wasAddedAsCurrentFile: false } + state: { wasAddedAsCurrentFile: false }, }) } }) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index dcae2587..d7ba0024 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -92,7 +92,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn return { title: 'Groq', } } else if (providerName === 'xAI') { - return { title: 'xAI', } + return { title: 'Grok', } } else if (providerName === 'mistral') { return { title: 'Mistral', } @@ -120,9 +120,9 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => { if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).` if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).' if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).' - if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' - if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' - if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).' + if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' + if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' + if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).' if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).' throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`) From 097d89366cb70b44de164762400586a119b7349a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 6 May 2025 05:38:57 -0700 Subject: [PATCH 12/14] file and folder dump! --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../browser/convertToLLMMessageService.ts | 13 +- .../void/browser/react/src/util/inputs.tsx | 50 ++++- .../src/void-onboarding/VoidOnboarding.tsx | 26 ++- .../react/src/void-settings-tsx/Settings.tsx | 10 +- .../contrib/void/browser/toolsService.ts | 2 +- .../directoryStrService.ts | 186 ++++++++++++------ .../contrib/void/common/prompt/prompts.ts | 81 +++++--- .../contrib/void/common/voidSettingsTypes.ts | 2 +- 9 files changed, 266 insertions(+), 112 deletions(-) rename src/vs/workbench/contrib/void/{browser => common}/directoryStrService.ts (74%) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 49a6dd09..db482108 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -35,6 +35,8 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; import { timeout } from '../../../../base/common/async.js'; import { deepClone } from '../../../../base/common/objects.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IDirectoryStrService } from '../common/directoryStrService.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; // related to retrying when LLM message has error @@ -319,6 +321,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { @INotificationService private readonly _notificationService: INotificationService, @IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService, + @IFileService private readonly _fileService: IFileService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -1189,14 +1193,12 @@ We only need to do it for files that were edited since `from`, ie files between this._addUserCheckpoint({ threadId }) } - const { chatMode } = this._settingsService.state.globalSettings // add user's message to chat history const instructions = userMessage const currSelns: StagingSelectionItem[] = _chatSelections ?? thread.state.stagingSelections - const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const - const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content) + const userMessageContent = await chat_userMessageContent(instructions, currSelns, { directoryStrService: this._directoryStringService, fileService: this._fileService }) // user message + names of files (NOT content) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index c4ffa286..48b21b77 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -11,7 +11,7 @@ import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/p import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js'; -import { IDirectoryStrService } from './directoryStrService.js'; +import { IDirectoryStrService } from '../common/directoryStrService.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IVoidModelService } from '../common/voidModelService.js'; import { URI } from '../../../../base/common/uri.js'; @@ -38,7 +38,7 @@ type SimpleLLMMessage = { const EMPTY_MESSAGE = '(empty message)' -const CHARS_PER_TOKEN = 4 +const CHARS_PER_TOKEN = 4 // assume abysmal chars per token const TRIM_TO_LEN = 120 @@ -271,7 +271,10 @@ const prepareOpenAIOrAnthropicMessages = ({ reservedOutputTokenSpace: number | null | undefined, }): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => { - reservedOutputTokenSpace = reservedOutputTokenSpace ?? 4_096 // default to 4096 + reservedOutputTokenSpace = Math.max( + contextWindow * 1 / 2, // reserve at least 1/4 of the token window length + reservedOutputTokenSpace ?? 4_096 // defaults to 4096 + ) let messages: (SimpleLLMMessage | { role: 'system', content: string })[] = deepClone(messages_) // ================ system message ================ @@ -337,7 +340,7 @@ const prepareOpenAIOrAnthropicMessages = ({ for (const m of messages) { totalLen += m.content.length } const charsNeedToTrim = totalLen - Math.max( (contextWindow - reservedOutputTokenSpace) * CHARS_PER_TOKEN, // can be 0, in which case charsNeedToTrim=everything, bad - 4_096 // ensure we don't trim at least 4096 chars (just a random small value) + 5_000 // ensure we don't trim at least 5k chars (just a random small value) ) @@ -359,6 +362,7 @@ const prepareOpenAIOrAnthropicMessages = ({ // if can finish here, do const numCharsWillTrim = m.content.length - TRIM_TO_LEN if (numCharsWillTrim > remainingCharsToTrim) { + // trim remainingCharsToTrim + '...'.length chars m.content = m.content.slice(0, m.content.length - remainingCharsToTrim - '...'.length).trim() + '...' break } @@ -581,6 +585,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess `...Directories string cut off, use tools to read more...` : `...Directories string cut off, ask user for more if necessary...` }) + const includeXMLToolDefinitions = !specialToolFormat const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds() diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 8f98bdac..3788e62f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -249,7 +249,6 @@ const getOptionsAtPath = async (accessor: ReturnType, path: for (let i = 0; i < pathParts.length - 1; i++) { currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`; - console.log('filepath', currentPath); // Create a proper directory URI const directoryUri = URI.joinPath( @@ -420,17 +419,21 @@ export const VoidInputBox2 = forwardRef(fun setIsMenuOpen(false) insertTextAtCursor(option.abbreviatedName) - const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? { + let newSelection: StagingSelectionItem + if (option.leafNodeType === 'File') newSelection = { type: 'File', uri: option.uri, language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '', - state: { wasAddedAsCurrentFile: false } - } : option.leafNodeType === 'Folder' ? { + state: { wasAddedAsCurrentFile: false }, + } + else if (option.leafNodeType === 'Folder') newSelection = { type: 'Folder', uri: option.uri, language: undefined, state: undefined, - } : (undefined as never) + } + else throw new Error(`Unexpected leafNodeType ${option.leafNodeType}`) + chatThreadService.addNewStagingSelection(newSelection) console.log('selected', option.uri?.fsPath) } @@ -868,15 +871,44 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam compact?: boolean; passwordBlur?: boolean; } & React.InputHTMLAttributes) => { + // Create a ref for the input element to maintain the same DOM node between renders + const inputRef = useRef(null); + + // Track if we need to restore selection + const selectionRef = useRef<{ start: number | null, end: number | null }>({ + start: null, + end: null + }); + + // Handle value changes without recreating the input + useEffect(() => { + const input = inputRef.current; + if (input && input.value !== value) { + // Store current selection positions + selectionRef.current.start = input.selectionStart; + selectionRef.current.end = input.selectionEnd; + + // Update the value + input.value = value; + + // Restore selection if we had it before + if (selectionRef.current.start !== null && selectionRef.current.end !== null) { + input.setSelectionRange(selectionRef.current.start, selectionRef.current.end); + } + } + }, [value]); + + const handleChange = useCallback((e: React.ChangeEvent) => { + onChangeValue(e.target.value); + }, [onChangeValue]); return ( onChangeValue(e.target.value)} + ref={inputRef} + defaultValue={value} // Use defaultValue instead of value to avoid recreation + onChange={handleChange} placeholder={placeholder} disabled={disabled} - // className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root' - // className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm className={`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 ${compact ? 'py-1 px-2' : 'py-2 px-4 '} rounded diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index d03556a1..1cbb5081 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -130,6 +130,24 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP const settingsState = useSettingsState(); const [errorMessage, setErrorMessage] = useState(null); + // Clear error message after 5 seconds + useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null; + + if (errorMessage) { + timeoutId = setTimeout(() => { + setErrorMessage(null); + }, 5000); + } + + // Cleanup function to clear the timeout if component unmounts or error changes + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [errorMessage]); + return (
{/* Left Column - Fixed */} @@ -192,13 +210,13 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
Models
- + {currentTab === 'Local' && (
Local models should be detected automatically. You can add custom models below.
)} - + {currentTab === 'Local' && } {currentTab === 'Cloud/Other' && }
@@ -222,7 +240,7 @@ Only applies to models labeled \`:free\`. More information [here](https://openro {/* Navigation buttons in right column */}
{errorMessage && ( -
{errorMessage}
+
{errorMessage}
)}
setPageIndex(pageIndex - 1)} /> @@ -235,7 +253,7 @@ Only applies to models labeled \`:free\`. More information [here](https://openro setErrorMessage(null); } else { // Show error message - setErrorMessage("Please set up at least one Chat model first."); + setErrorMessage("Please set up at least one Chat model before moving on."); } }} /> 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 641d7370..2771cae1 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 @@ -607,15 +607,17 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam console.log('Error: Provider setting had a non-string value.') return } + + // Create a stable callback reference using useCallback with proper dependencies + const handleChangeValue = useCallback((newVal: string) => { + voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) + }, [voidSettingsService, providerName, settingName]); return
{ - voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) - }, [voidSettingsService, providerName, settingName])} - // placeholder={`${providerTitle} ${settingTitle} (${placeholder})`} + onChangeValue={handleChangeValue} placeholder={`${settingTitle} (${placeholder})`} passwordBlur={isPasswordField} compact={true} diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index a9d25dd1..ad6944d9 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -12,7 +12,7 @@ import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsSe import { IVoidModelService } from '../common/voidModelService.js' import { EndOfLinePreference } from '../../../../editor/common/model.js' import { IVoidCommandBarService } from './voidCommandBarService.js' -import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js' +import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from '../common/directoryStrService.js' import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js' import { timeout } from '../../../../base/common/async.js' import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/common/directoryStrService.ts similarity index 74% rename from src/vs/workbench/contrib/void/browser/directoryStrService.ts rename to src/vs/workbench/contrib/void/common/directoryStrService.ts index 80f70373..f661eca8 100644 --- a/src/vs/workbench/contrib/void/browser/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/common/directoryStrService.ts @@ -7,13 +7,10 @@ import { URI } from '../../../../base/common/uri.js'; 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 { IFileService } from '../../../../platform/files/common/files.js'; +import { IFileService, IFileStat } from '../../../../platform/files/common/files.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'; -import { IExplorerService } from '../../files/browser/files.js'; -import { SortOrder } from '../../files/common/files.js'; -import { ExplorerItem } from '../../files/common/explorerModel.js'; -import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js'; +import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; +import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js'; const MAX_FILES_TOTAL = 1000; @@ -28,8 +25,10 @@ const DEFAULT_MAX_ITEMS_PER_DIR = 3; export interface IDirectoryStrService { readonly _serviceBrand: undefined; - getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise - getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise + getDirectoryStrTool(uri: URI): Promise + getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise + + getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise } export const IDirectoryStrService = createDecorator('voidDirectoryStrService'); @@ -38,35 +37,35 @@ export const IDirectoryStrService = createDecorator('voidD // Check if it's a known filtered type like .git -const shouldExcludeDirectory = (item: ExplorerItem) => { - if (item.name === '.git' || - item.name === 'node_modules' || - item.name.startsWith('.') || - item.name === 'dist' || - item.name === 'build' || - item.name === 'out' || - item.name === 'bin' || - item.name === 'coverage' || - item.name === '__pycache__' || - item.name === 'env' || - item.name === 'venv' || - item.name === 'tmp' || - item.name === 'temp' || - item.name === 'artifacts' || - item.name === 'target' || - item.name === 'obj' || - item.name === 'vendor' || - item.name === 'logs' || - item.name === 'cache' || - item.name === 'resource' || - item.name === 'resources' +const shouldExcludeDirectory = (name: string) => { + if (name === '.git' || + name === 'node_modules' || + name.startsWith('.') || + name === 'dist' || + name === 'build' || + name === 'out' || + name === 'bin' || + name === 'coverage' || + name === '__pycache__' || + name === 'env' || + name === 'venv' || + name === 'tmp' || + name === 'temp' || + name === 'artifacts' || + name === 'target' || + name === 'obj' || + name === 'vendor' || + name === 'logs' || + name === 'cache' || + name === 'resource' || + name === 'resources' ) { return true; } - if (item.name.match(/\bout\b/)) return true - if (item.name.match(/\bbuild\b/)) return true + if (name.match(/\bout\b/)) return true + if (name.match(/\bbuild\b/)) return true return false; } @@ -138,10 +137,16 @@ export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], re // ---------- IN GENERAL ---------- +const resolveChildren = async (children: undefined | IFileStat[], fileService: IFileService): Promise => { + const res = await fileService.resolveAll(children ?? []) + const stats = res.map(s => s.success ? s.stat : null).filter(s => !!s) + return stats +} + // Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering const computeAndStringifyDirectoryTree = async ( - eItem: ExplorerItem, - explorerService: IExplorerService, + eItem: IFileStat, + fileService: IFileService, MAX_CHARS: number, fileCount: { count: number } = { count: 0 }, options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {} @@ -181,12 +186,13 @@ const computeAndStringifyDirectoryTree = async ( let remainingChars = MAX_CHARS - nodeLine.length; // Check if it's a directory we should skip - const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem); + const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem.name); + // Fetch and process children if not a filtered directory if (eItem.isDirectory && !isGitIgnoredDirectory) { // Fetch children with Modified sort order to show recently modified first - const eChildren = await eItem.fetchChildren(SortOrder.Modified); + const eChildren = await resolveChildren(eItem.children, fileService) // Then recursively add all children with proper tree formatting if (eChildren && eChildren.length > 0) { @@ -194,7 +200,7 @@ const computeAndStringifyDirectoryTree = async ( eChildren, remainingChars, '', - explorerService, + fileService, fileCount, { maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function ); @@ -208,10 +214,10 @@ const computeAndStringifyDirectoryTree = async ( // Helper function to render children with proper tree formatting const renderChildrenCombined = async ( - children: ExplorerItem[], + children: IFileStat[], maxChars: number, parentPrefix: string, - explorerService: IExplorerService, + fileService: IFileService, fileCount: { count: number }, options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number } ): Promise<{ childrenContent: string, childrenCutOff: boolean }> => { @@ -263,12 +269,12 @@ const renderChildrenCombined = async ( const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ '); // Skip processing children for git ignored directories - const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child); + const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child.name); // Create the prefix for the next level (continuation line or space) if (child.isDirectory && !isGitIgnoredDirectory) { // Fetch children with Modified sort order to show recently modified first - const eChildren = await child.fetchChildren(SortOrder.Modified); + const eChildren = await resolveChildren(child.children, fileService) if (eChildren && eChildren.length > 0) { const { @@ -278,7 +284,7 @@ const renderChildrenCombined = async ( eChildren, remainingChars, nextLevelPrefix, - explorerService, + fileService, fileCount, { maxDepth, currentDepth: nextDepth, maxItemsPerDir } ); @@ -311,7 +317,68 @@ const renderChildrenCombined = async ( }; -// --------------------------------------------------- +// ------------------------- FOLDERS ------------------------- + +export async function getAllUrisInDirectory( + directoryUri: URI, + maxResults: number, + fileService: IFileService, +): Promise { + const result: URI[] = []; + + // Helper function to recursively collect URIs + async function visitAll(folderStat: IFileStat): Promise { + // Stop if we've reached the limit + if (result.length >= maxResults) { + return false; + } + + try { + + if (!folderStat.isDirectory || !folderStat.children) { + return true; + } + + const eChildren = await resolveChildren(folderStat.children, fileService) + + // Process files first (common convention to list files before directories) + for (const child of eChildren) { + if (!child.isDirectory) { + result.push(child.resource); + + // Check if we've hit the limit + if (result.length >= maxResults) { + return false; + } + } + } + + // Then process directories recursively + for (const child of eChildren) { + const isGitIgnored = shouldExcludeDirectory(child.name) + if (child.isDirectory && !isGitIgnored) { + const shouldContinue = await visitAll(child); + if (!shouldContinue) { + return false; + } + } + } + + return true; + } catch (error) { + console.error(`Error processing directory ${folderStat.resource.fsPath}: ${error}`); + return true; // Continue despite errors in a specific directory + } + } + + const rootStat = await fileService.resolve(directoryUri) + await visitAll(rootStat); + return result; +} + + + +// -------------------------------------------------- class DirectoryStrService extends Disposable implements IDirectoryStrService { @@ -319,21 +386,25 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { constructor( @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExplorerService private readonly explorerService: IExplorerService, + @IFileService private readonly fileService: IFileService, ) { super(); } - async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) { - const eRoot = this.explorerService.findClosest(uri) + async getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise { + return getAllUrisInDirectory(uri, opts.maxResults, this.fileService) + } + + async getDirectoryStrTool(uri: URI) { + const eRoot = await this.fileService.resolve(uri) if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`) - const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR + const maxItemsPerDir = START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR // First try with START_MAX_DEPTH const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree( eRoot, - this.explorerService, + this.fileService, MAX_DIRSTR_CHARS_TOTAL_TOOL, { count: 0 }, { maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir } @@ -344,7 +415,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { if (initialCutOff) { const result = await computeAndStringifyDirectoryTree( eRoot, - this.explorerService, + this.fileService, MAX_DIRSTR_CHARS_TOTAL_TOOL, { count: 0 }, { maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR } @@ -363,7 +434,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { return c } - async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) { + async getAllDirectoriesStr({ cutOffMessage, }: { cutOffMessage: string, }) { let str: string = ''; let cutOff = false; const folders = this.workspaceContextService.getWorkspace().folders; @@ -371,7 +442,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { return '(NO WORKSPACE OPEN)'; // Use START_MAX_ITEMS_PER_DIR if not specified - const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; + const startMaxItemsPerDir = START_MAX_ITEMS_PER_DIR; for (let i = 0; i < folders.length; i += 1) { if (i > 0) str += '\n'; @@ -381,13 +452,13 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { str += `Directory of ${f.uri.fsPath}:\n`; const rootURI = f.uri; - const eRoot = this.explorerService.findClosestRoot(rootURI); + const eRoot = await this.fileService.resolve(rootURI) if (!eRoot) continue; // First try with START_MAX_DEPTH and startMaxItemsPerDir const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree( eRoot, - this.explorerService, + this.fileService, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length, { count: 0 }, { maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir } @@ -398,7 +469,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { if (initialCutOff) { const result = await computeAndStringifyDirectoryTree( eRoot, - this.explorerService, + this.fileService, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length, { count: 0 }, { maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR } @@ -417,11 +488,8 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { } } - if (cutOff) { - return `${str.trimEnd()}\n${cutOffMessage}` - } - - return str + const ans = cutOff ? `${str.trimEnd()}\n${cutOffMessage}` : str + return ans } } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 14544216..bbc4e2c3 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -3,12 +3,13 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { EndOfLinePreference } from '../../../../../editor/common/model.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IDirectoryStrService } from '../directoryStrService.js'; import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { os } from '../helpers/systemInfo.js'; import { RawToolParamsObj } from '../sendLLMMessageTypes.js'; import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js'; -import { IVoidModelService } from '../voidModelService.js'; import { ChatMode } from '../voidSettingsTypes.js'; // Triple backtick wrapper used throughout the prompts for code blocks @@ -524,40 +525,66 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`) // chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', })) // } +const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{ + val: string, + truncated: boolean, + fullFileLen: number, +} | { + val: null, + truncated?: undefined + fullFileLen?: undefined, +}> => { + try { + const fileContent = await fileService.readFile(uri) + const val = fileContent.value.toString() + if (val.length > fileSizeLimit) return { val: val.substring(0, fileSizeLimit), truncated: true, fullFileLen: val.length } + return { val, truncated: false, fullFileLen: val.length } + } + catch (e) { + return { val: null } + } +} + + + + + + + export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null, - opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService } + opts: { directoryStrService: IDirectoryStrService, fileService: IFileService } ) => { const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})` let selnsStrs: string[] = [] - if (opts.type === 'references') { - selnsStrs = currSelns?.map((s) => { - if (s.type === 'File') return `${s.uri.fsPath}` - if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}` - if (s.type === 'Folder') return `${s.uri.fsPath}/` - return '' - }) ?? [] - } - if (opts.type === 'fullCode') { - selnsStrs = await Promise.all(currSelns?.map(async (s) => { - if (s.type === 'File' || s.type === 'CodeSelection') { - const voidModelService = opts.voidModelService - const { model } = await voidModelService.getModelSafe(s.uri) - if (!model) return '' - const val = model.getValue(EndOfLinePreference.LF) + selnsStrs = await Promise.all(currSelns?.map(async (s) => { - const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : '' - const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}` + if (s.type === 'File' || s.type === 'CodeSelection') { + const { val } = await readFile(opts.fileService, s.uri, 2_000_000) + const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : '' + const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}` + const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}` + return str + } + else if (s.type === 'Folder') { + const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri) + const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}` + + const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: 1_000 }) + const strOfFiles = await Promise.all(uris.map(async uri => { + const { val, truncated } = await readFile(opts.fileService, uri, 100_000) + const truncationStr = truncated ? `\n... file truncated ...` : '' + const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}` + const str = `${uri.fsPath}:\n${content}` return str - } - if (s.type === 'Folder') { - // TODO - return '' - } + })) + const contentStr = [folderStructure, ...strOfFiles].join('\n\n') + return contentStr + } + else return '' - }) ?? []) - } + }) ?? []) const selnsStr = selnsStrs.join('\n') ?? '' let str = '' diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index d7ba0024..dc60602d 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -92,7 +92,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn return { title: 'Groq', } } else if (providerName === 'xAI') { - return { title: 'Grok', } + return { title: 'Grok (xAI)', } } else if (providerName === 'mistral') { return { title: 'Mistral', } From 2de4cacf0fa3e767229308b4a198f75c8418bb6d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 6 May 2025 05:39:26 -0700 Subject: [PATCH 13/14] 1.3.2 --- product.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product.json b/product.json index 1cc09ea6..6b3e7bdd 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,7 @@ { "nameShort": "Void", "nameLong": "Void", - "voidVersion": "1.3.0", + "voidVersion": "1.3.2", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", From 84f95170cb46b004ecf54d6f5f692fa0e29bf3d8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 6 May 2025 05:58:30 -0700 Subject: [PATCH 14/14] finish --- .../react/src/void-editor-widgets-tsx/VoidCommandBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index a4e961e3..9d57e443 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -212,7 +212,7 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } }} > - Open + Next