From a5b43ce1465c87958d6db82335997f67a8752603 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 5 May 2025 21:44:40 -0700 Subject: [PATCH] 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'