mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
misc state fixes and fix model background (add voidModelService, delete voidFileService)
This commit is contained in:
parent
39e989d3cc
commit
9350c0dcdf
16 changed files with 295 additions and 339 deletions
|
|
@ -14,7 +14,6 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
|||
import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js';
|
||||
import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IVoidFileService } from '../common/voidFileService.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
|
|
@ -28,6 +27,7 @@ import { Position } from '../../../../editor/common/core/position.js';
|
|||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { shorten } from '../../../../base/common/labels.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
|
|
@ -203,7 +203,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IVoidFileService private readonly _voidFileService: IVoidFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IToolsService private readonly _toolsService: IToolsService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
|
|
@ -658,7 +658,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// define helper functions so we can tell what's going on
|
||||
const getLatestMessages = async () => {
|
||||
// recompute files in last message
|
||||
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped
|
||||
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidModelService) // all the file CONTENTS or "selections" de-duped
|
||||
const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files
|
||||
|
||||
// replace last userMessage with userMessageFullContent (which contains all the files too)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference
|
|||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
// import { throttle } from '../../../../base/common/decorators.js';
|
||||
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
|
||||
import { IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
|
||||
import { Color, RGBA } from '../../../../base/common/color.js';
|
||||
|
|
@ -40,14 +40,10 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
|
|||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidFileService } from '../common/voidFileService.js';
|
||||
import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { ILanguageService } from '../../../../editor/common/languages/language.js';
|
||||
import { getFullLanguage, getLanguageFromModel } from '../common/helpers/getLanguage.js';
|
||||
// import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
// import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
|
|
@ -245,8 +241,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
|
||||
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
|
||||
|
||||
_sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId
|
||||
_sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId
|
||||
_sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId (computed from it)
|
||||
_sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId (computed from it)
|
||||
|
||||
// only applies to diffZones
|
||||
// streamingDiffZones: Set<number> = new Set()
|
||||
|
|
@ -278,16 +274,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IVoidFileService private readonly _voidFileService: IVoidFileService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@ILanguageService private readonly _languageService: ILanguageService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
// this function initializes data structures and listens for changes
|
||||
const registeredModelListeners = new Set<string>()
|
||||
const initializeModel = (model: ITextModel) => {
|
||||
const initializeModel = async (model: ITextModel) => {
|
||||
|
||||
await this._voidModelService.initializeModel(model.uri)
|
||||
|
||||
// do not add listeners to the same model twice - important, or will see duplicates
|
||||
if (registeredModelListeners.has(model.uri.fsPath)) return
|
||||
|
|
@ -442,7 +439,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
private _addDiffAreaStylesToURI = (uri: URI) => {
|
||||
const model = this._getModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
|
@ -471,7 +468,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
private _computeDiffsAndAddStylesToURI = (uri: URI) => {
|
||||
const fullFileText = this._voidFileService.readModel(uri) ?? ''
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (model === null) return
|
||||
const fullFileText = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
|
@ -636,8 +635,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
const disposeInThisEditorFns: (() => void)[] = []
|
||||
|
||||
const model = this._getModel(uri)
|
||||
if (model === null) return
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
|
||||
// green decoration and minimap decoration
|
||||
if (type !== 'deletion') {
|
||||
|
|
@ -775,38 +773,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
// private _readURI(uri: URI, range?: IRange): string | null {
|
||||
// if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null
|
||||
// else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null
|
||||
// }
|
||||
// private _getNumLines(uri: URI): number | null {
|
||||
// return this._getModel(uri)?.getLineCount() ?? null
|
||||
// }
|
||||
|
||||
|
||||
private _getModel(uri: URI) {
|
||||
const model = this._modelService.getModel(uri)
|
||||
if (!model || model.isDisposed()) {
|
||||
return null
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
// not obvious at all, but if we want the model we can just create it. our listeners in the constructor handle it. call this the moment we know we have a uri that we want to make changes to
|
||||
private async _ensureModelExists(uri: URI) {
|
||||
const m = this._getModel(uri)
|
||||
if (m !== null) return m
|
||||
|
||||
const fileStr = await this._voidFileService.readFile(uri)
|
||||
if (fileStr === null) return null
|
||||
const lang = getFullLanguage(this._languageService, { uri, fileContents: fileStr })
|
||||
const model = this._modelService.createModel(fileStr, lang, uri);
|
||||
return model
|
||||
}
|
||||
|
||||
|
||||
private _getActiveEditorURI(): URI | null {
|
||||
const editor = this._codeEditorService.getActiveCodeEditor()
|
||||
if (!editor) return null
|
||||
|
|
@ -817,8 +783,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
weAreWriting = false
|
||||
private _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) {
|
||||
const model = this._getModel(uri)
|
||||
if (model === null) return
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return // TODO!!!! make sure this works
|
||||
|
||||
const range: IRange = range_ === 'wholeFileRange' ?
|
||||
{ startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER } // whole file
|
||||
|
|
@ -831,7 +797,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
this._realignAllDiffAreasLines(uri, newText, oldRange)
|
||||
}
|
||||
|
||||
const uriStr = model.getValue()
|
||||
const uriStr = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
// heuristic check
|
||||
const dontNeedToWrite = uriStr === text
|
||||
|
|
@ -852,6 +818,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) {
|
||||
|
||||
const getCurrentSnapshot = async (): Promise<HistorySnapshot> => {
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
|
||||
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
|
|
@ -864,7 +832,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
) as DiffAreaSnapshot
|
||||
}
|
||||
|
||||
const entireFileCode = await this._voidFileService.readFile(uri) ?? ''
|
||||
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : '' // TODO!!! make sure this isn't '' usually
|
||||
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode, // the whole file's code
|
||||
|
|
@ -915,11 +885,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
// restore file content
|
||||
this._writeURIText(uri, entireModelCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}
|
||||
|
||||
const beforeSnapshot: Promise<HistorySnapshot> = getCurrentSnapshot()
|
||||
|
|
@ -1330,13 +1302,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
uri = uri_
|
||||
await this._ensureModelExists(uri)
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
const c_ = await this._voidFileService.readFile(uri)
|
||||
if (c_ === null) return
|
||||
currentFileStr = c_
|
||||
|
||||
const numLines = numLinesOfStr(currentFileStr)
|
||||
currentFileStr = model.getValue(EndOfLinePreference.LF)
|
||||
const numLines = model.getLineCount()
|
||||
|
||||
// remove all diffZones on this URI, adding to history (there can't possibly be overlap after this)
|
||||
const behavior: 'accept' | 'reject' = opts.startBehavior === 'accept-conflicts' ? 'accept' : 'reject'
|
||||
|
|
@ -1353,11 +1324,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
|
||||
uri = _URI
|
||||
await this._ensureModelExists(uri)
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
const c_ = await this._voidFileService.readFile(uri)
|
||||
if (c_ === null) return
|
||||
currentFileStr = c_
|
||||
currentFileStr = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
startLine = startLine_
|
||||
endLine = endLine_
|
||||
|
|
@ -1366,8 +1337,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
throw new Error(`Void: diff.type not recognized on: ${from}`)
|
||||
}
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
|
||||
const language = getLanguageFromModel(uri, this._modelService)
|
||||
const language = model.getLanguageId()
|
||||
|
||||
let streamRequestIdRef: { current: string | null } = { current: null }
|
||||
|
||||
|
|
@ -1517,7 +1491,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
this._voidFileService.saveOrWriteFileAssumingModelExists(uri)
|
||||
|
||||
const { editorModel } = this._voidModelService.getModel(uri)
|
||||
editorModel?.save() // save the file
|
||||
onDone()
|
||||
resMessageDonePromise()
|
||||
},
|
||||
|
|
@ -1535,7 +1511,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
console.log('done waiting')
|
||||
}
|
||||
|
||||
writeover().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e))
|
||||
writeover().then(() => {
|
||||
resApplyPromise()
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}).catch((e) => rejApplyPromise(e))
|
||||
|
||||
return [diffZone, applyPromise]
|
||||
}
|
||||
|
|
@ -1555,14 +1534,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
else {
|
||||
uri = givenURI
|
||||
}
|
||||
await this._ensureModelExists(uri)
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
// generate search/replace block text
|
||||
const originalFileCode = await this._voidFileService.readFile(uri)
|
||||
if (originalFileCode === null) return
|
||||
|
||||
const numLines = numLinesOfStr(originalFileCode)
|
||||
const originalFileCode = model.getValue(EndOfLinePreference.LF)
|
||||
const numLines = model.getLineCount()
|
||||
|
||||
// reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
|
||||
this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
|
||||
|
|
@ -1738,7 +1715,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const originalBounds = findTextInCode(block.orig, originalFileCode)
|
||||
// if error
|
||||
if (typeof originalBounds === 'string') {
|
||||
console.log('TEXT NOT FOUND')
|
||||
console.log('TEXT NOT FOUND, RETRYING')
|
||||
const content = errMsgOfInvalidStr(originalBounds, block.orig)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
|
||||
|
|
@ -1776,7 +1753,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const trackingZone = this._addDiffArea(adding)
|
||||
addedTrackingZoneOfBlockNum.push(trackingZone)
|
||||
latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
} // <-- done adding diffarea
|
||||
} // end adding diffarea
|
||||
|
||||
|
||||
// should always be in streaming state here
|
||||
|
|
@ -1846,7 +1823,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
|
||||
this._voidFileService.saveOrWriteFileAssumingModelExists(uri)
|
||||
const { editorModel } = this._voidModelService.getModel(uri)
|
||||
editorModel?.save() // save the file // TODO!!! make sure this works
|
||||
onDone()
|
||||
resMessageDonePromise()
|
||||
},
|
||||
|
|
@ -1868,7 +1846,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
} // end retryLoop
|
||||
|
||||
retryLoop().then(() => resApplyDonePromise()).catch((e) => rejApplyDonePromise(e))
|
||||
retryLoop().then(() => {
|
||||
resApplyDonePromise();
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}).catch((e) => rejApplyDonePromise(e))
|
||||
|
||||
return [diffZone, applyDonePromise]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
|
||||
import { useRefState } from '../util/helpers.js'
|
||||
import { usePromise, useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { LucideIcon, RotateCw } from 'lucide-react'
|
||||
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
|
||||
|
|
@ -16,7 +16,7 @@ enum CopyButtonText {
|
|||
|
||||
|
||||
type IconButtonProps = {
|
||||
onClick: () => void
|
||||
onClick: () => void;
|
||||
title: string
|
||||
Icon: LucideIcon
|
||||
disabled?: boolean
|
||||
|
|
@ -27,7 +27,11 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu
|
|||
<button
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick?.();
|
||||
}}
|
||||
className={`
|
||||
size-[22px]
|
||||
p-[4px]
|
||||
|
|
@ -106,6 +110,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
|
|||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
|
|
@ -212,17 +217,27 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
|
|||
/>
|
||||
)
|
||||
|
||||
const jumpToFileHTML = uri !== 'current' && (
|
||||
<IconShell1
|
||||
Icon={FileSymlink}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
|
||||
title="Reject changes"
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
let buttonsHTML = <></>
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
buttonsHTML = <>
|
||||
{jumpToFileHTML}
|
||||
{stopButton}
|
||||
</>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle') {
|
||||
buttonsHTML = <>
|
||||
{jumpToFileHTML}
|
||||
{copyButton}
|
||||
{playButton}
|
||||
</>
|
||||
|
|
@ -230,6 +245,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
|
|||
|
||||
if (currStreamState === 'acceptRejectAll') {
|
||||
buttonsHTML = <>
|
||||
{jumpToFileHTML}
|
||||
{reapplyButton}
|
||||
{rejectButton}
|
||||
{acceptButton}
|
||||
|
|
@ -250,7 +266,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
|
|||
|
||||
return {
|
||||
statusIndicatorHTML,
|
||||
buttonsHTML
|
||||
buttonsHTML,
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -259,7 +275,6 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
|
|||
|
||||
|
||||
|
||||
|
||||
export const BlockCodeApplyWrapper = ({
|
||||
children,
|
||||
initValue,
|
||||
|
|
@ -272,7 +287,7 @@ export const BlockCodeApplyWrapper = ({
|
|||
children: React.ReactNode;
|
||||
applyBoxId: string;
|
||||
canApply: boolean;
|
||||
language: string;
|
||||
language:string;
|
||||
uri: URI | 'current',
|
||||
}) => {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +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 { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
|
||||
|
||||
|
||||
export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => {
|
||||
const isSingleLine = !codeEditorProps.initValue.includes('\n')
|
||||
return <VoidCodeEditor {...codeEditorProps} />
|
||||
}
|
||||
|
|
@ -5,14 +5,16 @@
|
|||
|
||||
import React, { JSX, useMemo, useState } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { convertToVscodeLang, getFirstLine, getLanguage } from '../../../../common/helpers/getLanguage.js'
|
||||
import { BlockCodeApplyWrapper, useApplyButtonHTML } from './ApplyBlockHoverButtons.js'
|
||||
|
||||
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
|
||||
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
|
||||
import { useAccessor } from '../util/services.js'
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { getBasename } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { isAbsolute } from '../../../../../../../base/common/path.js'
|
||||
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
|
||||
import { BlockCode } from '../util/inputs.js'
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
|
|
@ -27,7 +29,7 @@ export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocati
|
|||
}
|
||||
|
||||
function isValidUri(s: string): boolean {
|
||||
return s.includes('/') && s.length > 5 && isAbsolute(s)
|
||||
return s.length > 5 && isAbsolute(s) && !s.startsWith('//') // common case that is a false positive is comments like //
|
||||
}
|
||||
|
||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||
|
|
@ -124,25 +126,28 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
|||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
const [firstLine, remainingContents] = getFirstLine(t.text)
|
||||
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
|
||||
const firstLineIsURI = isValidUri(firstLine)
|
||||
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
|
||||
|
||||
// figure out langauge and URI
|
||||
let uri: URI | null
|
||||
let language: string | undefined = undefined
|
||||
if (firstLineIsURI) { // get lang from the uri in the first line of the markdown
|
||||
uri = codeURI ?? URI.from(URI.file(firstLine))
|
||||
let language: string
|
||||
if (codeURI) {
|
||||
uri = codeURI
|
||||
}
|
||||
else if (firstLineIsURI) { // get lang from the uri in the first line of the markdown
|
||||
uri = URI.file(firstLine)
|
||||
}
|
||||
else {
|
||||
uri = codeURI || null
|
||||
uri = null
|
||||
}
|
||||
|
||||
if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined
|
||||
language = convertToVscodeLang(languageService, t.lang) // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell)
|
||||
}
|
||||
else { // no language provided - fallback - get lang from the uri and contents
|
||||
language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined })
|
||||
language = detectLanguage(languageService, { uri, fileContents: contents })
|
||||
}
|
||||
|
||||
if (options.isApplyEnabled && chatMessageLocation) {
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
|
|||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
|
|
@ -29,7 +28,6 @@ import { getModelSelectionState, getModelCapabilities } from '../../../../common
|
|||
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react';
|
||||
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
|
||||
import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js';
|
||||
import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { DiffZone } from '../../../editCodeService.js';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
|
|
@ -504,7 +502,7 @@ export const SelectedFiles = (
|
|||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const modelService = accessor.get('IModelService')
|
||||
const modelReferenceService = accessor.get('IVoidModelService')
|
||||
|
||||
// state for tracking prospective files
|
||||
const { currentUri } = useUriState()
|
||||
|
|
@ -519,21 +517,39 @@ export const SelectedFiles = (
|
|||
return withCurrent.slice(0, maxRecentUris)
|
||||
})
|
||||
}, [currentUri])
|
||||
let prospectiveSelections: StagingSelectionItem[] = []
|
||||
if (type === 'staging' && showProspectiveSelections) { // handle prospective files
|
||||
const [prospectiveSelections, setProspectiveSelections] = useState<StagingSelectionItem[]>([])
|
||||
|
||||
|
||||
// handle prospective files
|
||||
useEffect(() => {
|
||||
const computeRecents = async () => {
|
||||
const prospectiveURIs = recentUris
|
||||
.filter(uri => !selections.find(s => s.type === 'File' && s.fileURI.fsPath === uri.fsPath))
|
||||
.slice(0, maxProspectiveFiles)
|
||||
|
||||
const answer: StagingSelectionItem[] = []
|
||||
for (const uri of prospectiveURIs) {
|
||||
answer.push({
|
||||
type: 'File',
|
||||
fileURI: uri,
|
||||
language: (await modelReferenceService.getModelSafe(uri)).model?.getLanguageId() || 'plaintext',
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
state: { isOpened: false },
|
||||
})
|
||||
}
|
||||
return answer
|
||||
}
|
||||
|
||||
// add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet
|
||||
prospectiveSelections = recentUris
|
||||
.filter(uri => !selections.find(s => s.type === 'File' && s.fileURI.fsPath === uri.fsPath))
|
||||
.slice(0, maxProspectiveFiles)
|
||||
.map(uri => ({
|
||||
type: 'File',
|
||||
fileURI: uri,
|
||||
language: getLanguageFromModel(uri, modelService),
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
state: { isOpened: false },
|
||||
}))
|
||||
}
|
||||
if (type === 'staging' && showProspectiveSelections) {
|
||||
computeRecents().then((a) => setProspectiveSelections(a))
|
||||
}
|
||||
else {
|
||||
setProspectiveSelections([])
|
||||
}
|
||||
}, [recentUris, selections, type, showProspectiveSelections])
|
||||
|
||||
|
||||
const allSelections = [...selections, ...prospectiveSelections]
|
||||
|
||||
|
|
@ -685,8 +701,8 @@ const ToolHeaderWrapper = ({
|
|||
isRejected,
|
||||
}: ToolHeaderParams) => {
|
||||
|
||||
const [isExpanded_, setIsExpanded] = useState(false);
|
||||
const isExpanded = isOpen ? isOpen : isExpanded_
|
||||
const [isOpen_, setIsOpen] = useState(false);
|
||||
const isExpanded = isOpen !== undefined ? isOpen : isOpen_
|
||||
|
||||
const isDropdown = children !== undefined // null ALLOWS dropdown
|
||||
const isClickable = !!(isDropdown || onClick)
|
||||
|
|
@ -697,7 +713,7 @@ const ToolHeaderWrapper = ({
|
|||
<div
|
||||
className={`select-none flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
|
||||
onClick={() => {
|
||||
if (isDropdown) { setIsExpanded(v => !v); }
|
||||
if (isDropdown) { setIsOpen(v => !v); }
|
||||
if (onClick) { onClick(); }
|
||||
}}
|
||||
>
|
||||
|
|
@ -742,7 +758,7 @@ const ToolHeaderWrapper = ({
|
|||
};
|
||||
|
||||
|
||||
const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'user' } }) => {
|
||||
const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCommitted: boolean, }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
|
@ -851,7 +867,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
|
|||
}
|
||||
}
|
||||
|
||||
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show).
|
||||
if (!chatMessage.content && isCommitted) { // don't show if empty and not loading (if loading, want to show).
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -938,7 +954,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
|
|||
|
||||
|
||||
|
||||
const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => {
|
||||
const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
|
@ -956,7 +972,7 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
|
|||
}
|
||||
|
||||
const isEmpty = !chatMessage.content && !chatMessage.reasoning
|
||||
const isLastAndLoading = isLoading && isLast
|
||||
const isLastAndLoading = !isCommitted && isLast
|
||||
if (isEmpty && !isLastAndLoading) return null
|
||||
|
||||
return <>
|
||||
|
|
@ -988,7 +1004,7 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
|
|||
>
|
||||
|
||||
{/* reasoning token */}
|
||||
{hasReasoning && <ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!!isLoading}>
|
||||
{hasReasoning && <ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
|
||||
<ChatMarkdownRender
|
||||
string={reasoningStr}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
|
|
@ -1007,7 +1023,7 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
|
|||
/>
|
||||
|
||||
{/* loading indicator */}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
{!isCommitted && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
</>
|
||||
|
||||
|
|
@ -1066,7 +1082,6 @@ const ToolRequestAcceptRejectButtons = () => {
|
|||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
try { // this doesn't need to be wrapped in try/catch anymore
|
||||
const threadId = chatThreadsService.state.currentThreadId
|
||||
|
|
@ -1154,15 +1169,7 @@ const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDes
|
|||
|
||||
|
||||
const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
return <ToolContentsWrapper className='bg-void-bg-3'>
|
||||
<ListableToolItem
|
||||
showDot={false}
|
||||
name={uri.fsPath}
|
||||
className='w-full overflow-auto py-1'
|
||||
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
|
||||
/>
|
||||
<div className='!select-text cursor-auto my-4'>
|
||||
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
|
|
@ -1200,9 +1207,9 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe
|
|||
const isWriting = !isDone
|
||||
const [isOpen, setIsOpen] = useState(isWriting)
|
||||
useEffect(() => {
|
||||
if (!isWriting) setIsOpen(isWriting) // if just finished reasoning, close
|
||||
if (!isWriting) setIsOpen(false) // if just finished reasoning, close
|
||||
}, [isWriting])
|
||||
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen}>
|
||||
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}>
|
||||
<ToolContentsWrapper className='bg-void-bg-3 prose-sm'>
|
||||
<div className='!select-text cursor-auto'>
|
||||
{children}
|
||||
|
|
@ -1585,36 +1592,36 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isLoading: boolean, isLast: boolean }
|
||||
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps) => {
|
||||
type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isCommitted: boolean, isLast: boolean, showApproveRejectButtons: boolean }
|
||||
|
||||
const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, showApproveRejectButtons }: ChatBubbleProps) => {
|
||||
const role = chatMessage.role
|
||||
|
||||
if (role === 'user') {
|
||||
return <UserMessageComponent
|
||||
chatMessage={chatMessage}
|
||||
messageIdx={messageIdx}
|
||||
isLoading={isLoading}
|
||||
isLast={isLast}
|
||||
isCommitted={isCommitted}
|
||||
/>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
return <AssistantMessageComponent
|
||||
chatMessage={chatMessage}
|
||||
messageIdx={messageIdx}
|
||||
isLoading={isLoading}
|
||||
isCommitted={isCommitted}
|
||||
isLast={isLast}
|
||||
/>
|
||||
}
|
||||
else if (role === 'tool_request') {
|
||||
const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
|
||||
if (!isLast) return null
|
||||
if (!ToolRequestWrapper) return null
|
||||
return <>
|
||||
<ToolRequestWrapper toolRequest={chatMessage} />
|
||||
<ToolRequestAcceptRejectButtons />
|
||||
</>
|
||||
if (ToolRequestWrapper) {
|
||||
return <>
|
||||
{isLast && <ToolRequestWrapper toolRequest={chatMessage} />}
|
||||
{showApproveRejectButtons && <ToolRequestAcceptRejectButtons />}
|
||||
</>
|
||||
|
||||
}
|
||||
return null
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any, messageIdx: number }> // ts isnt smart enough...
|
||||
|
|
@ -1624,10 +1631,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
|
|||
}
|
||||
|
||||
|
||||
const VoidCommandBarNavButtonsShell = () => {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const VoidCommandBar = () => {
|
||||
const accessor = useAccessor()
|
||||
|
|
@ -1638,7 +1641,6 @@ const VoidCommandBar = () => {
|
|||
const [_, rerender] = useState(0)
|
||||
// Add a state variable to track focus
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
console.log('rerender count: ', _)
|
||||
|
||||
// state for what the user is currently focused on (both URI and diff)
|
||||
const [diffIdxOfFspath, setDiffIdxOfFspath] = useState<Record<string, number | undefined>>({})
|
||||
|
|
@ -1843,6 +1845,7 @@ export const SidebarChat = () => {
|
|||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
const isRunning = !!currThreadStreamState?.isRunning
|
||||
const isStreaming = !!currThreadStreamState?.streamingToken // might be running but not streaming
|
||||
const latestError = currThreadStreamState?.error
|
||||
const messageSoFar = currThreadStreamState?.messageSoFar
|
||||
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
|
||||
|
|
@ -1906,23 +1909,32 @@ export const SidebarChat = () => {
|
|||
const numMessages = previousMessages.length
|
||||
|
||||
const previousMessagesHTML = useMemo(() => {
|
||||
return previousMessages.map((message, i) =>
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, i)} chatMessage={message} messageIdx={i} isLast={i === numMessages - 1} isLoading={false} />
|
||||
return previousMessages.map((message, i) => {
|
||||
const isLast = i === numMessages - 1 && !isStreaming // last if there is no streaming assistant message currently
|
||||
return <ChatBubble key={getChatBubbleId(currentThread.id, i)}
|
||||
chatMessage={message}
|
||||
messageIdx={i}
|
||||
isLast={isLast}
|
||||
isCommitted={true}
|
||||
showApproveRejectButtons={isLast}
|
||||
/>
|
||||
}
|
||||
)
|
||||
}, [previousMessages, currentThread, numMessages])
|
||||
}, [previousMessages, isStreaming, currentThread, numMessages])
|
||||
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ?
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning || isStreaming) ?
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
|
||||
messageIdx={streamingChatIdx}
|
||||
chatMessage={{
|
||||
role: 'assistant',
|
||||
content: messageSoFar ?? '',
|
||||
reasoning: reasoningSoFar ?? '',
|
||||
anthropicReasoning: null,
|
||||
}}
|
||||
isLoading={isRunning}
|
||||
messageIdx={streamingChatIdx}
|
||||
isCommitted={!isRunning}
|
||||
isLast={true}
|
||||
showApproveRejectButtons={false}
|
||||
/> : null
|
||||
|
||||
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
|
||||
|
||||
|
|
@ -17,3 +17,12 @@ export const useRefState = <T,>(initVal: T): ReturnType<T> => {
|
|||
}, [])
|
||||
return [ref, setState]
|
||||
}
|
||||
|
||||
|
||||
export const usePromise = <T,>(promise: Promise<T>): T | undefined => {
|
||||
const [val, setVal] = useState<T | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
promise.then((v) => setVal(v))
|
||||
}, [promise])
|
||||
return val
|
||||
}
|
||||
|
|
|
|||
|
|
@ -485,7 +485,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
|||
arrowTouchesText?: boolean;
|
||||
matchInputWidth?: boolean;
|
||||
gapPx?: number;
|
||||
offsetPx?:number;
|
||||
offsetPx?: number;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -787,8 +787,8 @@ const normalizeIndentation = (code: string): string => {
|
|||
|
||||
|
||||
const modelOfEditorId: { [id: string]: ITextModel | undefined } = {}
|
||||
export type VoidCodeEditorProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
|
||||
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
|
||||
export type BlockCodeProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
|
||||
export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: BlockCodeProps) => {
|
||||
|
||||
initValue = normalizeIndentation(initValue)
|
||||
|
||||
|
|
@ -803,7 +803,6 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
|
|||
// const languageDetectionService = accessor.get('ILanguageDetectionService')
|
||||
const modelService = accessor.get('IModelService')
|
||||
|
||||
|
||||
const id = useId()
|
||||
|
||||
// these are used to pass to the model creation of modelRef
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ import { URI } from '../../../../../../../base/common/uri.js'
|
|||
import { IChatThreadService, ThreadsState, ThreadStreamState } from '../../../chatThreadService.js'
|
||||
import { ITerminalToolService } from '../../../terminalToolService.js'
|
||||
import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'
|
||||
import { IVoidModelService } from '../../../../common/voidModelService.js'
|
||||
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
|
||||
|
||||
|
||||
|
||||
|
|
@ -213,6 +215,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
IMetricsService: accessor.get(IMetricsService),
|
||||
ITerminalToolService: accessor.get(ITerminalToolService),
|
||||
ILanguageService: accessor.get(ILanguageService),
|
||||
IVoidModelService: accessor.get(IVoidModelService),
|
||||
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import { Emitter, Event } from '../../../../base/common/event.js';
|
|||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
// import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
// import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -17,7 +15,7 @@ export interface ISearchReplaceService {
|
|||
}
|
||||
|
||||
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
|
||||
class SearchReplaceService extends Disposable implements ISearchReplaceService {
|
||||
export class SearchReplaceService extends Disposable implements ISearchReplaceService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
|
|||
import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../services/search/common/search.js'
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js'
|
||||
import { IVoidFileService } from '../common/voidFileService.js'
|
||||
import { ITerminalToolService } from './terminalToolService.js'
|
||||
import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
|
@ -183,7 +184,7 @@ export class ToolsService implements IToolsService {
|
|||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService searchService: ISearchService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IVoidFileService voidFileService: IVoidFileService,
|
||||
@IVoidModelService voidModelService: IVoidModelService,
|
||||
@IEditCodeService editCodeService: IEditCodeService,
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
) {
|
||||
|
|
@ -271,8 +272,10 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
this.callTool = {
|
||||
read_file: async ({ uri, pageNumber }) => {
|
||||
const readFileContents = await voidFileService.readFile(uri)
|
||||
if (readFileContents === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
|
||||
const { model } = await voidModelService.getModelSafe(uri)
|
||||
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
|
||||
const readFileContents = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
|
||||
|
|
|
|||
|
|
@ -5,58 +5,26 @@
|
|||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
|
||||
export const getFirstLine = (content: string): [string, string] | [string, undefined] => {
|
||||
const newLineIdx = content.indexOf('\r\n')
|
||||
if (newLineIdx !== -1) {
|
||||
const A = content.substring(0, newLineIdx + 2)
|
||||
const B = content.substring(newLineIdx + 2, Infinity);
|
||||
return [A, B]
|
||||
}
|
||||
|
||||
const newLineIdx2 = content.indexOf('\n')
|
||||
if (newLineIdx2 !== -1) {
|
||||
const A = content.substring(0, newLineIdx2 + 1)
|
||||
const B = content.substring(newLineIdx2 + 1, Infinity);
|
||||
return [A, B]
|
||||
}
|
||||
|
||||
return [content, undefined]
|
||||
}
|
||||
import { separateOutFirstLine } from './util.js';
|
||||
|
||||
|
||||
export function getFullLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }) {
|
||||
const firstLine = opts.fileContents ? getFirstLine(opts.fileContents)?.[0] : undefined
|
||||
// this works better than model.getLanguageId()
|
||||
export function detectLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }) {
|
||||
const firstLine = opts.fileContents ? separateOutFirstLine(opts.fileContents)?.[0] : undefined
|
||||
const fullLang = languageService.createByFilepathOrFirstLine(opts.uri, firstLine)
|
||||
return fullLang
|
||||
}
|
||||
|
||||
export function getLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }): string {
|
||||
return getFullLanguage(languageService, opts).languageId
|
||||
}
|
||||
|
||||
export function getLanguageFromModel(uri: URI, modelService: IModelService): string {
|
||||
return modelService.getModel(uri)?.getLanguageId() || ''
|
||||
}
|
||||
|
||||
|
||||
|
||||
// conversions
|
||||
const convertToVoidLanguage = (languageService: ILanguageService, language: string) => {
|
||||
const { languageId } = languageService.createById(language)
|
||||
return languageId
|
||||
return fullLang.languageId || 'plaintext'
|
||||
}
|
||||
|
||||
// --- conversions
|
||||
export const convertToVscodeLang = (languageService: ILanguageService, markdownLang: string) => {
|
||||
if (markdownLang in markdownLangToVscodeLang)
|
||||
return markdownLangToVscodeLang[markdownLang]
|
||||
return convertToVoidLanguage(languageService, markdownLang)
|
||||
|
||||
const { languageId } = languageService.createById(markdownLang)
|
||||
return languageId
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// // eg "bash" -> "shell"
|
||||
const markdownLangToVscodeLang: { [key: string]: string } = {
|
||||
// Web Technologies
|
||||
18
src/vs/workbench/contrib/void/common/helpers/util.ts
Normal file
18
src/vs/workbench/contrib/void/common/helpers/util.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
export const separateOutFirstLine = (content: string): [string, string] | [string, undefined] => {
|
||||
const newLineIdx = content.indexOf('\r\n')
|
||||
if (newLineIdx !== -1) {
|
||||
const A = content.substring(0, newLineIdx)
|
||||
const B = content.substring(newLineIdx + 2, Infinity);
|
||||
return [A, B]
|
||||
}
|
||||
|
||||
const newLineIdx2 = content.indexOf('\n')
|
||||
if (newLineIdx2 !== -1) {
|
||||
const A = content.substring(0, newLineIdx2)
|
||||
const B = content.substring(newLineIdx2 + 1, Infinity);
|
||||
return [A, B]
|
||||
}
|
||||
|
||||
return [content, undefined]
|
||||
}
|
||||
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { IVoidFileService } from '../voidFileService.js';
|
||||
import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
|
||||
|
||||
// this is just for ease of readability
|
||||
|
|
@ -81,10 +82,11 @@ ${tripleTick[1]}
|
|||
}
|
||||
|
||||
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], voidFileService: IVoidFileService) => {
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
|
||||
if (fileSelections.length === 0) return null
|
||||
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
|
||||
const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr
|
||||
const { model } = await voidModelService.getModelSafe(sel.fileURI)
|
||||
const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
|
||||
return { ...sel, content }
|
||||
}))
|
||||
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
|
||||
|
|
@ -115,7 +117,7 @@ export const chat_userMessageContent = async (instructions: string, currSelns: S
|
|||
|
||||
export const chat_selectionsString = async (
|
||||
prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
|
||||
voidFileService: IVoidFileService,
|
||||
voidModelService: IVoidModelService,
|
||||
) => {
|
||||
|
||||
// ADD IN FILES AT TOP
|
||||
|
|
@ -141,7 +143,7 @@ export const chat_selectionsString = async (
|
|||
}
|
||||
}
|
||||
|
||||
const filesStr = await stringifyFileSelections(fileSelections, voidFileService)
|
||||
const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
|
||||
const selnsStr = stringifyCodeSelections(codeSelections)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,114 +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 { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
|
||||
export interface IVoidFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null>;
|
||||
readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null;
|
||||
|
||||
saveOrWriteFileAssumingModelExists(uri: URI): Promise<void>;
|
||||
}
|
||||
|
||||
export const IVoidFileService = createDecorator<IVoidFileService>('VoidFileService');
|
||||
|
||||
// implemented by calling channel
|
||||
export class VoidFileService implements IVoidFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
|
||||
|
||||
// attempt to read the model
|
||||
const modelResult = this.readModel(uri, range);
|
||||
if (modelResult) return modelResult;
|
||||
|
||||
// if no model, read the raw file
|
||||
const fileResult = await this._readFileRaw(uri, range);
|
||||
if (fileResult) return fileResult;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
|
||||
|
||||
try { // this throws an error if no file exists (eg it was deleted)
|
||||
const res = await this.fileService.readFile(uri);
|
||||
const str = res.value.toString().replace(/\r\n/g, '\n'); // even if not on Windows, might read a file with \r\n
|
||||
if (range) return str.split('\n').slice(range.startLineNumber - 1, range.endLineNumber).join('\n')
|
||||
return str;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readModel = (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null => {
|
||||
|
||||
// read saved model (sometimes null if the user reloads application)
|
||||
let model = this.modelService.getModel(uri);
|
||||
|
||||
// check all opened models for the same `fsPath`
|
||||
if (!model) {
|
||||
const models = this.modelService.getModels();
|
||||
for (const m of models) {
|
||||
if (m.uri.fsPath === uri.fsPath) {
|
||||
model = m
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if still not found, return
|
||||
if (!model) { return null }
|
||||
|
||||
// if range, read it
|
||||
if (range) {
|
||||
return model.getValueInRange({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, startColumn: 1, endColumn: Number.MAX_VALUE }, EndOfLinePreference.LF);
|
||||
} else {
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
saveOrWriteFileAssumingModelExists = async (uri: URI): Promise<void> => {
|
||||
|
||||
const editorsOpen = [...this._editorService.findEditors(uri)]
|
||||
if (editorsOpen.length !== 0) {
|
||||
this._editorService.save(editorsOpen)
|
||||
}
|
||||
else {
|
||||
// write the file using the contents of the existing model
|
||||
const fileStr = this.modelService.getModel(uri)?.getValue()
|
||||
if (fileStr === undefined) {
|
||||
console.error('model not found for uri', uri.fsPath)
|
||||
return
|
||||
}
|
||||
const buffer = VSBuffer.fromString(fileStr)
|
||||
await this.fileService.writeFile(uri, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager);
|
||||
68
src/vs/workbench/contrib/void/common/voidModelService.ts
Normal file
68
src/vs/workbench/contrib/void/common/voidModelService.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextFileEditorModel, ITextFileService } from '../../../services/textfile/common/textfiles.js';
|
||||
|
||||
|
||||
type VoidModelType = { model: ITextModel | null, editorModel: ITextFileEditorModel | null }
|
||||
export interface IVoidModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
initializeModel(uri: URI): Promise<void>
|
||||
getModel(uri: URI): VoidModelType
|
||||
getModelSafe(uri: URI): Promise<VoidModelType>
|
||||
}
|
||||
|
||||
export const IVoidModelService = createDecorator<IVoidModelService>('voidVoidModelService');
|
||||
class VoidModelService extends Disposable implements IVoidModelService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidVoidModelService';
|
||||
|
||||
private readonly _modelRefOfURI: Record<string, ITextFileEditorModel> = {}
|
||||
|
||||
constructor(
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
initializeModel = async (uri: URI) => {
|
||||
if (uri.fsPath in this._modelRefOfURI) return
|
||||
if (uri.scheme !== 'file') return
|
||||
const model = await this._textFileService.files.resolve(uri)
|
||||
|
||||
this._modelRefOfURI[uri.fsPath] = model
|
||||
}
|
||||
getModel = (uri: URI) => {
|
||||
const editorModel = this._modelRefOfURI[uri.fsPath]
|
||||
if (!editorModel) return { model: null, editorModel: null }
|
||||
const model = editorModel.textEditorModel
|
||||
if (!model)
|
||||
return { model: null, editorModel }
|
||||
return { model, editorModel }
|
||||
}
|
||||
|
||||
getModelSafe = async (uri: URI) => {
|
||||
if (!(uri.fsPath in this._modelRefOfURI)) await this.initializeModel(uri)
|
||||
return this.getModel(uri)
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose()
|
||||
for (const [_, reference] of Object.entries(this._modelRefOfURI)) {
|
||||
reference?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidModelService, VoidModelService, InstantiationType.Eager);
|
||||
|
||||
Loading…
Reference in a new issue