diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 8a589aa0..e768fe6d 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1226,8 +1226,6 @@ class EditCodeService extends Disposable implements IEditCodeService { // treat like full file, unless linkedCtrlKZone was provided in which case use its diff's range - - const startLine = linkedCtrlKZone ? linkedCtrlKZone.startLine : 1 const endLine = linkedCtrlKZone ? linkedCtrlKZone.endLine : model.getLineCount() const range = { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER } @@ -1291,7 +1289,16 @@ class EditCodeService extends Disposable implements IEditCodeService { - + private _uriIsStreaming(uri: URI) { + const diffAreas = this.diffAreasOfURI[uri.fsPath] + if (!diffAreas) return false + for (const diffareaid of diffAreas) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea?.type !== 'DiffZone') continue + if (diffArea._streamState.isStreaming) return true + } + return false + } private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise] | undefined> { @@ -1358,7 +1365,8 @@ class EditCodeService extends Disposable implements IEditCodeService { } else { throw new Error(`featureName ${from} is invalid`) } - + // if URI is already streaming, return (should never happen, caller is responsible for checking) + if (this._uriIsStreaming(uri)) return // start diffzone const res = this._startStreamingDiffZone({ @@ -1526,6 +1534,9 @@ class EditCodeService extends Disposable implements IEditCodeService { { role: 'user', content: userMessageContent }, ] + // if URI is already streaming, return (should never happen, caller is responsible for checking) + if (this._uriIsStreaming(uri)) return + // start diffzone const res = this._startStreamingDiffZone({ uri, 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 f7104242..01fc042c 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 @@ -10,7 +10,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI } from '../util/services.js'; +import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState } from '../util/services.js'; import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js'; import { URI } from '../../../../../../../base/common/uri.js'; @@ -762,6 +762,50 @@ const ToolHeaderWrapper = ({ }; + + +const SimplifiedToolHeader = ({ + title, + children, +}: { + title: string; + children?: React.ReactNode; +}) => { + const [isOpen, setIsOpen] = useState(false); + const isDropdown = children !== undefined; + return ( +
+
+ {/* header */} +
{ + if (isDropdown) { setIsOpen(v => !v); } + }} + > + {isDropdown && ( + + )} +
+ {title} +
+
+ {/* children */} + {
+ {children} +
} +
+
+ ); +}; + + + + const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCommitted: boolean, }) => { const accessor = useAccessor() @@ -1812,6 +1856,34 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin } + + +const CommandBarInChat = () => { + const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + const [isExpanded, setIsExpanded] = useState(false) + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + if (!sortedCommandBarURIs || sortedCommandBarURIs.length === 0) { + return null + } + + return ( + + {sortedCommandBarURIs.map((uri, i) => ( + { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + /> + ))} + + + ) +} + + export const SidebarChat = () => { const textAreaRef = useRef(null) const textAreaFnsRef = useRef(null) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 2eebdfbf..057261b8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -45,13 +45,22 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + // latestUriIdx is used to remember place in leftRight + const _latestValidUriIdxRef = useRef(null) - // changes if the user clicks left/right or if the user goes on a uri with changes - const [currUriIdx, setUriIdx] = useState(null) + // i is the current index of the URI in sortedCommandBarURIs + const i_ = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) + const currFileIdx = i_ === -1 ? null : i_ useEffect(() => { - const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) - if (i !== -1) { setUriIdx(i) } - }, [sortedCommandBarURIs, uri]) + if (currFileIdx !== null) _latestValidUriIdxRef.current = currFileIdx + }, [currFileIdx]) + + const uriIdxInStepper = currFileIdx !== null ? currFileIdx // use currFileIdx if it exists, else use latestNotNullUriIdxRef + : _latestValidUriIdxRef.current === null ? null + : _latestValidUriIdxRef.current < sortedCommandBarURIs.length ? _latestValidUriIdxRef.current + : null + + const getNextDiffIdx = (step: 1 | -1) => { // check undefined @@ -74,14 +83,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const diffid = sortedDiffIds[idx] const diff = editCodeService.diffOfId[diffid] const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; - editor.revealRange(range, ScrollType.Immediate) + editor.revealRangeInCenter(range, ScrollType.Immediate) commandBarService.setDiffIdx(uri, idx) } } - - const getNextUriIdx = (step: 1 | -1) => { - return stepIdx(currUriIdx, sortedCommandBarURIs.length, step) + return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step) } const goToURIIdx = async (idx: number | null) => { if (idx === null) return @@ -101,13 +108,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { setTimeout(() => { // check undefined if (!uri) return - const s = commandBarState[uri.fsPath] + const s = commandBarService.stateOfURI[uri.fsPath] if (!s) return const { diffIdx } = s goToDiffIdx(diffIdx) }, 50) - - }, [uri]) + }, [uri, commandBarService]) const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null @@ -115,11 +121,6 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : [] - const nextDiffIdx = getNextDiffIdx(1) - const prevDiffIdx = getNextDiffIdx(-1) - const nextURIIdx = getNextUriIdx(1) - const prevURIIdx = getNextUriIdx(-1) - const isAChangeInThisFile = sortedDiffIds.length !== 0 const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0 const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0 @@ -127,14 +128,13 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const streamState = uri ? commandBarService.getStreamState(uri) : null const showAcceptRejectAll = streamState === 'idle-has-changes' - - if (!isADiffZoneInAnyFile) { // no changes for the user to accept - return null - } - + const nextDiffIdx = getNextDiffIdx(1) + const prevDiffIdx = getNextDiffIdx(-1) + const nextURIIdx = getNextUriIdx(1) + const prevURIIdx = getNextUriIdx(-1) const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null - const leftRightDisabled = prevURIIdx === null || currUriIdx === null + const leftRightDisabled = prevURIIdx === null || nextURIIdx === null const upButton = - const filesDescription = (isADiffZoneInThisFile ? - currUriIdx !== null && sortedCommandBarURIs.length !== 0 && - `File ${currUriIdx + 1} of ${sortedCommandBarURIs.length}` - : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` - ); - - const changesDescription = (isADiffZoneInThisFile ? - isAChangeInThisFile ? - `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` - : `No changes` - : '' - ); // accept/reject if current URI has changes const onAcceptAll = () => { @@ -231,6 +219,8 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } + if (!isADiffZoneInAnyFile) return null + const acceptAllButton = + const acceptRejectAllButtons =
+ {acceptAllButton} + {rejectAllButton} +
// const closeCommandBar = useCallback(() => { // commandService.executeCommand('void.hideCommandBar'); @@ -283,41 +277,43 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { // >x // - const gridLayout =
- {/* First row */} - {filesDescription && -
- {leftButton} -
{/* Divider */} - {rightButton} -
{/* Divider */} -
{filesDescription}
-
- } + const leftRightUpDownButtons =
+
+ {/* Changes in file */} + {isADiffZoneInThisFile && +
+ {downButton} + {upButton} +
+ {isAChangeInThisFile ? + `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` + : `No changes` + } +
+
+ } - {/* Second row */} - {changesDescription && -
- {upButton} -
{/* Divider */} - {downButton} -
{/* Divider */} -
{changesDescription}
-
- } + {/* Files */} + {isADiffZoneInAnyFile && +
+ {leftButton} + {/*
*/} + {rightButton} + {/*
*/} +
+ {currFileIdx !== null ? + `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` + : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` + } +
+
+ } +
- return
- {showAcceptRejectAll && -
- {acceptAllButton} - {rejectAllButton} -
- } -
- {gridLayout} - {/* {oldLayout} */} -
+ return
+ {showAcceptRejectAll && acceptRejectAllButtons} + {leftRightUpDownButtons}
} diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 271322d5..45048458 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -12,6 +12,7 @@ import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../ import { IVoidModelService } from '../common/voidModelService.js' import { EndOfLinePreference } from '../../../../editor/common/model.js' import { basename } from '../../../../base/common/path.js' +import { IVoidCommandBarService } from './voidCommandBarService.js' // tool use for AI @@ -193,6 +194,7 @@ export class ToolsService implements IToolsService { @IVoidModelService voidModelService: IVoidModelService, @IEditCodeService editCodeService: IEditCodeService, @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + @IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -348,6 +350,9 @@ export class ToolsService implements IToolsService { edit: async ({ uri, changeDescription }) => { await voidModelService.initializeModel(uri) + if (this.commandBarService.getStreamState(uri) === 'streaming') { + throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`) + } const res = await editCodeService.startApplying({ uri, applyStr: changeDescription, diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 2b01e37b..9fd0c654 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -31,7 +31,11 @@ export interface IVoidCommandBarService { onDidChangeActiveURI: Event<{ uri: URI | null }>; getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes'; - setDiffIdx(uri: URI, newIdx: number | null): void + setDiffIdx(uri: URI, newIdx: number | null): void; + + acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void; + anyFileIsStreaming(): boolean; + } @@ -64,7 +68,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // depends on uri -> diffZone -> {streaming, diffs} public stateOfURI: { [uri: string]: CommandBarStateType } = {} public sortedURIs: URI[] = [] // keys of state (depends on diffZones in the uri) - private readonly _hooks = new Set() // uriFsPaths + private readonly _listenToTheseURIs = new Set() // uriFsPaths // Emits when a URI's stream state changes between idle, streaming, and acceptRejectAll private readonly _onDidChangeState = new Emitter<{ uri: URI }>(); @@ -76,7 +80,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar private readonly _onDidChangeActiveURI = new Emitter<{ uri: URI | null }>(); readonly onDidChangeActiveURI = this._onDidChangeActiveURI.event; - constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @@ -91,7 +94,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // do not add listeners to the same model twice - important, or will see duplicates if (registeredModelURIs.has(model.uri.fsPath)) return registeredModelURIs.add(model.uri.fsPath) - this._hooks.add(model.uri) + this._listenToTheseURIs.add(model.uri) } // initialize all existing models + initialize when a new model mounts this._modelService.getModels().forEach(model => { initializeModel(model) }) @@ -117,7 +120,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // mount the command bar const d1 = this._instantiationService.createInstance(AcceptRejectAllFloatingWidget, { editor }); disposablesOfEditorId[id].push(d1); - const d2 = editor.onDidChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() }) + const d2 = editor.onWillChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() }) disposablesOfEditorId[id].push(d2); } const onCodeEditorRemove = (editor: ICodeEditor) => { @@ -133,7 +136,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // state updaters this._register(this._editCodeService.onDidAddOrDeleteDiffZones(e => { - for (const uri of this._hooks) { + for (const uri of this._listenToTheseURIs) { if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: delete if empty, add if not in state yet const diffZones = this._getDiffZonesOnURI(uri) @@ -173,7 +176,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar })) this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => { - for (const uri of this._hooks) { + for (const uri of this._listenToTheseURIs) { if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: no change // --- state: @@ -191,7 +194,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar } })) this._register(this._editCodeService.onDidChangeStreamingInDiffZone(e => { - for (const uri of this._hooks) { + for (const uri of this._listenToTheseURIs) { if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: no change // --- state: @@ -342,6 +345,21 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar } + anyFileIsStreaming() { + return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming') + } + + acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) { + const { behavior } = opts + // if anything is streaming, do nothing + const anyIsStreaming = this.anyFileIsStreaming() + if (anyIsStreaming) return + for (const uri of this.sortedURIs) { + this._editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior, removeCtrlKs: false }) + } + } + + } registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :( diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index bd9dbb1b..aacc5627 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -19,9 +19,9 @@ export const tripleTick = ['```', '```'] const changesExampleContent = `\ // ... existing code ... // {{change 1}} -// // ... existing code ... +// ... existing code ... // {{change 2}} -// // ... existing code ... +// ... existing code ... // {{change 3}} // ... existing code ...`